You are building a tool that turns a customer's electricity load into a solar + battery proposal priced against that customer's actual utility tariff — not a blended $/kWh average. This guide takes you from an empty project to a working proposal in one session.
Everything below runs against the WattChop MCP server. Connect once, then use the ten tools by name.
1 · Connect
No key connects you to the sandbox: the certified tariff catalog, three bundled load profiles, load synthesis from monthly bills, and real sizing runs against demo scope. That is enough to build and test the whole flow. Send your API key when you want to run against your own interval data.
# Claude Code — sandbox
claude mcp add --transport http wattchop https://mcp.wattchop.com/mcp
# Claude Code — with your key
claude mcp add --transport http wattchop https://mcp.wattchop.com/mcp \
--header "X-Api-Key: $WC_API_KEY"
// Cursor — .cursor/mcp.json
{
"mcpServers": {
"wattchop": {
"url": "https://mcp.wattchop.com/mcp",
"headers": { "X-Api-Key": "wc_org_..." } // omit for sandbox
}
}
}
Keep the key in an environment variable. Never commit it, and never put it in a payload — it belongs in the X-Api-Key header and nowhere else.
Confirm the connection before writing any code: call docs_search with no arguments. It lists the developer documentation and proves the transport works without spending anything.
2 · The ten tools
| Tool | What it answers |
|---|---|
docs_search | "How do I…" — the developer documentation |
tariff_catalog_search | "Which tariffs can I price against?" |
tariff_get | "What exactly is tariff 57, and which version?" |
building_types | "What building types does synthesis accept?" |
list_samples | "What demo loads can I build against?" |
synthesize_load | "I only have 12 monthly bills — make me a year" |
sizing_run | "What should this customer build?" — the optimizer chooses |
sizing_score | "How good is this specific system?" — you supply the sizes |
sizing_result | "That run was still going — finish it" |
wallet_status | Balance and tier |
The distinction that matters most: sizing_run chooses the sizes, sizing_score grades sizes you already have. sizing_score requires both pv_kw and battery_kwh and refuses the call without them. Use sizing_run for a proposal; use sizing_score to show a customer how a competitor's quote compares to what the optimizer would have picked. Both return the same envelope.
3 · Pick a tariff
tariff_catalog_search(query="TOU-GS-3", limit=10)
Rows come back with tariff_id, code, name, utility_name and a certification marker. Two values appear:
certified— WattChop has validated the tariff's mechanics against the
published rate sheet.
provisional— in the WattChop Library, not yet certified. Still
runnable; say so if you surface it.
certified_as_of is the date WattChop verified the mechanics. version.effective_from is the date the utility's rates took effect. They are different facts and must never be shown as the same thing.
Then pin the exact tariff:
tariff_get(tariff_id=57)
{
"tariff_id": 57,
"code": "B-20-P",
"name": "PG&E B-20 Primary (large commercial anytime demand)",
"utility_name": "Pacific Gas & Electric",
"certification": "certified",
"certified_as_of": "2026-07-14",
"freshness": "current",
"versions": [ { "rate_version_id": 412, "catalog_version_id": 88,
"effective_from": "2026-03-01", "effective_through": null } ]
}
freshness is current, or update_pending when someone has reported that the utility changed its rates and WattChop is checking. update_pending is active diligence, not a defect — a tariff being watched. Show it; don't hide it.
4 · Get a load
Three ways, in descending order of fidelity.
a. The customer's own interval data. Pass a CSV. This needs your API key.
b. Twelve monthly bills. Nearly every proposal starts here:
building_types() # the accepted vocabulary
synthesize_load(
building_type="office",
monthly_kwh=[10400, 9800, 10100, 11200, 12600, 14100,
15300, 15100, 13400, 11800, 10200, 10500],
billed_peak_kw=48.0 # optional, sharpens demand modelling
)
The result carries "estimate": true. That flag is not decoration. A synthesized year is modelled from bills, not measured. Label it as an estimate wherever the numbers appear.
c. A bundled sample, for building and testing:
list_samples()
| id | profile | annual kWh | paired tariff |
|---|---|---|---|
sample_1 | Residential coastal, single family | 12,000 | 109 · TOU-D-PRIME |
sample_2 | Small commercial, daytime office/retail | 150,000 | 50 · TOU-GS-3-E |
sample_3 | Large commercial, light industrial | 600,000 | 57 · B-20-P |
The key (sample_1) is the id the sizing tools take. There is no sample_site_id field inside the entry to read — the key is the id.
5 · Size the system
sizing_run(tariff_id=57, sample_id="sample_3")
This submits a sweep — the optimizer searches PV and battery sizes — and polls it for you. Expect roughly 30–60 seconds on a residential load and up to two minutes on a large commercial one. Show a progress state in your UI; do not block a page render on it.
The envelope:
{
"engine": { "engine_revision": "01392", "generated_at": "2026-08-31T18:42:11Z" },
"scenarios": [{
"id": "solar_battery",
"label": "Solar + Battery",
"is_recommended": true,
"system": { "pv_kw": 412.5, "battery_kwh": 1080.0, "battery_kw": 540.0 },
"savings_static": 198400.0,
"savings_optimized": 241900.0,
"optimization_uplift": 43500.0,
"demand_savings": 96200.0,
"energy_savings": 145700.0
}],
"diminishing_returns": [
{ "battery_kwh": 540.0, "annual_bill": 402100.0, "savings_optimized": 201300.0, "is_recommended": false },
{ "battery_kwh": 1080.0, "annual_bill": 361500.0, "savings_optimized": 241900.0, "is_recommended": true },
{ "battery_kwh": 1620.0, "annual_bill": 358800.0, "savings_optimized": 244600.0, "is_recommended": false }
],
"dispatch": { "recommended_cadence": "autonomous", "annual_uplift": 43500.0,
"requires": "WattChop dispatch subscription", "schedule_available": false },
"attribution": {
"powered_by": "WattChop",
"required_display": "Sizing and savings by WattChop",
"methodology_url": "https://app.wattchop.com/methodology"
},
"tariff": {
"id": 57, "code": "B-20-P", "certification": "certified",
"version": { "rate_version_id": 412, "catalog_version_id": 88,
"effective_from": "2026-03-01" }
}
}
Notes that will save you an hour:
scenariosholds 0 or 1 element. Empty is a legitimate answer — "no
viable recommendation for this load and tariff" — not an error. Render it as a finding, not a crash.
is_recommendedis the only flag. There is nowinnerfield.- The recommended point on
diminishing_returnsis **not always the curve
maximum**. The largest battery often saves a little more for a lot more money. Plot the curve and mark the recommended point; that chart is usually the most persuasive thing in the proposal.
battery_kwis power,battery_kwhis energy. Quote both.
If the run is still going
On a long run sizing_run returns a handle instead of an envelope:
{ "run_id": "b6f1…", "status": "running", "progress_pct": 32 }
The run is not lost, and it is not restartable — retrieve it, don't resubmit.
sizing_result(run_id="b6f1…")
Poll every few seconds until you get the envelope. Treat progress_pct as a liveness signal, not an ETA — it is not linear, and a run can read 32% most of the way through.
6 · The three things every proposal must show
Non-negotiable. If your renderer can drop any of these, fix the renderer.
a. Attribution. Display attribution.required_display wherever the numbers appear, and link methodology_url. One line under the savings figure is enough.
b. Both savings figures, side by side.
savings_static— annual savings under naive dispatch: the battery charging
and discharging on a fixed schedule.
savings_optimized— annual savings under WattChop dispatch.optimization_uplift— the difference.
Showing only the optimized number overstates what a battery achieves on its own, and showing only the static one hides the reason to run WattChop dispatch. Show both, always, with a label a customer can read:
Estimated annual savings: $241,900 with WattChop dispatch, $198,400 with standard scheduling — a $43,500/yr difference. Sizing and savings by WattChop.
c. The tariff version pin. Print tariff.code and tariff.version.effective_from, and keep rate_version_id in your record of the run. Rates change. Six months from now, "what did we quote and against which rates?" has exactly one correct answer, and this is it.
Two version numbers come back — rate_version_id (the rates the engine priced against) and catalog_version_id (the effective-date record). They belong to different namespaces. Store both, never join them, never present them as one.
7 · Errors you will actually hit
| Code | Meaning | What to do |
|---|---|---|
key_required | Keyed-only tool called without a key | Send X-Api-Key, or stay in sandbox scope |
unknown_tariff | No such tariff, or not visible to your key | Re-select from tariff_catalog_search |
invalid_argument | e.g. sizing_score without both sizes | Supply pv_kw and battery_kwh |
rate_limited | Exploration-class throttle | Back off; sizing calls are not throttled |
api_unreachable | Upstream did not answer | Retry; if you hold a run_id, use sizing_result rather than resubmitting |
unsupported_filters is not an error. tariff_catalog_search accepts state and zip, and reports them back unapplied when they are not available on your scope, rather than silently ignoring them. Check for the field before telling a user their ZIP filter worked.
8 · Ship checklist
- Both savings figures rendered, labelled, on the same screen
attribution.required_displayshown;methodology_urllinkedtariff.code+version.effective_fromon the proposal;rate_version_idstored- Synthesized loads labelled as estimates
certificationsurfaced;provisionalsaid out loud- Empty
scenariosrenders as a finding, not an error - Long runs poll
sizing_resultinstead of resubmitting - API key in an environment variable, never in a payload or a repo
Stuck on any of it: docs_search(query="…").