Autonomy Ladder

API, version 1

Base URL: https://ladder.ankitu.com. Every /v1 call and /mcp needs the workspace key:

Authorization: Bearer alk_live_…

Keys are stored only as SHA-256 hashes. /v1 and /mcp are outside the sign-in; every page is behind it. Times are ISO 8601. Errors come back as {"error": "plain sentence"} with a 4xx status.

Send work in

POST /v1/events

Body: {"events": [ ... ]} or a JSON array, up to 20,000 events, each in the log format. Every event is checked against the JSON Schema; invalid ones are listed and skipped, valid ones are stored. Idempotent on event id: an id already stored is counted as a duplicate and ignored, so a failed upload can be sent again as it is.

{
  "accepted": 1260, "duplicates": 0,
  "rejected": [{ "index": 7, "id": "bad", "errors": ["event.actor.kind must be one of operator, agent"] }],
  "lane_changes": [ ... ], "alerts": [ ... ], "signoff_requests": [ ... ],
  "closed_week": 2928, "week_number": 3, "waiting_settlement": 4
}

Work on a step must come after a queue.defined event for it (earlier, or earlier in the same batch).

The Ladder's clock is the data: when an event dated in a new week arrives, the weeks before it are closed and every step is reviewed (promotions happen only at a week close; demotions can happen at any moment). Workspaces that run on the wall clock are also closed by the cron a day after each week ends.

POST /v1/events/csv

Body: the flat CSV layout as text/csv. Same answer as above, plus csv_errors by line.

POST /v1/import/{adapter}

Adapters map an export you already have. Body: {"mapping": {...}, "data": "<the export as text>", "dry_run": false}. With dry_run: true the mapped events come back without being stored.

AdapterTakesMapping
generic-csvAny sheet with one row per piece of workWhich column is the item, step, actor, finish time, start time, decision, output fields and item features; how to tell an agent row from a person's (actor_kind + agentValues, or agents names); a step-name map. Example: samples/generic-bills.mapping.json.
servicenowA task table export (sc_task or task) as CSV from a list view, or Table API / JSONv2 JSONsteps (short_description or u_step to step id), agents (assigned_to names, prefix* allowed; a filled u_agent_version also marks an agent), fields, features. Item = parent; time = closed_at; decision = close_code; only closed states. Example: samples/servicenow-sc_task.mapping.json.
salesforceA Case export (Data Loader CSV or REST query JSON)steps (Step__c or Type), agents (owner names; Agent_Version__c also marks an agent), fields, features, decisionField (default Reason). Item = ParentId; time = ClosedDate; only closed cases. Example: samples/salesforce-cases.mapping.json.

Event ids are built from the source row (ServiceNow sys_id, Salesforce Id), so re-importing the same export stores nothing twice.

Ask before acting

POST /v1/route

An agent asks before it acts on an item:

{ "queue": "bills", "step": "decide_hold", "item": "VB-4100000003", "features": { "doc_format": "vtrw-pdf-v4" } }

Answer:

{
  "route_id": "rt_…", "decision": "act_alone", "lane": "auto", "audit": true, "audit_rate": 0.1,
  "reason": "Decide hold or release runs in Auto: act alone. This item is in the 10% audit sample, so a person will check it. Audits in the last 2 weeks: 98.8% right on 80; …",
  "queue": "bills", "step": "decide_hold", "item": "VB-4100000003", "checked_at": "…"
}
decisionLaneWhat the agent must do
human_onlyShadowNot act. A person does the item. The agent may send its own output as step.done with role shadow (that is how it earns promotion).
proposeAssistSend its output as role proposal; a person confirms (role confirm) before anything happens.
act_aloneAutoAct, and send role act. If audit is true a person will check this item and send an audit.result.

The audit pick is seeded and reproducible: audit = fnv32("<audit seed>|<queue>|<step>|<item>") / 2^32 < audit_rate (FNV-1a 32-bit with the murmur3 finaliser, src/core/stats.ts), so a client or an auditor can check every pick. In Auto, an item whose watched input value never appeared while the step earned Auto can be sent to a person instead (the novel-input guard, a rule setting).

Batch form: {"items": [ {...}, {...} ], "at": "…"} answers {"answers": [...]} (up to 2,000). The optional at replays a request at a past or simulated time (it also advances the data clock).

Read the lanes

CallAnswers
GET /v1/lanes?queue=Every step: lane, since which week, plain reason, latest week's figures (items seen, agreed, disagreements by severity, settled agent-right / operator-right / both-wrong, estimated accuracy, Wilson 95% lower bound, rolling 2- and 4-week windows, operator error rate), the last lane change with its rule and numbers, any sign-off waiting, and links to the evidence.
GET /v1/steps/{queue}/{step}The same for one step.
GET /v1/steps/{queue}/{step}/evidenceThe evidence pack: every week's figures and input mix, every lane change, the rules in force, the outcome test's state, and the latest disagreements and audits with their verdicts.
GET /v1/ledger?queue=Every step-week row of the ledger.
GET /v1/statusEvents received (total, today), route calls today, the last webhook, settlements waiting, the data clock.
GET /v1/settle-queue?limit=Disagreements waiting for a settlement, with both outputs and the differing fields.
POST /v1/weeks/close{"through": "<time>"} closes every week ending before it now, instead of waiting for data or the cron.

Webhooks

POST /v1/webhooks with {"url": "https://…", "events": ["lane.changed", "drift.alert", "signoff.requested"]} answers 201 with the hook's id and its signing secret (shown once; it is derived from a Worker secret, so nothing is stored). GET /v1/webhooks lists hooks, DELETE /v1/webhooks/{id} stops one, GET /v1/webhooks/deliveries is the delivery log.

Every delivery is a POST with JSON {"id", "type", "created_at", "workspace", "queue", "step", "at", "data"} and these headers:

Ladder-Signature: t=1791563488,v1=<hex HMAC-SHA256(secret, "1791563488.<raw body>")>
Ladder-Event: lane.changed
Ladder-Delivery: dl_…
Ladder-Webhook-Id: wh_…

Check the signature on the raw body and reject a t more than five minutes off (verifySignature in src/worker/crypto.ts does both). Any 2xx counts as delivered. Otherwise it is retried 1, 5, 30, 120 and 360 minutes later, then marked failed; every attempt is in the log.

EventWhendata
lane.changedAny promotion or demotionfrom, to, the rule that fired, the numbers behind it, week
drift.alertInput shift (PSI), outcome drift (CUSUM) or a critical audit errorsignal, value, threshold, plain detail, the lane it was demoted to
signoff.requestedA money-moving step has earned Autorequest_id, the numbers, and the exact signoff event to send back

Settlement

Disagreements (material or critical differences) wait in the settle queue until a settlement event arrives. The live settler (Claude Haiku 5.5, prompt caching, a $1 a day cap and a kill switch) is built and off: it runs only when the ANTHROPIC_API_KEY Worker secret exists and the settler_on switch is 1, and stops at the cap or when settler_kill is 1. GET /v1/settler shows its state; POST /v1/settler/run {"limit": 10} runs it once. Its verdicts go through the same ingest path as any other settlement.

MCP

POST /mcp (JSON-RPC 2.0, Streamable HTTP, stateless; same key). Tools: which_steps_run_alone, step_evidence, route_item. See gateway.md.

Limits

20,000 events or 50 MB per request; 2,000 items per route call. Event ids, queue ids and step ids: letters, digits and . _ : / # @ -, up to 128. People, agents and items: any printable text up to 128.