Home/Docs/Quickstart

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.

The live sandbox

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:

terminalno install
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:

terminalGET · public
# 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:

terminalagainst prod
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
No repo needed for the anonymous path

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:

terminal · window 1run once
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:

terminal · window 2agent CLI
# 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"}
Why PASSPORT_AGENT_HOME

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:

terminal · window 2onboard + issue
# 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
Custodial issuance (MVP)

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:

terminal · window 2the whole story
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
ok per_tx_cap period_cap mandate_revoked replay 403 missing_passport

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:

terminal · window 2revoke
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.

Claude Code · MCPpassport_refill_demo
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:

terminal
rm -f apps/passport/passport.db*     # wipe principals, agents, mandates, decision log
rm -rf .passport-agent               # forget the local agent identity