APIx402Register a guarded Safe
Register a guarded Safe
Register, and deploy, a guarded Safe governing one or several assets on one EVM chain.
- Auth Node Token
- Rate-limited Yes
- Guide Create and register a wallet
Register a guarded Safe on the EVM chain of its governed assets: Base, Ethereum, Arbitrum, Optimism or Polygon for a mainnet node, Base Sepolia, Sepolia, Arbitrum Sepolia, OP Sepolia or Polygon Amoy for a preprod node, wherever Exchain has an RPC (GET /v1/x402/signers lists them in chainIds). Govern one asset with governedAsset and constitution, or several with governedAssets, one mandate each; a Safe lives on one chain, so every asset must be on the same chain. The Safe is a stock Safe 1.4.1 whose owners are the agent and every Exchain enclave, threshold all of them, with ownerAddress (never an owner) as its only module and no guard. Exchain accepts it only at the CREATE2 address of exactly that setup, and deploys it there when it can; otherwise 400 not_deployed returns the address and calldata.
The owner must consent: without a valid ownerSignature the answer is 400 owner_signature_required with the message to sign in detail. Send the same body again with the signature; the Safe address does not change. With shadow the agent’s own address is registered and decisions are only journalled.
nodeTokenAuthorizationBearer <token>Node Token from the Exchain console. Works for wallets this node registered; the node’s network (preprod or mainnet) decides the network of the request. Use a preprod token in the playground.
application/json- body
One governed asset (governedAsset with constitution) or several (governedAssets, one mandate each; the first is the one reported wherever a single asset is). A Safe lives on one chain, so every governed asset must be on the same chain (else 400 chain_mismatch); the Safe approves Permit2 for permit2Cap of each, in order.
nodeId?string1 <= lengthorgId?string1 <= lengthagentAddress?EvmAddressOptional treasury Safe. Its AllowanceModule allowance to this Safe counts in the bound statement’s worstCase.
^0x[0-9a-fA-F]{40}$ownerAddress?EvmAddressOptional treasury Safe. Its AllowanceModule allowance to this Safe counts in the bound statement’s worstCase.
^0x[0-9a-fA-F]{40}$walletAddress?EvmAddressOptional treasury Safe. Its AllowanceModule allowance to this Safe counts in the bound statement’s worstCase.
^0x[0-9a-fA-F]{40}$shadow?booleanRegister the agent’s own address: no Safe and no signatures; decisions are only journalled.
falsepermit2Cap?AmountApproval to Permit2 of each governed token, set at deployment, in minor units. Default 0.
^[0-9]+$treasurySafe?EvmAddressOptional treasury Safe. Its AllowanceModule allowance to this Safe counts in the bound statement’s worstCase.
^0x[0-9a-fA-F]{40}$sellers?array<>Seller binding. When set, a payment must carry the seller’s signed x402 offer (offer-receipt) from one of these hosts, signed by a pinned key or a key in the host’s /.well-known/did.json, and matching the authorization. Refusals use payee_unpinned, price_above_published, registry_gate and body_mismatch.
items <= 64unsignedSellers?|A payment without a signed offer, when sellers is set. Default refuse.
ownerSignature?stringThe owner's EIP-191 signature over the consent message naming the Safe, chain, agent and owner module. Without it the 400 owner_signature_required carries that message as detail. It is left out of the request hash, so it does not change the Safe address.
^0x[0-9a-f]{130}$saltNonce?stringCREATE2 salt nonce, 32 bytes; defaults to keccak256 of the registration's request hash.
^0x[0-9a-fA-F]{64}$governedAsset*The one governed ERC-20, on one of the EVM chains of the node’s network and enabled there.
constitution*||The mandate of governedAsset.
governedAssets?array<>Several governed ERC-20s on one chain, each judged only against its own mandate; a payment is judged by the mandate of the asset its intent names. Every asset must be enabled on the network and listed once.
1 <= items <= 16sellerAllowList?array<>sellerDenyList?array<>This node registered the same body before; the same walletId is returned. A repeat answer carries no owners or shadow.
application/json- response
walletId*WalletId^wal_[0-9A-HJKMNP-TV-Z]{26}$walletAddress*EvmAddress^0x[0-9a-fA-F]{40}$chainId*integerowners?array<>shadow?booleanconstitutionId*string^[0-9a-f]{64}$mandateEnglish*stringmandateRules*array<>governedAssets?array<>Every governed asset with its own mandate, the first first. constitutionId, mandateEnglish and mandateRules above are the first asset’s.
asOf*string^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$date-timeschemaVersion*stringWire schema version, "1.1" today.
"1.0""1.1"chainTip*|nullcurl -X POST 'https://cosign.exchain.network/v1/x402/wallets' \ -H 'Content-Type: application/json' \ -d '{ "nodeId": "acme-node-1", "orgId": "org_acme", "agentAddress": "0x3c8e1f7a2b9d4e6c0a5f8b1d7e3c9a2f4b6d8e01", "ownerAddress": "0xa7d4e2c9b1f6038e5d2a7c4b9e1f3d6a8c0b5e72", "permit2Cap": "100000000", "governedAsset": { "id": "eip155:84532/erc20:0x036cbd53842c5426634e7929541ec2318f3dcf7e", "decimals": 6 }, "constitution": { "template": "x402-micro", "params": { "perTxCap": "1000000", "daily": "500000000", "perSeller": "50000000", "perAgent": "250000000", "envelope": "20000000000", "burstPerMinute": 60 } }, "sellers": [ { "domain": "api.seller.example" } ], "unsignedSellers": "refuse", "ownerSignature": "0x509bef563a5eff0f73d5ec50aa4773b2e733af594464eb2277e7b4184e388a8d69fdf389571ea3d892907e65074d87a37b5852e925c5b9b50fde8e5f90739fdd1b"}'{ "walletId": "wal_01M53M3ZNQQ4DDNPE4262QAZ28", "walletAddress": "0x9f2b6c1e4d7a3058b2e6f1c9d4a7b3e5f0c8a216", "chainId": 84532, "constitutionId": "477f981f3a1edbdc1fa7cbc46f78feea3fed2c693b116fb4dc6b0acf5794f541", "mandateEnglish": "No single payment above 1 USDC. No more than 60 payments in any 60 seconds. No more than 500 USDC in any rolling 24 hours. No more than 50 USDC to any one counterparty in any rolling 24 hours. No more than 250 USDC by any one agent in any rolling 24 hours. Total lifetime envelope: 20,000 USDC.", "mandateRules": [ { "name": "per_tx_cap", "metric": "max(payments.amount)", "window": 60, "scope": null, "bound": "1000000", "level": "member" }, { "name": "velocity_burst", "metric": "count(payments)", "window": 60, "scope": null, "bound": "60", "level": "member" }, { "name": "daily_outflow", "metric": "sum(payments.amount)", "window": 86400, "scope": null, "bound": "500000000", "level": "member" }, { "name": "per_seller_cap", "metric": "sum(payments.amount)", "window": 86400, "scope": "counterparty", "bound": "50000000", "level": "member" }, { "name": "per_agent_cap", "metric": "sum(payments.amount)", "window": 86400, "scope": "agent", "bound": "250000000", "level": "member" }, { "name": "envelope", "metric": "sum(payments.amount)", "window": 315360000, "scope": null, "bound": "20000000000", "level": "member" } ], "governedAssets": [ { "id": "eip155:84532/erc20:0x036cbd53842c5426634e7929541ec2318f3dcf7e", "decimals": 6, "symbol": "USDC", "constitutionId": "477f981f3a1edbdc1fa7cbc46f78feea3fed2c693b116fb4dc6b0acf5794f541", "mandateEnglish": "No single payment above 1 USDC. No more than 60 payments in any 60 seconds. No more than 500 USDC in any rolling 24 hours. No more than 50 USDC to any one counterparty in any rolling 24 hours. No more than 250 USDC by any one agent in any rolling 24 hours. Total lifetime envelope: 20,000 USDC.", "mandateRules": [ { "name": "per_tx_cap", "metric": "max(payments.amount)", "window": 60, "scope": null, "bound": "1000000", "level": "member" }, { "name": "velocity_burst", "metric": "count(payments)", "window": 60, "scope": null, "bound": "60", "level": "member" }, { "name": "daily_outflow", "metric": "sum(payments.amount)", "window": 86400, "scope": null, "bound": "500000000", "level": "member" }, { "name": "per_seller_cap", "metric": "sum(payments.amount)", "window": 86400, "scope": "counterparty", "bound": "50000000", "level": "member" }, { "name": "per_agent_cap", "metric": "sum(payments.amount)", "window": 86400, "scope": "agent", "bound": "250000000", "level": "member" }, { "name": "envelope", "metric": "sum(payments.amount)", "window": 315360000, "scope": null, "bound": "20000000000", "level": "member" } ] } ], "asOf": "2026-11-05T14:22:07.318Z", "schemaVersion": "1.1", "chainTip": { "slot": 104118120, "blockHash": "9a301ac2e5747e7ebeeecececd2f114f55695614277514e085211c5417d04c26" }}Errors
| Status | Code | Meaning |
|---|---|---|
400 | bad_request | The body is not JSON or fails the schema; detail names the field. |
400 | node_mismatch | nodeId differs from the token’s node. |
400 | network_mismatch | A governed asset is not an ERC-20 on one of the EVM chains of the token’s network. |
400 | asset_not_allowed | A governed asset is not enabled on this network. |
400 | duplicate_asset | An asset is listed twice in governedAssets. |
400 | chain_mismatch | The governed assets are on different chains; a Safe lives on one chain. |
400 | invalid_constitution | A mandate does not compile, or a bound is not a constant. |
400 | unknown_invariant | A custom mandate names a rule with no refusal code. |
400 | asset_mismatch | A mandate’s base asset differs from its governed asset. |
400 | quorum_mismatch | An enclave’s EVM address is not known yet. |
400 | owner_signature_required | ownerSignature is missing or invalid; detail is the message the owner signs (EIP-191). |
400 | safe_mismatch | walletAddress is not the expected CREATE2 address, or the deployed Safe’s setup differs. |
400 | not_deployed | Nothing is deployed at the address and Exchain cannot deploy it; detail has the address and calldata. |
401 | unauthorized | The bearer credential is missing, unknown or revoked. |
403 | owner_unbound | ownerAddress is not bound to your console account. Bind it in the console first. |
409 | wallet_exists | This Safe is registered with a different body or by another node. |
413 | payload_too_large | The request body is larger than 64 KiB. |
429 | rate_limited | Over the limit; retry after Retry-After seconds (also in retryAfterSec). |
502 | rpc_failed | The chain could not be read. |
502 | deploy_failed | The Safe deployment failed. |
503 | x402_unavailable | Exchain has no RPC for the asset’s chain. |
Every code and what to do about it: Errors and codes.