Docs · Cross-rail · Stripe DAA

Governing agent payments on Stripe DAA

Direct-Account-Authorized (DAA) lets AI agents initiate Stripe payments on behalf of your platform. This guide covers every step from connecting your Stripe account through rolling from shadow to enforcing — so no DAA payment settles without a valid Axiru authorization.

What you'll integrate

Stripe DAA allows a platform's agent to initiate payment intents, payouts, and authorizations on connected accounts. Without a governance layer, agents can move arbitrary amounts with no policy check, no approval chain, and no audit trail. Axiru adds a mandatory pre-authorization step: your agent calls POST /api/v1/authorizations before touching Stripe. If the policy allows the transfer, Axiru returns a signed JWS (the AuthorizationToken); if not, it returns deny or quarantine — and no token is ever issued. Your settling code verifies the JWS before executing the Stripe call, making the contract fail-closed end-to-end.

Agent ──── POST /api/v1/authorizations ────► Axiru Decision Engine │ allow? ◄─────┘ │ AuthorizationToken (JWS) ◄── signed by Axiru JWKS │ Agent ── verify JWS ── stripe.paymentIntents.create() ──► Stripe DAA │ ledger entry ──► Audit Ledger

Prerequisites

  • A Stripe account with Direct-Account-Authorized payments enabled. Contact your Stripe account manager or enable DAA from the Stripe Dashboard under Settings → Payment methods → Direct Account Authorization.
  • An Axiru workspace with at least one connected Stripe account. Connect from /onboarding/connect-stripe.
  • An Axiru API key with scope authorizations:write. Generate keys from Dashboard → Settings → API keys.
  • The jose library (or equivalent) in your agent runtime for JWS verification.

Connect Stripe in the Axiru dashboard

Axiru needs a Stripe connection to associate authorization decisions with the correct Stripe account and to write DAA-specific action labels to the audit ledger.

  1. Go to Onboarding → Connect Stripe and complete the OAuth flow. You'll need a stripe_conn_… connection ID — it'll appear on the connectors page once connected.
  2. Verify the connection status on the Dashboard → Connectors page. The Stripe DAA surface is available as soon as the connection status is active.
  3. The connection defaults to shadow mode: decisions are recorded and the audit ledger is populated, but no Stripe call is gated. Stay in shadow mode until Step 5.

Author policies for DAA traffic

Policies for DAA traffic use the v2 rule DSL. The ten v2 rule kinds (from T-049) are: rail, rail_action, amount, initiator_kind, initiator_id, agent_scope, counterparty, rolling_window, time_of_day, and custom_expression. You can target DAA exclusively by filtering on rail = "stripe_daa" — legacy Stripe traffic is unaffected.

Three DAA starter templates from T-048 are available in the policy library:

  • DAA — Agent payment cap: deny any single agent_payment above a configurable per-transaction limit.
  • DAA — Rolling 24h window: deny once a given agent's rolling 24-hour spend exceeds a configurable ceiling.
  • DAA — Counterparty country blocklist: deny agent_payout when the destination country is in the sanctioned list; uses the counterparty rule kind.
Browse the policy template library →Open the policy editor →

Wire your agent runtime to POST /api/v1/authorizations

Before any DAA Stripe call, your agent must request an authorization. The shape is identical to the legacy /api/v1/decisions endpoint but uses the cross-rail OVT body — include rail, rail_action, and initiator.

curl

curl -X POST https://www.axiru.com/api/v1/authorizations \
  -H "Authorization: Bearer ak_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "rail": "stripe_daa",
    "rail_action": "agent_payment",
    "amount_cents": 25000,
    "currency": "usd",
    "initiator": {
      "kind": "agent",
      "id": "agent_billing_reconciler_v2",
      "scopes": ["payments:write"]
    },
    "counterparty": {
      "stripe_account_id": "acct_1Abc…",
      "country": "US"
    },
    "idempotency_key": "example-daa-invoice-id"
  }'
# → on allow:
# {
#   "outcome": "allow",
#   "authorization_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6ImF4aXJ1LTIwMjYtMDEifQ…",
#   "expires_at": "2026-05-01T14:05:00Z",
#   "decision_id": "ed_…",
#   "audit": { "ledger_id": "lr_…", "logged_at": "2026-05-01T14:00:00Z" }
# }
#
# → on deny:
# {
#   "outcome": "deny",
#   "reason": "axiru.deny.rolling_window_exceeded",
#   "decision_id": "ed_…"
# }

Node / TypeScript

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({
    rail: "stripe_daa",
    rail_action: "agent_payment",
    amount_cents: 25_000,
    currency: "usd",
    initiator: {
      kind: "agent",
      id: "agent_billing_reconciler_v2",
      scopes: ["payments:write"],
    },
    counterparty: {
      stripe_account_id: "acct_1Abc…",
      country: "US",
    },
    idempotency_key: "example-daa-invoice-id",
  }),
});

const auth = await res.json();

if (auth.outcome !== "allow") {
  // Fail closed — no token, no settlement.
  throw new Error(`Payment blocked: ${auth.reason}`);
}

// Pass authorization_token to the DAA settling call as a header.
await stripe.paymentIntents.create({
  amount: 25_000,
  currency: "usd",
  payment_method_types: ["card"],
  metadata: {
    axiru_authorization_token: auth.authorization_token,
    axiru_decision_id: auth.decision_id,
  },
});
Idempotency

Pass a unique idempotency_key per OVT. Replays within 24 hours return the original decision with cached: true, so retrying on a transient network error never double-counts a spending window.

Verify the signed AuthorizationToken (JWS) before settling

The authorization_token in the response is a compact JWS signed by the rotating Axiru JWKS. Before passing it to any Stripe call, your settling code must verify the signature, check the issuer, confirm the token hasn't expired, and bind it to the OVT fingerprint you computed. This is the fail-closed contract: if verification fails for any reason, your code must not proceed with the Stripe call.

Node / TypeScript — verify with jose

import * as jose from "jose";

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

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

async function verifyAuthorizationToken(jws: string) {
  const { payload } = await jose.compactVerify(jws, jwks);
  const claims = JSON.parse(new TextDecoder().decode(payload));

  // Validate required claims.
  if (claims.iss !== ISSUER) throw new Error("Unexpected issuer");
  if (Date.now() / 1000 > claims.exp) throw new Error("Token expired");

  // Bind the token to the OVT fingerprint you computed before calling
  // /api/v1/authorizations. This prevents replay across different OVTs.
  if (claims.ovt_fingerprint !== yourOvtFingerprint) {
    throw new Error("OVT fingerprint mismatch — replay attack suspected");
  }

  return claims; // { jti, iss, iat, exp, sub, ovt_fingerprint, rail, … }
}
JWKS endpoint

Keys are published at https://www.axiru.com/axiru-jwks.json. The kid in the JWS header identifies which key was used to sign. Rotate your JWKS cache no less frequently than once per hour. The @axiru/auth-token-verifier package (landed in T-038) wraps this pattern with automatic key rotation and pre-built claim validation.

Fail-closed contract

The contract from spec §11.4 is explicit: if verification throws, if the issuer doesn't match, if the token is expired, or if the OVT fingerprint doesn't match — do not call Stripe. There is no fallback-allow path. This is what the SOC 2 control points at: no agent DAA payment proceeds without a fresh, valid, tenant-bound authorization.

Shadow → enforcing rollout

Start in shadow mode. Wire the pre-authorization call and verification in your agent runtime, but do not gate Stripe calls on the outcome yet. Every decision is recorded in the audit ledger, including the policy version matched and the outcome.

  1. Run shadow mode for at least one full billing cycle. Compare the v1 policy outcomes versus the v2 DAA evaluator for agreement. The decision ledger surfaces disagreements as a filter.
  2. Fix any policy drift — rules that trigger unexpectedly on DAA traffic, rolling windows set too tightly, or missing agent_scope guards.
  3. When the v1 / v2 agreement rate is satisfactory (Axiru recommends >98% over 7 days), flip the DAA surface to enforcing from Dashboard → Connectors.
  4. Roll out by rail action if you want more granular control: flipagent_payment to enforcing first, thenagent_payout once you're confident.

Audit + replay

Every DAA authorization attempt — whether allowed or denied — lands in the append-only decision ledger with:

  • decision_id and ledger_id for correlation with Stripe webhook events.
  • The OVT fingerprint (SHA-256 of the canonical OVT), so the decision can be deterministically re-derived from the original OVT payload.
  • 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 you to correlate the signed token to the exact ledger entry.
  • Shadow vs enforcing mode flag and v1/v2 agreement result.

Browse decisions and replay specific OVTs from the decision ledger. Replay re-evaluates the original OVT against the policy version active at evaluation time, so the re-derived outcome is deterministic.

Troubleshooting

Common errors and how to fix them.

These are the most frequent issues when first wiring Stripe DAA with Axiru.

Error / symptomCauseFix
401 invalid_api_keyThe Authorization: Bearer ak_… header is missing, malformed, or the key has been revoked.Re-generate the key from Dashboard → Settings → API keys. Verify the key starts with ak_live_ in production.
400 invalid_request — missing railThe body is missing rail, rail_action, or initiator.Check the field_errors array in the response for the exact missing field. All three fields are required for the cross-rail authorizations endpoint.
outcome: "deny" — rolling_window_exceededA rolling_window rule has been triggered. The agent has exceeded the configured ceiling within the window.Check the policy rule that matched in the matched_rule field. Adjust the window ceiling in the policy editor or wait for the window to reset.
JWS verification fails — kid not foundYour JWKS cache is stale. Axiru rotates signing keys regularly; the kid in the token no longer appears in your cached JWKS.Refresh the JWKS from https://www.axiru.com/axiru-jwks.json and retry verification. The @axiru/auth-token-verifier package handles automatic rotation.
JWS verification fails — OVT fingerprint mismatchThe OVT payload sent to /api/v1/authorizations differs from the OVT the settling code computed the fingerprint against.Ensure the canonical JSON serialization (field ordering + no whitespace) matches between the authorization request and the settling verification. Use the canonicalSha256FingerprintSync helper from@axiru/auth-token-verifier.
Token expired on receiptThe AuthorizationToken has a 5-minute expiry. The settling call arrived after the token expired.Request a fresh authorization immediately before the settling call. Do not pre-fetch tokens or store them for re-use across requests. The idempotency key prevents double-counts on retries.

Next steps

x402 integration guide

Govern HTTP-native agent payments using the x402 protocol with the Privy policy hook.

Legacy decisions API

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

Agentic payments overview

High-level overview of cross-rail governance, the four primitives, and the full rail roadmap.