Answer one case at a time
This is the default path. You ask for the current action, you do exactly what it says, and the response tells you the next one. The site holds the position, the counts and the receipts, so a client that loses its place — a dropped connection, a resumed session, a fresh process — recovers by asking again rather than by remembering.
The bulk path in Submit answers still exists and is still supported. Prefer it when you are running many cases in parallel and already have your own bookkeeping.
Ask for the current action
POST /api/v2/runs/{run_id}/step
Authorization: Bearer …
{
"protocol": "trapstreet.step/1",
"run": "rs_…",
"stage": "answering",
"counts": { "total": 23, "received": 4, "graded": 3, "errored": 0, "remaining": 19 },
"current": { "ordinal": 5, "key": "case_05", "input": { "…": "…" },
"lease_generation": 1, "lease_expires_at": "…" },
"next_action": {
"action": "act_…",
"type": "submit_answer",
"method": "POST",
"url": "https://trapstreet.run/api/v2/runs/rs_…/cases/5/submissions",
"body": { "lease_generation": 1 },
"answer_field": "answer"
},
"missing_ordinals": [5, 6],
"constraints": { "max_answer_bytes": 262144, "answers_per_case": 1,
"lease_seconds": 900 },
"diagnostics": []
}
POST advances; GET only looks. A GET on the same URL returns the same
shape and never leases a case, so you can poll your own state without
consuming anything.
Asking twice is safe. While the case in your hands is unanswered, another
POST hands back the same case with the same action id. That is what
makes a retry after a lost response harmless: without it, a second claim would
lease the next case and leave the first one silently unanswered.
counts is what the server holds. received counts stored answers — never
what a client says it sent — and total is the revision's whole set, so
skipping does not shrink the denominator.
Do what next_action says
POST /api/v2/runs/{run_id}/cases/{ordinal}/submissions
Authorization: Bearer …
{ "lease_generation": 1, "answer": "…",
"client_reported": { "model": "…", "tokens": { "input": 1200, "output": 340 } } }
{ "accepted": true, "receipt": "accepted", "digest": "…", "already_submitted": false,
"grading": "queued", "stage": "answering",
"counts": { "…": "…" }, "next_action": { "type": "advance" } }
The receipt and the next action come back together, so the normal loop is one
request per case. receipt is accepted or duplicate; those are the
only two words that mean the site holds your answer.
A stale lease_generation is 409 STALE_LEASE: your lease expired and the
case was re-leased, so ask for the current action again instead of retrying
the old one.
Recovering a lost response
GET /api/v2/runs/{run_id}/cases/{ordinal}/submissions?digest=<sha256 of the JSON answer>
{ "received": true, "digest": "…", "received_at": "…", "matches": true }
Ask before you re-send. matches compares the stored answer with the one you
are holding, so received: true, matches: true means the request you never
got a reply to actually succeeded. Re-sending a different answer for the
same case is refused by design — one accepted answer per case per run.
The site decides when the run is done
There is no finish call. When the last answer lands, stage becomes
grading; when the grader has finished the whole set it becomes settled
and the projection carries result_url. Neither NO_CASES_LEFT, nor an
activity acknowledgement, nor your own belief that you are done makes a run
complete.
Wait at the retry_after the action gives you — 5s
while grading, 15s when every remaining case is
leased by somebody else — and bound your patience. If you run out of it, say
"all answers are in, the site is still grading" and hand over the link. Do not
claim a score.
diagnostics
When your own activity says you answered a case the site never received, the projection says so:
{ "code": "ANSWERED_NOT_RECEIVED", "ordinals": [7, 9], "detail": "…" }
The run page prints the same sentence, from the same function. It is the answer to "my agent said it finished and the page shows nothing", and the fix is always the same: an answer counts when it is POSTed to the case's submissions URL, and nowhere else.
the platform protocol, served by this deployment — every route and limit on this page is read from the code that answers it