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