An API key proves something made the call. Writ proves who authorised it — a signed, revocable mandate from a real person, on every agent action, in one verify call. 1,000 verifications a month free.
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(
"/api/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 permissions console. Verify is a live check, so the next request denies.
KYC level, country, accreditation flag — never the underlying documents.
Onboarding a principal, registering an agent's key, and issuing or revoking mandates cost nothing — no cap, no trial, no plan to be on. Principals are never metered and never billed. Metering lives on the other side of the call: the platform pays for the verifications it makes, and its first 1,000 a month are free too.
Agents already sign NDAs and order forms. Making the signature is the easy part. Proving the agent was allowed to make it is the hard part. Writ answers that in one call: this agent, this document, this much liability, this mandate, this KYC'd human.
signature ← agent ← mandate ← principal (KYC'd) ← liability
The counterparty resolves the whole chain before it executes the signature — and gets a signed record of the answer.
{
"id": "mnd_ka9…",
"principal": "prn_a1…",
"agent": "agt_9f…",
"scopes": [{
"action": "document.sign",
"document_classes": ["nda", "order_form"],
"max_amount_per_tx": 5000000, // $50,000 per document
"max_amount_per_period": 20000000, // $200,000 a month
"period": "month",
"currency": "USD",
"platforms": ["plt_counterparty"],
"purpose": "vendor-paperwork"
}],
"expires_at": "2026-12-31T00:00:00Z",
"status": "active",
"issuer_sig": "<JWS by passport issuance key>"
}
NDAs and order forms — nothing else. Any other class denies document_class.
$50,000 of liability on any single document. Over it denies per_tx_cap.
$200,000 a month across every document. Over it denies period_cap.
The payment wedge meters money. This meters liability. Same code, same server-side counters, same deny reasons.
Writ says whether the agent was authorized to sign, and returns a signed record of that decision. Your e-signature platform still makes the signature. This is not a qualified electronic signature (eIDAS/QES) and does not try to be.
UETA §14 and ESIGN already say a contract can be formed by electronic agents, and agency law already says an agent binds its principal only within the authority it was granted. Writ is the record of that authority.
One command, no signup — an agent 403s anonymously, signs two documents inside its mandate, denies on class, both caps, replay and revocation. 13 checks against production.
Integrate as an e-signature platform How a counterparty verifies it
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.
Three fields at api.writhq.com/signup — platform name, email, and password — get you a login and a plt_ id with an API key, shown exactly once. No invite, no waitlist, no admin token, no call. Plans meter verifications per calendar month, nothing else: every feature is on every plan. Allow and deny both count — a deny is the product working. This page is for platforms — the side that calls /v1/verify. Agent owners issuing mandates are never metered at all; see who pays.
Mandates, caps, instant revocation, countersigning, audit export, signature authority — all of it. The free plan is the product, metered lower.
sign up → plt_ id + key in under a minuteSame surface, more room. Upgrade yourself from the platform dashboard — Stripe Checkout, no sales step, effective immediately.
self-serve upgrade · manage billing in-dashboardPast 100k verifications a month, or you need volume pricing or a regional deployment: [email protected].
volume pricing · regional deploymentcurl -s -X POST https://api.writhq.com/v1/auth/signup \
-H "content-type: application/json" \
-d '{"platform_name":"My Platform","email":"[email protected]","password":"…"}'
# → 201 {"account":{"id":"acc_…"},"platform":{"id":"plt_…","api_key":"plt_sk_…","plan":"free"}}
# the key is shown once — put it in your secret store now
# the same email + password log you in at /login
Over the monthly limit, /v1/verify returns 429 with code: "quota_exceeded" — and the body carries no decision field at all. It is a billing limit on your account, not a judgment about the agent: the assertion was never evaluated. Never log it as a deny, never show the caller a KYA failure, and never let it fall through to executing the action.
No signup for the demo itself — it creates a principal, an agent and a mandate, then walks allow → replay-block → cap-deny → live revocation → anonymous 403 against the live sandbox. When you want your own platform login and key, that is three fields and $0 — the free plan is 1,000 verifications a month with every feature on.
→ 11 passed of 11 · the passport runs at api.writhq.com · curl the 403 yourself on the Launch page.