Home/Docs/For platforms

Integrate as a platform

Add one middleware in front of the routes agents call. It verifies the full chain of agency in one round trip and fails closed — so you can accept agent-originated volume you'd otherwise reject as fraud.

Install & get keys #

Create a platform (plt_…) in the platform dashboard and mint an API key. The key authenticates your server-to-server calls to /v1/verify. Then install the middleware:

terminal
npm i @passport/verify

The package ships adapters for Express, Hono, and Next.js route handlers. Set PASSPORT_API_KEY (and, for local dev, PASSPORT_BASE_URL=http://localhost:4000) in your environment.

The requireKYA middleware #

Guard a route by naming the action it performs. The middleware reads the X-Passport header, calls verify, and either attaches the resolved chain to the request or short-circuits with a deny / 403. Roughly ten lines to integrate:

server.ts · Hono@passport/verify
import { requireKYA } from "@passport/verify";

// Accept agent traffic — one round trip, fails closed.
app.post(
  "/v1/account/refill",
  requireKYA({ action: "account.refill" }),
  async (c) => {
    const { chain, verification_id } = c.get("kya");
    await accounts.refill(c.req.valid("json"));
    return c.json({ ok: true, principal: chain.principal });
  }
);
Amount binding

By default the middleware reads the transaction amount from the request body (amount, minor units) and binds it into the verify call, so the passport enforces per-tx and period caps against the real amount. Override with the amount resolver option below.

Options #

OptionTypeDescription
actionstringRequired. The scope action this route performs, e.g. account.refill.
amount(req) => numberResolver for the amount in minor units. Defaults to body.amount.
currencystringExpected currency. Defaults to body.currency or "USD".
onDeny(res, decision)Customize the deny response. Default returns 403 with the reason and onboarding link.
failOpenbooleanDefault false. The middleware fails closed; a passport outage denies rather than admits.

The 403 "KYA required" block page #

When a request arrives with no X-Passport header — an anonymous agent — the middleware never runs your handler. It returns a structured 403 with an onboarding link, so rejected traffic becomes a lead instead of a dead end.

responseHTTP 403
{
  "error": "kya_required",
  "message": "This action requires a verified agent passport.",
  "action": "account.refill",
  "onboarding_url": "https://northbank.sandbox/kya/start"
}

Point onboarding_url at your co-branded onboarding flow. The funnel: anonymous agent → 403 → onboard → mandate → verified buyer.

Verify API #

The middleware calls this for you, but here is the raw contract for custom integrations. One round trip.

POST/v1/verify

Request — the raw agent assertion plus your platform context:

request body
{
  "assertion": "<JWS signed by the agent key>",
  "platform_id": "plt_northbank",
  "action": "account.refill",
  "amount": 50000,
  "currency": "USD"
}

Response — a decision, the resolved chain (attributes only), a verification id, and a signed receipt:

200 · allowreceipt = JWS
{
  "decision": "allow",
  "reason": "ok",
  "chain": {
    "principal": { "type": "individual", "country": "US",
                    "kyc": "verified", "accredited": true },
    "agent": { "id": "agt_9f…", "name": "treasury-bot", "runtime": "claude-code" },
    "mandate": { "id": "mnd_tr7…", "remaining_this_period": 150000 }
  },
  "verification_id": "vrf_9c1…",
  "receipt": "<JWS signed by passport>"
}
Selective disclosure

You receive attributes — KYC level, country, an accreditation flag — never the principal's identity documents, unless the mandate grants disclosure or a lawful request compels it. Subject access, correction, and dispute flows are first-class and FCRA-shaped.

The check pipeline #

On every verify the passport runs, in order — first failure decides:

  1. Agent signature — the assertion verifies against the registered agent public key.
  2. Replay — timestamp within a ±120s window and the nonce is single-use.
  3. Mandate liveness — active, not expired, not revoked (a live check — revocation is instant).
  4. Scope — the mandate covers this action, amount, and platform.
  5. Period counters — cumulative spend this period stays under the cap (we maintain the counters).
  6. KYC status — the principal's KYC has not lapsed.
  7. Sanctions — screen passes (stub interface in the alpha).

Decision reasons #

ok mandate_revoked per_tx_cap period_cap kyc_lapsed unknown_agent replay out_of_scope expired sanctions_hit

allow always carries ok. Any other reason accompanies a deny. The list is open — treat unknown reasons as a deny.

Countersigning #

After you execute the action, countersign the verification. A record signed by both sides — the passport's receipt plus your platform key — is nearly incontestable. This is the evidence-layer law, on from day zero.

POST/v1/verifications/{id}/countersign
request body
{
  "outcome": "executed",
  "platform_ref": "txn_88f2…",
  "platform_sig": "<JWS signed by your platform key>"
}
// → { "id": "vrf_9c1…", "cosigned": true }

Audit export #

Every decision is an immutable, signed vrf_… record. Export the log for reconciliation, dispute handling, or compliance review:

GET/v1/verifications?since=&platform_id=

The platform dashboard renders the same decision log with API-key management alongside it.