The API reference lists each endpoint’s request and response shapes. This page reads the other direction: for the account, wallet, and signing endpoints, what does changing a given parameter actually do (its effect and side effects), and what appears in the response (its output). Every row reflects current behavior; see the linked API pages for the full schemas.
Two conventions used below:
- Effect — what the parameter changes, including error paths and custody or quota side effects.
- Output — the response field(s) the parameter drives. A parameter with no output row is never echoed.
Creating an account
POST /v1/accounts — see Accounts. Creating an account always
derives its first wallet, charged to the tenant’s maxWallets allowance
before any key material is generated — so a create with no wallet headroom
left returns 409 quota_wallets_exceeded and nothing is created.
| Parameter | Effect | Output |
|---|---|---|
displayName |
Normalized (NFKD) to 3–64 characters and must keep at least one Latin letter; the account slug is derived from it. A name that cannot normalize returns 400 invalid_parameter. |
account.slug (server-generated), account.displayName |
kind |
HD makes a mnemonic account you can derive more wallets from; PK is a single imported key, where derive is refused 422 unsupported_for_kind and import is used instead. A PK create without secret is 400 invalid_body. |
account.kind |
secret |
On HD, omit it to have the service generate a mnemonic; on PK it is the private key and is required. The secret itself is never returned — it leaves only through the sealed export path. |
drives generatedSecret (true when omitted, i.e. generated) |
defaultNetwork |
Fixes the chain each derived address signs on, and therefore the default chainId for typed-data signing. A well-formed slug for a network the workspace lacks is 400 unsupported_network. Not changeable after creation. |
account.defaultNetwork; every address.network / address.chainId |
hasOwnPassword |
The Cold Vault tier: the account gets a password separate from the session and is locked immediately after creation (no custody window), so it must be unlocked before first use. Requires accountPassword. Omitted or false creates the account unlocked with a custody window. |
account.hasOwnPassword; a Cold Vault create returns account.locked: true |
accountPassword |
The Cold Vault password, held to the same policy as a workspace password (under 12 characters is 422 weak_password). Required when hasOwnPassword is set and rejected without it (400 invalid_body). Used once, never stored. |
— |
Unlocking and locking an account
POST /v1/accounts/{slug}/unlock and .../lock — see
Workspace token routes. Both return 204 with no body and
are scope-free (any workspace token).
| Parameter | Effect | Output |
|---|---|---|
accountPassword (unlock) |
Opens the account and starts a fixed custody window (accountAbsoluteSec). Re-verifying an account that is already live does not renew its deadline. The workspace idle clock slides on admission even when the password then turns out wrong. Shares the tenant mint/KDF budget, so it can return 429 mint_rate_limited. A wrong password is 401 bad_password with a per-account backoff (1/2/4/8s → 429 account_unlock_rate_limited); the fifth consecutive failure — or the mint-budget limit, whichever is lower — retires the whole session, after which calls return 401 session_expired. |
204 |
| (lock, no body) | Locks the account immediately and zeroizes its in-memory key material. A manual lock is not auto-reopened even while a durable scoped key pins the session. | 204 |
Wallet endpoints
See Accounts. Every wallet-producing call reserves the
tenant’s cross-workspace maxWallets headroom before the core call, so a call
over the ceiling is 409 quota_wallets_exceeded and derives nothing.
| Endpoint / parameter | Effect | Output |
|---|---|---|
POST .../wallets — count (1–500) |
Derives count wallets on an HD account (PK is 422 unsupported_for_kind). |
before and after — the account’s total wallet count on each side, not the number added |
POST .../wallets/allocate (no body) |
Hands out the next unallocated wallet of an HD account: in buffer mode a pre-derived one, refilling asynchronously; otherwise derived on demand. Marks the wallet with an internal reserved tag. |
the allocated wallet |
POST .../wallets/seed-allocated — count (0–100,000) |
Migration prep: marks the lowest count wallet ids as already allocated so allocate skips them. Idempotent; a count above the account’s wallet total is 400. |
allocated, total |
POST .../wallets/import — privateKey, vm? |
Imports one wallet into a PK account (HD is 422 unsupported_for_kind); an unparseable key is 400 invalid_private_key. vm (evm or svm) pins the scheme for an ambiguous key form, otherwise it is inferred. A PK wallet carries an address on both chains. |
the imported wallet |
PUT .../wallets/{id}/tags — tags (max 32) |
Replaces the whole caller-visible tag list rather than merging; each tag is trimmed, Unicode-normalized, and de-duplicated. Reserved sys:* tags are stripped from the input and preserved from the existing set. |
the wallet with its public tags |
Signing (and how it stands in for transactions)
POST /v1/sign/* — see Sign. tee-docker signs; it never builds,
simulates, submits, or broadcasts, and there are no /transactions or balance
routes. The transaction-signing surface is /sign/digest (EVM) and
/sign/bytes (Solana): you build and hash the transaction yourself, have the
service sign, then assemble the signed transaction and broadcast it from your
own client.
Across all four routes, address (1–128 chars) is resolved by public key across
the whole workspace, and its owning account is unlocked lazily — so a locked
Cold Vault owner returns 423 account_locked and an unknown or other-tenant
address returns 404 account_not_found. Every route requires the sign scope
and is workspace-token-only: a scoped API key is refused 403 scope_denied
regardless of tier. Every response echoes the resolved address.
| Endpoint / parameter | Effect | Output |
|---|---|---|
/sign/message — message, encoding? |
encoding is personal_sign, raw, or ed25519. With an encoding the service uses the encoded path and the response gains a messageHash; without one it uses the address’s default scheme and returns no messageHash. An encoding the address’s VM cannot perform is 501 unsupported_operation. |
signature, and messageHash only when an encoding was given |
/sign/typed-data — typedData, chainId? |
EVM only (422 unsupported_for_kind otherwise). The envelope must carry domain, types, primaryType, and message, and primaryType must name a struct in types. chainId defaults to the address’s own network — there is no silent mainnet default. If the payload’s domain.chainId is readable and disagrees with the signing chain, the request is 400 chain_id_mismatch naming both. The caller’s exact object is signed; validation never rewrites the signed bytes. |
signature, domainSeparator, structHash |
/sign/digest — digest |
EVM (secp256k1) only. digest must be a lowercase 0x-hex 32-byte value and is signed verbatim — no keccak, no EIP-191 prefix. Build the transaction v from recovery (the yParity: use it directly for typed transactions, or 35 + 2·chainId + recovery for legacy EIP-155). |
r, s, recovery, signature |
/sign/bytes — message, encoding? |
Solana (ed25519) only. message is hex (default, lowercase 0x) or base64 per encoding, decoded with a strict round-trip, and must be 1 byte to 16 KiB. The exact bytes are signed verbatim — you compile the Solana message, the service signs, and you attach the signature. |
signature (base58) and signatureHex |