Provenance

Guide: agent payments (AP2 / x402)

Agent-payment protocols are standardizing now, led by Google's AP2 (mandates as Verifiable Credentials) and x402 (HTTP 402 payments). Each defines where a proof-of-human-intent slots in; none provides the intent layer itself. Provenance fills that slot. @provenance/rails is a pair of pure mapping and verification layers. It has no API surface and moves no money.

Version pin. These protocols are young and may change. We pin to specific draft versions (AP2_SPEC_VERSION, X402_SPEC_VERSION) and map exactly the fields below. Vendored fixtures are the contract, so an upstream change fails the drift test. Full reconciliation against AP2's published sample credentials + trust-list/certified-signer status is a human step (see the release checklist).

AP2 — mandates as Verifiable Credentials

A Provenance delegation mandate maps to an AP2 IntentMandate VC: our ES256 mandate token is the VC's cryptographic proof, and our published JWKS is the verification method.

import { provenanceMandateToAp2, ap2ToProvenanceMandate } from "@provenance/rails/ap2";

const vc = provenanceMandateToAp2(mandate, { issuer, kid });   // → AP2 IntentMandate
const back = ap2ToProvenanceMandate(vc);                       // ← round-trips

| Provenance | AP2 | | --- | --- | | mandate_id | credentialSubject.id | | agent_session_id | credentialSubject.agent | | constraints.max_amount | credentialSubject.constraints.maxAmount | | constraints.budget_* | credentialSubject.constraints.budget | | constraints.allowed_counterparties | credentialSubject.constraints.allowedCounterparties | | constraints.expires_at | validUntil | | mandate token (ES256 JWS) | proof.jws | | JWKS | proof.verificationMethod |

x402 — verify at the 402 slot

Client — wrap fetch; a 402-challenged request is retried once with an intent token in the X-PAYMENT header:

import { intentPaymentFetch } from "@provenance/rails/x402";

const payingFetch = intentPaymentFetch(fetch, (challenge) => getIntentTokenFor(challenge));
await payingFetch("https://merchant.example/report"); // 402 → retried with the token

Merchant — verify the token before accepting payment (offline JWKS + an optional online revocation check):

import { verifyX402Payment } from "@provenance/rails/x402";
import { fetchJwks } from "@provenance/sdk";

const jwks = await fetchJwks(issuerBaseUrl);
const result = await verifyX402Payment(req.headers["x-payment"], {
  jwks,
  onlineStatus: async (jti) => fetch(`${issuer}/v1/public/intent-status/${jti}`).then((r) => r.json()),
});
if (!result.accepted) return res.status(402).send(result.reason);

We verify human intent at the payment slot — we don't move money. Issuing/acquiring integrations are recipes, not packages, until demand shows.

The dead-man switch — authorization heartbeat

A standing mandate is powerful because the agent can act without asking each time — which is exactly why it should not outlive the human's attention. A heartbeat constraint makes a mandate stay valid only while its human periodically re-attests:

await prov.mandates.grant({
  deviceId, agentSessionId,
  constraints: { expiresAt, heartbeat: { interval: "weekly", grace_hours: 72 } },
});

The granting UX shows it plainly ("you'll check in weekly"). To check in, answer the period's challenge with a passkey — a lightweight biometric ceremony:

const { challenge } = await prov.mandates.heartbeatChallenge(mandateId);
// … run navigator.credentials.get over `challenge` …
await prov.mandates.heartbeat(mandateId, { biometricAssertion });

The challenge is sha256(mandate ‖ period) — it changes every period, so replaying last period's assertion fails. If a period lapses past its grace window, the mandate auto-suspends (distinct from revoked): the next verify denies with mandate_heartbeat_lapsed, the mandate.heartbeat_lapsed webhook fires, and the human is notified. A check-in within the reactivation window (default 7 days) revives it; beyond that it is terminally expired. Every check-in is ledger-logged and receipt-able. Check-ins are free — we never charge a human to keep their own kill-switch armed.