Multisig Wallets

Open a treasury transfer lot for signature

Create a lot of 1 to 20 USDC transfers from the organization treasury, and open the signing window. Each leg names EITHER an agent wallet of the organization OR a contact of this treasury address book — never a raw address. The lot is ONE signing subject: a single signature per signer covers every leg, and execution is atomic — every leg lands or none does. Nothing moves yet: the lot broadcasts once the wallet threshold of signatures is collected from the signer apps.

The server owns every calldata — each leg is rebuilt from its validated destination and amount — so this endpoint can only ever move USDC to an agent wallet of the caller organization or a contact of this treasury, never an arbitrary address. The response carries the structured pre-image (transfers, relayNonce, relayHash): a signer app must recompute the hash locally from those fields — the leg order is part of it — and refuse to sign on a mismatch, so a tampered server cannot get something else signed. Each leg also carries destinationLabel, destinationKind and destinationFirstUse, resolved from the frozen address at read time for display only, and the lot carries fiat, an indicative rate in the organization display currency: all of them are outside the signed pre-image and must never enter that computation.

One live lot per treasury: the signatures are bound to a single account nonce, so a second concurrent intent could never both execute. A previous intent whose outcome is undetermined (unknown) must be reconciled first. The creation is also refused when the treasury balance visibly cannot cover the lot total.

post/v1/multisig-wallets/{walletId}/intents

Request

  • Base URL: https://api.prod.keyban.io
  • URL: https://api.prod.keyban.io/v1/multisig-wallets/{walletId}/intents
  • Auth: one of:
    • API key in header X-Api-Key
    • bearer (a scheme the document does not define)

Path parameters

walletIdstring uuid required

The unique identifier of the multisig wallet to debit

Request body

Example request

{
  "transfers": [
    {
      "agentWalletId": "550e8400-e29b-41d4-a716-446655440000",
      "amount": "1500000"
    }
  ]
}

Response

Transfer awaiting signatures

idstring uuid required

Entity UUID

createdAtstring date-time required

Creation date (ISO 8601)

updatedAtstring date-time required

Last update date (ISO 8601)

status'pending' | 'broadcasting' | 'unknown' | 'executed' | 'failed' | 'cancelled' | 'expired' required

Lifecycle: pending (collecting signatures), broadcasting (threshold reached, relay in progress), executed (mined, txHash set), failed (reverted or never broadcast), unknown (outcome undetermined — the transfer may have landed, and no new intent is accepted until it is reconciled), cancelled, expired.

totalAmountstring required

Sum of the leg amounts in USDC sub-units, as a decimal string. Derived, never stored.

relayNoncestring required

Account relay nonce the signatures are bound to, as a decimal string. One nonce covers the whole lot. If the account moves past it, the intent fails without broadcasting.

relayHashstring required

What each signer signs. Single-leg lot: keccak256(abi.encode(chainId, account, nonce, dest, value, keccak256(data))). Two legs or more: keccak256(abi.encode(chainId, account, nonce, keccak256(abi.encode(calls)))) over the ordered (dest, value, data) legs. The app must recompute it locally from the other fields and refuse to sign on a mismatch.

approvalCountinteger required

Signatures collected so far, out of the wallet threshold.

expiresAtstring date-time required

End of the signing window. Past it the intent expires on its next touch and the collected signatures are purged.

txHashstring nullable

Hash of the relayed transaction. Null until executed — and can stay null on an intent reconciled as executed after an ambiguous broadcast, where only the consumed account nonce proves execution.

errorstring nullable

Why the transfer failed, or what made its outcome undetermined. Null while pending or once executed.

Example response

{
  "status": "pending",
  "transfers": [
    {
      "agentWalletId": "550e8400-e29b-41d4-a716-446655440000",
      "targetAddress": "0xcb0c0dca4246bb0f6b3309d64e7e35002cc3af1f",
      "amount": "1500000",
      "destinationLabel": "Prestataire Acme",
      "destinationKind": "contact"
    }
  ],
  "totalAmount": "4000000",
  "fiat": {
    "currency": "EUR",
    "centiPerUsdc": 92
  },
  "relayNonce": "3",
  "relayHash": "0x4d2f8c1e9b7a3d5f0c2e8a6b4d1f9c3e7a5b8d2f4c6e9a1b3d5f7c9e2a4b6d8f",
  "approvalCount": 1,
  "txHash": "0x4d2f8c1e9b7a3d5f0c2e8a6b4d1f9c3e7a5b8d2f4c6e9a1b3d5f7c9e2a4b6d8f"
}

Changes

No recorded changes to this operation across all 1 revision of this API.