ExchainDocs
DocsAPI

APIWalletsRegister a wallet

Register a wallet

Register a Cardano guarded wallet and its mandate.

POST
/v1/wallets

Register a Cardano guarded wallet and its mandate. The mandate is fixed at registration: there is no route that changes it, so a different mandate needs a new wallet. Registration is idempotent by content: the same body from the same node returns 200 with the same walletId; a different body for the same wallet returns 409 wallet_exists. network must equal the Node Token’s network.

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 per asset, the first listed first everywhere a single asset is reported).

walletAddress?Bech32Address
Match^addr(_test)?1[02-9ac-hj-np-z]+$
stateToken?string
Match^[0-9a-f]{56}\.([0-9a-f]{2}){1,32}$
ownerKeyHash?KeyHash
Match^[0-9a-f]{56}$
agentKeyHashes?array<>
Items1 <= items
quorumKeyHashes?array<>
Items1 <= items
quorumThreshold?integer
Range1 <= value
escrowAddresses?array<>
Items1 <= items
governedAsset*
constitution*||
governedAssets?array<>

Several governed assets, each judged only against its own mandate. Every asset must be enabled on the network and listed once. When lovelace is one of them, the lovelace riding with a token payment also counts against the lovelace mandate.

Items1 <= items <= 16
network?string
Value in"preprod""mainnet"
orgId?string
Length1 <= length
nodeId?string
Length1 <= length
priceGate?boolean
priceTolerancePct?integer
Range0 <= value <= 100
sellerAllowList?array<>
sellerDenyList?array<>
hydraParties?array<>

The owner’s own Hydra verification keys (raw 32-byte Ed25519, hex). An in-head spend is co-signed when the head’s layer-1 party set contains one of them, or when every party is on Exchain’s list for the node.

Itemsitems <= 16
registryGate?boolean
Defaulttrue

Response Body

This node registered the same body before; the same walletId is returned.

application/json
  1. response
walletId*WalletId
Match^wal_[0-9A-HJKMNP-TV-Z]{26}$
constitutionId*Hash32
Match^[0-9a-f]{64}$
mandateEnglish*string
mandateRules*array<>
Items1 <= items
governedAssets?array<>

Every governed asset with its own mandate; the top-level fields describe the first.

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
Example Request
curl -X POST 'https://cosign.exchain.network/v1/wallets' \  -H 'Content-Type: application/json' \  -d '{  "walletAddress": "addr_test1wz8nv2s4u5yqjm3pv3k8x5c7tdgfm8wq0r6cjyh4ldm9a2gqv7x5e",  "stateToken": "15e9acb98b2868a013203378a6b514c9dcf97a5a905e3932c0ef9cef.6578636861696e2d7374617465",  "ownerKeyHash": "241c5e9eed0f49da2cddd68cfcf3e7dad9201cf8def2c06e4c9f5dac",  "agentKeyHashes": [    "903105119401d2618ca8b5193adfc74592a1362c07a375c3490fe90e"  ],  "quorumKeyHashes": [    "b02e5e1f6167ef12fe823d259b1034f56818e92ef2b4c73722ee3412",    "32c72c8379803dd1957e426ca9c11ffeb47c8dd1a644028214ef39ef",    "347d7c288cd19c106e46e990bd0d38be27fe73fe7aa04b6cbdb9d4ad"  ],  "quorumThreshold": 3,  "escrowAddresses": [    "addr_test1wqag3rt979nep9g2wtdwu8mr4gz6m4kjdpp5zp705km8wys6t2kla"  ],  "governedAsset": {    "id": "lovelace",    "decimals": 6  },  "constitution": {    "params": {      "perTxCap": "200000000",      "daily": "8000000000",      "perSeller": "500000000",      "perAgent": "4000000000",      "envelope": "200000000000",      "burstPerMinute": 10    }  },  "network": "preprod",  "orgId": "org_acme",  "nodeId": "acme-node-1"}'
Example Response
{  "walletId": "wal_01J9XK3M4N5P6Q7R8S9T0V1W2Z",  "constitutionId": "394b588411cfdb72c9377adba91bb86fad77b65e6be25bdb2a00558ec853e0f2",  "mandateEnglish": "No single payment above 200 tADA. No more than 10 payments in any 60 seconds. No more than 8,000 tADA in any rolling 24 hours. No more than 500 tADA to any one counterparty in any rolling 24 hours. No more than 4,000 tADA by any one agent in any rolling 24 hours. Total lifetime envelope: 200,000 tADA. Only registry-verified agents can be paid. No payment above the seller's published price.",  "mandateRules": [    {      "name": "per_tx_cap",      "metric": "max(payments.amount)",      "window": 60,      "scope": null,      "bound": "200000000",      "level": "member"    },    {      "name": "velocity_burst",      "metric": "count(payments)",      "window": 60,      "scope": null,      "bound": "10",      "level": "member"    },    {      "name": "daily_outflow",      "metric": "sum(payments.amount)",      "window": 86400,      "scope": null,      "bound": "8000000000",      "level": "member"    },    {      "name": "per_seller_cap",      "metric": "sum(payments.amount)",      "window": 86400,      "scope": "counterparty",      "bound": "500000000",      "level": "member"    },    {      "name": "per_agent_cap",      "metric": "sum(payments.amount)",      "window": 86400,      "scope": "agent",      "bound": "4000000000",      "level": "member"    },    {      "name": "envelope",      "metric": "sum(payments.amount)",      "window": 315360000,      "scope": null,      "bound": "200000000000",      "level": "member"    },    {      "name": "registry_gate",      "metric": "counterparty.registered",      "window": null,      "scope": "counterparty",      "bound": null,      "level": "member"    },    {      "name": "price_gate",      "metric": "payments.amount <= counterparty.published_price",      "window": null,      "scope": "counterparty",      "bound": null,      "level": "member"    }  ],  "governedAssets": [    {      "id": "lovelace",      "decimals": 6,      "symbol": "tADA",      "constitutionId": "394b588411cfdb72c9377adba91bb86fad77b65e6be25bdb2a00558ec853e0f2",      "mandateEnglish": "No single payment above 200 tADA. No more than 10 payments in any 60 seconds. No more than 8,000 tADA in any rolling 24 hours. No more than 500 tADA to any one counterparty in any rolling 24 hours. No more than 4,000 tADA by any one agent in any rolling 24 hours. Total lifetime envelope: 200,000 tADA. Only registry-verified agents can be paid. No payment above the seller's published price.",      "mandateRules": [        {          "name": "per_tx_cap",          "metric": "max(payments.amount)",          "window": 60,          "scope": null,          "bound": "200000000",          "level": "member"        },        {          "name": "velocity_burst",          "metric": "count(payments)",          "window": 60,          "scope": null,          "bound": "10",          "level": "member"        },        {          "name": "daily_outflow",          "metric": "sum(payments.amount)",          "window": 86400,          "scope": null,          "bound": "8000000000",          "level": "member"        },        {          "name": "per_seller_cap",          "metric": "sum(payments.amount)",          "window": 86400,          "scope": "counterparty",          "bound": "500000000",          "level": "member"        },        {          "name": "per_agent_cap",          "metric": "sum(payments.amount)",          "window": 86400,          "scope": "agent",          "bound": "4000000000",          "level": "member"        },        {          "name": "envelope",          "metric": "sum(payments.amount)",          "window": 315360000,          "scope": null,          "bound": "200000000000",          "level": "member"        },        {          "name": "registry_gate",          "metric": "counterparty.registered",          "window": null,          "scope": "counterparty",          "bound": null,          "level": "member"        },        {          "name": "price_gate",          "metric": "payments.amount <= counterparty.published_price",          "window": null,          "scope": "counterparty",          "bound": null,          "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.
400network_mismatchnetwork or an address is not on the Node Token’s network.
400node_mismatchnodeId differs from the token’s node.
400asset_not_allowedThe asset is an ERC-20 (use POST /v1/x402/wallets) or not enabled on this network.
400duplicate_assetAn asset is listed twice in governedAssets.
400asset_mismatchThe mandate’s base asset differs from the governed asset.
400invalid_constitutionThe mandate does not compile, or a bound is not a constant.
400unknown_invariantA custom mandate names a rule with no refusal code.
400quorum_mismatchquorumKeyHashes or quorumThreshold do not match Exchain’s quorum; retry once every member answers.
401unauthorizedThe bearer credential is missing, unknown or revoked.
409wallet_existsThis wallet is registered with a different body or by another node. A new mandate needs a new wallet.
413payload_too_largeThe request body is larger than 64 KiB.
429rate_limitedOver the limit; retry after Retry-After seconds (also in retryAfterSec).

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