Quickstart
Two live services are already deployed. Hit them with one curl, then run the whole chain of agency — allow, cap-deny, live revocation, 403 block — against production or on your own machine.
Nothing to install for the first step. Two services are up right now: the passport (verify + mandates) at api.writhq.com, and the Northbank Sandbox brokerage (its /api/refill guarded by the real requireKYA middleware) at northbank-production.up.railway.app. KYC is a sandbox stub with the same interface shape as Persona / Stripe Identity; the sanctions screen is a stub. No real identity data, no real money.
Poke the live sandbox #
Send an anonymous refill. There is no passport on the request, so the middleware fails closed and hands back a structured 403 with an onboarding link — the block that turns rejected traffic into a lead:
curl -i -X POST https://northbank-production.up.railway.app/api/refill \
-d '{"amount_minor":50000}'
# → HTTP/2 403
# {"error":"KYA required","message":"This action requires a verified agent passport.",
# "reason":"missing_passport",
# "onboarding_url":"https://api.writhq.com/onboarding"}
Three more read-only endpoints prove the rest of the surface is live:
# the passport is healthy
curl -s https://api.writhq.com/v1/health
# → {"ok":true,"service":"agent-passport","ts":"…"}
# the issuer's Ed25519 public key — verify any receipt against this
curl -s https://api.writhq.com/v1/issuer/jwks
# → {"keys":[{"crv":"Ed25519","x":"…","kty":"OKP","use":"sig","alg":"EdDSA"}]}
# the demo brokerage balance
curl -s https://northbank-production.up.railway.app/api/account
# → {"balance_minor":500000,"balance":"$5,000.00","currency":"USD"}
Run the full loop against production #
The signed ALLOW / DENY path needs the agent SDK to sign Ed25519 assertions. One command runs the entire SPEC §6 storyline headlessly against the deployed services and prints a colored transcript — it is the acceptance test, and it exits non-zero if any expectation fails:
npx @writhq/demo # onboard → mandate → allow → replay-block → cap-deny → revoke → 403
# → Result: 11 passed of 11
# or from source:
git clone https://github.com/Zilula/writ
cd writ && npm install && npm run demo
Everything under Poke the live sandbox works with nothing installed. The signed path needs the SDK because assertions are Ed25519-signed locally.
Drive it by hand, locally #
Run both services on your machine and step through the chain yourself. Boot the stack:
npm install
npm run dev
# → passport http://localhost:3000 (verify API + dashboards)
# → northbank http://localhost:3100 (guarded by requireKYA)
In a second terminal, pin the agent's keystore to one location so every CLI call shares the same identity, then create the local keypair:
# the agent keystore resolves against the current dir — pin it to the repo root
export PASSPORT_AGENT_HOME="$PWD/.passport-agent"
npm run cli -w @passport/agent -- init
# agent id : (not registered yet)
# public jwk : {"crv":"Ed25519","x":"…","kty":"OKP"}
npm run cli -w @passport/agent executes from packages/agent, so without this the CLI would read a different .passport-agent/ than seed writes and report "agent not registered". Exporting an absolute path keeps init, register, use and refill on the same identity.
Onboard a principal, issue a mandate, present #
You can do the human parts in the dashboard at http://localhost:3000/principal, or by API. By API:
# 1 · onboard a KYC'd principal (sandbox outcome = verified) → prn_ + secret
curl -s -X POST http://localhost:3000/v1/principals \
-H "content-type: application/json" \
-d '{"name":"Alice Chen","country":"US","accredited":true,"kyc":{"sandboxOutcome":"verified"}}'
# → {"id":"prn_…","secret":"prn_sk_…","kyc":"verified","accredited":true}
# 2 · register the agent's public key under that principal
npm run cli -w @passport/agent -- register --principal-secret prn_sk_…
# → registered as agt_…
# 3 · issue the mandate: account.refill ≤ $1,000/tx, ≤ $2,500/week @ Northbank
curl -s -X POST http://localhost:3000/v1/mandates \
-H "content-type: application/json" \
-H "authorization: Bearer prn_sk_…" \
-d '{"agent":"agt_…","scopes":[{"action":"account.refill",
"max_amount_per_tx":100000,"max_amount_per_period":250000,
"period":"week","currency":"USD","platforms":["plt_northbank"],
"purpose":"trading-capital"}]}'
# → {"id":"mnd_…","status":"active","issuer_sig":"<JWS by passport issuance key>"}
# 4 · point the agent at that mandate + platform
npm run cli -w @passport/agent -- use --mandate mnd_… --platform plt_northbank
In the alpha the passport signs the mandate after the principal authorizes it — the CA model. The documented upgrade is principal-held keys / W3C Verifiable Credentials with identical semantics.
Now present signed assertions. The agent signs { mandate_id, action, amount, currency, platform_id, nonce, ts } with its key, attaches the X-Passport header, and calls Northbank:
npm run cli -w @passport/agent -- refill --amount 500 # HTTP 200 · allow · new balance $3,500.00
npm run cli -w @passport/agent -- refill --amount 2000 # HTTP 403 · reason per_tx_cap
npm run cli -w @passport/agent -- refill --amount 500 # allow … repeat until the $2,500/week cap trips → period_cap
Each ALLOW returns the full resolved chain of agency and a JWS receipt. After Northbank executes the refill it countersigns the verification — a record signed by both sides.
Revoke, and watch the block #
Revoke the mandate in the principal dashboard (one click), or by API. Verification is a live check, so the next present denies within seconds:
curl -s -X POST http://localhost:3000/v1/mandates/mnd_…/revoke \
-H "authorization: Bearer prn_sk_…"
# → {"status":"revoked"}
npm run cli -w @passport/agent -- refill --amount 500 # HTTP 403 · reason mandate_revoked
Load the immutable decision log at http://localhost:3000/platform and paste Northbank's seeded key plt_sk_northbank_dev to see every allow/deny you just produced, with co-signature status and a one-click audit-JSON export.
Drive it from Claude Code (MCP) #
npm run seed provisions a principal, agent and mandate in one shot (remember PASSPORT_AGENT_HOME), so the MCP tools and the refill CLI work immediately. Point your MCP config at packages/agent/src/mcp.ts — tools passport_status, passport_present, passport_refill_demo — and see the agents guide.
passport_refill_demo({ amount: 500 })
// → { decision: "allow", chain: {…}, verification_id: "vrf_…", countersigned: true }
Teardown #
Ctrl-C the npm run dev process. Local state lives in SQLite (apps/passport/passport.db*) and the agent keystore (.passport-agent/) — both are git-ignored. Delete them for a clean slate:
rm -f apps/passport/passport.db* # wipe principals, agents, mandates, decision log
rm -rf .passport-agent # forget the local agent identity