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.
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
joselibrary (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.
- 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. - Verify the connection status on the Dashboard → Connectors page. The Stripe DAA surface is available as soon as the connection status is active.
- 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
counterpartyrule kind.
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,
},
});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, … }
}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.
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.
- 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.
- Fix any policy drift — rules that trigger unexpectedly on DAA traffic, rolling windows set too tightly, or missing
agent_scopeguards. - When the v1 / v2 agreement rate is satisfactory (Axiru recommends >98% over 7 days), flip the DAA surface to enforcing from Dashboard → Connectors.
- Roll out by rail action if you want more granular control: flip
agent_paymentto enforcing first, thenagent_payoutonce you're confident.
Audit + replay
Every DAA authorization attempt — whether allowed or denied — lands in the append-only decision ledger with:
decision_idandledger_idfor 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 is
allow), 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.
Common errors and how to fix them.
These are the most frequent issues when first wiring Stripe DAA with Axiru.
| Error / symptom | Cause | Fix |
|---|---|---|
401 invalid_api_key | The 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 rail | The 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_exceeded | A 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 found | Your 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 mismatch | The 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 receipt | The 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
Govern HTTP-native agent payments using the x402 protocol with the Privy policy hook.
Legacy decisions APIThe v1 decisions endpoint for Stripe refunds, payouts, and disputes — analogous request/response shape.
Agentic payments overviewHigh-level overview of cross-rail governance, the four primitives, and the full rail roadmap.