Rails: card & crypto

Mandate Labs settles agent payments over two rails: a card-issuing rail (Mastercard today) and a Base L2 crypto rail (x402 stablecoin transfers, USDC, EIP-3009). The decision engine is rail-agnostic — the same trust, mandate, and intent checks run either way — and the rail only selects the transaction context, the constraints, and the form of evidence.

The dual-rail model

An agent can transact over one of two rails today: card or crypto. The design principle is that the rail is a thin layer at the edges. Everything that makes Mandate Labs a trust layer — KYA scoring, mandate and authority enforcement, Verifiable Intent, decision-quality scoring, and the outcome feedback loop — is identical across rails. The rail only selects which transaction context applies and which risk adapter scores it.

card is the default, so every existing card payload is unchanged. crypto is opt-in and adds a rail-specific context block plus two pieces a card transaction doesn't need: a signed transaction-authorization attestation and a settlement watcher.

Decision engine card railMastercard today crypto railBase L2 · x402 · USDC KYA · mandate · intent · EDQS · outcome loop — rail-agnostic
One engine, two rails. The shared pipeline scores and authorizes; the rail decides the context block, the mandate constraints, and what evidence the approval produces.

The card rail

card rail=CARD

The card-issuing rail — Mastercard today. An agent transacts against an issued card credential, and the authorization is evaluated the way a card payment is: against a merchant category code (MCC), an amount and currency, a merchant, and a country. The decision maps cleanly onto card-scheme data elements (the structured risk assessment is designed for DE 48.75).

Card is the default rail — omit the rail field on /authorize and it is treated as CARD. Settlement happens through the scheme; clearing and settlement milestones flow back as outcomes that close the trust loop.

Constraints that apply: per-transaction and velocity limits, allowed MCCs, allowed countries, currency.

The crypto rail

crypto rail=CRYPTO

A Base L2 stablecoin rail built for the agentic-payments pattern where volume is concentrated: x402 pay-per-call settlement in USDC. Instead of a card credential, the agent authorizes an on-chain transfer to a destination address, and the value is enforced on the economic amount just like a card transaction (1 USDC ≈ 1 USD).

On an APPROVE, Mandate Labs issues a signed transaction-authorization attestation — a compact JWS (EdDSA / Ed25519) bound to the exact EIP-3009 transferWithAuthorization the wallet is about to sign. A wallet, account-abstraction module, or x402 facilitator verifies the attestation independently against the published JWKS and releases the transfer signature only if the attestation is valid and matches the transfer. After broadcast, a settlement watcher reconciles the on-chain transaction against Base and records a SETTLED outcome once it confirms.

Chains and assets today: Base (mainnet, chain id 8453) and Base Sepolia (testnet, 84532), asset USDC. The rail is feature-gated per environment.

Build the logic, rent the custody

Mandate Labs authorizes and attests; it does not hold the agent's keys or move funds itself. The attestation is an authorization an external signer redeems — the wallet keeps custody, and the attestation binds the approval to one specific transfer so it cannot be replayed onto a different recipient, amount, token, chain, or time window.

Enabling rails

Rails are enabled at the Program level. When a Program is provisioned it carries an enabled-rails list — by default ["card", "crypto"] — and a Principal enrolled in that Program can hold mandates on any enabled rail. A mandate that names a rail the Program (or environment) hasn't enabled is rejected, and an attempt to authorize over a disabled rail declines with RAIL_NOT_ENABLED.

Declaring rails on a mandate

A Mandate declares which rails the agent may use via the rails array — a subset of {"card", "crypto"} that must be non-empty. An agent can hold a single mandate covering both rails, or separate mandates per rail.

When "crypto" is in rails, a crypto block is required (omitting it is a validation error). The block bounds the on-chain surface the agent may touch:

FieldTypeRequiredMeaning
chainsstring[]YesAllowed settlement chains, e.g. ["base"]. An authorize on any other chain declines with CHAIN_NOT_ALLOWED.
assetsstring[]YesAllowed asset symbols, e.g. ["USDC"]. Any other asset declines with ASSET_NOT_ALLOWED.
max_transfernumberNoAn optional per-transfer ceiling specific to the crypto rail, on top of the mandate's amount limits.

The mandate's standard limits (max_amount_per_transaction, max_daily_amount, currency) still apply on either rail — the economic amount is enforced regardless of how the value moves.

How authorize differs

The same POST /authorize endpoint serves both rails; the rail discriminator selects what else the request must carry.

Card authorize

rail defaults to CARD. The request carries the card-world fields — amount, currency, mcc, merchant_id/merchant_name, country — plus the mandatory intent_context. The engine checks the mandate's MCC, country, amount, and velocity constraints, scores trust and intent, and returns APPROVE / STEP_UP / DECLINE. No crypto block is allowed on a card request.

Crypto authorize

Set rail: "CRYPTO" and include a crypto context block describing the on-chain transfer:

The engine runs the same trust + intent pipeline, additionally enforces the crypto mandate constraints (chain, asset, counterparty), and on APPROVE attaches an attestation object to the response: { jws, kid, nonce, valid_after, valid_before, expires_at }. EVM address formats are validated at the edge, so malformed input is rejected before it reaches the adapter.

Authorize Attestation (JWS) Verify vs JWKS Sign + broadcast SETTLED
The crypto approval path: an attestation the wallet verifies and binds to its EIP-3009 transfer, then a settlement watcher that turns the on-chain confirmation into a SETTLED outcome.

Card vs crypto

Dimensioncardcrypto
Rail valueCARD (default)CRYPTO
NetworkCard issuing (Mastercard today)Base L2 (x402 / stablecoin)
Identifier of the payeeMCC + merchant id/nameDestination address (x402 payTo)
AssetFiat currency (e.g. USD)USDC (6 decimals)
Authorize contextmcc, merchant_*, countrycrypto block: chain, asset, counterparty, from_address
Mandate constraintsMCCs, countries, amount, velocitychains, assets, max_transfer, amount, velocity
Limits enforced onAmount / currencyAmount / currency (1 USDC ≈ 1 USD)
Approval evidenceAuthorization decision recordSigned EIP-3009 attestation (JWS) + decision record
SettlementVia the scheme (clearing/settlement)On-chain; watcher reconciles vs Base
Positive outcomeCLEARED / SETTLEDSETTLED (on confirmation)
Rail-specific declinesMCC_NOT_ALLOWED, COUNTRY_NOT_ALLOWEDCHAIN_NOT_ALLOWED, ASSET_NOT_ALLOWED, COUNTERPARTY_NOT_ALLOWED

Worked examples

Mandates are created with your Client API key; authorize calls use the Principal API key (see Authentication).

Card — mandate & authorize

curl -X POST https://api.mandatelabs.ai/api/v1/agents/agt_…/mandates \
  -H "X-API-Key: mdt_live_…" -H "Content-Type: application/json" \
  -d '{
    "rails": ["card"],
    "limits": {
      "max_amount_per_transaction": 500,
      "max_daily_amount": 2000,
      "currency": "USD"
    },
    "allowed_mccs": ["5812", "5814"],
    "allowed_countries": ["USA"]
  }'
curl -X POST https://api.mandatelabs.ai/api/v1/authorize \
  -H "X-API-Key: mdt_live_…" -H "Content-Type: application/json" \
  -d '{
    "rail": "CARD",
    "agent_id": "agt_…",
    "amount": 42.00,
    "currency": "USD",
    "mcc": "5812",
    "merchant_name": "Blue Bottle Coffee",
    "country": "USA",
    "intent_context": {
      "intent_type": "PURCHASE",
      "task_reference": "Team lunch — order from the cafe nearest the office"
    }
  }'
# → { "decision": "APPROVE", "reason_codes": [...], "kya_score": 0.87, ... }

Crypto — mandate & authorize

curl -X POST https://api.mandatelabs.ai/api/v1/agents/agt_…/mandates \
  -H "X-API-Key: mdt_live_…" -H "Content-Type: application/json" \
  -d '{
    "rails": ["crypto"],
    "limits": {
      "max_amount_per_transaction": 50,
      "max_daily_amount": 500,
      "currency": "USD"
    },
    "crypto": {
      "chains": ["base"],
      "assets": ["USDC"],
      "max_transfer": 25
    }
  }'
curl -X POST https://api.mandatelabs.ai/api/v1/authorize \
  -H "X-API-Key: mdt_live_…" -H "Content-Type: application/json" \
  -d '{
    "rail": "CRYPTO",
    "agent_id": "agt_…",
    "amount": 1.20,
    "currency": "USD",
    "crypto": {
      "chain": "base",
      "asset": "USDC",
      "counterparty": "0x1234abcd...ef01",
      "from_address": "0xabcd1234...5678",
      "operation": "transfer"
    },
    "intent_context": {
      "intent_type": "TRANSFER",
      "task_reference": "Pay per-call API fee for the pricing data request"
    }
  }'
# → APPROVE with a bound attestation the wallet verifies before signing:
{
  "decision": "APPROVE",
  "kya_score": 0.83,
  "attestation": {
    "jws": "eyJ…compact-JWS…",
    "kid": "a1b2c3d4e5f60718",
    "nonce": "0x…bytes32…",
    "valid_after": 1782561600,
    "valid_before": 1782561900,
    "expires_at": "2026-06-27T12:05:00Z"
  }
}

The wallet / AA module / x402 facilitator fetches the verification keys from GET /api/v1/crypto/attestations/jwks.json, verifies the JWS, and confirms the attestation binds this exact transfer (recipient, value, nonce, chain id, token, and — when present — the from address and time window). It signs and broadcasts the EIP-3009 transferWithAuthorization only on a match. Afterward it reports the broadcast tx to POST /api/v1/crypto/settlements, and the watcher records a SETTLED outcome once the transaction reaches the required confirmations on Base.

The approval can't be replayed

The attestation binds the recipient, value, nonce, chain, and token of one specific transfer. An approval for a $1.20 USDC payment to one address cannot be re-used to authorize a different amount, a different recipient, a different token, or a different chain — substitution and replay both fail verification.

When to use each

Next steps