CosignCo-sign a payment
Co-sign a payment
Send each agent payment to Exchain before it goes on chain, and get the attested quorum's signatures back.
Send each agent payment to Exchain before it goes on chain. If it fits the mandate, the answer is 200 with the attested quorum's signatures, and the payment becomes valid on chain.
A 200 commits budget. A 200 returns signatures that make the payment valid on chain. Send only the payment you asked about. Its budget is committed until it lands or its validity window closes.
How co-signing works
- The agent builds the payment and signs it.
- Your backend sends it to Exchain with your Node Token, before it goes on chain.
- Exchain checks it against the chain's rules and the mandate. Each quorum member checks it again and signs.
- You attach the signatures and send the payment, or Exchain sends it for you on the EVM direct rail.
- Exchain watches the chain. A payment that lands is linked in the journal. A payment that never lands returns its budget.
Every request carries Authorization: Bearer <Node Token> and an Idempotency-Key equal to batchId. A retry with the same batchId and body returns the stored answer. Refusals, retries and error codes: Refusals and retries.
Endpoints
| Rail | Endpoint | Section |
|---|---|---|
| Cardano layer 1 | POST /v1/cosign | Cardano |
| Cardano Hydra head | POST /v1/cosign | Hydra in-head payments |
| EVM direct, to any address | POST /v1/evm/cosign | EVM chains |
| EVM inside x402 | POST /v1/x402/cosign | EVM chains |
Cardano
On Cardano, the payment is a transaction from the guarded Masumi smart wallet that pays one or more Masumi escrows. Exchain returns the quorum's vkey witnesses. Inside a Hydra head, the same endpoint takes a snapshot instead of a layer 1 UTxO: Hydra in-head payments.
Transaction rules
The body must follow every rule. The first rule that fails refuses the whole batch.
| Rule | Refusal (code: detail) |
|---|---|
| Standard body keys only (0, 1, 2, 3, 7, 8, 11, 13, 14, 15, 16, 17, 18); no reference scripts | body_mismatch: body_key:N, payee_unpinned: script_ref |
| Network id and every address on the wallet's network | body_mismatch: network_id, network |
walletUtxoRef is an input exactly once and is the wallet's current UTxO | utxo_unknown: wallet_input_missing, stale_ref |
| Exactly one continuing output at the wallet address, with exactly the state token and an inline datum | payee_unpinned: continuing_output |
| Every other output is a registered escrow address or pays an agent key | payee_unpinned: output:i |
| Only governed assets leave; a token output carries at most 6 ADA | asset_not_listed: output:i, lovelace_rider |
| Fee ≤ 10 ADA; unclaimed ADA outflow ≤ 10 ADA | body_mismatch: unclaimed_outflow |
| Each escrow output: Masumi V2 escrow inline datum, buyer = agent, state FundsLocked, claimed by exactly one intent with matching seller, asset and amount | body_mismatch: escrow_datum:i, buyer, escrow_state:i, claim:i |
Datum: agent, limit, period and minimum balance unchanged; spent_in_period and period_start exactly as the contract computes; every spent ≤ limit; continuing ADA ≥ minimum | body_mismatch: datum_arith |
| Validity start and end set, at most 7,200 slots (2 h) apart, open now on Exchain's clock | clock_skew: unbounded, range, too_wide, not_open |
required_signers includes the agent and every quorum key | body_mismatch: quorum_incomplete, agent_unsigned |
| Collateral is never the wallet UTxO; collateral return pays an agent key | body_mismatch: collateral_is_wallet, payee_unpinned: collateral_return |
CIP-20 messages (metadata label 674) are allowed.
Cardano request
{
"batchId": "7f0c3d2e-9a4b-4c1d-8e2f-000000000001",
"walletUtxoRef": "<64 hex tx hash>#0",
"txBodyHex": "a40081825820…",
"intents": [
{ "purchaseId": "pur_1", "outputIndex": 0, "counterparty": "sellerVkeyHash:<56 hex>",
"amount": "19000000", "asset": "lovelace",
"jobHash": "blake2b_256:<64 hex>", "agentIdentifier": "<registry asset id>" },
{ "purchaseId": "pur_2", "outputIndex": 1, "counterparty": "sellerVkeyHash:<56 hex>",
"amount": "5000000", "asset": "6ba199f81440944474fb29139bc7a0f736f70615bb26cb7b20d3d6fd.0014df10745553444378",
"jobHash": "blake2b_256:<64 hex>", "agentIdentifier": "<registry asset id>" }
],
"context": { "nodeId": "node_1", "orgId": "org_acme", "submittedAt": "2026-11-05T10:00:00Z" }
}19000000 = 19 tADA; 5000000 = 5 tUSDCx (both 6 decimals).
batchIdis a lowercase UUID;intentsholds 1 to 10 purchases, one per escrow output.walletUtxoRefis required on layer 1;txBodyHexis at most 32,768 hex characters.context.submittedAtis recorded only. Decisions use Exchain's clock.counterparty,amountandassetare checked against the escrow output in the body.agentIdentifierandjobHashare taken from your request as sent, and the registry gate, price gate andperAgentlimit read them.
Every field and format: API.
Cardano response
{
"decisionId": "dec_01J9XK3M4N5P6Q7R8S9T0V1W2X",
"txBodyHash": "blake2b_256:f13f1c97…3c247f18",
"requiredSigners": ["<agent key hash>", "<quorum key hash 1>", "<quorum key hash 2>", "<quorum key hash 3>"],
"quorumMembers": ["member-a", "member-b", "member-c"],
"witnessSetHex": "a10083825820…",
"witnesses": [
{ "member": "member-a", "vkeyHex": "<64 hex>", "signatureHex": "<128 hex>" },
{ "member": "member-b", "vkeyHex": "<64 hex>", "signatureHex": "<128 hex>" },
{ "member": "member-c", "vkeyHex": "<64 hex>", "signatureHex": "<128 hex>" }
],
"members": [
{ "purchaseId": "pur_1", "outputIndex": 0, "verdict": "allowed" },
{ "purchaseId": "pur_2", "outputIndex": 1, "verdict": "allowed" }
],
"boundsRemaining": {
"daily_outflow": { "limit": "1000000000", "used": "24000000", "remaining": "976000000",
"windowResetsAt": "2026-11-06T10:00:00Z" }
},
"journalRef": "https://cosign.exchain.network/v1/decisions/dec_01J9XK3M4N5P6Q7R8S9T0V1W2X",
"asOf": "2026-11-05T10:00:01.412Z",
"schemaVersion": "1.1",
"chainTip": { "slot": 138150155, "blockHash": "9a301ac2…" }
}| Field | Meaning |
|---|---|
witnessSetHex | CBOR witness set with the quorum's vkey witnesses only |
requiredSigners | The required signers decoded from the body |
boundsRemaining | What is left in each mandate window, for the first intent's asset only |
journalRef | The decision, at GET /v1/decisions/{decisionId} |
Submit
- Add the agent's vkey witness to the set in
witnessSetHex. - Attach the set to the unchanged body.
- Submit. The transaction id does not change.
After you submit
| Outcome | What Exchain does |
|---|---|
| Lands | The watcher checks every 20 s, marks the decision landed, and the journal links the transaction |
| Never lands | After invalidHereafter + 120 slots, the reserved budget is returned. The journal shows "Budget returned" |
| You want budget back early | Not possible. Only Exchain's watcher releases budget |
EVM chains
On EVM chains, the payment is one authorization from the guarded Safe, in one of the tokens it governs, valid for at most 600 seconds. For Ethereum USDT and Polygon USDT0 on the direct rail, it is a Safe transaction at the Safe's current nonce instead. The agent signs the Safe message hash of the typed data (for a Safe transaction, its SafeTx hash): a raw 65-byte signature, not personal_sign. Exchain returns every owner's signature in one packed signature. Methods and how the Safe verifies them: Signing.
Typed data rules
typedData must follow every rule.
| Rule | Refusal (code: detail) |
|---|---|
| Exactly the standard shape: no extra types, domain fields or message fields | body_mismatch: types, domain, message |
TransferWithAuthorization (EIP-3009). On x402 only, also PermitWitnessTransferFrom (Permit2). On direct only, SafeTx for Ethereum USDT and Polygon USDT0 | body_mismatch: primary_type, method |
The domain is the token named by intent.asset (Permit2 for Permit2, the Safe for SafeTx) on the Safe's chain | body_mismatch: domain, chain_id, token |
| The payer is the Safe | body_mismatch: from |
Payee and value equal the intent's counterparty and amount | body_mismatch: payee, amount |
| Permit2 only: the spender is the x402 Permit2 proxy | body_mismatch: spender |
SafeTx only: exactly transfer(payee, amount) on the token, all other fields zero, at the Safe's current nonce | body_mismatch: data, safe_nonce |
Valid now, and expires within 600 seconds (not for SafeTx) | body_mismatch: window |
batchId is the nonce (for SafeTx, the Safe nonce) | 400 batch_id_mismatch |
| The nonce was never decided before | body_mismatch: nonce_reused |
Checks, in order
- Fees due in the payment's token:
402 fee_due. Settle them first: Fees. - Amount and counterparty format:
400. - The asset is one the wallet governs and is still enabled:
asset_not_listed. - The typed data rules above.
- The agent signature recovers to the registered agent:
401 agent_signature_invalid. - Allow and deny lists, then seller binding: member codes.
- Mandate limits: member codes.
Then every member of the attested quorum checks the typed data again on its own clock and signs.
Direct and x402 differences
| Direct | x402 | |
|---|---|---|
| Endpoint | POST /v1/evm/cosign | POST /v1/x402/cosign |
| Pays | Any address: an invoice, a supplier, another agent | The seller in an x402 flow |
| Method | EIP-3009, or SafeTx for Ethereum USDT and Polygon USDT0 | EIP-3009, or Permit2 if the Safe was registered with permit2Cap. Ethereum USDT and Polygon USDT0: Permit2 only |
batchId | The EIP-3009 nonce, or the Safe nonce for SafeTx, 0x + 64 hex | The EIP-3009 nonce, or the Permit2 nonce as 32 bytes big-endian |
| Extra intent fields | reference: what the payment is for | resource: the paid URL; offer: the seller's signed offer |
| Sending | relay: true lets Exchain send it and pay the gas | The x402 facilitator sends it |
| Answer adds | transaction; with relay, relayed or relayError | receipt |
| Seller binding | The payee counts as an unsigned seller | The signed offer is checked |
| Journal rail | evm | x402 |
EVM request
{
"batchId": "0xd830…f946",
"walletId": "wal_01M53M3ZNQQ4DDNPE4262QAZ28",
"typedData": {
"domain": { "name": "USDC", "version": "2", "chainId": 84532,
"verifyingContract": "0x036cbd53842c5426634e7929541ec2318f3dcf7e" },
"types": { "TransferWithAuthorization": [
{ "name": "from", "type": "address" }, { "name": "to", "type": "address" },
{ "name": "value", "type": "uint256" }, { "name": "validAfter", "type": "uint256" },
{ "name": "validBefore", "type": "uint256" }, { "name": "nonce", "type": "bytes32" } ] },
"primaryType": "TransferWithAuthorization",
"message": { "from": "0xSafe…", "to": "0x6d2f…e284", "value": "500000",
"validAfter": "0", "validBefore": "<now + 300>", "nonce": "0xd830…f946" }
},
"agentSignature": "0x<130 hex>",
"relay": true,
"intent": {
"purchaseId": "inv_2026_118",
"counterparty": "eip155:84532:0x6d2f…e284",
"amount": "500000",
"asset": "eip155:84532/erc20:0x036cbd53842c5426634e7929541ec2318f3dcf7e",
"agentIdentifier": "research-agent",
"reference": "invoice 2026-118"
}
}With the SDK, the same payment is one call:
import { exchainPay } from "@exchain-network/x402";
const answer = await exchainPay(
{ cosign: { url: "https://cosign.exchain.network", token }, walletId, safe, agent, chainId: 84532 },
{ payTo: "0x6d2f…e284", amount: 500_000n, agentIdentifier: "research-agent", reference: "invoice 2026-118" },
);
answer.relayed.txHash;exchainPay pays in USDC. For another token, send the request above with that token's domain and asset id. A refusal rejects with ExchainDenied { code, detail, decisionId }, and nothing is signed. Every field: API.
500000 = 0.50 USDC (6 decimals).
| Intent field | Value |
|---|---|
purchaseId | Your payment id |
counterparty | eip155:<chain>:0x<payee> |
amount | Minor-unit string, equal to the authorization value |
asset | One of the wallet's governed asset ids. The payment counts against that asset's mandate |
agentIdentifier | Recorded. perAgent counts the registered agent address |
EVM response
| Field | Rail | Meaning |
|---|---|---|
decisionId, journalRef | Both | The decision, at GET /v1/decisions/{decisionId} |
signature | Both | Every owner's 65-byte signature, the agent's included, sorted by owner address. Absent in shadow mode |
safeMessageHash | Both | The hash the owners signed |
walletAddress, nonce, digest | Both | The Safe, the authorization nonce and its EIP-712 digest |
boundsRemaining | Both | What is left in each mandate window |
transaction | Direct | { to, data }: the token's transferWithAuthorization call, or for SafeTx the Safe's execTransaction call, with signature. Anyone can send it until it expires, or for SafeTx until the Safe's nonce moves past it |
relayed.txHash | Direct, relay: true | The transaction Exchain sent |
relayError | Direct, relay: true | Why Exchain could not send it. transaction still works |
receipt | x402 | Guard receipt (JWS). Attach it at extensions["exchain-guard"].receipt: Guard receipt |
On x402, pass signature to the x402 client as the payment signature. Relay details: Relay.
EVM settlement
Exchain watches the Safe's own chain.
| Outcome | What Exchain does |
|---|---|
| The nonce is used on chain | Marks the decision landed. The decision and the journal link the transaction on that chain's block explorer |
The nonce is still unused at a finalized block past validBefore + 60 s | Returns the reserved budget |
SafeTx: the Safe logs ExecutionSuccess for its hash | Marks the decision landed |
SafeTx: another transaction takes its Safe nonce, at a finalized block | Returns the reserved budget |
Per-chain explorers and fees: Settlement.