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.
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.
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.
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:
| Field | Type | Required | Meaning |
|---|---|---|---|
chains | string[] | Yes | Allowed settlement chains, e.g. ["base"]. An authorize on any other chain declines with CHAIN_NOT_ALLOWED. |
assets | string[] | Yes | Allowed asset symbols, e.g. ["USDC"]. Any other asset declines with ASSET_NOT_ALLOWED. |
max_transfer | number | No | An 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:
- chain & asset — the settlement chain and asset (e.g.
base,USDC). For a known pair, the chain id, ERC-20 contract, and token decimals are resolved automatically (base→ 8453, USDC → 6 decimals). - counterparty — the destination address (the x402
payTo), screened by the crypto risk adapter. - from_address — the payer/agent wallet, bound into the attestation as the EIP-3009
from. - value & operation — the human-readable token amount (defaults to the request
amountfor a stablecoin transfer) and the on-chain operation (transferby default;swap,contract_call,bridgeare recognized so the adapter can weight them).
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.
Card vs crypto
| Dimension | card | crypto |
|---|---|---|
| Rail value | CARD (default) | CRYPTO |
| Network | Card issuing (Mastercard today) | Base L2 (x402 / stablecoin) |
| Identifier of the payee | MCC + merchant id/name | Destination address (x402 payTo) |
| Asset | Fiat currency (e.g. USD) | USDC (6 decimals) |
| Authorize context | mcc, merchant_*, country | crypto block: chain, asset, counterparty, from_address |
| Mandate constraints | MCCs, countries, amount, velocity | chains, assets, max_transfer, amount, velocity |
| Limits enforced on | Amount / currency | Amount / currency (1 USDC ≈ 1 USD) |
| Approval evidence | Authorization decision record | Signed EIP-3009 attestation (JWS) + decision record |
| Settlement | Via the scheme (clearing/settlement) | On-chain; watcher reconciles vs Base |
| Positive outcome | CLEARED / SETTLED | SETTLED (on confirmation) |
| Rail-specific declines | MCC_NOT_ALLOWED, COUNTRY_NOT_ALLOWED | CHAIN_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 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
- Use the card rail when the agent buys from ordinary merchants — anything that accepts a card, priced in fiat, where MCC/merchant/country controls and the card-scheme settlement and dispute machinery are what you want.
- Use the crypto rail for machine-to-machine, pay-per-call commerce — x402-style USDC micropayments to APIs and services on Base, where you want sub-cent-overhead settlement and a cryptographic authorization the payer's wallet can verify independently before it signs.
- Use both by giving the agent a mandate with
rails: ["card", "crypto"](and acryptoblock), or separate per-rail mandates. The same agent, the same trust score, and the same intent model govern both — only the context and the evidence differ.