← Watt Desk Token page

Watt Desk API

Everything the web app does, you can do from your own code: paste an energy decision — a bill with its tariff components, an interval or peak-demand summary, a supplier bid table, a PPA or VPPA term sheet, a hedge position, a rate-case notice, a broker email thread — and get it worked the way a senior energy procurement manager would work it. The scenario named, the risk called, the single next move argued from the numbers on the page, and the traps flagged: demand ratchets, PPA basis and curtailment, credit postings, riders, unhedged index exposure into a scarcity-priced season. Useful for sweeping a portfolio of accounts before a renewal window, screening a bid round before the analyst sees it, or refusing to sign anything that comes back “Walk away”. Base URL https://api.skillsafe.ai/v1/app-api. Start at the token page, or go back to the app.

The base URL, the envelope and the error codes

Every path below hangs off https://api.skillsafe.ai/v1/app-api. There are five of them — /guest, /me, /estimate, /run, /run-stream — plus /jobs/{job_id} for polling. There is no /apps/{slug}/ segment anywhere: the app slug is bound to the token once, at /guest.

Every response is {"ok": true, "data": {...}} or {"ok": false, "error": {"code": "...", "message": "...", "details": {...}}}. Check ok before you read data; the HTTP status and error.code always agree.

StatusCodeWhat to do
400VALIDATION_ERRORThe input shape is wrong. error.details names the field — most often a missing situation, or an input object wrapped in {"input": ...} when it should be sent directly.
401UNAUTHORIZEDMissing, malformed or expired token. Mint a new one from the token page or POST /guest.
402PAYMENT_REQUIREDThe balance is below the run's hold. Call /estimate first and compare against /me.
404NOT_FOUNDWrong slug or job id.
429RATE_LIMITEDBack off and retry with the same idempotency key.
5xxINTERNALRetry with the same idempotency key; a completed job is returned rather than re-billed.

Input fields

The object you send — and it is the input object directly, not wrapped in {"input": ...}. Only situation is required.

FieldTypeMeaning
situationstringRequired. The energy decision as pasted: bill line items and tariff components, an interval or peak-demand summary, a supplier bid table, a PPA or VPPA term sheet, a hedge position, a rate-case notice, a broker email thread. The browser client clips long input at 40,000 characters, keeping the head and the tail and announcing the cut in-band with a marker of the form [... N characters of the middle omitted ...], because a renewal worksheet carries its identifying header at the top and its live pressure — the bids, the deadline, the broker pushing — at the bottom. A trailing [situation truncated] marker tells the model it is not seeing everything, so it will not claim completeness it does not have. Send whatever you like from your own code; clip it the same way if you want the same behaviour.
contextstringOptional, clipped at 6,000 characters. The market or ISO (or that supply is regulated and bundled), the facilities in scope, budget tolerance and how finance reacts to variance, any sustainability or REC target, and the decision actually needed. It materially changes the answer: the same bid table reads differently when a mid-year true-up is worse for the buyer than a higher locked price.
factsstringOptional. Deterministic arithmetic the browser computes from numbers the user typed, one line per computed quantity. It is a hint, not ground truth: the prompt tells the model to reconcile every figure against situation and to disbelieve facts where the two conflict. An API caller may compute and pass its own. Lines look like Load factor computed: 46% from 8,400,000 kWh over 365 days at a 2,100 kW peak and Demand charge computed: $58,800/month, $705,600/year at $28/kW on a 2,100 kW peak., plus peak-shave value with a simple payback banded under 5 / 5-8 / over 8 years, and a PPA strike-versus-forward spread annualized over contract volume net of a basis drag.
retry_notestringOptional, normally omitted, and not for humans. It is sent only on the single automatic retry that follows a reply which failed to parse, and it tells the model to answer again in the exact required shape — the five tag lines, then the six ## sections in order, every section line a - bullet, no code fence. It is a formatting instruction only: it can never change the scenario, the risk, the action or any number. The retry reuses an idempotency key derived from the same input with an incremented attempt counter, so it stays inside one idempotency family and cannot double-bill.

1. A tiny client

One helper, reused by every step below. It sets the bearer header, sends JSON when there is a body, passes an optional Idempotency-Key through, and raises on ok: false so a failure never gets read as data.

2. Get a token

Every call carries Authorization: Bearer <token>. Open the token page to sign in, reveal your personal token and copy a ready-made shell export — that is the supported way to get one, and it never asks you to open the DevTools console. If you would rather script it, POST /guest mints a guest token for a named slug. That is where the app slug is bound, which is why no later path contains an /apps/{slug}/ segment. A guest token works for reading and estimating; a signed-in token is needed to run.

POST/guest

3. Check the session and balance

Confirms who the token belongs to and how many credits are available. Do this before a run: a 402 after submitting is avoidable.

GET/me

4. Estimate - free, no job created

Returns the credit hold a run would reserve, plus the resolved model and markup. It creates no job and charges nothing, so it is safe to call on every keystroke — the app debounces it at half a second and does exactly that. The response carries model, model_alias, markup_bps, hold_credits, min_credits and sponsor_enabled. Assert on model_alias and markup_bps if you want a tripwire when the deployment changes underneath you.

POST/estimate
What this app resolves to, and what the hold means. Watt Desk requests the gpt-terra alias, which resolves to gpt-5.6-terra at markup_bps 1000. hold_credits is the ceiling reserved against the balance while the job runs — it is not the price. The actual charge comes back with the finished job and is normally well under the hold; the unused remainder is released. Read model and hold_credits from the estimate rather than assuming either: the alias is the stable part, the resolved model is not. When sponsor_enabled is true, a guest run costs nothing at all.

5. Run and poll

Creates a job and returns {"job_id": "..."} immediately; poll GET /jobs/{job_id} until status is terminal (succeeded, failed or cancelled), then read data.output.output. The body is the input object directly. Always send an Idempotency-Key: a retried request with the same key returns the original job instead of billing twice.

POST/run
GET/jobs/{job_id}

6. Run with streaming (SSE)

The same job, delivered as server-sent events — this is what the web app uses. delta events carry incremental text, job carries the job id, and done carries the authoritative full output plus the actual charged_credits. Trust done over the concatenated deltas, which can drop the tail. done may also carry truncated: true, which means the model hit its output ceiling and the assessment is cut short: surface that rather than passing a clipped answer off as finished. Send the same Idempotency-Key discipline here as on /run.

POST/run-stream

7. Parse the output

data.output.output is plain text — no code fence around the response as a whole — in exactly this shape: five tag lines, then six ## sections in this order. This is what the app's parser decodes; a reply that breaks any rule below is discarded and retried once with retry_note.

SCENARIO: <Procurement strategy | Demand charge | PPA evaluation | Rate review
           | Budget and hedging | Market event | Sustainability
           | Insufficient information>
RISK: <Low | Moderate | High | Severe>
ACTION: <Lock fixed price | Go block-and-index | Layer purchases | Stay on index
         | Shave the peak | Sign with conditions | Walk away | Investigate first
         | Insufficient information>
CONFIDENCE: <integer 0-100>
SUMMARY: <2 to 4 sentences, ends at the first blank line>

## Next moves
- <move, argued from a number on the page>

## The numbers
- <figure, with where it came from>

## Risk exposure
- <exposure and what it costs>

## Strategy
- <the structure and the term it argues for>

## Watch items
- <what to re-check, and when>

## Open questions
- <question>
Parsing rules, as implemented. SCENARIO: is the first line and must be exactly one of the eight values. RISK: is one of Low, Moderate, High, Severe. ACTION: is exactly one of the nine values — there is one next move, not a menu. CONFIDENCE: is a bare integer 0-100, no percent sign. SUMMARY: is 2 to 4 sentences, may wrap, and ends at the first blank line. All six ## headings must appear, spelled exactly, in that order. Every line inside a section is a - bullet, which may wrap onto indented continuation lines. A section with nothing to report carries the single bullet - None.

Cross-rules the client enforces

Two consistency gates sit on top of the shape, and it is worth re-checking both on your side: if RISK is High or Severe, Risk exposure can never be - None.; and if ACTION is Insufficient information, Open questions can never be - None. A reply that breaks the shape or either gate is discarded and retried once with the shape spelled out in retry_note, reusing the same idempotency family with an incremented attempt counter so the reformat cannot double-bill.

Grounding. The assessment only argues from what is on the page. A figure in facts that cannot be reconciled against situation is dropped rather than repeated. Where the record does not support a call, the scenario and the action are both Insufficient information and the questions that would settle it go in Open questions. That is a real answer, not a failure — treat it as one.

Cost and idempotency, in short

/estimate is free and creates no job. /run and /run-stream reserve hold_credits up front and charge only what the run actually uses; the finished job and the done event both carry the real charged_credits, and the unused reserve is released. Check /me against the estimate before submitting and a 402 PAYMENT_REQUIRED never happens.

Send Idempotency-Key on every /run and /run-stream. Derive it from a content hash of the input plus an attempt counter, exactly as the samples above do: a network retry or a 429 backoff replays the same key and collapses server-side into the original job, while a genuine re-assessment of a changed situation hashes differently and gets its own key. The automatic reformat retry — the one that carries retry_note — increments only the attempt counter, so it stays inside the same idempotency family and cannot double-bill.