build and operate · api reference
the api, as it runs.
every endpoint on slct.sh: what it takes, what it returns, and how it fails. planning needs no key. executing does.
post /v1/select/plan · no key
plan a decision
send a contract, get one ranked decision back. nothing runs and nothing is charged. the body is the contract itself.
curl -s https://slct.sh/v1/select/plan \
-H 'content-type: application/json' \
-d '{"task":"extract the invoice total",
"success":{"validators":["json_schema"],"required_fields":["total"]},
"limits":{"max_total_cost_usd":0.02,"deadline_ms":4000,"max_attempts":2}}'| field | rule |
|---|---|
task | required. a non-empty string. |
success.validators | required. one or more of json_schema, tool_semantics, state_check, semantic; anything else is a 422. what runs today: the schema check on success.required_fields always, the tool check when you send allowed_tools, and semantic for key holders. state_check is accepted but no route runs it yet. |
limits.max_total_cost_usdlimits.deadline_mslimits.max_attempts | required. positive numbers. absent is reason: "missing"; present but zero, negative or not a number is reason: "invalid". |
success.required_fields | the typed fields a result must carry. optional to plan, required to execute. |
policy.regions, policy.allow_models, policy.deny_models | string lists. a route outside them is rejected with POLICY_VIOLATION or DENIED_BY_POLICY. |
policy.task_slice | which measured evidence slice to rank on. without it, the slice with the most trials. |
policy.retention | "zero" stores nothing, so the decision can't be read back later. |
route_shape | optional escalate or probe: adds the cost ladder an execute would walk. |
what comes back
a sample response, trimmed to two routes. it is a static fixture, not a live decision: the values come from a real plan made on 2026-09-29, and plans are deleted 30 days after they are made. full file: samples/plan-response.sample.json.
{ "decision_id": "slct_sample_plan",
"policy_version": "policy-1.0.0",
"evidence_live": true,
"evidence_slice": "slots-8way-v1",
"chosen": { "model": "grok-4.6", "provider": "xai",
"evidence": { "passes": 21, "trials": 36 },
"predicted_pass_rate": { "value": 0.583, "low": 0.422, "high": 0.729 },
"predicted_latency_ms": null, "expected_total_cost_usd": null,
"reason_codes": [] },
"rejected": [ { "model": "command-a-03-2025", "provider": "cohere",
"reason_codes": ["OUTRANKED"], ... } ],
"caps_unverified": [ { "limit": "limits.max_total_cost_usd",
"reason": "COST_UNMEASURED", ... } ] }predicted_pass_rateis a wilson 95% range overevidence.trialsmeasured runs. zero trials is[0, 1], ranked last.predicted_latency_msandexpected_total_cost_usdstaynulluntil they are measured. null means unknown, not free.- a route whose known cost breaks your cap is rejected with
OVER_BUDGET. a route whose known p95 breaks your deadline carriesP95_DEADLINE_RISK. caps_unverifiedlists each hard cap the plan can't check because the chosen route's cost or latency is unmeasured. execute still holds the cap: it prices every attempt first and stops with402 COST_CAPrather than cross it.- a route slct holds a list price for is checked on it: the smallest call it can make (one attempt, empty input, full output) must fit your cap. a route whose floor doesn't carries
COST_CAPandlist_price_floor_usd. when that leaves no route slct can price, the plan is a422 COST_CAPwithstop_reason: "cost_cap"and arouteslist saying why each one is out; a route with no price at all saysCOST_UNPRICED. nothing is stored. - the ranking reads measured pass counts on the evidence slice, not your task text. name
policy.task_sliceto rank on a different slice. - if no route clears your limits,
chosenisnulland every route inrejectedsays why. - with a key, a plan is attributed to it, and a key with no credit left gets
402 OVER_BUDGET.
get /v1/select/{decision_id} · no key
read a decision
every kept decision can be read back by its id. anyone holding the id can read it; your task text never leaves.
| route | returns |
|---|---|
GET /v1/select/{id} | run_id, decision (status, policy version, created), trace (declare, preview), and receipts once it was executed or verified. |
GET /v1/select/{id}/events | the same trace as a list of events. |
GET /r/{id} | the public receipt: chosen and rejected routes, validator verdicts, landed cost. a browser gets a page, anything else gets json. a decision that was only planned has no receipt yet: it comes back with state: "planned" and receipt_recorded: false, and the page says planned, not executed. |
an unknown id, or one planned with retention: "zero", is a 404 with a hint back here.
post /v1/select/execute · key required
execute
plan, run the chosen route on slct's provider keys, check the result, and write a receipt. it spends credit, so it needs a key: Authorization: Bearer slct_…. get one.
curl -s https://slct.sh/v1/select/execute \
-H "authorization: Bearer $SLCT_KEY" \
-H 'content-type: application/json' \
-d '{"contract":{"task":"extract the invoice total",
"success":{"validators":["json_schema"],"required_fields":["total"]},
"limits":{"max_total_cost_usd":0.02,"deadline_ms":4000,"max_attempts":2}},
"input":{"invoice":"..."}}'- body:
contract(the same shape as plan, withsuccess.required_fields),input(what the model works on), and optionalallowed_tools. - back:
decision_id,chosen,result,checks,action(accept,repairorfail),providerusage, and the itemizedreceipt. a 200 is not a pass: readaction. - every attempt is priced from list price before it runs. a key without the credit gets
402 OVER_BUDGET; a contract whose cap no route fits gets402 COST_CAP. neither spends anything. - a paid meaning check is held up front too: its worst case for the judge you picked (plus the fallback judge, if you allow one), at most $0.01 per request. a shaped route holds that for every rung it might run, so a key within $0.01 per rung of its budget gets
402 OVER_BUDGETbefore anything runs. the default route never 402s for it; it skips the check (SEMANTIC_BUDGET_CEILING) instead. the hold covers the check's estimate; if the judge's vendor reports more usage than it estimated, the charge can land above the hold, as it could before. policy.retention: "zero"can't be executed: metering needs a receipt.- already on an openai client?
POST /v1/chat/completionswithmodel: "slct/auto"and the same key runs this same path. without a key it is a401in the openai error shape,code: "KEY_REQUIRED", with ahintback here. - to check a result you ran yourself,
POST /v1/select/verifywith the contract, the result anddecision_id. it needs no key for the schema and tool checks, and answers with a verdict and a receipt in the response. a verify is stored only when it comes from the key that planned or executed that decision, so only that key fills in the decision's receipt at/r/{id}(any other key gets 403NOT_OWNER);semanticneeds a key. - no command line yet: the cli is planned, not published.
every route
errors
errors come back as json: error (a stable code), detail, and when it helps, missing_fields, invalid_fields, reason and hint.
| status | code | when |
|---|---|---|
| 401 | KEY_REQUIRED | execute, compose or select without a working Bearer slct_… key. |
| 402 | OVER_BUDGET | the account's credit (or the key's optional budget cap, if set) can't cover the call. |
| 402 | COST_CAP | no route fits limits.max_total_cost_usd, priced per attempt. stop_reason: "cost_cap". |
| 403 | NOT_OWNER | verify named a decision_id another key planned or executed. use that key, or omit decision_id. checked before any paid validator runs. |
| 404 | NOT_FOUND | unknown route or decision id. |
| 405 | METHOD_NOT_ALLOWED | a known route with the wrong method. the Allow header names the right one. |
| 409 | OPERATOR_REQUIRED | the service isn't wired for this call yet. |
| 413 | TASK_AMBIGUOUS | body over 32 kb. |
| 422 | TASK_AMBIGUOUS | a required field is missing (reason: "missing", named in missing_fields), or the body isn't json. |
| 422 | POLICY_VIOLATION | a field is present but unusable (reason: "invalid", named in invalid_fields), including an unknown validator. |
| 422 | COST_CAP | plan: at list price, the smallest call on every route slct can price costs more than your cap. routes names each floor. |
| 422 | MODEL_UNAVAILABLE | execute found no route it can run. it never swaps in one the plan didn't name. |
| 429 | RATE_LIMITED, PROVIDER_RATE_LIMITED | too many compose calls, or the provider throttled twice. |
| 502 | PROVIDER_FAILED | the provider failed. the body still carries decision_id and the receipt. |
| 503 | OPERATOR_REQUIRED | metering is down, so execute refuses to spend unmetered. |
| 503 | SPEND_UNAVAILABLE | a key with a budget, but slct couldn't read its account spend, so plan and execute refuse rather than count it as $0. retry shortly. |
why a route was rejected
each route in a plan's rejected list carries reason_codes: OUTRANKED, EVIDENCE_THIN, OVER_BUDGET, COST_CAP, P95_DEADLINE_RISK, POLICY_VIOLATION, DENIED_BY_POLICY, PROVIDER_UNRELIABLE, DOMINATED, HIGHER_COST_AT_EQUAL_RATE, LOWER_PASS_RATE_AT_EQUAL_COST.
get · no key
models
GET /v1/models: the openai-style model list. one entry,slct/auto.GET /v1/models/capabilities?slice=slots-8way-v1: measured passes and trials per model on a slice, with the wilson range.sourcesays whether rows are the live rollup or the frozen lab set.
get · no key
health
GET /v1/health:{"ok": true, "service": "slct-edge", "version": "0.4.0"}.GET /v1/health/detailed: which rails are wired (yes or no, never a secret) and per-instance counters.
keys
get a key
- sign in at /login with your email. you get a one-time link and a 6-digit code; there is no password.
- in the console, mint a key. it is shown once; slct stores only a hash.
- $5 of free credit, one time per verified account (not per key), shared by all of the account's keys. top up the account in the console. see pricing.
- send it as
Authorization: Bearer slct_…on execute. plan works without it.
post /v1/account/delete · login required
delete your account
deletes your account in one step. it cannot be undone. the easiest way is the delete account button in the console, which asks you to type the confirmation first. over the api, send your sign-in token, not a key: a slct_ key gets 401.
curl -s https://slct.sh/v1/account/delete \
-H "Authorization: Bearer $SLCT_LOGIN_TOKEN" \
-H 'content-type: application/json' \
-d '{"confirm":"DELETE MY ACCOUNT"}'the body must be exactly {"confirm":"DELETE MY ACCOUNT"}. anything else is a 422 and nothing is deleted. you must have signed in within the last 10 minutes; otherwise it is a 401 with "reauth_required": true and nothing is deleted, so sign in again and retry.
- deleted now: your sign-in record and email address (with the sign-in log entries, pending sign-ins and sign-in tokens kept with it), every key (revoked, then deleted), the ranking data from your runs, your plans (run or not) and their receipts, and your WAVE sign-in link. plans made without signing in carry no account and are deleted 30 days after they were made, like all plans.
- kept: payment records (for tax, refunds and disputes), one spend total per key with no task data and no email, a keyed one-way hash of your email, kept for 12 months after deletion only to prevent repeat free credit, and the closure and refund records. supabase, our sign-in provider, keeps its own sign-in logs, which include your email, for about 7 days.
- refund: unused paid credit is refunded at the price you paid for each purchase, after anything you owe (a negative balance or an unrecovered dispute fee). it goes back to the card you paid with; a charge older than 180 days is paid out another way. the free $5 is never refunded. a purchase from before the current credit ledger is checked in stripe by hand instead of computed. runs still in progress settle at their actual cost first. the answer's
refundfield shows the amount and how it will be paid. - a payment that lands after deletion is not credited; it is recorded and refunded in full. a deleted account cannot buy credit (403).
the answer is 200 with deleted: true, email_deleted, keys_revoked, refund, and the full deleted_now and kept lists.