ExchainDocs
DocsAPI

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

  1. The agent builds the payment and signs it.
  2. Your backend sends it to Exchain with your Node Token, before it goes on chain.
  3. Exchain checks it against the chain's rules and the mandate. Each quorum member checks it again and signs.
  4. You attach the signatures and send the payment, or Exchain sends it for you on the EVM direct rail.
  5. 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

RailEndpointSection
Cardano layer 1POST /v1/cosignCardano
Cardano Hydra headPOST /v1/cosignHydra in-head payments
EVM direct, to any addressPOST /v1/evm/cosignEVM chains
EVM inside x402POST /v1/x402/cosignEVM 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.

RuleRefusal (code: detail)
Standard body keys only (0, 1, 2, 3, 7, 8, 11, 13, 14, 15, 16, 17, 18); no reference scriptsbody_mismatch: body_key:N, payee_unpinned: script_ref
Network id and every address on the wallet's networkbody_mismatch: network_id, network
walletUtxoRef is an input exactly once and is the wallet's current UTxOutxo_unknown: wallet_input_missing, stale_ref
Exactly one continuing output at the wallet address, with exactly the state token and an inline datumpayee_unpinned: continuing_output
Every other output is a registered escrow address or pays an agent keypayee_unpinned: output:i
Only governed assets leave; a token output carries at most 6 ADAasset_not_listed: output:i, lovelace_rider
Fee ≤ 10 ADA; unclaimed ADA outflow ≤ 10 ADAbody_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 amountbody_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 ≥ minimumbody_mismatch: datum_arith
Validity start and end set, at most 7,200 slots (2 h) apart, open now on Exchain's clockclock_skew: unbounded, range, too_wide, not_open
required_signers includes the agent and every quorum keybody_mismatch: quorum_incomplete, agent_unsigned
Collateral is never the wallet UTxO; collateral return pays an agent keybody_mismatch: collateral_is_wallet, payee_unpinned: collateral_return

CIP-20 messages (metadata label 674) are allowed.

Cardano request

POST /v1/cosign
{
  "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).

  • batchId is a lowercase UUID; intents holds 1 to 10 purchases, one per escrow output.
  • walletUtxoRef is required on layer 1; txBodyHex is at most 32,768 hex characters.
  • context.submittedAt is recorded only. Decisions use Exchain's clock.
  • counterparty, amount and asset are checked against the escrow output in the body. agentIdentifier and jobHash are taken from your request as sent, and the registry gate, price gate and perAgent limit read them.

Every field and format: API.

Cardano response

200 OK
{
  "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…" }
}
FieldMeaning
witnessSetHexCBOR witness set with the quorum's vkey witnesses only
requiredSignersThe required signers decoded from the body
boundsRemainingWhat is left in each mandate window, for the first intent's asset only
journalRefThe decision, at GET /v1/decisions/{decisionId}

Submit

  1. Add the agent's vkey witness to the set in witnessSetHex.
  2. Attach the set to the unchanged body.
  3. Submit. The transaction id does not change.

After you submit

OutcomeWhat Exchain does
LandsThe watcher checks every 20 s, marks the decision landed, and the journal links the transaction
Never landsAfter invalidHereafter + 120 slots, the reserved budget is returned. The journal shows "Budget returned"
You want budget back earlyNot 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.

RuleRefusal (code: detail)
Exactly the standard shape: no extra types, domain fields or message fieldsbody_mismatch: types, domain, message
TransferWithAuthorization (EIP-3009). On x402 only, also PermitWitnessTransferFrom (Permit2). On direct only, SafeTx for Ethereum USDT and Polygon USDT0body_mismatch: primary_type, method
The domain is the token named by intent.asset (Permit2 for Permit2, the Safe for SafeTx) on the Safe's chainbody_mismatch: domain, chain_id, token
The payer is the Safebody_mismatch: from
Payee and value equal the intent's counterparty and amountbody_mismatch: payee, amount
Permit2 only: the spender is the x402 Permit2 proxybody_mismatch: spender
SafeTx only: exactly transfer(payee, amount) on the token, all other fields zero, at the Safe's current noncebody_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 beforebody_mismatch: nonce_reused

Checks, in order

  1. Fees due in the payment's token: 402 fee_due. Settle them first: Fees.
  2. Amount and counterparty format: 400.
  3. The asset is one the wallet governs and is still enabled: asset_not_listed.
  4. The typed data rules above.
  5. The agent signature recovers to the registered agent: 401 agent_signature_invalid.
  6. Allow and deny lists, then seller binding: member codes.
  7. 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

Directx402
EndpointPOST /v1/evm/cosignPOST /v1/x402/cosign
PaysAny address: an invoice, a supplier, another agentThe seller in an x402 flow
MethodEIP-3009, or SafeTx for Ethereum USDT and Polygon USDT0EIP-3009, or Permit2 if the Safe was registered with permit2Cap. Ethereum USDT and Polygon USDT0: Permit2 only
batchIdThe EIP-3009 nonce, or the Safe nonce for SafeTx, 0x + 64 hexThe EIP-3009 nonce, or the Permit2 nonce as 32 bytes big-endian
Extra intent fieldsreference: what the payment is forresource: the paid URL; offer: the seller's signed offer
Sendingrelay: true lets Exchain send it and pay the gasThe x402 facilitator sends it
Answer addstransaction; with relay, relayed or relayErrorreceipt
Seller bindingThe payee counts as an unsigned sellerThe signed offer is checked
Journal railevmx402

EVM request

POST /v1/evm/cosign
{
  "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:

pay.mjs
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 fieldValue
purchaseIdYour payment id
counterpartyeip155:<chain>:0x<payee>
amountMinor-unit string, equal to the authorization value
assetOne of the wallet's governed asset ids. The payment counts against that asset's mandate
agentIdentifierRecorded. perAgent counts the registered agent address

EVM response

FieldRailMeaning
decisionId, journalRefBothThe decision, at GET /v1/decisions/{decisionId}
signatureBothEvery owner's 65-byte signature, the agent's included, sorted by owner address. Absent in shadow mode
safeMessageHashBothThe hash the owners signed
walletAddress, nonce, digestBothThe Safe, the authorization nonce and its EIP-712 digest
boundsRemainingBothWhat is left in each mandate window
transactionDirect{ 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.txHashDirect, relay: trueThe transaction Exchain sent
relayErrorDirect, relay: trueWhy Exchain could not send it. transaction still works
receiptx402Guard 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.

OutcomeWhat Exchain does
The nonce is used on chainMarks 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 sReturns the reserved budget
SafeTx: the Safe logs ExecutionSuccess for its hashMarks the decision landed
SafeTx: another transaction takes its Safe nonce, at a finalized blockReturns the reserved budget

Per-chain explorers and fees: Settlement.

On this page