ExchainDocs
DocsAPI

APIx402Register a guarded Safe

Register a guarded Safe

Register, and deploy, a guarded Safe governing one or several assets on one EVM chain.

POST
/v1/x402/wallets

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.

Authorization

nodeToken
headerAuthorizationBearer <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.

Request Body

application/json
  1. 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?string
Length1 <= length
orgId?string
Length1 <= length
agentAddress?EvmAddress

Optional treasury Safe. Its AllowanceModule allowance to this Safe counts in the bound statement’s worstCase.

Match^0x[0-9a-fA-F]{40}$
ownerAddress?EvmAddress

Optional treasury Safe. Its AllowanceModule allowance to this Safe counts in the bound statement’s worstCase.

Match^0x[0-9a-fA-F]{40}$
walletAddress?EvmAddress

Optional treasury Safe. Its AllowanceModule allowance to this Safe counts in the bound statement’s worstCase.

Match^0x[0-9a-fA-F]{40}$
shadow?boolean

Register the agent’s own address: no Safe and no signatures; decisions are only journalled.

Defaultfalse
permit2Cap?Amount

Approval to Permit2 of each governed token, set at deployment, in minor units. Default 0.

Match^[0-9]+$
treasurySafe?EvmAddress

Optional treasury Safe. Its AllowanceModule allowance to this Safe counts in the bound statement’s worstCase.

Match^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.

Itemsitems <= 64
unsignedSellers?|

A payment without a signed offer, when sellers is set. Default refuse.

ownerSignature?string

The 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.

Match^0x[0-9a-f]{130}$
saltNonce?string

CREATE2 salt nonce, 32 bytes; defaults to keccak256 of the registration's request hash.

Match^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.

Items1 <= items <= 16
sellerAllowList?array<>
sellerDenyList?array<>

Response Body

This node registered the same body before; the same walletId is returned. A repeat answer carries no owners or shadow.

application/json
  1. response
walletId*WalletId
Match^wal_[0-9A-HJKMNP-TV-Z]{26}$
walletAddress*EvmAddress
Match^0x[0-9a-fA-F]{40}$
chainId*integer
owners?array<>
shadow?boolean
constitutionId*string
Match^[0-9a-f]{64}$
mandateEnglish*string
mandateRules*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
Match^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$
Formatdate-time
schemaVersion*string

Wire schema version, "1.1" today.

Value in"1.0""1.1"
chainTip*|null
curl -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"}'
Example Response
{  "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

StatusCodeMeaning
400bad_requestThe body is not JSON or fails the schema; detail names the field.
400node_mismatchnodeId differs from the token’s node.
400network_mismatchA governed asset is not an ERC-20 on one of the EVM chains of the token’s network.
400asset_not_allowedA governed asset is not enabled on this network.
400duplicate_assetAn asset is listed twice in governedAssets.
400chain_mismatchThe governed assets are on different chains; a Safe lives on one chain.
400invalid_constitutionA mandate does not compile, or a bound is not a constant.
400unknown_invariantA custom mandate names a rule with no refusal code.
400asset_mismatchA mandate’s base asset differs from its governed asset.
400quorum_mismatchAn enclave’s EVM address is not known yet.
400owner_signature_requiredownerSignature is missing or invalid; detail is the message the owner signs (EIP-191).
400safe_mismatchwalletAddress is not the expected CREATE2 address, or the deployed Safe’s setup differs.
400not_deployedNothing is deployed at the address and Exchain cannot deploy it; detail has the address and calldata.
401unauthorizedThe bearer credential is missing, unknown or revoked.
403owner_unboundownerAddress is not bound to your console account. Bind it in the console first.
409wallet_existsThis Safe is registered with a different body or by another node.
413payload_too_largeThe request body is larger than 64 KiB.
429rate_limitedOver the limit; retry after Retry-After seconds (also in retryAfterSec).
502rpc_failedThe chain could not be read.
502deploy_failedThe Safe deployment failed.
503x402_unavailableExchain has no RPC for the asset’s chain.

Every code and what to do about it: Errors and codes.