Home/Docs/Playbook

Onboard your first API

Take one endpoint from "blocks agent traffic" to "verifies it" — a guarded route, a test agent making signed calls, caps enforced at the boundary, and an audit log of every decision. Budget about 30 minutes.

Pick one route to start — the one where agent traffic already shows up and gets rejected. The refill/deposit primitive is the sweet spot: account top-ups, API-credit refills, ad-budget top-ups, order placement. Nothing here moves real money: KYC is a sandbox stub and the sanctions screen is a stub.

Funnel: an anonymous agent with no X-Passport header gets a structured 403 with an onboarding URL; the principal onboards through KYC, a mandate is issued, and the agent returns as a verified buyer with co-signed receipts.
The end state: your 403s convert into verified, mandate-scoped buyers.

Step 0 — See the end state #

2 minutes, nothing to install. Northbank is a live demo brokerage whose /api/refill is guarded by the exact middleware you're about to add. Hit it anonymously and watch it fail closed:

terminallive sandbox
curl -i -X POST https://northbank-production.up.railway.app/api/refill \
  -d '{"amount_minor":50000}'

# → HTTP/2 403
# {"error":"KYA required","reason":"missing_passport",
#  "onboarding_url":"https://api.writhq.com/onboarding"}

That 403 — with an onboarding link instead of a dead end — is what your unguarded route is missing today.

Step 1 — Get your platform record and API key #

5 minutes. Your platform is a plt_ record on the passport plus an API key that authenticates your server-to-server calls to /v1/verify.

Against the hosted alpha: platform registration is operator-gated — reach out and we provision plt_yourname + key same-day.

Self-serve, locally: run the whole stack on your machine and provision yourself:

terminal
git clone https://github.com/Zilula/writ && cd writ && npm install
npm run dev        # passport :3000 · northbank :3100

# register your platform (dev admin token; override with PASSPORT_ADMIN_TOKEN)
curl -s -X POST http://localhost:3000/v1/platforms \
  -H "authorization: Bearer dev_admin_token" \
  -H "content-type: application/json" \
  -d '{"name":"My Platform"}'
# → {"id":"plt_…","api_key":"plt_sk_plt_…","created":true}
Key hygiene

Keep the key in your secret store — the passport keeps only a scrypt hash at rest.

Step 2 — Guard the route #

10 minutes. Install the middleware and set two env vars in your service:

terminal
npm i @writhq/verify

# env
PASSPORT_URL=http://localhost:3000        # or the hosted passport URL
PLATFORM_API_KEY=plt_sk_…

Then wrap the route. Hono shown; requireKYAExpress is the Express twin, and evaluateRequest / KYAClient cover everything else:

server.ts · Hono@writhq/verify
import { requireKYA } from "@writhq/verify";

app.post(
  "/api/refill",
  requireKYA({
    action: "account.refill",
    // bind the REAL amount (minor units) — caps are enforced against this
    amount: async (c) => {
      const body = await c.req.json().catch(() => ({}));
      return Number(body.amount_minor ?? 0);
    },
  }),
  async (c) => {
    const kya = c.get("kya");   // decision, chain, verification_id, receipt
    await accounts.refill(/* … */); // your existing logic, unchanged
    return c.json({ ok: true, principal: kya.chain.principal });
  }
);

The middleware fails closed: missing header, deny, or an unreachable passport all block the request before your handler runs.

Step 3 — Confirm the block #

2 minutes.

terminal
curl -i -X POST http://localhost:4000/api/refill -d '{"amount_minor":50000}'
# → 403 {"error":"KYA required","reason":"missing_passport","onboarding_url":"…"}

Set onboardingUrl in the middleware options to your co-branded flow when you have one — the funnel is anonymous → 403 → onboard → mandate → verified buyer.

Step 4 — Run the full chain with a test agent #

8 minutes. Provision a principal, agent, and mandate, then present signed assertions at your route. From the repo checkout:

terminal
export PASSPORT_AGENT_HOME="$PWD/.passport-agent"   # one keystore for every call

# principal (sandbox KYC → verified) + agent + mandate in one shot:
npm run seed

Point the agent at your platform (the seeded default is Northbank). The mandate's scope must name your plt_ id, and the refill command presents to {--url}/api/refill:

terminal
npm run cli -w @passport/agent -- use --mandate mnd_… --platform plt_…
npm run cli -w @passport/agent -- refill --amount 500  --url http://localhost:4000   # → allow
npm run cli -w @passport/agent -- refill --amount 2000 --url http://localhost:4000   # → deny · per_tx_cap

If your route isn't /api/refill, present from code instead — @writhq/sdk's PassportAgent.load(…) then agent.present(url, { action, amount }) hits any endpoint — or drive it from Claude Code with npx @writhq/mcp. Then revoke the mandate (POST /v1/mandates/{id}/revoke, or one click in the principal dashboard) and watch the next present deny with mandate_revoked within seconds — revocation is a live check, not a cached one.

Exit criteria

You have seen allow, per_tx_cap, and mandate_revoked at your own route, and each response carried the resolved chain + a signed receipt.

Step 5 — Countersign and audit #

3 minutes. After your handler executes the action, countersign the verification — a record signed by both sides is nearly incontestable:

POST/v1/verifications/{id}/countersign
request body
{ "outcome": "executed", "platform_ref": "txn_…", "platform_sig": "<JWS by your platform key>" }

Export the decision log any time for reconciliation or compliance review — every decision is an immutable, signed vrf_ record:

GET/v1/verifications?since=&platform_id=

The platform dashboard (/platform on the passport) renders the same log with co-signature status and one-click JSON export.

Step 6 — Production checklist #

  • Caps make sense. Per-tx and period caps agreed with your principals — start narrow; widening a mandate is easy, clawing back is not.
  • Amount binding is real. The amount resolver reads what your ledger will actually move, not a client-declared field elsewhere in the body.
  • Fail-closed verified. Kill PASSPORT_URL in staging and confirm the route blocks instead of admitting.
  • Receipts verified. Check the receipt JWS against the issuer JWKS: GET /v1/issuer/jwks (Ed25519).
  • Countersigning wired into the post-execute path, not best-effort.
  • Deny monitoring. Alert on spikes of replay / unknown_agent (probing) and on per_tx_cap bursts (an agent mis-sized for its mandate).
  • Onboarding URL points somewhere real, so blocked traffic converts.
  • Key hygiene. Platform API key in a secret store; rotation path tested (POST /v1/agents/{id}/rotate for agent keys; re-registering your platform re-issues its key).

What to integrate second #

More actions on the same platform record — each is one more requireKYA({ action }) line and one more scope in the mandate schema: order.place, subscription.create, credits.refill. The chain, caps, revocation, and audit come along for free.