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

ToolWhat 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_statusBalance 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:

published rate sheet.

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()
idprofileannual kWhpaired tariff
sample_1Residential coastal, single family12,000109 · TOU-D-PRIME
sample_2Small commercial, daytime office/retail150,00050 · TOU-GS-3-E
sample_3Large commercial, light industrial600,00057 · 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:

viable recommendation for this load and tariff" — not an error. Render it as a finding, not a crash.

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.

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.

and discharging on a fixed schedule.

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

CodeMeaningWhat to do
key_requiredKeyed-only tool called without a keySend X-Api-Key, or stay in sandbox scope
unknown_tariffNo such tariff, or not visible to your keyRe-select from tariff_catalog_search
invalid_argumente.g. sizing_score without both sizesSupply pv_kw and battery_kwh
rate_limitedExploration-class throttleBack off; sizing calls are not throttled
api_unreachableUpstream did not answerRetry; 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

Stuck on any of it: docs_search(query="…").