{"ok":true,"name":"chai","what":"Signed canonical spreadsheets → ingested tables → stacked transforms → reports. Every report cell traces back to the exact spreadsheet cells it rests on. Replacing a canonical doc marks everything downstream stale until it is rebuilt.","conventions":{"envelope":"Success: { ok: true, ... }. Failure: { ok: false, error: { code, message, hint?, details? } } with a matching HTTP status. Branch on error.code, never on message text.","cell_address":"A cell is (node, row key, column) — e.g. node=rpt_ecl_allowance key=CRE col=ecl. Row keys are shown as `key` on every row. Historical cells are (snapshot, rid, col).","signing":"Non-public requests are signed (ag3nt-req:v1, the FANA scheme): headers x-agent-pub (base64 raw 32-byte ed25519 public key), x-agent-nonce (\"<unix_ms>.<random>\", within ±120s, used once), x-agent-sig (base64 ed25519 signature) over the newline-joined string \"ag3nt-req:v1\\n<METHOD>\\n<path?query>\\n<sha256hex(body)>\\n<nonce>\". Your address is bech32(\"agnt\", sha256(pub)[:20]); your handle derives from the address. Humans use a passkey at /login instead.","staleness":"A node is stale when its head snapshot was built from a doc or definition that has since been replaced. Stale data is still served, always with `warnings`."},"endpoints":[{"method":"GET","path":"/api/whoami","does":"Who this site thinks you are (human by passkey session, agent by signature, or anonymous), your FANA handle, and exactly what you may do."},{"method":"POST","path":"/api/agents/request","does":"Signed with YOUR key: ask the owner for access. Returns 202 and a pending record an owner can approve. This is how an agent gets in without a human typing its address."},{"method":"GET","path":"/api/agents","does":"Owner: the agent list (active and pending), each with its FANA handle and ag3ntsource profile."},{"method":"POST","path":"/api/agents","does":"Owner: add { address, role: owner|reader, label }."},{"method":"PATCH","path":"/api/agents/:address","does":"Owner: approve a pending request or change a role: { status: 'active', role }."},{"method":"DELETE","path":"/api/agents/:address","does":"Owner: remove an agent. The last owner cannot be removed while no human owner exists (LAST_OWNER)."},{"method":"POST","path":"/api/feedback","does":"Report anything that got in your way: { message, context? }. Friction is a bug."},{"method":"GET","path":"/api/status","does":"State of every node (fresh | stale | unbuilt | no_source) with the exact stale warnings."},{"method":"GET","path":"/api/slots","does":"Canonical doc slots, their current head doc, and who may sign for them."},{"method":"GET","path":"/api/slots/:slot","does":"Version history of one slot. `series` says whether it holds one doc or one per `part`; `parts` lists the current doc of each; `next_supersedes` is the hash a replacement must name (per part, for a series)."},{"method":"GET","path":"/api/slots/:slot/statement-template?filename=&doc_sha256=&signer=&signer_pub=&note=","does":"The unsigned statement to sign for a replacement, and its exact canonical bytes (`signing_input`)."},{"method":"POST","path":"/api/docs","does":"Submit a signed doc. JSON { statement, file_base64 } or multipart (file, statement). ?dry_run=true verifies without storing. ?rebuild=true rebuilds immediately after acceptance.","errors":["BAD_STATEMENT","NOT_A_SPREADSHEET","HASH_MISMATCH","BAD_SIGNATURE","SIGNER_MISMATCH","UNTRUSTED_SIGNER","SIGNER_NOT_AUTHORIZED_FOR_SLOT","UNKNOWN_SLOT","SUPERSEDES_MISMATCH","ALREADY_HEAD"]},{"method":"GET","path":"/api/docs/:sha256","does":"Doc metadata, signature statement(s), live re-verification of the stored bytes."},{"method":"GET","path":"/api/docs/:sha256/download","does":"The exact signed bytes."},{"method":"GET","path":"/api/docs/:sha256/sheets/:sheet?range=A1:D20","does":"Cell values as a grid. Use '-' as :sheet for a CSV doc."},{"method":"GET","path":"/api/events","does":"Audit log of every submission attempt, accepted or rejected."},{"method":"POST","path":"/api/builds","does":"Rebuild everything stale, in dependency order. JSON { actor?, message? }. Idempotent: a fresh pipeline returns status 'noop'."},{"method":"GET","path":"/api/builds","does":"Build ledger, newest first."},{"method":"GET","path":"/api/builds/:id","does":"One build and its per-node runs."},{"method":"GET","path":"/api/nodes","does":"The pipeline graph: every ingest, transform and report with inputs and state."},{"method":"GET","path":"/api/nodes/:id","does":"Definition (YAML), documentation, state, upstream/downstream, column formulas."},{"method":"GET","path":"/api/nodes/:id/rows?snapshot=&limit=&offset=","does":"The node's table (paged; `truncated` says there is more). Each cell carries value, stale flag, and what it changed from."},{"method":"GET","path":"/api/nodes/:id/history","does":"Run ledger for the node — the 'git log' of its data."},{"method":"GET","path":"/api/nodes/:id/diff?from=&to=","does":"Cell-level diff between two snapshots of the node (defaults: previous → head)."},{"method":"GET","path":"/api/reports","does":"Reports only."},{"method":"GET","path":"/api/reports/:id","does":"Same as /api/nodes/:id/rows, for a report, with presentation formatting."},{"method":"GET","path":"/api/trace?node=&key=&col=","does":"ONE hop back from a cell: how it was computed and the input cells that fed it (value vs control). Also accepts ?snapshot=&rid=&col=. Follow `deps[].cells[]` to walk further."},{"method":"GET","path":"/api/origins?node=&key=&col=","does":"ALL the way back: every canonical spreadsheet cell the cell rests on, per doc, as A1 ranges, with signer and supersession status. `chain` is the path itself: every table the value passed through (layered top→bottom), the columns and formulas involved at each, table-level and column-level edges, ending at the signed docs."}],"workflows":{"get_access_as_an_agent":["GET /api/whoami (signed) → role 'unlisted'","POST /api/agents/request { label } (signed) → 202, status 'pending'","wait for an owner to approve (they see it at /account); poll GET /api/whoami until role is 'reader' or 'owner'","readers may GET everything; owners may also POST /api/docs and POST /api/builds"],"replace_a_canonical_doc":["GET /api/slots/:slot → read next_supersedes","sha256 your new file; GET /api/slots/:slot/statement-template?...(&part= for a series slot) → statement + signing_input","signature = base64url(ed25519_sign(signing_input)); add it to the statement as `signature`","POST /api/docs { statement, file_base64 } → ok:true. Everything downstream is now stale: GET /api/status shows the warnings","POST /api/builds → rebuild. If it fails (e.g. the new doc has a different layout) the warnings stay and `runs[].error` says why"],"audit_a_number":["GET /api/reports/:id → pick a row key and column","GET /api/origins?node=&key=&col= → the spreadsheet cells, docs and signers behind it","GET /api/trace?... hop by hop if you need the formulas in between"]},"access":{"mode":"site","you":{"principal":"anonymous","role":"anonymous","may":{"read":false,"write":false,"submit_docs":false,"rebuild":false,"manage_agents":false},"next":"Sign a request with your agnt1 key (GET /api → conventions.signing), or sign in with a passkey at /login."}}}