You have an API. Writ lets verified agents buy credits, top up accounts, and pay for usage on it — with a real, accountable human behind every request.
It looks like a person, but it isn't — and nothing on the wire says whose agent it is, what it may spend, or who is accountable if it's wrong. So the safe answer is to block it, and real volume goes away.
Nothing on the wire distinguishes the two.
An anonymous API key is not a counterparty.
This action, this amount, this platform, right now?
If it goes wrong, who do you dispute with?
Setup happens once. After that, every agent call is one signed assertion and one verify call that fails closed.
Pass KYC once, register the agent's public key, grant narrow authority: this action, these caps, this platform, expiring — revocable in seconds.
prn_ → mnd_ · signed grantThe SDK (or the MCP server in Claude Code) holds the keypair and signs each call — action, amount, platform, nonce, timestamp — in an X-Passport header.
agt_ key · JWS · nonce + tsOne middleware line calls /v1/verify: signature, mandate, scope, live revocation, caps, KYC. Back comes allow/deny, the resolved chain, and a signed receipt.
plt_ → vrf_ · allow / deny · co-signableDrop one middleware in front of the routes agents call. It fails closed — and anonymous callers get a 403 KYA required with an onboarding link instead of a silent block.
import { requireKYA } from "@writhq/verify";
// Accept agent traffic — one round trip, fails closed.
app.post(
"/v1/account/refill",
requireKYA({ action: "account.refill" }), // ← the integration
async (c) => {
const { chain } = c.get("kya"); // resolved chain
await accounts.refill(c.req.valid("json"));
return c.json({ ok: true, principal: chain.principal });
}
);
The middleware never executes the action for an anonymous agent. It returns a structured block with a link to onboard — so the traffic you used to throw away comes back verified.
treasury-bot holds one mandate at the Northbank sandbox: account.refill ≤ $1,000/tx, ≤ $2,500/week. Everything below is what npx @writhq/demo runs against production.
ok · receipt vrf_9c1… signed
remaining this week $2,000.00
per_tx_cap · mandate cap $1,000 / tx
ok
remaining this week $1,100.00
ok
remaining this week $200.00
period_cap · only $200.00 left this week
mandate_revoked · live revocation check
Every ALLOW returns the resolved chain and a JWS receipt the platform can countersign — a record signed by both sides.
One principal, one agent, specific actions with hard caps, an expiry, and a live status. Platforms read the attributes they need — never the identity documents behind them.
{
"id": "mnd_tr7…",
"principal": "prn_a1…",
"agent": "agt_9f…",
"scopes": [{
"action": "account.refill",
"max_amount_per_tx": 100000,
"max_amount_per_period": 250000,
"period": "week",
"currency": "USD",
"platforms": ["plt_northbank"],
"purpose": "trading-capital"
}],
"not_before": "2026-07-22T00:00:00Z",
"expires_at": "2026-08-22T00:00:00Z",
"status": "active",
"issuer_sig": "<JWS by passport issuance key>"
}
Per-transaction and per-period ceilings. The passport keeps the counters server-side.
Authority is valid only inside a window. Outside it, the passport denies first.
One click in the dashboard. Verify is a live check, so the next request denies.
KYC level, country, accreditation flag — never the underlying documents.
Records, not rails. We attest decisions; we never custody or move value.
x402 · AP2 · ACP interop. AP2 mandates map to ours 1:1 — the KYA layer any rail can call.
No custody, no money transmission. KYC through licensed vendors; attestation only.
FCRA-shaped by design. Access, correction, and dispute flows are first-class.
W3C VC / JWS migration. Token format tracks the standard; migration is a serializer change.
No signup. It creates a principal, an agent and a mandate, then walks allow → replay-block → cap-deny → live revocation → anonymous 403 against the live sandbox.
→ 11 passed of 11 · the passport runs at api.writhq.com · curl the 403 yourself on the Launch page.