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/initiateandPOST /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/sessionadditionally requiresX-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 group | Path | Auth |
|---|---|---|
| Users / auth | /users/register, /users/me, /users/me/settings, /auth/session, /auth/logout | Mixed (see above) |
| Directory | /directory/resolve | authenticate |
| Portfolio | /wallet/addresses, /portfolio/assets, /prices/:assetId/series | authenticate |
| Transactions | /transactions/prepare, /transactions/send, /transactions | authenticate |
| Staking | /staking/stake, /staking/unstake, /staking/network-stats | Mixed (stake/unstake authenticate, network-stats public) |
| Guardian | /guardian/* | Mixed (see Guardian section) |
| Credentials | /credentials, /oid4vci/redeem | authenticate |
| Address book | /address-book, /address-book/:id | authenticate |
| Health | /health, /v1/health | None |
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/send — stake/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.
| Route | Auth | Purpose |
|---|---|---|
GET /guardian/my-guardians | authenticate | List my guardians |
POST /guardian/invite | authenticate | Invite a guardian by DID and/or email |
POST /guardian/confirm | authenticate | A guardian accepts an invite by token |
DELETE /guardian/:guardianId | authenticate | Remove a guardian |
GET /guardian/pending-requests | authenticate | Recovery requests where I’m a guardian |
GET /guardian/pending-invites | authenticate | Invites where I’m the invitee (wallet-specific addition — identity surfaces this via inbox instead) |
POST /guardian/recovery/initiate | None | Start a recovery for a lost DID |
POST /guardian/recovery/:requestId/approve | authenticate | A guardian submits their Ed25519 approval signature |
POST /guardian/recovery/:requestId/finalize | None | Relay the client-signed DidRecover |
GET /guardian/recovery/:requestId | None | Recovery status + collected approvals |
GET /guardian/recovery-prep | authenticate | My DID + current chain nonce, for building SetRecoveryPolicy |
POST /guardian/policy | authenticate | Relay 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.