Home/Docs/For agents

Give your agent a passport

Hold a keypair, sign a short assertion on every request, and present it as an X-Passport header. Three ways to do it: the SDK, the MCP server for Claude Code, or the CLI.

Keys & the model #

An agent (agt_…) is an Ed25519 keypair plus self-declared runtime metadata — name, model, framework. The private key stays in the agent runtime; only the public key is registered with the passport. A principal then issues your agent a mandate that scopes exactly what it may do.

You never send the key

The agent signs each request locally. The platform and the passport only ever see a signature over a small, single-use payload — never your private key, and never the raw mandate.

Agent SDK #

The SDK holds the keypair, signs assertions, and attaches headers.

terminal
npm i @writhq/sdk
agent.ts@writhq/sdk
import { PassportAgent } from "@writhq/sdk";

// loads (or creates) the local Ed25519 keystore — PASSPORT_AGENT_HOME
const agent = await PassportAgent.load({
  passportUrl: "https://api.writhq.com",
  name: "treasury-bot", runtime: "claude-code",
});

// one-time: principal registers agent.publicJwk → agt_…, then pin context
agent.setAgentId("agt_9f…");
agent.setContext({ mandate: "mnd_tr7…", platform: "plt_northbank" });

// every call after: signs the assertion & presents X-Passport for you
const res = await agent.present("https://northbank-production.up.railway.app/api/refill", {
  action: "account.refill",
  amount: 50000, currency: "USD", // minor units
});
// res.status === 200 · res.body carries the decision, chain, and receipt

Register & rotate #

Register the public key once; rotate it any time without re-issuing mandates. Registration is principal-authorized — the principal registers your public key under their account. From a repo checkout the CLI wraps it:

terminalCLI
export PASSPORT_AGENT_HOME="$PWD/.passport-agent"   # pin the keystore

npm run cli -w @passport/agent -- init
# generates the local keypair, prints the public JWK

npm run cli -w @passport/agent -- register --principal-secret prn_sk_…
# principal registers the public key → agt_9f…

Under the hood these are POST /v1/agents (principal-authorized) and POST /v1/agents/{id}/rotate — a rotation issues a new keypair and old signatures stop verifying, but mandates stay attached to the agent id.

What gets signed #

Each request signs this exact payload with the agent key, as a JWS, and sends it in the X-Passport header. Nonce + timestamp make it single-use (see replay rules):

assertion payloadsigned per request
{
  "mandate_id":  "mnd_tr7…",
  "action":      "account.refill",
  "amount":      50000,
  "currency":    "USD",
  "platform_id": "plt_northbank",
  "nonce":       "3f9a…",
  "ts":          "2026-07-22T18:04:11Z"
}

MCP server (Claude Code) #

@writhq/mcp is an MCP server so Claude Code — or any MCP-capable runtime — can transact with a passport today, no glue code. Register it in your MCP config (e.g. ~/.claude/mcp.json, or a project .mcp.json):

.mcp.jsonClaude Code
{
  "mcpServers": {
    "writ": {
      "command": "npx",
      "args": ["-y", "@writhq/mcp"],
      "env": {
        "PASSPORT_URL": "https://api.writhq.com",
        "PASSPORT_AGENT_HOME": "/path/to/.passport-agent"
      }
    }
  }
}

The server holds the agent's local keystore (PASSPORT_AGENT_HOME); register the public key and set the mandate/platform context once (SDK or CLI, above) and every MCP client that launches it shares the identity.

MCP tools #

The server exposes three tools the model can call directly:

ToolArgsWhat it does
passport_presentaction, amount, currency, url?Signs the assertion, attaches X-Passport, calls the platform (or returns the header), and reports the decision + resolved chain. Amount in minor units.
passport_statusReturns the agent's identity and mandate context: registered agt_ id, configured mandate/platform, and whether the passport is reachable — so the model knows what it may do before trying.
passport_refill_demoamountRefills the Northbank Sandbox with a signed assertion (amount in dollars). Demonstrates ALLOW and the per-tx / period / revoked DENYs end-to-end.
Claude Code · tool callpassport_present
passport_present({ action: "account.refill", amount: 50000, currency: "USD" })

// → {
//     decision: "allow", reason: "ok",
//     chain: { principal, agent, mandate: { remaining_this_period: 150000 } },
//     verification_id: "vrf_9c1…"
//   }
Check before you spend

Have the model call passport_status first to read remaining_this_period, then size the transaction to fit the mandate. It turns a blind per_tx_cap / period_cap deny into a decision the agent makes on purpose.

CLI reference #

From a repo checkout: npm run cli -w @passport/agent -- <command> (set PASSPORT_AGENT_HOME so every call shares one keystore).

CommandDescription
initGenerate the local Ed25519 keypair and print the public JWK.
register --principal-secret prn_sk_…Register the public key under a principal → agt_….
use --mandate mnd_… --platform plt_…Pin the mandate + platform context for presents.
statusShow the agent id, mandate context, and passport reachability.
refill --amount 500Sign an assertion and present it to the Northbank refill endpoint (amount in dollars).

The MCP server itself is npx @writhq/mcp — see above.