Skip to content

Accounts, wallets, and addresses explained

HD versus PK and why each refuses the other's operations, plus the account field that fixes which chain an address can ever sign for.

Updated View as Markdown

Inside a workspace, key material nests three levels deep. Each level exists for a reason, and knowing which one you are addressing removes most of the guesswork from the endpoint list.

The wallet object domainA left-to-right chain of one-to-many relationships: Tenant, then Workspace, Account, Wallet, and Address. A tenant owns many workspaces; a workspace holds many accounts; an account holds many wallets; a wallet exposes one address per VM.1∗1∗1∗1∗TenantAPI key + secretWorkspacepassword-lockedAccountHD or PKWalletid + tagsAddressone per VM
Every address descends from a tenant: a tenant owns workspaces, a workspace holds accounts, an account holds wallets, and a wallet exposes one address per VM.

Accounts hold the secret

An account is created with POST /v1/accounts and comes in two kinds.

Kind Secret Derivation
HD A BIP-39 mnemonic, generated when you do not supply one Many wallets from one mnemonic
PK A single imported private key No derivation

The kind field defaults to HD. Supply secret to import an existing mnemonic (HD) or private key (PK); omit it and an HD account generates its own. A PK account has nothing to generate, so secret is required there — omitting it is 400 with invalid_body.

Creating an account of either kind produces its first wallet immediately, and that wallet counts against the tenant’s limits.maxWallets allowance. An account is never empty, and account creation is never quota-free.

hasOwnPassword and accountPassword are a pair: sending either without the other is refused, because the flag without a distinct password would leave the account “separately locked” by the very secret that opened the session. The two directions answer differently — hasOwnPassword alone is 400 invalid_body, while an accountPassword alone is checked for strength first, so a weak one comes back 422 weak_password and only a strong one reaches the 400. An account password is held to the same strength minimum as a workspace password, though unlike a workspace password it is also capped at 256 characters. Omit both and the account shares the workspace password, which is the ordinary case.

The account slug is generated by the service, not chosen by you. Read it from the create response — it is the identifier every later call uses.

displayName is normalized before use and must be 3–64 characters afterwards. It must also still contain a Latin letter, because the slug is projected from it with accents stripped; a name that projects to nothing is refused with 400 and invalid_parameter. The slug is truncated to leave room for a numeric collision suffix, so two accounts sharing a display name get distinct slugs.

Wallets are derived

POST /v1/accounts/{slug}/wallets derives more wallets on an HD account, in bulk:

{ "count": 5 }

The response reports the count on both sides of the operation, which makes the call safe to retry and easy to reconcile. Because creation already made one, a brand-new account starts from 1:

{ "before": 1, "after": 6 }

Deriving on a PK account is refused with 422 and unsupported_for_kind. A PK account instead takes POST /v1/accounts/{slug}/wallets/import with a privateKey, which adds exactly one wallet on top of the one its creation key already produced — and importing into an HD account is the mirrored 422.

Derivation is bounded: count must be a positive integer up to 500, and the tenant’s limits.maxWallets caps the total. Crossing it returns 409 with quota_wallets_exceeded and derives nothing.

Tags

Wallets carry a list of up to 32 string tags, replaced wholesale with PUT /v1/accounts/{slug}/wallets/{id}/tags. The service attaches no meaning to them, but it does not store them verbatim either: each tag is trimmed and Unicode-normalized, and the list is de-duplicated after that. Read the tags back from the response rather than assuming you got what you sent.

Individual tags have rules, and breaking them is refused rather than repaired: a tag must be 1–64 characters after normalization and must not contain control characters, zero-width characters, or BIDI overrides, or the request comes back 400 invalid_parameter. Zero-width and BIDI characters are refused because a tag is displayed to people, and either can make two different tags render identically. The 32 cap is a separate check on the array you sent, applied before de-duplication and reported as 400 invalid_body — so 33 tags that would collapse to 5 are still refused. De-duplication is case-sensitive: prod and Prod both survive.

Addresses are what you sign with

Each wallet exposes one address per VM — an evm one and an svm one — and each address reports the single network it is on:

{ "publicKey": "0xabc…", "vm": "evm", "network": "ethereum", "chainId": 1 }

Read vm, network and chainId off the address itself. They are the authoritative answer to “what chain will this sign for”; the account’s defaultNetwork is a creation-time input, not a per-address guarantee, and a wallet’s svm address is necessarily on a Solana network however the account was configured.

The signing endpoints take a public key directly rather than an account/wallet path. You do not walk the tree to sign. Resolution is scoped to your own workspace, so one tenant can never reach another’s key — and an unknown public key returns 404 with account_not_found, the same as a public key that exists but belongs elsewhere.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close