slct

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}}'
fieldrule
taskrequired. a non-empty string.
success.validatorsrequired. 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_usd
limits.deadline_ms
limits.max_attempts
required. positive numbers. absent is reason: "missing"; present but zero, negative or not a number is reason: "invalid".
success.required_fieldsthe typed fields a result must carry. optional to plan, required to execute.
policy.regions, policy.allow_models, policy.deny_modelsstring lists. a route outside them is rejected with POLICY_VIOLATION or DENIED_BY_POLICY.
policy.task_slicewhich 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_shapeoptional 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_rate is a wilson 95% range over evidence.trials measured runs. zero trials is [0, 1], ranked last.
  • predicted_latency_ms and expected_total_cost_usd stay null until 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 carries P95_DEADLINE_RISK.
  • caps_unverified lists 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 with 402 COST_CAP rather 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_CAP and list_price_floor_usd. when that leaves no route slct can price, the plan is a 422 COST_CAP with stop_reason: "cost_cap" and a routes list saying why each one is out; a route with no price at all says COST_UNPRICED. nothing is stored.
  • the ranking reads measured pass counts on the evidence slice, not your task text. name policy.task_slice to rank on a different slice.
  • if no route clears your limits, chosen is null and every route in rejected says 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.

routereturns
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}/eventsthe 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, with success.required_fields), input (what the model works on), and optional allowed_tools.
  • back: decision_id, chosen, result, checks, action (accept, repair or fail), provider usage, and the itemized receipt. a 200 is not a pass: read action.
  • 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 gets 402 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_BUDGET before 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/completions with model: "slct/auto" and the same key runs this same path. without a key it is a 401 in the openai error shape, code: "KEY_REQUIRED", with a hint back here.
  • to check a result you ran yourself, POST /v1/select/verify with the contract, the result and decision_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 403 NOT_OWNER); semantic needs 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.

statuscodewhen
401KEY_REQUIREDexecute, compose or select without a working Bearer slct_… key.
402OVER_BUDGETthe account's credit (or the key's optional budget cap, if set) can't cover the call.
402COST_CAPno route fits limits.max_total_cost_usd, priced per attempt. stop_reason: "cost_cap".
403NOT_OWNERverify named a decision_id another key planned or executed. use that key, or omit decision_id. checked before any paid validator runs.
404NOT_FOUNDunknown route or decision id.
405METHOD_NOT_ALLOWEDa known route with the wrong method. the Allow header names the right one.
409OPERATOR_REQUIREDthe service isn't wired for this call yet.
413TASK_AMBIGUOUSbody over 32 kb.
422TASK_AMBIGUOUSa required field is missing (reason: "missing", named in missing_fields), or the body isn't json.
422POLICY_VIOLATIONa field is present but unusable (reason: "invalid", named in invalid_fields), including an unknown validator.
422COST_CAPplan: at list price, the smallest call on every route slct can price costs more than your cap. routes names each floor.
422MODEL_UNAVAILABLEexecute found no route it can run. it never swaps in one the plan didn't name.
429RATE_LIMITED, PROVIDER_RATE_LIMITEDtoo many compose calls, or the provider throttled twice.
502PROVIDER_FAILEDthe provider failed. the body still carries decision_id and the receipt.
503OPERATOR_REQUIREDmetering is down, so execute refuses to spend unmetered.
503SPEND_UNAVAILABLEa 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. source says 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 refund field 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.