Skip to Content
API ReferenceWallet API

Wallet API

The Wallet API backs wallet.solidus.network, a multi-chain wallet with a DID-linked address book, guardian-based social recovery, and a KYC credential you verify once and reuse everywhere.

Non-custodial by construction: this backend builds unsigned transactions and relays already-client-signed ones. It never receives, holds, or signs with a user’s seed or private key — all cryptographic operations happen client-side. The backend’s own signer key is a value-only service/sponsor identity (no DID), used only to relay and sponsor-fund fresh accounts.

swap, nfts, and buy are disabled for launch — no on-chain DEX, NFT indexer, or fiat on-ramp exists yet, and no routes for them are wired.

Base URL: https://wallet.solidus.network/v1


Authentication

A session cookie carrying a short-TTL JWT, minted by POST /users/register or the OIDC handoff at POST /auth/session. Routes that require it are marked authenticate below; they return 401 { "error": "Unauthorized" } on a missing or expired session — this specific check bypasses the RFC 7807 error format used everywhere else on this API (see Error format below).

curl https://wallet.solidus.network/v1/users/me \ -H "Cookie: solidus_wallet_session=<jwt>"

Two routes deliberately skip session auth for reasons specific to their flow:

  • POST /guardian/recovery/initiate and POST /guardian/recovery/:requestId/finalize — the recovering user has, by definition, lost their session. The new-key envelope plus the collected guardian signatures are the authority; the chain verifies them.
  • GET /guardian/recovery/:requestId — polled by the unguessable request UUID; the collected approvals are chain-bound public signatures anyway.
  • POST /auth/session additionally requires X-Internal-Secret: <INTERNAL_API_SECRET> — it’s a server-to-server call from the wallet frontend’s own OIDC callback, not a public route.

Route index

Route groupPathAuth
Users / auth/users/register, /users/me, /users/me/settings, /auth/session, /auth/logoutMixed (see above)
Directory/directory/resolveauthenticate
Portfolio/wallet/addresses, /portfolio/assets, /prices/:assetId/seriesauthenticate
Transactions/transactions/prepare, /transactions/send, /transactionsauthenticate
Staking/staking/stake, /staking/unstake, /staking/network-statsMixed (stake/unstake authenticate, network-stats public)
Guardian/guardian/*Mixed (see Guardian section)
Credentials/credentials, /oid4vci/redeemauthenticate
Address book/address-book, /address-book/:idauthenticate
Health/health, /v1/healthNone

Users and auth

Register

The entry point to the seed-based flow — relays a client-signed DidCreate (new wallet) or binds an existing DID (import), then mints the session cookie. Public.

curl -X POST https://wallet.solidus.network/v1/users/register \ -H "content-type: application/json" \ -d '{ "signedDidCreate": "<client-signed DidCreate tx>", "addresses": [{ "chain": "solidus", "address": "3GfjBM7v6LZ1N5zp1b12gfpwCN8j", "derivation": "m/44'\''/9999'\''/0'\''/0/0" }], "displayName": "Ada" }'

Exactly one of signedDidCreate (create) or did (import) is required.

// Response 201 { "id": "usr_...", "did": "did:solidus:testnet:..." }

Me

curl https://wallet.solidus.network/v1/users/me -H "Cookie: solidus_wallet_session=<jwt>"

PATCH /users/me/settings updates hideBalances and/or displayName.

OIDC session handoff

curl -X POST https://wallet.solidus.network/v1/auth/session \ -H "X-Internal-Secret: <INTERNAL_API_SECRET>" -H "content-type: application/json" \ -d '{"sub": "<verified OIDC subject>", "email": "[email protected]"}'
// Response 200 — no wallet linked yet { "needsOnboarding": true }
// Response 200 — linked { "id": "usr_...", "did": "did:solidus:testnet:...", "needsOnboarding": false }

Directory

curl "https://wallet.solidus.network/v1/directory/resolve?did=did:solidus:testnet:..." \ -H "Cookie: solidus_wallet_session=<jwt>"

Resolves a DID or handle to a registered wallet user’s Solidus receive address. Requires exactly one of did or handle.


Portfolio

curl https://wallet.solidus.network/v1/wallet/addresses -H "Cookie: solidus_wallet_session=<jwt>" curl https://wallet.solidus.network/v1/portfolio/assets -H "Cookie: solidus_wallet_session=<jwt>" curl "https://wallet.solidus.network/v1/prices/slds/series?range=7d" -H "Cookie: solidus_wallet_session=<jwt>"

portfolio/assets returns live per-chain balances plus USD valuation. assetId for the price series is one of slds, eth, btc; range is 1d, 7d (default), or 30d.


Transactions

The whole flow is prepare-client-sign-relay — the server never sees a key.

Prepare

curl -X POST https://wallet.solidus.network/v1/transactions/prepare \ -H "Cookie: solidus_wallet_session=<jwt>" -H "content-type: application/json" \ -d '{"chain":"solidus","kind":"send","assetId":"slds","amount":25,"toAddress":"28gvczeVVVQjy6MpP4yexmd3xEsm"}'

kind is send, stake, or unstake; toAddress is required for send. Returns the unsigned payload plus the caller’s live on-chain nonce, ready to sign in the browser.

Send

curl -X POST https://wallet.solidus.network/v1/transactions/send \ -H "Cookie: solidus_wallet_session=<jwt>" -H "content-type: application/json" \ -d '{ "sender_pubkey": [ /* 32 bytes */ ], "nonce": 4, "payload": { "Transfer": { "to": "28gvczeVVVQjy6MpP4yexmd3xEsm", "amount": 2500000000 } }, "signature": [ /* 64 bytes */ ] }'

The full, already-signed ChainTransaction — the server checks sender_pubkey belongs to the caller, then relays it as-is. It is never signed or re-signed server-side.

List

curl "https://wallet.solidus.network/v1/transactions?dateRange=30d&type=send" \ -H "Cookie: solidus_wallet_session=<jwt>"

Query params: dateRange (30d, 90d, 1y, all default), assetId (default all), type (all default, send, receive, stake, unstake, credential), search.


Staking

curl -X POST https://wallet.solidus.network/v1/staking/stake \ -H "Cookie: solidus_wallet_session=<jwt>" -H "content-type: application/json" \ -d '{"sender_pubkey":[...],"nonce":5,"payload":{"Stake":{"amount":1000000000}},"signature":[...]}'

Same client-signed relay pattern as transactions/sendstake/unstake bodies carry a {Stake:{amount}} / {Unstake:{amount}} payload. 403s if the signature doesn’t match the caller’s own registered Solidus key.

curl https://wallet.solidus.network/v1/staking/network-stats
// Response 200 — measured live { "totalStaked": 10000, "activeValidators": 5, "apy": { "value": 8.2, "sample": true }, "validators": [ { "address": "3GfjBM7v6LZ1N5zp1b12gfpwCN8j", "staked": 10000, "reputation": 1000, "active": true }, { "address": "28gvczeVVVQjy6MpP4yexmd3xEsm", "staked": 0, "reputation": 0, "active": true } ] }

apy.sample: true is an explicit label — no on-chain APY source exists yet, this is a placeholder value, not a live computation. totalStaked and activeValidators come from a real, live solidus_getValidators call.


Guardian — social recovery

DID-guardian-based social recovery: invite guardians, collect their signatures, recover a lost key.

RouteAuthPurpose
GET /guardian/my-guardiansauthenticateList my guardians
POST /guardian/inviteauthenticateInvite a guardian by DID and/or email
POST /guardian/confirmauthenticateA guardian accepts an invite by token
DELETE /guardian/:guardianIdauthenticateRemove a guardian
GET /guardian/pending-requestsauthenticateRecovery requests where I’m a guardian
GET /guardian/pending-invitesauthenticateInvites where I’m the invitee (wallet-specific addition — identity surfaces this via inbox instead)
POST /guardian/recovery/initiateNoneStart a recovery for a lost DID
POST /guardian/recovery/:requestId/approveauthenticateA guardian submits their Ed25519 approval signature
POST /guardian/recovery/:requestId/finalizeNoneRelay the client-signed DidRecover
GET /guardian/recovery/:requestIdNoneRecovery status + collected approvals
GET /guardian/recovery-prepauthenticateMy DID + current chain nonce, for building SetRecoveryPolicy
POST /guardian/policyauthenticateRelay a client-signed SetRecoveryPolicy for my own DID
curl -X POST https://wallet.solidus.network/v1/guardian/invite \ -H "Cookie: solidus_wallet_session=<jwt>" -H "content-type: application/json" \ -d '{"guardianEmail": "[email protected]", "guardianName": "Bea"}'

At least one of guardianDid or guardianEmail is required — an email-only invite has no DID until the guardian accepts. POST /guardian/recovery/initiate needs subjectDid and newPublicKey; POST /guardian/recovery/:requestId/approve needs a 64-byte Ed25519 signature over the canonical recovery message.


Credentials

curl https://wallet.solidus.network/v1/credentials -H "Cookie: solidus_wallet_session=<jwt>"

Reads a credentials_cache, best-effort refreshing each row’s status against verify — an empty cache returns [], credentials are never invented.

curl -X POST https://wallet.solidus.network/v1/oid4vci/redeem \ -H "Cookie: solidus_wallet_session=<jwt>" -H "content-type: application/json" \ -d '{"credentialOffer": "<scanned OID4VCI credential_offer URL or JSON>"}'

Drives the OID4VCI pre-authorized-code flow server-to-server against verify.solidus.network for a scanned offer, then caches the issued credential. txCode is optional, for offers whose grant carries a PIN (OID4VCI §3.5).


Address book

curl https://wallet.solidus.network/v1/address-book -H "Cookie: solidus_wallet_session=<jwt>" curl -X POST https://wallet.solidus.network/v1/address-book \ -H "Cookie: solidus_wallet_session=<jwt>" -H "content-type: application/json" \ -d '{"name":"Bea","did":"did:solidus:testnet:...","addresses":[{"chain":"solidus","address":"28gvczeVVVQjy6MpP4yexmd3xEsm"}]}' curl -X DELETE https://wallet.solidus.network/v1/address-book/<id> -H "Cookie: solidus_wallet_session=<jwt>"

Health

curl https://wallet.solidus.network/v1/health
// Response 200 — measured live { "status": "ok", "service": "wallet-backend", "build": { "commit": "36cdec1...", "builtAt": "2026-07-16T00:40:15.183Z" }, "timestamp": "2026-07-29T11:25:13.108Z" }

Note: wallet.solidus.network/health (no /v1) resolves to the Next.js frontend, not the backend — use /v1/health for the backend’s own health check.


Error format

Validation and business-logic errors (badRequest, forbidden, etc.) follow RFC 7807 Problem Details, the same shape used across every Solidus backend:

{ "type": "https://solidus.network/errors/400", "title": "Bad Request", "status": 400, "detail": "toAddress is required for a send transaction", "instance": "/v1/transactions/prepare" }

Session-auth failures from the authenticate decorator are the one exception — a plain 401 { "error": "Unauthorized" }, not the Problem Details shape.

Last updated on