Docs · Cross-rail · x402

Governing HTTP-native agent payments on x402

x402 revives the HTTP 402 Payment Required status code for machine-to-machine commerce: an agent hits a resource, receives a 402 with payment terms, and settles in-band. This guide covers every step to put Axiru in front of that settlement — so no x402 payment goes through without a valid, tenant-bound authorization.

What x402 is

x402 is a protocol that restores HTTP 402 Payment Required as a first-class status code for agent-to-service commerce. A merchant resource returns a 402 with machine-readable payment terms; a client agent parses those terms, requests authorization from its governance layer, and submits a signed settlement intent back to the merchant's facilitator. The facilitator verifies the settlement before releasing the resource.

Axiru's role is to be the governance layer for the agent's side. Before the agent's wallet (Privy) signs anything, it calls the Axiru policy hook. Axiru evaluates your policies, and if the transfer is allowed, returns a signed AuthorizationTokenClaims JWS. Privy verifies the JWS and proceeds to sign the payment. If no valid JWS is returned, Privy must not sign — fail-closed by construction.

Architecture

+--------+ x402 request +-------+ policy-hook +------------+ | Agent | ───────────────► | Privy | ───────────────► | Axiru | +--------+ +-------+ POST /api/v1/ | /authoriza-| | authorizations | tions | | +-----+------+ │ │ │ AuthorizationToken (JWS) │ │ ◄──────────────────────────────┘ │ │ verifies JWS, signs payment ▼ (x402 facilitator)

Privy is the wallet that holds the agent's signing key. Before it signs anything, the policy hook calls Axiru. There is no fallback — if Axiru is unreachable or the JWS doesn't verify, Privy must not sign. This is the fail-closed contract from spec §11.4.

Prerequisites

  • A Privy embedded wallet provisioned for your tenant, with Policy Hooks enabled in the Privy dashboard. Alternatively, any compatible signer that implements thePrivyPolicyHookClient interface (signWithPolicyKey RPC).
  • An x402 facilitator endpoint that handles the payment-required handshake on the merchant side.
  • An Axiru workspace with at least one active connection. The rail is x402; no Stripe account is required for x402-only traffic.
  • An Axiru API key with scope x402:authorize. Generate from Dashboard → Settings → API keys.
  • The @axiru/auth-token-verifier package (T-038) in your TypeScript runtime, or the equivalent Go verifier (T-039 / T-040 / T-041) for Go facilitators.

Provision the Privy wallet and set environment variables

The Privy policy-hook adapter reads four environment variables. These must be set on your Axiru host before the production signer is active.

VariablePurpose
AXIRU_PRIVY_WALLET_IDPrivy wallet ID for the issuing tenant.
AXIRU_PRIVY_KEY_IDPrivy key ID — used as the JWS kid header. Must match the key configured in the Privy token-acceptance policy.
AXIRU_PRIVY_API_KEYPrivy server-API key. Used to authenticate the signing RPC from Axiru to Privy.
AXIRU_PRIVY_API_SECRETPrivy server-API secret.

When any of the four AXIRU_PRIVY_* variables are missing, the route falls back to the fail-closed default-deny signer — which itself is never invoked because the Decision Engine returns deny when the production signer is not wired. This is the correct behaviour for local development before Privy onboarding is complete.

In the Privy dashboard, for the issuing wallet:

  1. Enable Policy Hooks for the wallet.
  2. Set the policy-hook URL to https://www.axiru.com/api/v1/authorizations.
  3. Provision an Axiru API key with scope x402:authorize and set it as the Authorization: Bearer ak_… header on the policy-hook request.
  4. Set the token-acceptance policy to "require Axiru JWS with kid matching AXIRU_PRIVY_KEY_ID". This is what makes the integration fail-closed at Privy's end: even a successful response without a valid JWS is rejected.

Configure the policy hook to call POST /api/v1/authorizations

The Privy policy hook fires before the wallet signs. Your hook handler calls the Axiru pre-authorization endpoint and returns the result to Privy. The handler must be synchronous from Privy's perspective — no fire-and-forget.

curl — direct policy-hook request

curl -i -X POST https://www.axiru.com/api/v1/authorizations \
  -H "Authorization: Bearer ak_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "challenge": {
      "facilitator_url": "https://facilitator.x402.example.com",
      "resource_url": "https://merchant.example.com/api/articles/42",
      "asset": "USDC",
      "chain": "base-sepolia",
      "amount_minor_units": "500",
      "currency": "USD",
      "pay_to_address": "0xMerchantPayoutAddress…"
    },
    "context": {
      "initiator": {
        "kind": "agent",
        "id": "agent_content_fetcher_v1",
        "agent_metadata": {
          "model": "gpt-5",
          "version": "2026-04-01",
          "scope": "payments:write"
        }
      },
      "agent_run_id": "run_abc123",
      "idempotency_key": "example-x402-request-id"
    }
  }'
# → on allow (200):
# HTTP/1.1 200 OK
# content-type: application/json
# axiru-authorization: eyJhbGciOiJFUzI1NiIsImtpZCI6ImF4aXJ1LTIwMjYtMDEifQ…
# axiru-event-id: ev_…
# axiru-ovt-fingerprint: sha256:…
#
# { "decision": "allow", "reason_code": "…", "reason_text": "…", "event_id": "ev_…", "ovt_fingerprint": "sha256:…" }
#
# → on deny (403):
# HTTP/1.1 403 Forbidden
# content-type: application/json
#
# { "decision": "deny", "reason_code": "axiru.deny.agent_scope_missing", "reason_text": "…", "event_id": "ev_…" }

Node / TypeScript — hook handler

// In your x402 facilitator or Privy policy-hook handler:
const res = await fetch("https://www.axiru.com/api/v1/authorizations", {
  method: "POST",
  headers: {
    authorization: `Bearer ${process.env.AXIRU_API_KEY}`,
    "content-type": "application/json",
  },
  body: JSON.stringify({
    challenge: {
      facilitator_url: req.facilitatorUrl,
      resource_url: req.resourceUrl,
      asset: req.asset,                 // e.g. "USDC"
      chain: req.chain,                 // e.g. "base-sepolia"
      // amount_minor_units is a decimal string so values above 2^53 are
      // not silently truncated by JSON. Server coerces with BigInt().
      amount_minor_units: String(req.amountMinorUnits),
      currency: req.currency,           // e.g. "USD"
      pay_to_address: req.payToAddress,
    },
    context: {
      initiator: {
        kind: "agent",
        id: req.agentId,
        agent_metadata: {
          model: req.agentModel,
          version: req.agentVersion,
          scope: req.agentScope,        // single space-delimited scope string
        },
      },
      agent_run_id: req.agentRunId,
      idempotency_key: req.idempotencyKey,
    },
  }),
});

if (res.status !== 200) {
  // Fail closed: deny/quarantine/require_approval all return non-200.
  // Privy must not sign if no valid JWS was returned.
  const body = await res.json().catch(() => ({}));
  return { allowed: false, reason: body.reason_code ?? "http_" + res.status };
}

// The signed JWS is delivered in the response HEADER, not the body.
// The JSON body carries decision metadata only.
const authorizationToken = res.headers.get("axiru-authorization");
if (!authorizationToken) {
  // 200 without the header is a contract violation; fail closed.
  return { allowed: false, reason: "missing_axiru_authorization_header" };
}

// Pass authorizationToken to Privy for signing.
// Privy verifies the JWS against AXIRU_PRIVY_KEY_ID before signing.
return { allowed: true, authorizationToken };
Author x402 policies

Use the rail = "x402" rule kind to scope policies to x402 traffic exclusively. The counterparty rule kind matches on the resource URL or counterparty country. Therolling_window rule kind enforces per-agent spend ceilings.

Open the policy editor → · Browse policy templates →

Verify the AuthorizationTokenClaims JWS

The authorization_token is a compact JWS signed by the Axiru JWKS. Both sides of the trust boundary — Axiru to Privy, and the x402 facilitator receiving the settlement — must verify the JWS independently. The @axiru/auth-token-verifier package (landed in T-038) provides the TypeScript verifier with automatic JWKS rotation. The x402 Go verifier (T-039 / T-040 / T-041) provides an idiomatic Go alternative for facilitators written in Go.

Node / TypeScript — verify with jose

import { createRemoteJWKSet, compactVerify } from "jose";

const JWKS_URL = "https://www.axiru.com/axiru-jwks.json";
const ISSUER   = "https://api.axiru.com";

const jwks = createRemoteJWKSet(new URL(JWKS_URL));

/**
 * Verify an AuthorizationTokenClaims JWS returned by Axiru.
 * Mirrors the @axiru/auth-token-verifier package (T-038).
 */
async function verifyX402AuthorizationToken(jws: string, expectedFingerprint: string) {
  const { payload } = await compactVerify(jws, jwks);
  const claims = JSON.parse(new TextDecoder().decode(payload));

  if (claims.iss !== ISSUER)                          throw new Error("Unexpected issuer");
  if (Date.now() / 1000 > claims.exp)                 throw new Error("Token expired");
  if (claims.rail !== "x402")                         throw new Error("Wrong rail in claims");
  if (claims.ovt_fingerprint !== expectedFingerprint) throw new Error("OVT fingerprint mismatch");

  // audit_hash links this token to the exact ledger entry.
  return claims; // { jti, iss, iat, exp, rail, ovt_fingerprint, audit_hash, … }
}

Go — verify with the Axiru Go verifier

// Using the Axiru x402 Go verifier (T-039 / T-040 / T-041).
// go get github.com/axiru/auth-token-verifier/go

import (
    axiru "github.com/axiru/auth-token-verifier/go"
)

verifier := axiru.NewJWSVerifier(axiru.Config{
    JWKSURL:  "https://www.axiru.com/axiru-jwks.json",
    Issuer:   "https://api.axiru.com",
    CacheTTL: 30 * time.Minute,
})

claims, err := verifier.Verify(ctx, jwsToken)
if err != nil {
    // Fail closed — do not proceed with settlement.
    return fmt.Errorf("authorization token invalid: %w", err)
}

if claims.OVTFingerprint != expectedFingerprint {
    return fmt.Errorf("OVT fingerprint mismatch")
}

// claims.JTI links to the audit ledger entry.
JWKS endpoint and key rotation

Keys are published at https://www.axiru.com/axiru-jwks.json. The kid in the JWS header identifies which key signed the token. Set your JWKS cache TTL to 30 minutes or less. The @axiru/auth-token-verifier package manages rotation transparently. Both the TS and Go verifiers surface a kid_not_found error on stale caches, so your error handler should refresh the JWKS and retry once before failing.

Decision outcomes and the fail-closed contract

The Decision Engine returns one of three outcomes. The contract from spec §11.4 is explicit: Privy must not sign unless the outcome is allow and the JWS verifies.

OutcomeJWS issued?Required action
allowYes — authorization_token in the response.Verify the JWS (signature, issuer, expiry, OVT fingerprint). Pass to Privy for signing only if verification passes.
denyNo JWS.Privy must not sign. Return an error to the agent. The decision is recorded in the ledger for audit.
quarantineNo JWS.Privy must not sign. The agent's request is flagged for manual review. The approval queue entry links to the ledger record.

The integration is fail-closed at three layers:

  1. Route layer: if the Decision Engine throws or returns a non-allow outcome, the signer is never called. Proven by the T-044 route test suite.
  2. Adapter layer: if the Privy client throws or returns a malformed JWS, a PrivySignerError is thrown and the route renders a 5xx — no silent fallback to an allow.
  3. Privy layer: if Axiru returns no JWS or an unverifiable JWS, Privy refuses to sign (enforced by the token-acceptance policy set in Step 1).
SOC 2 note

This three-layer fail-closed design is what the SOC 2 control references: no agent x402 payment proceeds without a fresh, valid, tenant-bound authorization. No PII or secret material appears in logs or response bodies — only decision IDs, reason codes, and the public JWS.

Shadow → enforcing rollout

Start in shadow mode. Every decision is recorded and auditable, but no x402 transaction is blocked. This lets you observe policy behavior against real agent traffic before flipping enforcement.

  1. Wire the Privy policy hook (Steps 1–2) with the Axiru endpoint. Verify the hook fires for every x402 authorization attempt by checking the decision ledger for rail = x402 entries.
  2. Compare v1 and v2 policy agreement in the ledger. Fix rules that fire too broadly (e.g., rolling-window thresholds that would block legitimate agent traffic) or too narrowly (missing counterparty guards).
  3. When the agreement rate is satisfactory (>98% over 7 days of representative traffic), flip the x402 surface to enforcing from Dashboard → Connectors.
  4. Enforcing mode activates the Privy token-acceptance policy: only x402 settlements with a valid Axiru JWS will be signed. Privy rejects settlements for any request that returnsdeny or quarantine.

Audit + replay

Every x402 authorization attempt lands in the append-only decision ledger. Each entry includes:

  • decision_id and ledger_id for correlation with facilitator logs and x402 settlement records.
  • audit_hash — the SHA-256 hash of the canonical OVT, linking the authorization token to the exact ledger entry deterministically. The audit_hash claim appears in the JWS payload; facilitators can verify the hash matches their own canonical serialization.
  • The policy version matched, the rule kind that fired, and the full policy snapshot at evaluation time.
  • The JTI of the issued AuthorizationToken (when outcome isallow), enabling correlation from the signed token back to the exact ledger entry.
  • Shadow vs enforcing mode flag and v1/v2 agreement result.

Browse and filter decisions from the decision ledger. Replay re-evaluates the original OVT against the policy version active at the time, so re-derived outcomes are deterministic and regulator-ready.

Troubleshooting

Common errors and how to fix them.

These are the most frequent issues when integrating x402 with the Axiru policy hook.

Error / symptomCauseFix
outcome: "deny" on every request in local devThe four AXIRU_PRIVY_* env vars are not set. The route has fallen back to the fail-closed default-deny signer.Set all four env vars (or mock them in your test harness). The deny with reason pre_authorization.engine_not_wired signals the signer stub is active.
Privy refuses to sign despite outcome: allowThe kid in the JWS doesn't match theAXIRU_PRIVY_KEY_ID configured in the Privy token-acceptance policy.Verify AXIRU_PRIVY_KEY_ID matches the key ID in the published JWKS and in the Privy policy. Privy enforces an exact kid match.
Verifier throws kid_not_foundThe JWKS cache is stale. Axiru has rotated the signing key since the last JWKS fetch.Force a JWKS cache refresh and retry verification once. Set cache TTL to 30 minutes or less. @axiru/auth-token-verifier does this automatically.
OVT fingerprint mismatch on verificationThe canonical OVT serialization differs between the policy-hook caller and the verifying code.Use canonicalSha256FingerprintSync from@axiru/auth-token-verifier on both sides. Ensure field ordering is consistent — the canonical form sorts keys alphabetically with no whitespace.
Token expired on receipt at facilitatorThe AuthorizationToken has a 5-minute expiry. Network latency between Privy, the policy hook, and the facilitator consumed the window.Request a fresh authorization immediately before each settlement attempt. Do not pre-fetch or cache tokens across requests.
PrivySignerError: malformed_jws in logsThe Privy client returned a JWS that is not a valid compact three-part token (or has an empty segment).Check your Privy SDK version and the wallet configuration. The malformed_jws error code in the log indicates the response body shape doesn't match the expected { jws: string } contract.

Next steps

Stripe DAA integration guide

Govern Stripe Direct-Account-Authorized agent payments — same decision endpoint, different rail.

Legacy decisions API

The v1 decisions endpoint for Stripe refunds and payouts — analogous request/response shape.

Agentic payments overview

Cross-rail governance overview, the four primitives, and the full Phase 1 + Phase 2 rail roadmap.