| every write control in the console (/) | page.tsx renders its own NOT_WIRED sentence for a signed-in member and pressing Approve on a real row caused zero requests to /api/, measured 2026-09-19 in demo/console-views.txt. No task can be driven through the browser, so browser_tasks is 0. |
| POST /api/billing/checkout | reads STRIPE_PRICE_ID_* before it touches Stripe and answers 500 'No price configured'. Grading it needs a live price id and a live Stripe key, and no Stripe call is made from this environment. |
| GET /api/billing/portal | opens a Stripe billing portal session on the customer id. Needs a live Stripe key. |
| POST /api/firm/checkout | the firm tier's checkout session. Needs a live Stripe key. |
| POST /api/webhooks/stripe | reads STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET before it verifies anything, so its HMAC path cannot be exercised without arming a key that also makes api.stripe.com reachable. Measured 500 {"error":"billing not configured"}. |
| POST /api/invoices/[id]/payment-link | creates a Stripe Payment Link on the operator's connected account. createInvoicePaymentLink returns null at its first line with no key, so the only branch reachable here is the refusal, which the late fee and plan tasks already guard as no-payment-link-was-minted. |
| POST /api/integrations/connect, GET /api/integrations/oauth/[provider] and its callback | need an app registration at Intuit, Xero or HubSpot and a live authorization code exchange. With no client id the start route answers 501 and builds no vendor URL, and the callback breaks out of its switch before any exchange. Disconnecting is the half of the lifecycle that touches no third party, and it is a task. |
| POST /api/integrations/sync, POST /api/sync/run | read a customer's ledger over a QuickBooks or Xero access token. Every token column in this fixture is NULL by design, which is what production looks like a moment before the exchange, so readToken answers null and every consumer refuses before a request is built. |
| GET /api/graph/maintain, POST /api/webhooks/graph | the Microsoft Graph subscription lifecycle. Needs a live mailbox grant and a subscription Microsoft itself calls back on. |
| POST /api/reminders/draft, POST /api/reminders/plan, POST /api/ai/triage, GET /api/ai/client-narrative, POST /api/ai/voice-preview, POST /api/voice | every one of these calls a model. HARD RULE #12: the paid inference keys are spent by a paying customer's own request inside a shipped product and by nothing else, not even to check a key works. With all three rails unkeyed the refusal branch is the only one reachable, and run-the-ladder-for-this-workspace already grades it through drafted_by and draft_reason. |
| POST /api/account/delete | cascades every invoices_* row for the tenant and then the auth user itself. auth.users is shared by every environment on this stack (rule 11), so a task that deletes one would take a shared row out from under a neighbour's suite mid run. |
| GET /api/account/export | a pure read. It returns every invoices_* row as one JSON file and writes nothing, and every reward in this repo reads rows (rule 3). |
| GET and POST /api/unsubscribe | writes fetchdue_suppressions off a cold outreach footer link. That is the estate's marketing lane rather than the desk, and the table is shared with the send queue, which rule 11a puts out of reach of a fixture that would have to scope its own rows into somebody else's suppression list. |
| POST /api/starter-seed | refuses unless the workspace has zero clients. This fixture ships six clients and twelve invoices, so the only branch reachable is the refusal. |
| POST /api/revalidate | clears a Next.js cache path behind a shared secret. It writes no rows. |
| POST /api/auth/signout, POST /api/auth/track | session teardown and a signup analytics ping. Neither touches desk state. |
| POST /api/admin/backfill-conversations | a one off migration that walks historical reminders into conversations. It is not an action an operator takes. |
| GET /api/invoices, GET /api/invoices/[id], GET /api/invoices/[id]/detail, GET /api/clients, GET /api/clients/[id]/detail, GET /api/agent-events, GET /api/firm/context, GET /api/firm/clients | reads. They return rows and write none. |
| POST /api/cadence, POST /api/late-fees/policy, POST /api/conversations/[id]/state, POST /api/messages/[id]/discard, POST /api/integrations/reminders-guard, POST /api/invoices/[id]/late-fee | each writes exactly the one row it was handed, with no second table, no derived figure and no exclusion to get wrong. Rule 4 asks what a capable model fakes cheaply, and on these the fake and the honest outcome are the same single row. |
| POST /api/reminders/send-batch | runs the same sendReminderForInvoice core as POST /api/reminders/send over up to fifty invoices. Every guard on stage-the-westbourne-chase already holds it; a batch of the same call is throughput rather than a new seam. |
| POST /api/messages/[id]/approve | the one place an agent drafted reply leaves the building. With no mail rail on this stack it can only ever finalise not delivered, which is the branch run-the-dispatcher-once already grades, and its schedule and approval writes are the ones stage-the-westbourne-chase grades. |