Files
smirk-backend-core/openapi.json
T
jwinterm fb48e04ea6 feat(wallet): subaddress provisioning and batched UTXO endpoints (off by default)
Backend support for two address-privacy features. Both are behind env flags
that default to off, so an unconfigured instance behaves exactly as before:
the routes are not mounted and no provisioning is attempted.

FEATURE_XMR_SUBADDR_PROVISIONING
  Adds POST /wallet/lws/provision_subaddrs and provisions account-0 subaddress
  indices on the LWS at register/import.

  Three money-safety properties worth calling out:

  * The provisioned ceiling is read from the LWS response, never echoed back
    from the request. It is the contiguous run starting at minor 0, not
    max(hi): given ranges [[0,99],[200,299]], reporting 299 would let a wallet
    hand out indices 100..199 that the LWS never scans, so funds sent there
    would be invisible. No usable run is a hard error, not a guess.
  * import_account now always runs the backward rescan even when provisioning
    fails. Previously a provisioning error aborted first, leaving the account
    registered at the chain tip while every retry returned 200 OK, so a
    restoring wallet would show a zero balance permanently.
  * A capacity probe reads max_subaddresses from the LWS and refuses up front
    with a message naming the required --max-subaddresses value, rather than
    failing per-account later. The option defaults to 0 upstream.

  Spent outputs now carry their subaddress index. monero-lws emits this as
  `sender` (the index the spent output was received at), not `recipient`;
  without it a client recomputes a primary key image for a subaddress spend,
  treats the mismatch as a decoy, and never subtracts the amount.

FEATURE_UTXO_MULTI_ADDRESS
  Adds balance_multi / utxos_multi / history_multi for HD gap-limit wallets.
  Capped at 32 addresses, summed with checked_add, per-UTXO address tag
  preserved so the client never has to re-guess which key owns an output.
  Bounded concurrency with a per-batch deadline returning 504, so a slow
  Electrum server cannot stack n * IO_TIMEOUT.

Both flags are advertised in /capabilities so a client can negotiate instead
of calling routes that 404.
2026-07-28 16:01:25 -04:00

5563 lines
162 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"openapi": "3.1.0",
"info": {
"title": "smirk-backend-core",
"description": "Open, self-hostable backend for the Smirk non-custodial multi-chain wallet.",
"contact": {
"name": "John Winter Murphy",
"email": "jw@such.software"
},
"license": {
"name": "MIT",
"identifier": "MIT"
},
"version": "0.3.0"
},
"paths": {
"/.well-known/nostr.json": {
"get": {
"tags": [
"nip05"
],
"summary": "NIP-05 resolution for Smirk usernames.",
"description": "Looks up the user by lowercased `?name=` and, if they have a linked\n`nostr_pubkey`, returns it in `names` (with the default relay hints under\n`relays`). An unknown name, an unlinked user, or an absent `name` parameter\nall return an empty document — this endpoint never errors on the lookup path\nand never discloses which usernames exist.",
"operationId": "well_known_nostr",
"parameters": [
{
"name": "name",
"in": "query",
"description": "NIP-05 local part to resolve",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "NIP-05 directory document (empty when unresolved)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WellKnownResponse"
}
}
}
}
}
}
},
"/auth/check-restore": {
"post": {
"tags": [
"auth"
],
"summary": "Check whether a wallet restore is valid (created here + keys match).",
"description": "Two governors run BEFORE any user lookup: a per-IP limit\n([`RESTORE_IP_THRESHOLD`]) that bounds distinct-fingerprint scanning, and a\nper-fingerprint failure limit ([`RESTORE_FAIL_THRESHOLD`]); either trips a\n429. The per-IP limit uses [`client_ip`] so an untrusted `X-Forwarded-For`\ncannot evade it. Every attempt is recorded (peppered fingerprint, salted IP).\n\nThe known/unknown branches are equalized: the unknown branch runs the same\nkey-comparison loop against an empty stored set, so the two paths do the same\nwork and the response does not become a timing oracle for existence beyond the\nalready-intentional `exists` field.",
"operationId": "check_restore",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CheckRestoreRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Restore validation result",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CheckRestoreResponse"
}
}
}
}
}
}
},
"/auth/extension": {
"post": {
"tags": [
"auth"
],
"summary": "Register a new extension wallet or authenticate an existing one.",
"description": "The BTC pubkey hash is the unique identity. A signed timestamp proves control\nof the BTC private key (defeats registration with a stolen public key). When\nthe PoW gate applies and the wallet is new, a valid `altcha_solution` is\nrequired; a returning user (a known `pubkey_hash`) bypasses PoW.\n\n## Derivation rotation is authenticated (account-takeover defense)\n\nA known `seed_fingerprint` at a NEW `pubkey_hash` (the wallet changed its\nderivation scheme) may re-point the existing user row — but ONLY when the\nrequest also carries a `rotation_signature` that verifies against the BTC key\nalready on file for that user (proving control of the seed-derived key, not\nmerely knowledge of the fingerprint, which `check_restore` discloses and is\nnot secret). A bare fingerprint match WITHOUT a valid rotation proof is\ntreated as a brand-new identity: a fresh user row is created on the new\n`pubkey_hash` and the matched victim row is never modified. The rotation path\nis gated by PoW exactly like any other new-pubkey registration.",
"operationId": "extension_register",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExtensionRegisterRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Wallet registered or authenticated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AuthResponse"
}
}
}
}
}
}
},
"/auth/logout": {
"post": {
"tags": [
"auth"
],
"summary": "Revoke the session backing a refresh token. Idempotent: an unknown or\nalready-revoked token still returns success (logout should never error).",
"operationId": "logout",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LogoutRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Session revoked",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LogoutResponse"
}
}
}
}
}
}
},
"/auth/me": {
"get": {
"tags": [
"auth"
],
"summary": "The authenticated user's own info.",
"operationId": "get_me",
"responses": {
"200": {
"description": "Current authenticated user",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserInfo"
}
}
}
},
"401": {
"description": "Missing or invalid token"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/auth/nostr": {
"post": {
"tags": [
"auth"
],
"summary": "Sign in with a Nostr identity (NIP-98 HTTP Auth, login grade).",
"description": "The `Nostr <base64(event)>` token is read from the `Authorization` header and\nverified against the canonical `config.identity.public_api_url` + `/auth/nostr`\n(never the request Host). The npub must ALREADY be linked to a user (see\n[`nostr_link`]); this endpoint NEVER creates a user — an unlinked npub is 401.",
"operationId": "nostr_login",
"responses": {
"200": {
"description": "Session for the linked Nostr identity",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AuthResponse"
}
}
}
},
"401": {
"description": "Invalid NIP-98 token or no linked account"
}
}
}
},
"/auth/nostr/link": {
"post": {
"tags": [
"auth"
],
"summary": "Link a Nostr identity (npub) to the authenticated user.",
"description": "Dual auth: the Bearer JWT identifies the user; a NIP-98 *signed-action* proof\n(not a login token) proves control of the npub AND binds a server-issued\nsingle-use `nonce`, the purpose `nostr_link`, and this exact request via the\nrequest-descriptor hash. On success the npub is stored so a later\n[`nostr_login`] resolves to the same wallet; a collision is 409.\n\nFlow: the wallet first calls `GET /auth/nostr/link-challenge` (authenticated)\nto obtain a single-use `nonce`, signs an action committing to it, then POSTs\nhere. The nonce is consumed atomically ([`Database::consume_challenge`]) and\ncross-checked to have been issued to THIS user.\n\n## Descriptor binding is a CONTRACT (not an implementation detail)\n\nThe `payload` tag binds `descriptor_sha256(request_descriptor(\"POST\",\n\"/api/v1/auth/nostr/link\", \"\", b\"\"))` — an EMPTY body hash, with no query.\nThe JSON `{nostr_token, nonce}` rides in the HTTP body but is deliberately NOT\npart of the signed descriptor (it carries the proof itself, so it cannot also\nbe inside it). The wallet MUST build the identical descriptor. This exact\nmethod/path/query/empty-body shape is a cross-impl contract, pinned in a shared\ntest vector (`descriptor_sha256` KAT in this file's tests, mirroring the\nnip98.rs interop test) so the binding cannot silently drift at integration time.",
"operationId": "nostr_link",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NostrLinkRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Linked npub",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NostrLinkResponse"
}
}
}
},
"401": {
"description": "Invalid proof or missing session"
},
"409": {
"description": "npub already linked to another account"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/auth/nostr/link-challenge": {
"get": {
"tags": [
"auth"
],
"summary": "Issue a single-use nonce for linking a Nostr identity.",
"description": "Authenticated (Bearer JWT). The nonce is bound to the calling user (as the\nchallenge `subject`) and the `nostr_link` purpose, valid for a short TTL, and\nconsumed atomically by [`nostr_link`]. It pairs with the signed-action proof\nthere: the wallet signs an action committing to THIS nonce, so the server can\nprove the npub-holder authorized the link for THIS account and the request\ncannot be replayed.",
"operationId": "nostr_link_challenge",
"responses": {
"200": {
"description": "Single-use link nonce",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NostrLinkChallengeResponse"
}
}
}
},
"401": {
"description": "Missing or invalid session"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/auth/nostr/register": {
"post": {
"tags": [
"auth"
],
"summary": "npub-native registration. Verifies a NIP-98 signed-action over a server nonce,\nre-runs the abuse gates keyed on the npub (a create path must not bypass the\nPoW/invite/payment gates that `/auth/extension` enforces), then get-or-creates\nby npub with a `seed_fingerprint` MERGE so a wallet that already has a row\n(BTC-anchored, or previously linked) is never split into two identities.",
"description": "SECURITY NOTE: the proof binds an EMPTY-body descriptor (the same contract as\n`/auth/nostr/link`), so replay is prevented by the single-use nonce and npub\ncontrol is proven, but the chain `keys` are TLS-bound rather than\nproof-bound. Binding the key list into the signed payload is a documented\nhardening follow-up (defends a TLS-breaking active MITM swapping the keys).",
"operationId": "nostr_register",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NostrRegisterRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Wallet registered or authenticated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AuthResponse"
}
}
}
},
"401": {
"description": "Invalid proof or nonce"
},
"409": {
"description": "Seed already registered under a different Nostr identity"
}
}
}
},
"/auth/nostr/register-challenge": {
"get": {
"tags": [
"auth"
],
"summary": "Issue a single-use nonce for npub-native registration. UNAUTHENTICATED: no\nuser exists yet, so (unlike the link challenge) the nonce is subject-less; the\nregister signed-action binds it purely as replay protection.",
"operationId": "nostr_register_challenge",
"responses": {
"200": {
"description": "Single-use register nonce",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NostrLinkChallengeResponse"
}
}
}
}
}
}
},
"/auth/payment-invoice": {
"post": {
"tags": [
"auth"
],
"summary": "Create a registration-payment invoice on the operator's processor.",
"description": "Public (pre-registration) and deliberately cheap: it only binds an invoice to\nthe wallet's `pubkey_hash`, which is worthless to anyone who cannot later\nprove control of that BTC key at `/auth/extension`. The unauthenticated-surface\nrate limiter bounds invoice spam and unpaid invoices expire on the processor,\nso no signature proof is required here. Returns 400 when the pay gate is off\nor the wallet is already registered (it would bypass payment anyway).",
"operationId": "payment_invoice",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentInvoiceRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Payment invoice created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentInvoiceResponse"
}
}
}
},
"400": {
"description": "Pay gate off, already registered, or invalid key"
},
"503": {
"description": "Payment processor unavailable"
}
}
}
},
"/auth/pow-challenge": {
"post": {
"tags": [
"auth"
],
"summary": "Issue a fresh proof-of-work challenge for the wallet to solve before calling\n`/auth/extension`. The solved payload is sent back as the request's\n`altcha_solution`.",
"description": "Stateless: the challenge embeds an HMAC signature over its own fields plus an\nexpiry, so no issued-challenge store is needed. See [`crate::core::pow`].",
"operationId": "pow_challenge",
"responses": {
"200": {
"description": "Proof-of-work challenge for wallet registration"
}
}
}
},
"/auth/refresh": {
"post": {
"tags": [
"auth"
],
"summary": "Rotate a refresh token.",
"description": "Verifies the JWT, then re-looks-up the ACTIVE session by peppered hash\n(`get_session_by_token_hash` already filters revoked/expired) — a missing row\nmeans the token was revoked, expired, or already rotated, and is rejected.\nThe session's `user_id` is asserted equal to the JWT `sub` (defense-in-depth\nagainst any future token-confusion class). The old session is revoked and a\nfresh pair issued (revoke-then-issue); the `revoke_session` race-loser also\nrejects, so a stolen token cannot be reused.",
"operationId": "refresh_token",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RefreshTokenRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Token refreshed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AuthResponse"
}
}
}
}
}
}
},
"/auth/website/challenge": {
"post": {
"tags": [
"auth"
],
"summary": "Issue a website-auth challenge bound to the requesting origin.",
"description": "The challenge is stored in `state.web_challenges` keyed by its own nonce and\nexpires shortly (see [`WebChallenge`]). The returned `challenge` message is\nwhat the wallet signs; `challenge_id` is the nonce to present at verify.",
"operationId": "website_challenge",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WebsiteChallengeRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Website authentication challenge created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WebsiteChallengeResponse"
}
}
}
},
"400": {
"description": "Invalid origin"
}
}
}
},
"/auth/website/verify": {
"post": {
"tags": [
"auth"
],
"summary": "Verify a website-auth signature and mint a `Platform::Web` session.",
"description": "The challenge is CONSUMED (removed from the map) before any signature work,\nso it is strictly single-use: a replayed `challenge_id` — or the loser of two\nconcurrent verifies — finds nothing and is rejected. After consume we check\nexpiry, verify the signature over the challenge message with the algorithm\nfor the submitted asset, resolve the ALREADY-registered user by the proven\nkey's `pubkey_hash` (never creating one), and issue a session. All failure\nmessages are literals (no oracle).",
"operationId": "website_verify",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WebsiteVerifyRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Website authentication verified; session issued",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AuthResponse"
}
}
}
},
"400": {
"description": "Invalid asset or signature format"
},
"401": {
"description": "Invalid/expired challenge, bad signature, or unknown wallet"
}
}
}
},
"/capabilities": {
"get": {
"tags": [
"system"
],
"summary": "Describe this instance's enabled chains and features.",
"operationId": "capabilities",
"responses": {
"200": {
"description": "Enabled chains and features",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CapabilitiesResponse"
}
}
}
}
}
}
},
"/health": {
"get": {
"tags": [
"system"
],
"summary": "Liveness probe.",
"description": "Returns `200` with `{ \"status\": \"ok\" }` whenever the server is up. Used by\nload balancers and uptime checks; requires no authentication.",
"operationId": "health",
"responses": {
"200": {
"description": "Service is up",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HealthResponse"
}
}
}
}
}
}
},
"/keys": {
"post": {
"tags": [
"users"
],
"summary": "Register or update one of the authenticated user's per-asset public keys.",
"description": "Idempotent: the DB upsert keys on `(user_id, asset, key_type)`, so re-posting\nthe same asset replaces the stored key material in place.",
"operationId": "register_key",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RegisterKeyRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Key registered or updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserKeyInfo"
}
}
}
},
"400": {
"description": "Invalid asset"
},
"401": {
"description": "Missing or invalid token"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/premium/activate": {
"post": {
"tags": [
"premium"
],
"summary": "Verify a settled premium invoice and extend the caller's premium window.",
"operationId": "activate",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ActivateReq"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Premium extended",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StatusResp"
}
}
}
},
"400": {
"description": "Unknown, unpaid, or already-used invoice"
},
"401": {
"description": "Missing or invalid token"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/premium/invoice": {
"post": {
"tags": [
"premium"
],
"summary": "Mint a premium invoice for a chosen plan, bound to the caller.",
"operationId": "invoice",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvoiceReq"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Invoice minted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvoiceResp"
}
}
}
},
"400": {
"description": "Premium off or unknown plan"
},
"401": {
"description": "Missing or invalid token"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/premium/status": {
"get": {
"tags": [
"premium"
],
"summary": "The caller's current premium status.",
"operationId": "status",
"responses": {
"200": {
"description": "Premium status",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StatusResp"
}
}
}
},
"401": {
"description": "Missing or invalid token"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/prices": {
"get": {
"tags": [
"prices"
],
"summary": "Current fiat prices for the enabled feeds.",
"operationId": "prices",
"responses": {
"200": {
"description": "Latest cached prices",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PricesResponse"
}
}
}
},
"404": {
"description": "Price feed disabled on this server"
}
}
}
},
"/tips/social": {
"post": {
"tags": [
"tips"
],
"summary": "Create a public tip (optionally as a two-phase draft).",
"operationId": "create_social_tip",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateSocialTipRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Tip created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateSocialTipResponse"
}
}
}
},
"400": {
"description": "Tips off, targeted tip, or invalid input"
},
"401": {
"description": "Missing or invalid token"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/tips/social/claimable": {
"get": {
"tags": [
"tips"
],
"summary": "Tips CLAIMABLE by the caller. As with `received`, a public-only instance has\nno targeted-claimable inbox, so this is always empty (served 200, not 404).",
"operationId": "get_claimable_social_tips",
"responses": {
"200": {
"description": "Claimable tips (empty on a public-only instance)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SocialTipsResponse"
}
}
}
},
"400": {
"description": "Tips off"
},
"401": {
"description": "Missing or invalid token"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/tips/social/received": {
"get": {
"tags": [
"tips"
],
"summary": "Tips RECEIVED by the caller. Public-only instances have no targeted-recipient\ninbox (public tips are claimed via share URL, never delivered to a user), so\nthis is always empty — served (200) rather than 404 so the client's inbox\npoll doesn't error on a targeted-only endpoint this instance doesn't serve.",
"operationId": "get_received_social_tips",
"responses": {
"200": {
"description": "Received tips (empty on a public-only instance)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SocialTipsResponse"
}
}
}
},
"400": {
"description": "Tips off"
},
"401": {
"description": "Missing or invalid token"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/tips/social/sent": {
"get": {
"tags": [
"tips"
],
"summary": "All tips the caller has sent, newest first.",
"operationId": "get_sent_social_tips",
"responses": {
"200": {
"description": "Sent tips",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SocialTipsResponse"
}
}
}
},
"400": {
"description": "Tips off"
},
"401": {
"description": "Missing or invalid token"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/tips/social/{tip_id}/attach-funding": {
"post": {
"tags": [
"tips"
],
"summary": "Attach a broadcast funding tx to a draft tip, advancing it into the funding\nlifecycle (`pending_confirmation`; the funding worker later flips it to\n`pending` once the on-chain receipt is confirmed AND covers `amount`).",
"description": "Idempotent: re-attaching the SAME txid is a no-op that returns the row; a\nDIFFERENT txid on a tip that already has one (or a non-draft tip) is a 400;\nan unknown / not-owned tip is a 404. Response mirrors create: the same\n`CreateSocialTipResponse { tip_id, status, share_url }`.",
"operationId": "attach_funding",
"parameters": [
{
"name": "tip_id",
"in": "path",
"description": "Tip id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AttachFundingRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Funding attached",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateSocialTipResponse"
}
}
}
},
"400": {
"description": "Tips off, empty/oversized txid, or a conflicting funding tx"
},
"401": {
"description": "Missing or invalid token"
},
"404": {
"description": "No such tip owned by the caller"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/tips/social/{tip_id}/cancel": {
"post": {
"tags": [
"tips"
],
"summary": "Cancel a still-unfunded draft (owner-only, draft-only). A funded/claimed tip\nis recovered via clawback, not cancel.",
"operationId": "cancel_social_tip",
"parameters": [
{
"name": "tip_id",
"in": "path",
"description": "Tip id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Draft cancelled",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CancelTipResponse"
}
}
}
},
"400": {
"description": "Tips off"
},
"401": {
"description": "Missing or invalid token"
},
"404": {
"description": "No cancellable draft"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/tips/social/{tip_id}/claim": {
"post": {
"tags": [
"tips"
],
"summary": "Claim a public tip: lock it into `claiming` and return the encrypted claim\nkey + tip address so the caller can sweep the funds to their own wallet.",
"description": "AUTHED, no body. The `claiming` state is a UX signal, not a cryptographic\nlock — any URL holder may claim while the sweep hasn't confirmed (whoever\nwins the on-chain sweep race wins the tip; the reconciler resolves the\nwinner). A non-claimable tip (missing, underfunded, clawed back, already\nswept) is a 400.",
"operationId": "claim_social_tip",
"parameters": [
{
"name": "tip_id",
"in": "path",
"description": "Tip id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Tip locked into claiming",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClaimTipResponse"
}
}
}
},
"400": {
"description": "Tips off or tip not claimable"
},
"401": {
"description": "Missing or invalid token"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/tips/social/{tip_id}/clawback": {
"post": {
"tags": [
"tips"
],
"summary": "Sender reclaims a tip's funds. AUTHED, no body. Succeeds for the sender on a\npending / pending_confirmation / claiming / funding_mismatch / cancelled tip\nwhile its sweep hasn't confirmed; a settled or non-clawable tip is a 400.",
"operationId": "clawback_social_tip",
"parameters": [
{
"name": "tip_id",
"in": "path",
"description": "Tip id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Tip clawed back",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ClawbackTipResponse"
}
}
}
},
"400": {
"description": "Tips off or tip not clawable"
},
"401": {
"description": "Missing or invalid token"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/tips/social/{tip_id}/confirm-sweep": {
"post": {
"tags": [
"tips"
],
"summary": "Record the claimer's broadcast sweep txid (first-recorder-wins). RECORD not\nsettle: the tip stays `claiming` and the reconciler owns the settle to\n`claimed` once the sweep confirms on-chain. Idempotent; a second, different\ntxid does NOT overwrite the recorded winner. A tip that was never claimed is\na 400.",
"operationId": "confirm_sweep",
"parameters": [
{
"name": "tip_id",
"in": "path",
"description": "Tip id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConfirmSweepRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Sweep txid recorded",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConfirmSweepResponse"
}
}
}
},
"400": {
"description": "Tips off, empty/oversized txid, or tip not claiming"
},
"401": {
"description": "Missing or invalid token"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/tips/social/{tip_id}/public": {
"get": {
"tags": [
"tips"
],
"summary": "Public tip metadata for a share-URL holder. UNAUTHENTICATED: the tip id (a\nUUID) is the bearer token. 404s for an unknown or non-public tip.",
"operationId": "get_public_social_tip",
"parameters": [
{
"name": "tip_id",
"in": "path",
"description": "Tip id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Public tip info",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PublicTipInfo"
}
}
}
},
"400": {
"description": "Tips off"
},
"404": {
"description": "No such public tip"
}
}
}
},
"/users/by-username/{username}": {
"get": {
"tags": [
"users"
],
"summary": "Resolve a username to its user and that user's receiving public keys.",
"description": "Public: this is how a sender discovers where to send. The lookup is\ncase-insensitive (the handle is canonicalized lowercase). An unknown handle\nreturns the same constant-shape response with `registered: false`.",
"operationId": "lookup_username",
"parameters": [
{
"name": "username",
"in": "path",
"description": "Username to resolve",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Username resolution result",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LookupUsernameResponse"
}
}
}
}
}
}
},
"/users/me/username": {
"get": {
"tags": [
"users"
],
"summary": "The authenticated user's own username (or `null`).",
"operationId": "get_my_username",
"responses": {
"200": {
"description": "Caller's username",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MyUsernameResponse"
}
}
}
},
"401": {
"description": "Missing or invalid token"
}
},
"security": [
{
"bearer_auth": []
}
]
},
"post": {
"tags": [
"users"
],
"summary": "Claim or update the authenticated user's username.",
"description": "The submitted value is lowercased, validated, and checked against the\nreserved list before the DB write. The UNIQUE constraint is the atomic claim:\na collision (taken or reserved) is a 409, never a 500.",
"operationId": "set_username",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SetUsernameRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Username claimed or updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SetUsernameResponse"
}
}
}
},
"401": {
"description": "Missing or invalid token"
},
"409": {
"description": "Username is taken or reserved"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/users/{user_id}/keys": {
"get": {
"tags": [
"users"
],
"summary": "List a user's per-asset receiving public keys.",
"description": "Public: a sender fetches these to construct a payment to `user_id`.",
"operationId": "get_user_keys",
"parameters": [
{
"name": "user_id",
"in": "path",
"description": "User id (UUID)",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "The user's public keys",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserKeysResponse"
}
}
}
},
"400": {
"description": "Malformed user id"
}
}
}
},
"/users/{user_id}/keys/{asset}": {
"get": {
"tags": [
"users"
],
"summary": "Fetch a user's receiving public key for one asset.",
"operationId": "get_user_key_for_asset",
"parameters": [
{
"name": "user_id",
"in": "path",
"description": "User id (UUID)",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "asset",
"in": "path",
"description": "Asset (btc, ltc, xmr, wow, grin)",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "The user's key for this asset",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserKeyInfo"
}
}
}
},
"400": {
"description": "Invalid asset"
},
"404": {
"description": "User has no key for this asset"
}
}
}
},
"/wallet/grin/address/{addr}/user": {
"get": {
"tags": [
"grin"
],
"summary": "Resolve a bare Grin address to its owning user's Nostr routing identity.",
"description": "The address→npub bridge (see [`GrinAddressUserResponse`]). JWT-gated: only\nauthenticated senders resolve routing. An address unknown to this backend\nreturns the constant-shape `registered: false` response — the caller then\nfalls back to manual clipboard or the same-instance relay.",
"operationId": "address_user",
"parameters": [
{
"name": "addr",
"in": "path",
"description": "Grin address (registered public key)",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Address resolution result",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GrinAddressUserResponse"
}
}
}
},
"401": {
"description": "Missing or invalid token"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/grin/broadcast": {
"post": {
"tags": [
"grin"
],
"summary": "Broadcast a finalized, signed Grin transaction.",
"operationId": "broadcast",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GrinBroadcastRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Transaction broadcast",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GrinBroadcastResponse"
}
}
}
},
"400": {
"description": "Grin disabled or malformed transaction"
},
"401": {
"description": "Missing or invalid token"
},
"503": {
"description": "Upstream node unavailable"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/grin/height": {
"get": {
"tags": [
"grin"
],
"summary": "Current Grin chain tip height (for confirmation counting).",
"operationId": "height",
"responses": {
"200": {
"description": "Chain tip height",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GrinHeightResponse"
}
}
}
},
"400": {
"description": "Grin disabled"
},
"401": {
"description": "Missing or invalid token"
},
"503": {
"description": "Upstream node unavailable"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/grin/relay/cancel": {
"post": {
"tags": [
"grin_relay"
],
"summary": "Cancel a relay (sender or recipient), unless already finalized.",
"operationId": "cancel",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SlateIdRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Relay cancelled",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RelayEntry"
}
}
}
},
"401": {
"description": "Missing or invalid token"
},
"404": {
"description": "Relay not found, not yours, or feature disabled"
},
"409": {
"description": "Relay is already finalized"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/grin/relay/create": {
"post": {
"tags": [
"grin_relay"
],
"summary": "Post a slatepack addressed to a registered recipient (sender = caller).",
"operationId": "create",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateRelayRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Relay created (PendingRecipient)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RelayEntry"
}
}
}
},
"400": {
"description": "Invalid input or unregistered recipient"
},
"401": {
"description": "Missing or invalid token"
},
"404": {
"description": "Relay feature disabled"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/grin/relay/finalize": {
"post": {
"tags": [
"grin_relay"
],
"summary": "Record that the sender finalized + broadcast the transaction (sender only).\nThe backend does NOT finalize or broadcast — the wallet does that locally and\nreports the resulting tx hash here.",
"operationId": "finalize",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FinalizeRelayRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Relay marked finalized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RelayEntry"
}
}
}
},
"400": {
"description": "Invalid tx hash"
},
"401": {
"description": "Missing or invalid token"
},
"404": {
"description": "Relay not found, not yours, or feature disabled"
},
"409": {
"description": "Relay is not awaiting finalization"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/grin/relay/get": {
"post": {
"tags": [
"grin_relay"
],
"summary": "Poll a relay's current state (sender or recipient). The sender uses this to\nfetch the recipient's response once status is `pending_sender`.",
"operationId": "get_relay",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SlateIdRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "The relay entry",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RelayEntry"
}
}
}
},
"401": {
"description": "Missing or invalid token"
},
"404": {
"description": "Relay not found, not yours, or feature disabled"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/grin/relay/pending": {
"get": {
"tags": [
"grin_relay"
],
"summary": "The caller's inbox: relays awaiting their response.",
"operationId": "pending",
"responses": {
"200": {
"description": "Relays awaiting the caller's response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PendingRelaysResponse"
}
}
}
},
"401": {
"description": "Missing or invalid token"
},
"404": {
"description": "Relay feature disabled"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/grin/relay/respond": {
"post": {
"tags": [
"grin_relay"
],
"summary": "Attach the recipient's response, advancing to `pending_sender`.",
"operationId": "respond",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RespondRelayRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Response stored (PendingSender)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RelayEntry"
}
}
}
},
"400": {
"description": "Invalid response slatepack"
},
"401": {
"description": "Missing or invalid token"
},
"404": {
"description": "Relay not found, not yours, or feature disabled"
},
"409": {
"description": "Relay is not awaiting a recipient response"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/grin/scan": {
"post": {
"tags": [
"grin"
],
"summary": "View-only scan for the outputs a `rewind_hash` recognizes.",
"operationId": "scan",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GrinScanRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Recognized outputs + balance + resume index",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GrinScanResponse"
}
}
}
},
"400": {
"description": "Grin disabled or malformed rewind_hash"
},
"401": {
"description": "Missing or invalid token"
},
"503": {
"description": "Upstream node unavailable"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/heights": {
"get": {
"tags": [
"system"
],
"summary": "Current tip height for every enabled chain (one round-trip).",
"operationId": "heights",
"responses": {
"200": {
"description": "Tip height per chain (null if disabled/unreachable)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HeightsResponse"
}
}
}
},
"401": {
"description": "Missing or invalid token"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/lws/balance": {
"post": {
"tags": [
"xmr_wow"
],
"summary": "Balance + scan state for an account.",
"operationId": "balance",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ViewRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Account balance and scan state",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LwsBalanceResponse"
}
}
}
},
"400": {
"description": "Invalid/disabled asset, address, or view key"
},
"401": {
"description": "Missing or invalid token"
},
"503": {
"description": "Upstream node unavailable"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/lws/confirmations": {
"post": {
"tags": [
"xmr_wow"
],
"summary": "Confirmation count for a transaction (daemon).",
"operationId": "confirmations",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConfirmationsRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Confirmation count",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConfirmationsResponse"
}
}
}
},
"400": {
"description": "Invalid/disabled asset or txid"
},
"401": {
"description": "Missing or invalid token"
},
"503": {
"description": "Upstream node unavailable"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/lws/height": {
"post": {
"tags": [
"xmr_wow"
],
"summary": "Current chain height (daemon) for confirmation counting.",
"operationId": "height",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LwsAssetRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Chain height",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HeightResponse"
}
}
}
},
"400": {
"description": "Invalid/disabled asset"
},
"401": {
"description": "Missing or invalid token"
},
"503": {
"description": "Upstream node unavailable"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/lws/history": {
"post": {
"tags": [
"xmr_wow"
],
"summary": "Transaction history (confirmed + mempool) for an account.",
"operationId": "history",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ViewRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Account transaction history",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LwsHistoryResponse"
}
}
}
},
"400": {
"description": "Invalid/disabled asset, address, or view key"
},
"401": {
"description": "Missing or invalid token"
},
"503": {
"description": "Upstream node unavailable"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/lws/provision_subaddrs": {
"post": {
"tags": [
"xmr_wow"
],
"summary": "Provision account-0 subaddress indices at the LWS for an already-registered\naccount, and report the ceiling the LWS confirmed.",
"description": "Mounted only when `FEATURE_XMR_SUBADDR_PROVISIONING` is on (see\n`/capabilities` → `features.xmr_subaddr_provisioning`); otherwise the route\ndoes not exist and the request 404s.",
"operationId": "provision_subaddrs",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProvisionRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "LWS-confirmed subaddress ceiling for account 0",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProvisionResponse"
}
}
}
},
"400": {
"description": "Invalid/disabled asset, address, or view key"
},
"401": {
"description": "Missing or invalid token"
},
"404": {
"description": "Subaddress provisioning is not enabled on this instance"
},
"503": {
"description": "Upstream node unavailable, or it cannot provision subaddresses"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/lws/random_outs": {
"post": {
"tags": [
"xmr_wow"
],
"summary": "Random decoy outputs for ring construction.",
"operationId": "random_outs",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RandomOutsRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Decoy outputs",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RandomOutsResponse"
}
}
}
},
"400": {
"description": "Invalid/disabled asset or request"
},
"401": {
"description": "Missing or invalid token"
},
"503": {
"description": "Upstream node unavailable"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/lws/register": {
"post": {
"tags": [
"xmr_wow"
],
"summary": "Register an account (its view key) with the LWS so it begins scanning.\nIdempotent at the LWS; pass `start_height` to scan from a wallet birthday.",
"operationId": "register",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RegisterRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Account registered for scanning",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OkResponse"
}
}
}
},
"400": {
"description": "Invalid/disabled asset, address, or view key"
},
"401": {
"description": "Missing or invalid token"
},
"503": {
"description": "Upstream node unavailable"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/lws/submit_tx": {
"post": {
"tags": [
"xmr_wow"
],
"summary": "Broadcast a finalized, signed Monero/Wownero transaction.",
"operationId": "submit_tx",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SubmitRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Transaction submitted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OkResponse"
}
}
}
},
"400": {
"description": "Invalid/disabled asset or malformed tx hex"
},
"401": {
"description": "Missing or invalid token"
},
"503": {
"description": "Upstream node unavailable"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/lws/unspent_outs": {
"post": {
"tags": [
"xmr_wow"
],
"summary": "Unspent outputs for an account (for client-side spend construction).",
"operationId": "unspent_outs",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ViewRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Unspent outputs + fee parameters",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnspentOutsResponse"
}
}
}
},
"400": {
"description": "Invalid/disabled asset, address, or view key"
},
"401": {
"description": "Missing or invalid token"
},
"503": {
"description": "Upstream node unavailable"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/utxo/balance": {
"post": {
"tags": [
"btc_ltc"
],
"summary": "Confirmed/unconfirmed balance for a BTC/LTC address.",
"operationId": "balance",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AddressRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Address balance in satoshis",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BalanceResponse"
}
}
}
},
"400": {
"description": "Invalid/disabled asset or address"
},
"401": {
"description": "Missing or invalid token"
},
"503": {
"description": "Upstream node unavailable"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/utxo/balance_multi": {
"post": {
"tags": [
"btc_ltc"
],
"summary": "Confirmed/unconfirmed balance summed across a batch of BTC/LTC addresses.",
"operationId": "balance_multi",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MultiAddressRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Summed balance in satoshis",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MultiBalanceResponse"
}
}
}
},
"400": {
"description": "Invalid/disabled asset, address, or oversized batch"
},
"401": {
"description": "Missing or invalid token"
},
"503": {
"description": "Upstream node unavailable"
},
"504": {
"description": "The batch exceeded its deadline; retry with a smaller batch"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/utxo/broadcast": {
"post": {
"tags": [
"btc_ltc"
],
"summary": "Broadcast a finalized, signed BTC/LTC transaction.",
"operationId": "broadcast",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BroadcastRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Broadcast accepted; returns the txid",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BroadcastResponse"
}
}
}
},
"400": {
"description": "Invalid/disabled asset or malformed tx hex"
},
"401": {
"description": "Missing or invalid token"
},
"503": {
"description": "Upstream node unavailable"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/utxo/fee": {
"post": {
"tags": [
"btc_ltc"
],
"summary": "Fee-rate estimate (sat/vB) for confirmation within `blocks` blocks.",
"operationId": "fee",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FeeRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Fee estimate",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FeeResponse"
}
}
}
},
"400": {
"description": "Invalid/disabled asset"
},
"401": {
"description": "Missing or invalid token"
},
"503": {
"description": "Upstream node unavailable"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/utxo/history": {
"post": {
"tags": [
"btc_ltc"
],
"summary": "Confirmed + mempool transaction history for a BTC/LTC address.",
"operationId": "history",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AddressRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Transaction history",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HistoryResponse"
}
}
}
},
"400": {
"description": "Invalid/disabled asset or address"
},
"401": {
"description": "Missing or invalid token"
},
"503": {
"description": "Upstream node unavailable"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/utxo/history_multi": {
"post": {
"tags": [
"btc_ltc"
],
"summary": "Confirmed + mempool history across a batch of BTC/LTC addresses, each entry\ntagged with its owning address.",
"operationId": "history_multi",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MultiAddressRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Address-tagged transaction history",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MultiHistoryResponse"
}
}
}
},
"400": {
"description": "Invalid/disabled asset, address, or oversized batch"
},
"401": {
"description": "Missing or invalid token"
},
"503": {
"description": "Upstream node unavailable"
},
"504": {
"description": "The batch exceeded its deadline; retry with a smaller batch"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/utxo/tip": {
"post": {
"tags": [
"btc_ltc"
],
"summary": "Best-chain tip height (for client-side confirmation counting).",
"operationId": "tip",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AssetRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Chain tip height",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TipResponse"
}
}
}
},
"400": {
"description": "Invalid/disabled asset"
},
"401": {
"description": "Missing or invalid token"
},
"503": {
"description": "Upstream node unavailable"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/utxo/utxos": {
"post": {
"tags": [
"btc_ltc"
],
"summary": "Unspent outputs for a BTC/LTC address.",
"operationId": "utxos",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AddressRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Unspent outputs",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UtxosResponse"
}
}
}
},
"400": {
"description": "Invalid/disabled asset or address"
},
"401": {
"description": "Missing or invalid token"
},
"503": {
"description": "Upstream node unavailable"
}
},
"security": [
{
"bearer_auth": []
}
]
}
},
"/wallet/utxo/utxos_multi": {
"post": {
"tags": [
"btc_ltc"
],
"summary": "Unspent outputs across a batch of BTC/LTC addresses, each tagged with its\nowning address (the wallet never re-guesses UTXO ownership).",
"operationId": "utxos_multi",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MultiAddressRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Address-tagged unspent outputs",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MultiUtxosResponse"
}
}
}
},
"400": {
"description": "Invalid/disabled asset, address, or oversized batch"
},
"401": {
"description": "Missing or invalid token"
},
"503": {
"description": "Upstream node unavailable"
},
"504": {
"description": "The batch exceeded its deadline; retry with a smaller batch"
}
},
"security": [
{
"bearer_auth": []
}
]
}
}
},
"components": {
"schemas": {
"ActivateReq": {
"type": "object",
"required": [
"invoice_id"
],
"properties": {
"invoice_id": {
"type": "string"
}
}
},
"AddressRequest": {
"type": "object",
"description": "An address query for a UTXO asset.",
"required": [
"asset",
"address"
],
"properties": {
"address": {
"type": "string",
"description": "The address to query (validated by the Electrum client)."
},
"asset": {
"type": "string",
"description": "`btc` or `ltc`."
}
}
},
"AmountOutsDto": {
"type": "object",
"required": [
"amount",
"outputs"
],
"properties": {
"amount": {
"type": "string"
},
"outputs": {
"type": "array",
"items": {
"$ref": "#/components/schemas/RandomOutputDto"
}
}
}
},
"AssetPublicKey": {
"type": "object",
"description": "A per-asset public key submitted by the wallet.",
"required": [
"asset",
"public_key"
],
"properties": {
"asset": {
"type": "string"
},
"public_key": {
"type": "string"
},
"public_spend_key": {
"type": [
"string",
"null"
],
"description": "XMR/WOW only: public spend key."
}
}
},
"AssetRequest": {
"type": "object",
"description": "An asset-only query.",
"required": [
"asset"
],
"properties": {
"asset": {
"type": "string",
"description": "`btc` or `ltc`."
}
}
},
"AssetSignature": {
"type": "object",
"description": "One asset's signature over the challenge message.",
"required": [
"asset",
"signature",
"public_key"
],
"properties": {
"asset": {
"type": "string",
"description": "Asset whose key produced the signature (`btc`, `ltc`, `xmr`, `wow`, `grin`)."
},
"public_key": {
"type": "string",
"description": "The public key that produced the signature (format depends on asset)."
},
"signature": {
"type": "string",
"description": "Signature over the challenge message. BIP-137 base64 for BTC/LTC; 64-byte\nhex Ed25519 for XMR/WOW/Grin."
}
}
},
"AttachFundingRequest": {
"type": "object",
"description": "Attach a broadcast funding transaction to a draft tip.",
"required": [
"funding_txid"
],
"properties": {
"funding_txid": {
"type": "string",
"description": "The on-chain funding transaction id (XMR/WOW txid, BTC/LTC txid)."
}
}
},
"AuthResponse": {
"type": "object",
"description": "Successful session response: a JWT pair plus minimal user info.",
"required": [
"access_token",
"refresh_token",
"expires_in",
"user",
"is_new"
],
"properties": {
"access_token": {
"type": "string"
},
"expires_in": {
"type": "integer",
"format": "int64",
"description": "Access-token lifetime in seconds."
},
"is_new": {
"type": "boolean",
"description": "`true` when this request created the user (extension registration only)."
},
"refresh_token": {
"type": "string"
},
"user": {
"$ref": "#/components/schemas/UserInfo"
}
}
},
"BalanceResponse": {
"type": "object",
"description": "Confirmed/unconfirmed balance in satoshis. The client computes any total\n(kept as separate integer fields — no lossy server-side sum).",
"required": [
"asset",
"address",
"confirmed",
"unconfirmed"
],
"properties": {
"address": {
"type": "string"
},
"asset": {
"type": "string"
},
"confirmed": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"unconfirmed": {
"type": "integer",
"format": "int64"
}
}
},
"BroadcastRequest": {
"type": "object",
"description": "A broadcast request: a finalized, signed raw transaction.",
"required": [
"asset",
"tx_hex"
],
"properties": {
"asset": {
"type": "string",
"description": "`btc` or `ltc`."
},
"tx_hex": {
"type": "string",
"description": "Hex-encoded signed transaction."
}
}
},
"BroadcastResponse": {
"type": "object",
"required": [
"txid"
],
"properties": {
"txid": {
"type": "string",
"description": "The broadcast transaction id."
}
}
},
"CancelTipResponse": {
"type": "object",
"required": [
"ok"
],
"properties": {
"ok": {
"type": "boolean"
}
}
},
"CapabilitiesResponse": {
"type": "object",
"required": [
"version",
"contract_version",
"chains",
"features",
"restore",
"registration"
],
"properties": {
"chains": {
"$ref": "#/components/schemas/ChainCapabilities"
},
"contract_version": {
"type": "integer",
"format": "int32",
"description": "Capabilities contract version (additive changes do not bump it).",
"minimum": 0
},
"features": {
"$ref": "#/components/schemas/FeatureCapabilities"
},
"feed": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/FeedCapability",
"description": "Public curated feed details; present only when `features.feed`."
}
]
},
"messaging": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/MessagingCapability",
"description": "First-party Nostr relay details; present only when `features.nostr_relay`."
}
]
},
"premium": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/PremiumCapability",
"description": "Premium tier details; present only when `features.premium_relay`."
}
]
},
"registration": {
"$ref": "#/components/schemas/RegistrationCapability",
"description": "Registration gates for a new wallet on this instance."
},
"restore": {
"$ref": "#/components/schemas/RestoreCapability",
"description": "Wallet restore (import) policy for this instance."
},
"version": {
"type": "string",
"description": "Backend version (Cargo package version)."
}
}
},
"ChainCapabilities": {
"type": "object",
"required": [
"btc",
"ltc",
"xmr",
"wow",
"grin"
],
"properties": {
"btc": {
"$ref": "#/components/schemas/ChainCapability"
},
"grin": {
"$ref": "#/components/schemas/ChainCapability"
},
"ltc": {
"$ref": "#/components/schemas/ChainCapability"
},
"wow": {
"$ref": "#/components/schemas/ChainCapability"
},
"xmr": {
"$ref": "#/components/schemas/ChainCapability"
}
}
},
"ChainCapability": {
"type": "object",
"description": "Per-chain availability. `network` is the configured network for UTXO chains\n(so the wallet derives addresses for the right one); `null` for chains whose\nnetwork isn't a backend setting.",
"required": [
"enabled"
],
"properties": {
"enabled": {
"type": "boolean"
},
"network": {
"type": [
"string",
"null"
]
}
}
},
"CheckRestoreRequest": {
"type": "object",
"description": "Ask whether a wallet (by seed fingerprint) was created on this backend, and\nwhether the submitted keys match.",
"required": [
"fingerprint",
"keys"
],
"properties": {
"fingerprint": {
"type": "string",
"description": "Seed fingerprint (16 or 64 hex chars)."
},
"keys": {
"type": "array",
"items": {
"$ref": "#/components/schemas/AssetPublicKey"
},
"description": "Submitted per-asset public keys, for verification."
}
}
},
"CheckRestoreResponse": {
"type": "object",
"description": "Restore-check result. Constant-shape: the same fields are always present so\nthe response is not a structure oracle, and `user_id` is never returned (no\nenumeration).\n\nNOTE: `exists` is an INTENTIONAL disclosure (the wallet uses it to decide\nwhether to offer a restore). It is throttled per fingerprint AND per IP, and\nnothing downstream treats fingerprint-existence as authority — the\nderivation-rotation path in [`extension_register`] requires a key-control\nproof, not a bare fingerprint.",
"required": [
"exists"
],
"properties": {
"exists": {
"type": "boolean",
"description": "Whether the fingerprint exists on this backend."
},
"keys_valid": {
"type": [
"boolean",
"null"
],
"description": "Whether every submitted key matches the stored key. `None` when the\nfingerprint does not exist."
},
"wow_start_height": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "WOW scan-start height, if the wallet was created here."
},
"xmr_start_height": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "XMR scan-start height, if the wallet was created here."
}
}
},
"ClaimTipResponse": {
"type": "object",
"description": "Claim a tip: lock it into `claiming` and receive the encrypted claim key +\ntip address to sweep. Load-bearing INCONSISTENT key: `success` (not `ok`).",
"required": [
"success"
],
"properties": {
"encrypted_key": {
"type": [
"string",
"null"
],
"description": "Hex-encoded AES-GCM ciphertext of the claim secret; `null` if the tip\nstored none. Useless without the URL-fragment key."
},
"success": {
"type": "boolean"
},
"tip_address": {
"type": [
"string",
"null"
]
}
}
},
"ClawbackTipResponse": {
"type": "object",
"description": "Sender clawback of a tip. Load-bearing INCONSISTENT key: `success` (not `ok`).",
"required": [
"success"
],
"properties": {
"success": {
"type": "boolean"
}
}
},
"ConfirmSweepRequest": {
"type": "object",
"description": "Record the claimer's broadcast sweep transaction (first-recorder-wins).",
"required": [
"sweep_txid"
],
"properties": {
"sweep_txid": {
"type": "string",
"description": "The broadcast sweep transaction id."
}
}
},
"ConfirmSweepResponse": {
"type": "object",
"required": [
"status"
],
"properties": {
"status": {
"type": "string"
},
"sweep_txid": {
"type": [
"string",
"null"
],
"description": "The WINNING (first-recorded) sweep txid, or `null` if none is recorded."
}
}
},
"ConfirmationsRequest": {
"type": "object",
"description": "Confirmation-count query for a tx.",
"required": [
"asset",
"txid"
],
"properties": {
"asset": {
"type": "string"
},
"txid": {
"type": "string",
"description": "Transaction id (64 hex)."
}
}
},
"ConfirmationsResponse": {
"type": "object",
"required": [
"asset"
],
"properties": {
"asset": {
"type": "string"
},
"confirmations": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "Confirmations, or `null` if the tx is unknown / not yet in a block.",
"minimum": 0
}
}
},
"CreateRelayRequest": {
"type": "object",
"description": "Create a relay: post an (encrypted) slatepack addressed to a registered user.",
"required": [
"recipient_user_id",
"slate_id",
"slatepack",
"amount_nanogrin"
],
"properties": {
"amount_nanogrin": {
"type": "integer",
"format": "int64",
"description": "Amount in nanogrin (informational; the real amount is in the slate)."
},
"recipient_user_id": {
"type": "string",
"format": "uuid",
"description": "Recipient (a registered Smirk user id)."
},
"slate_id": {
"type": "string",
"description": "The Grin slate id (the transaction's slate UUID)."
},
"slatepack": {
"type": "string",
"description": "The armored slatepack (encrypted to the recipient)."
}
}
},
"CreateSocialTipRequest": {
"type": "object",
"description": "Create a public tip. Targeted-only fields (`platform`, `username`,\n`grin_commitment`, `sender_anonymous`) are accepted for wire-compat but\nignored in this port.",
"required": [
"asset",
"amount",
"is_public"
],
"properties": {
"amount": {
"type": "integer",
"format": "int64"
},
"asset": {
"type": "string"
},
"claim_key_hash": {
"type": [
"string",
"null"
],
"description": "SHA256 of the claim key (hex); the key itself never touches the server."
},
"encrypted_key": {
"type": [
"string",
"null"
],
"description": "Hex-encoded AES-GCM ciphertext of the claim secret; the backend cannot\ndecrypt it."
},
"funding_txid": {
"type": [
"string",
"null"
]
},
"grin_commitment": {
"type": [
"string",
"null"
]
},
"is_public": {
"type": "boolean"
},
"platform": {
"type": [
"string",
"null"
]
},
"sender_anonymous": {
"type": "boolean"
},
"tip_address": {
"type": [
"string",
"null"
]
},
"tip_view_key": {
"type": [
"string",
"null"
],
"description": "Private view key for the tip address (XMR/WOW), so the backend can scan\nit for funding + sweep."
},
"username": {
"type": [
"string",
"null"
]
}
}
},
"CreateSocialTipResponse": {
"type": "object",
"required": [
"tip_id",
"status"
],
"properties": {
"share_url": {
"type": [
"string",
"null"
],
"description": "`{TIP_SHARE_BASE_URL}/{tip_id}` for a public tip, else null."
},
"status": {
"type": "string"
},
"tip_id": {
"type": "string"
}
}
},
"ExtensionRegisterRequest": {
"type": "object",
"description": "Register a new extension wallet or re-authenticate an existing one.",
"required": [
"keys",
"signed_timestamp",
"signature"
],
"properties": {
"altcha_solution": {
"type": "object",
"description": "Optional proof-of-work solution. Required when the PoW gate applies to\nthis pubkey (see [`pow_applies`]); otherwise ignored."
},
"invite_code": {
"type": [
"string",
"null"
],
"description": "Operator-minted invite code. Required when the instance enables the invite\nregistration gate (see `/capabilities` → `registration.invite_required`);\nignored otherwise and for returning wallets."
},
"keys": {
"type": "array",
"items": {
"$ref": "#/components/schemas/AssetPublicKey"
},
"description": "Public keys for each supported asset. A `btc` key is required (it is the\nidentity)."
},
"payment_invoice_id": {
"type": [
"string",
"null"
],
"description": "Settled payment-invoice id from `/auth/payment-invoice`. Required when the\ninstance enables the pay-to-register gate (see `/capabilities` →\n`registration.payment_required`); ignored otherwise and for returning\nwallets. Its settlement is verified against the processor and the invoice\natomically consumed (single-use) before the wallet is granted."
},
"rotation_signature": {
"type": [
"string",
"null"
],
"description": "Derivation-rotation proof: BIP-137 base64 signature of the SAME\n`smirk-auth-{signed_timestamp}` message under the BTC key ALREADY ON FILE\nfor the user identified by `seed_fingerprint`. Required to re-point an\nexisting user row; without it (or if it does not verify against the stored\nkey) a fingerprint match is treated as a brand-new identity and the\nexisting row is never touched. See [`extension_register`]."
},
"seed_fingerprint": {
"type": [
"string",
"null"
],
"description": "Seed fingerprint `hex(SHA256(SHA256(seed))[..])`. Used for restore and to\nLOCATE a candidate user row for the derivation-rotation path. By itself it\nis NOT authority: a rotation also requires `rotation_signature` below."
},
"signature": {
"type": "string",
"description": "BIP-137 base64 signature of `smirk-auth-{signed_timestamp}` under the\nSUBMITTED (new) BTC key. Proves control of the key in `keys`."
},
"signed_timestamp": {
"type": "integer",
"format": "int64",
"description": "Unix seconds that were signed to prove BTC key ownership."
},
"username": {
"type": [
"string",
"null"
],
"description": "Optional reserved username."
},
"wallet_birthday": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "Wallet creation time (unix seconds), to bound chain scans."
},
"wow_start_height": {
"type": [
"integer",
"null"
],
"format": "int64"
},
"xmr_start_height": {
"type": [
"integer",
"null"
],
"format": "int64"
}
}
},
"FeatureCapabilities": {
"type": "object",
"required": [
"grin_relay",
"grin_lws",
"prices",
"nostr_identity",
"nostr_native_auth",
"nostr_relay",
"premium_relay",
"feed",
"tips",
"xmr_subaddr_provisioning",
"utxo_multi_address"
],
"properties": {
"feed": {
"type": "boolean",
"description": "Public curated Nostr feed for this instance. See `feed`."
},
"grin_lws": {
"type": "boolean",
"description": "Grin light-wallet-server (grin-lws) scan path is available — fast balance\nscans via grin-lws, with the authoritative grin-wallet scan as fallback."
},
"grin_relay": {
"type": "boolean",
"description": "Grin async slatepack relay mailbox."
},
"nostr_identity": {
"type": "boolean",
"description": "Nostr-native identity (NIP-98 login/link, NIP-05 directory)."
},
"nostr_native_auth": {
"type": "boolean",
"description": "npub-native registration: a wallet can register + authenticate from its\nseed-derived Nostr key alone (POST /auth/nostr/register), no BTC signature.\nThe wallet uses this to choose the NIP-98 bootstrap over the legacy BTC one."
},
"nostr_relay": {
"type": "boolean",
"description": "First-party Nostr relay (encrypted DM inbox). See `messaging` for details."
},
"premium_relay": {
"type": "boolean",
"description": "Paid premium tier for general Nostr posting to the relay. See `premium`."
},
"prices": {
"type": "boolean",
"description": "Fiat price feed."
},
"tips": {
"type": "boolean",
"description": "Tipping (parked)."
},
"utxo_multi_address": {
"type": "boolean",
"description": "Batch (multi-address) BTC/LTC queries: this instance serves\n`POST /wallet/utxo/{balance,utxos,history}_multi`. Off ⇒ those routes are\nnot mounted (404) and the wallet must query one address at a time."
},
"xmr_subaddr_provisioning": {
"type": "boolean",
"description": "Monero/Wownero subaddress provisioning: this instance registers a batch\nof account-0 subaddress indices with its LWS and serves\n`POST /wallet/lws/provision_subaddrs`. Off ⇒ that route is not mounted\n(404) and the wallet must keep receiving on the primary address only."
}
}
},
"FeeRequest": {
"type": "object",
"description": "A fee-estimate request.",
"required": [
"asset",
"blocks"
],
"properties": {
"asset": {
"type": "string",
"description": "`btc` or `ltc`."
},
"blocks": {
"type": "integer",
"format": "int32",
"description": "Target confirmation within this many blocks.",
"minimum": 0
}
}
},
"FeeResponse": {
"type": "object",
"required": [
"asset"
],
"properties": {
"asset": {
"type": "string"
},
"sat_per_vb": {
"type": [
"number",
"null"
],
"format": "double",
"description": "Estimated fee rate in sat/vB, or `null` if the server can't estimate."
}
}
},
"FeedCapability": {
"type": "object",
"description": "Public curated feed details (present only when `features.feed`). A read-only\nweb feed (e.g. `feed.<domain>`) reads from the relay and shows posts per the\noperator's curation knobs.",
"required": [
"relay_url",
"show_owner",
"show_premium",
"allowlist_npubs",
"extra_relays"
],
"properties": {
"allowlist_npubs": {
"type": "array",
"items": {
"type": "string"
},
"description": "Specific featured npubs to surface."
},
"extra_relays": {
"type": "array",
"items": {
"type": "string"
},
"description": "Extra relays to also pull the allowlisted authors from."
},
"owner_npub": {
"type": [
"string",
"null"
],
"description": "The operator's npub for owner filtering + display; `null` when unset."
},
"relay_url": {
"type": "string",
"description": "The relay the feed reads from (mirrors `messaging.relay_url`)."
},
"show_owner": {
"type": "boolean",
"description": "Include the operator's own posts."
},
"show_premium": {
"type": "boolean",
"description": "Include premium members' posts (all general notes on the gated relay)."
}
}
},
"FinalizeRelayRequest": {
"type": "object",
"description": "Record that the sender finalized + broadcast the transaction.",
"required": [
"slate_id",
"tx_hash"
],
"properties": {
"slate_id": {
"type": "string"
},
"tx_hash": {
"type": "string",
"description": "The broadcast transaction hash/kernel (recorded for reference)."
}
}
},
"GrinAddressUserResponse": {
"type": "object",
"description": "Resolution of a bare Grin address to its owning user's routing identity.\n\nThe address→npub **bridge**: given a `grin1…` slatepack address, report\nwhether it belongs to a user registered on *this* backend and, if so, that\nuser's linked Nostr pubkey so a bare-address send can be upgraded to a\nfederated Nostr gift-wrap instead of the same-instance relay.\n\nNOTE (federation): this is a **same-instance convenience**. It can only\nresolve addresses whose owner registered their Grin key here (via `POST\n/keys`). It is *not* a federated directory — a wallet on another backend\nwon't be found. `registered: false` (a constant-shape miss, mirroring\n`GET /users/by-username`) means \"unknown to this instance\", at which point\nthe sender falls back to manual clipboard / the backend relay.",
"required": [
"registered"
],
"properties": {
"npub": {
"type": [
"string",
"null"
],
"description": "The owning user's linked Nostr pubkey (x-only hex), when they have one.\nThis is what the sender feeds the Nostr gift-wrap channel to route the\nslate over Nostr. `None` when the user registered no Nostr identity."
},
"registered": {
"type": "boolean",
"description": "Whether the address resolved to a user on this backend."
},
"user_id": {
"type": [
"string",
"null"
],
"description": "The owning user's id (UUID), when `registered`."
}
}
},
"GrinBroadcastRequest": {
"type": "object",
"description": "Broadcast a finalized, signed Grin transaction (built + signed by the wallet).",
"required": [
"tx"
],
"properties": {
"tx": {
"type": "object",
"description": "The finalized transaction object (grin node `push_transaction` input)."
}
}
},
"GrinBroadcastResponse": {
"type": "object",
"required": [
"ok"
],
"properties": {
"ok": {
"type": "boolean"
}
}
},
"GrinHeightResponse": {
"type": "object",
"required": [
"height"
],
"properties": {
"height": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},
"GrinOutput": {
"type": "object",
"description": "A single output recovered by a view-only scan. Amounts are nanogrin.",
"required": [
"commit",
"value",
"height",
"mmr_index",
"is_coinbase",
"lock_height"
],
"properties": {
"commit": {
"type": "string"
},
"height": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"is_coinbase": {
"type": "boolean"
},
"key_id": {
"type": [
"string",
"null"
],
"description": "Recovered derivation key id — populated only on the grin-lws path."
},
"lock_height": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"mmr_index": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"n_child": {
"type": [
"integer",
"null"
],
"format": "int32",
"description": "Recovered child index — populated only on the grin-lws path.",
"minimum": 0
},
"spendable": {
"type": [
"boolean",
"null"
],
"description": "Spendable at the current tip — populated only on the grin-lws path."
},
"value": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},
"GrinScanRequest": {
"type": "object",
"description": "A view-only scan request. Carries the `rewind_hash` view credential, so it\ndeliberately omits `Debug` (never logged).",
"required": [
"rewind_hash"
],
"properties": {
"restore_pow_nonce": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "Restore proof-of-work nonce. Required when the instance prices the\nrequested restore depth (see `/capabilities` → `restore.pow_*`); bound to\n`(grin, rewind_hash, start_height)` (see `restore_pow`).",
"minimum": 0
},
"rewind_hash": {
"type": "string",
"description": "The wallet's `rewind_hash` (64 hex). Forwarded to grin-wallet; not stored."
},
"start_height": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "Scan from this block height (wallet birthday / last scanned). Omit for full.",
"minimum": 0
}
}
},
"GrinScanResponse": {
"type": "object",
"description": "Result of a view-only scan: recognized outputs, total, and the resume index.",
"required": [
"outputs",
"total_balance",
"last_pmmr_index"
],
"properties": {
"blockchain_height": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "Chain tip observed by grin-lws at scan time — present only on the grin-lws\npath.",
"minimum": 0
},
"last_pmmr_index": {
"type": "integer",
"format": "int64",
"description": "Resume point (`last_pmmr_index`) for the next incremental scan.",
"minimum": 0
},
"outputs": {
"type": "array",
"items": {
"$ref": "#/components/schemas/GrinOutput"
}
},
"scanned_height": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "How far grin-lws has scanned this account — present only on the grin-lws\npath (the authoritative grin-wallet path leaves it null).",
"minimum": 0
},
"total_balance": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},
"HealthResponse": {
"type": "object",
"description": "Liveness response.",
"required": [
"status"
],
"properties": {
"status": {
"type": "string",
"description": "Always `\"ok\"` when the service is reachable."
}
}
},
"HeightResponse": {
"type": "object",
"required": [
"asset",
"height"
],
"properties": {
"asset": {
"type": "string"
},
"height": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},
"HeightsResponse": {
"type": "object",
"description": "Current tip height per chain. `null` = the chain is disabled on this instance\nor its source was unreachable for this request.",
"properties": {
"btc": {
"type": [
"integer",
"null"
],
"format": "int64",
"example": 955860
},
"grin": {
"type": [
"integer",
"null"
],
"format": "int64"
},
"ltc": {
"type": [
"integer",
"null"
],
"format": "int64"
},
"wow": {
"type": [
"integer",
"null"
],
"format": "int64"
},
"xmr": {
"type": [
"integer",
"null"
],
"format": "int64"
}
}
},
"HistoryEntry": {
"type": "object",
"description": "A transaction-history entry.",
"required": [
"txid",
"height"
],
"properties": {
"fee": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "Fee in satoshis (mempool entries only).",
"minimum": 0
},
"height": {
"type": "integer",
"format": "int64",
"description": "Block height (`0`/negative for unconfirmed)."
},
"txid": {
"type": "string"
}
}
},
"HistoryResponse": {
"type": "object",
"required": [
"asset",
"address",
"transactions"
],
"properties": {
"address": {
"type": "string"
},
"asset": {
"type": "string"
},
"transactions": {
"type": "array",
"items": {
"$ref": "#/components/schemas/HistoryEntry"
}
}
}
},
"InvoiceReq": {
"type": "object",
"required": [
"plan"
],
"properties": {
"plan": {
"type": "string",
"description": "Plan id (see `/capabilities` → `premium.plans`), e.g. `quarter`."
}
}
},
"InvoiceResp": {
"type": "object",
"required": [
"invoice_id",
"pay_to",
"plan",
"amount",
"currency"
],
"properties": {
"amount": {
"type": "string"
},
"currency": {
"type": "string"
},
"invoice_id": {
"type": "string"
},
"pay_to": {
"type": "string",
"description": "Where to pay (a checkout URL or address)."
},
"plan": {
"type": "string"
}
}
},
"LogoutRequest": {
"type": "object",
"required": [
"refresh_token"
],
"properties": {
"refresh_token": {
"type": "string"
}
}
},
"LogoutResponse": {
"type": "object",
"required": [
"success"
],
"properties": {
"success": {
"type": "boolean"
}
}
},
"LookupUsernameResponse": {
"type": "object",
"description": "Result of resolving a username. Constant-shape: the same fields are always\npresent, so the response is not a structure oracle.",
"required": [
"registered"
],
"properties": {
"public_keys": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/PublicKeysInfo",
"description": "The user's per-asset receiving public keys, if registered."
}
]
},
"registered": {
"type": "boolean",
"description": "Whether a user owns this username."
},
"user_id": {
"type": [
"string",
"null"
],
"description": "The resolved user id (UUID string), if registered."
},
"username": {
"type": [
"string",
"null"
],
"description": "The canonical (lowercased) username, if registered."
}
}
},
"LwsAssetRequest": {
"type": "object",
"description": "Asset-only query.",
"required": [
"asset"
],
"properties": {
"asset": {
"type": "string",
"description": "`xmr` or `wow`."
}
}
},
"LwsBalanceResponse": {
"type": "object",
"description": "Balance + scan state, as a **verification passthrough**. The backend holds no\nspend key, so it cannot net out spends; the wallet computes the true spendable\nbalance client-side: `total_received − sum(spent_outputs it verifies with the\nspend key) − locked_balance`. `spent_outputs` are therefore CANDIDATES (some\nare ring decoys of the user's own outputs); `pending_balance` is the 0-conf\n(mempool) received.",
"required": [
"asset",
"total_received",
"locked_balance",
"pending_balance",
"start_height",
"scanned_height",
"blockchain_height",
"transaction_count",
"spent_outputs"
],
"properties": {
"asset": {
"type": "string"
},
"blockchain_height": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"locked_balance": {
"type": "string"
},
"pending_balance": {
"type": "string",
"description": "Unconfirmed (mempool) received — 0-conf. `0` until the LWS reports mempool\nrows (the monero-lws mempool feature); never negative."
},
"scanned_height": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"spent_outputs": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SpentOutputDto"
},
"description": "Candidate spent outputs (confirmed + mempool) for client-side key-image\nverification with the spend key. Never authoritative server-side."
},
"start_height": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"total_received": {
"type": "string",
"example": "12345000000000"
},
"transaction_count": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},
"LwsHistoryResponse": {
"type": "object",
"required": [
"asset",
"transactions"
],
"properties": {
"asset": {
"type": "string"
},
"transactions": {
"type": "array",
"items": {
"$ref": "#/components/schemas/TxDto"
}
}
}
},
"MessagingCapability": {
"type": "object",
"description": "First-party Nostr relay details (present only when `features.nostr_relay`).\nThe wallet connects here as its DM inbox (alongside the public interop\nrelays) and adapts its UI to the policy.",
"required": [
"relay_url",
"write_policy",
"inbound_pow_bits",
"supported_nips"
],
"properties": {
"inbound_pow_bits": {
"type": "integer",
"format": "int32",
"description": "NIP-13 PoW bits required on cross-ecosystem inbound (0 = off).",
"minimum": 0
},
"relay_url": {
"type": "string",
"description": "The ws(s):// relay URL clients connect to."
},
"supported_nips": {
"type": "array",
"items": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"description": "NIPs the relay speaks (e.g. `[1, 17, 44, 59]`)."
},
"write_policy": {
"type": "string",
"description": "Write policy: `inbox-outbox` | `author-allowlist` | `open` | `premium-post`."
}
}
},
"MultiAddressRequest": {
"type": "object",
"description": "A batch query for several addresses of one UTXO asset. The list is bounded\nserver-side (see [`MAX_MULTI_ADDRESSES`]).",
"required": [
"asset",
"addresses"
],
"properties": {
"addresses": {
"type": "array",
"items": {
"type": "string"
},
"description": "The addresses to query (each validated by the Electrum client)."
},
"asset": {
"type": "string",
"description": "`btc` or `ltc`."
}
}
},
"MultiBalanceResponse": {
"type": "object",
"description": "Confirmed/unconfirmed balance summed across the requested addresses. Summed\nwith checked arithmetic server-side (a hostile value errors, never wraps);\nthe wallet computes any grand total from these integer fields.",
"required": [
"asset",
"confirmed",
"unconfirmed"
],
"properties": {
"asset": {
"type": "string"
},
"confirmed": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"unconfirmed": {
"type": "integer",
"format": "int64"
}
}
},
"MultiHistoryResponse": {
"type": "object",
"required": [
"asset",
"transactions"
],
"properties": {
"asset": {
"type": "string"
},
"transactions": {
"type": "array",
"items": {
"$ref": "#/components/schemas/TaggedHistoryEntry"
}
}
}
},
"MultiUtxosResponse": {
"type": "object",
"required": [
"asset",
"utxos"
],
"properties": {
"asset": {
"type": "string"
},
"utxos": {
"type": "array",
"items": {
"$ref": "#/components/schemas/TaggedUtxo"
}
}
}
},
"MyUsernameResponse": {
"type": "object",
"properties": {
"username": {
"type": [
"string",
"null"
],
"description": "The caller's username, or `null` if they have not claimed one."
}
}
},
"NostrLinkChallengeResponse": {
"type": "object",
"required": [
"nonce"
],
"properties": {
"nonce": {
"type": "string",
"description": "Server-issued single-use nonce (hex). The wallet embeds it as the signed\naction's `challenge` tag when calling `POST /auth/nostr/link`."
}
}
},
"NostrLinkRequest": {
"type": "object",
"description": "Link a Nostr identity to the authenticated user. This is a STATE CHANGE, so\nit carries a signed-action proof, not a login-grade token.",
"required": [
"nostr_token",
"nonce"
],
"properties": {
"nonce": {
"type": "string",
"description": "The server-issued single-use nonce the signed action must bind (the\nevent's `challenge` tag)."
},
"nostr_token": {
"type": "string",
"description": "The `Nostr <base64(event)>` signed-action token proving control of the\nnpub AND committing to the server nonce + this request."
}
}
},
"NostrLinkResponse": {
"type": "object",
"required": [
"nostr_pubkey"
],
"properties": {
"nostr_pubkey": {
"type": "string",
"description": "The linked x-only Nostr pubkey (hex)."
}
}
},
"NostrRegisterRequest": {
"type": "object",
"description": "Register (or resolve) a wallet keyed by its **Nostr identity**: the\nself-sovereign, npub-native create path. Unlike [`nostr_login`] (create-never)\nand [`nostr_link`] (needs a prior BTC-authed JWT), this MINTS a user from the\nnpub alone: no BTC signature is ever required. The npub is the identity anchor;\nthe chain `keys` still ship (for tip addresses + restore) but are NOT the auth\nproof.",
"required": [
"nostr_token",
"nonce",
"keys"
],
"properties": {
"altcha_solution": {
"type": "object"
},
"invite_code": {
"type": [
"string",
"null"
]
},
"keys": {
"type": "array",
"items": {
"$ref": "#/components/schemas/AssetPublicKey"
},
"description": "Chain public keys (for tip addresses + restore). A `btc` key is expected\nfor pay-to-register invoice binding, but it is no longer the identity."
},
"nonce": {
"type": "string",
"description": "The server-issued single-use nonce (from `/auth/nostr/register-challenge`)."
},
"nostr_token": {
"type": "string",
"description": "`Nostr <base64(event)>` signed-action token proving control of the npub AND\nbinding the server nonce + the `nostr_register` purpose."
},
"payment_invoice_id": {
"type": [
"string",
"null"
]
},
"seed_fingerprint": {
"type": [
"string",
"null"
]
},
"username": {
"type": [
"string",
"null"
]
},
"wallet_birthday": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "Wallet creation time (unix seconds), to bound chain scans."
},
"wow_start_height": {
"type": [
"integer",
"null"
],
"format": "int64"
},
"xmr_start_height": {
"type": [
"integer",
"null"
],
"format": "int64"
}
}
},
"OkResponse": {
"type": "object",
"required": [
"ok"
],
"properties": {
"ok": {
"type": "boolean"
}
}
},
"PaymentInvoiceRequest": {
"type": "object",
"description": "Request a registration-payment invoice for a wallet about to register.",
"required": [
"btc_public_key"
],
"properties": {
"btc_public_key": {
"type": "string",
"description": "The wallet's BTC public key. Its hash is the identity the invoice binds\nto; only a later `/auth/extension` proving control of this key can redeem\nthe settled invoice, so this endpoint needs no separate proof."
}
}
},
"PaymentInvoiceResponse": {
"type": "object",
"description": "A created payment invoice: what to pay, and the id to present at registration.",
"required": [
"invoice_id",
"pay_to",
"amount",
"currency"
],
"properties": {
"amount": {
"type": "string",
"description": "Price to pay."
},
"currency": {
"type": "string",
"description": "Price currency."
},
"invoice_id": {
"type": "string",
"description": "Opaque invoice id — send it back as `/auth/extension`'s `payment_invoice_id`."
},
"pay_to": {
"type": "string",
"description": "Where to pay (a hosted checkout URL, or an address)."
}
}
},
"PendingRelaysResponse": {
"type": "object",
"required": [
"relays"
],
"properties": {
"relays": {
"type": "array",
"items": {
"$ref": "#/components/schemas/RelayEntry"
}
}
}
},
"PremiumCapability": {
"type": "object",
"description": "Premium tier details (present only when `features.premium_relay`). The wallet\nrenders the plan tiers (discount visible) and gates general-Nostr posting to\nthe Smirk relay on the user's premium status; wallet events stay free.",
"required": [
"currency",
"plans",
"relay_url"
],
"properties": {
"currency": {
"type": "string"
},
"plans": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PremiumPlanInfo"
}
},
"relay_url": {
"type": "string",
"description": "The relay premium posting targets (mirrors `messaging.relay_url`)."
}
}
},
"PremiumPlanInfo": {
"type": "object",
"description": "A single premium plan (discount tier): `days` of relay-posting access for\n`amount` in [`PremiumCapability::currency`].",
"required": [
"id",
"days",
"amount"
],
"properties": {
"amount": {
"type": "string"
},
"days": {
"type": "integer",
"format": "int32"
},
"id": {
"type": "string"
}
}
},
"PricesResponse": {
"type": "object",
"required": [
"currency",
"prices"
],
"properties": {
"currency": {
"type": "string",
"description": "Fiat currency the quotes are in (e.g. `\"usd\"`)."
},
"prices": {
"type": "object",
"description": "Asset symbol → price in `currency`. Only enabled feeds appear.",
"additionalProperties": {
"type": "number",
"format": "double"
},
"propertyNames": {
"type": "string"
}
},
"updated_at": {
"type": [
"string",
"null"
],
"description": "RFC 3339 timestamp of the last successful refresh; `null` until the first."
}
}
},
"ProvisionRequest": {
"type": "object",
"description": "Provision a batch of account-0 subaddress indices for an account. Additive to\n`register`; the identity comes from the bearer token, so any `user_id` on the\nwire is ignored.",
"required": [
"asset",
"address",
"view_key"
],
"properties": {
"address": {
"type": "string"
},
"asset": {
"type": "string",
"description": "`xmr` or `wow`."
},
"max_minor": {
"type": [
"integer",
"null"
],
"format": "int32",
"description": "Highest minor index wanted for account 0. Clamped to the server ceiling.\nOmit for the instance default.",
"minimum": 0
},
"view_key": {
"type": "string",
"description": "Private view key (64 hex). Forwarded to the LWS; never stored or logged."
}
}
},
"ProvisionResponse": {
"type": "object",
"description": "The subaddress ceiling the LWS CONFIRMED for account 0.",
"required": [
"asset",
"provisioned_minor_max"
],
"properties": {
"asset": {
"type": "string"
},
"provisioned_minor_max": {
"type": "integer",
"format": "int32",
"description": "Highest minor index the LWS confirmed it is scanning for account 0, read\nback from its response - never an echo of the request. Every index in\n`0..=provisioned_minor_max` is provisioned; the wallet must not hand out\nan index above it.",
"minimum": 0
}
}
},
"PublicKeysInfo": {
"type": "object",
"description": "A user's per-asset receiving public keys, by asset.",
"properties": {
"btc": {
"type": [
"string",
"null"
]
},
"grin": {
"type": [
"string",
"null"
]
},
"ltc": {
"type": [
"string",
"null"
]
},
"wow": {
"type": [
"string",
"null"
]
},
"xmr": {
"type": [
"string",
"null"
]
}
}
},
"PublicTipInfo": {
"type": "object",
"required": [
"id",
"asset",
"amount",
"status",
"created_at",
"is_public",
"funding_confirmations",
"confirmations_required",
"is_claimable"
],
"properties": {
"amount": {
"type": "integer",
"format": "int64"
},
"asset": {
"type": "string"
},
"confirmations_required": {
"type": "integer",
"format": "int32"
},
"created_at": {
"type": "string"
},
"encrypted_key": {
"type": [
"string",
"null"
],
"description": "Hex-encoded AES-GCM ciphertext; useless without the URL-fragment key."
},
"funding_confirmations": {
"type": "integer",
"format": "int32"
},
"id": {
"type": "string"
},
"is_claimable": {
"type": "boolean"
},
"is_public": {
"type": "boolean"
},
"status": {
"type": "string"
},
"tip_address": {
"type": [
"string",
"null"
]
}
}
},
"RandomOutputDto": {
"type": "object",
"description": "A decoy output.",
"required": [
"global_index",
"public_key",
"rct"
],
"properties": {
"global_index": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"public_key": {
"type": "string"
},
"rct": {
"type": "string"
}
}
},
"RandomOutsRequest": {
"type": "object",
"description": "Request decoy outputs for ring construction.",
"required": [
"asset",
"count",
"amounts"
],
"properties": {
"amounts": {
"type": "array",
"items": {
"type": "string"
},
"description": "Amounts to request decoys for (`[\"0\"]` for RingCT)."
},
"asset": {
"type": "string"
},
"count": {
"type": "integer",
"format": "int32",
"description": "Decoys per real output (protocol ring size; clamped server-side).",
"minimum": 0
}
}
},
"RandomOutsResponse": {
"type": "object",
"required": [
"asset",
"amount_outs"
],
"properties": {
"amount_outs": {
"type": "array",
"items": {
"$ref": "#/components/schemas/AmountOutsDto"
}
},
"asset": {
"type": "string"
}
}
},
"RefreshTokenRequest": {
"type": "object",
"required": [
"refresh_token"
],
"properties": {
"refresh_token": {
"type": "string"
}
}
},
"RegisterKeyRequest": {
"type": "object",
"description": "Register or update one of the caller's per-asset public keys.",
"required": [
"asset",
"public_key"
],
"properties": {
"asset": {
"type": "string",
"description": "Asset the key is for (`btc`, `ltc`, `xmr`, `wow`, `grin`)."
},
"public_key": {
"type": "string",
"description": "The public key (format depends on the asset)."
},
"public_spend_key": {
"type": [
"string",
"null"
],
"description": "XMR/WOW only: the public spend key."
}
}
},
"RegisterRequest": {
"type": "object",
"description": "Register (or import-with-height) an account for LWS scanning.",
"required": [
"asset",
"address",
"view_key"
],
"properties": {
"address": {
"type": "string"
},
"asset": {
"type": "string"
},
"restore_pow_nonce": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "Restore proof-of-work nonce. Required when the instance prices the\nrequested restore depth (see `/capabilities` → `restore.pow_*`); ignored\notherwise. Bound to `(asset, address, start_height)` (see `restore_pow`).",
"minimum": 0
},
"start_height": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "Scan from this block height (wallet birthday). Omit to scan from now.",
"minimum": 0
},
"subaddr_count": {
"type": [
"integer",
"null"
],
"format": "int32",
"description": "How many account-0 minor subaddress indices to provision at the LWS.\nClamped to the server ceiling, and may only RAISE this instance's\ndefault; it can never enable provisioning on an instance that has it off.\nOmit to use the instance default.",
"minimum": 0
},
"view_key": {
"type": "string",
"description": "Private view key (64 hex). Forwarded to the LWS; never stored or logged."
}
}
},
"RegistrationCapability": {
"type": "object",
"description": "Registration gates this instance enforces for a NEW wallet (returning wallets\nand self-hosting bypass them). The wallet uses these to shape onboarding —\nprompt for an invite code, solve PoW, etc.",
"required": [
"invite_required",
"pow_required",
"payment_required",
"registration_mode"
],
"properties": {
"invite_required": {
"type": "boolean",
"description": "A valid operator-minted invite code is required to register."
},
"payment_amount": {
"type": [
"string",
"null"
],
"description": "The registration price + currency — present only when `payment_required`,\nso the wallet can show \"registration costs X\" before minting an invoice."
},
"payment_currency": {
"type": [
"string",
"null"
]
},
"payment_required": {
"type": "boolean",
"description": "A settled payment invoice (from `/auth/payment-invoice`) is required to\nregister a new wallet."
},
"pow_required": {
"type": "boolean",
"description": "A proof-of-work solution is required to register."
},
"registration_mode": {
"type": "string",
"description": "How the enabled gates combine: `\"all\"` (satisfy every gate; the default)\nor `\"any\"` (the gates are alternatives; satisfy one). PoW is orthogonal\nand applies regardless. The wallet routes onboarding on this: multiple\ngates + `\"any\"` => \"pick a method\" buttons; `\"all\"` => satisfy each."
}
}
},
"RelayEntry": {
"type": "object",
"description": "A relay entry as returned to an authorized party.",
"required": [
"slate_id",
"sender_user_id",
"slatepack_content",
"amount_nanogrin",
"status",
"created_at",
"expires_at"
],
"properties": {
"amount_nanogrin": {
"type": "integer",
"format": "int64"
},
"created_at": {
"type": "string"
},
"expires_at": {
"type": "string"
},
"finalized_at": {
"type": [
"string",
"null"
]
},
"recipient_user_id": {
"type": [
"string",
"null"
]
},
"response_slatepack": {
"type": [
"string",
"null"
],
"description": "The recipient's response slatepack, once provided."
},
"sender_user_id": {
"type": "string"
},
"slate_id": {
"type": "string"
},
"slatepack_content": {
"type": "string",
"description": "The sender's armored slatepack (what the recipient responds to)."
},
"status": {
"type": "string"
},
"tx_hash": {
"type": [
"string",
"null"
]
}
}
},
"RespondRelayRequest": {
"type": "object",
"description": "The recipient's response to a pending relay.",
"required": [
"slate_id",
"response_slatepack"
],
"properties": {
"response_slatepack": {
"type": "string",
"description": "The armored response slatepack (recipient's partial signature added)."
},
"slate_id": {
"type": "string"
}
}
},
"RestoreCapability": {
"type": "object",
"description": "This instance's wallet-restore (import) policy. The wallet uses it to adapt\nits import UX — hide/grey the restore-height field under `create-only`, warn\nwhen a chosen date exceeds the bound. `max_depth_days` is present only for\nthe `bounded` policy.",
"required": [
"policy",
"pow_free_days",
"pow_days_per_bit",
"pow_max_bits"
],
"properties": {
"max_depth_days": {
"type": [
"integer",
"null"
],
"format": "int32",
"minimum": 0
},
"policy": {
"type": "string",
"description": "`create-only` | `bounded` | `unlimited`."
},
"pow_days_per_bit": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"pow_free_days": {
"type": "integer",
"format": "int32",
"description": "Restore PoW pricing curve: a restore depth (days) free of PoW, then `+1`\nhashcash difficulty bit per `pow_days_per_bit` days beyond it\n(`0` = pricing off), capped at `pow_max_bits`. The wallet computes its\nrequired difficulty from this + the restore date and solves the hashcash.",
"minimum": 0
},
"pow_max_bits": {
"type": "integer",
"format": "int32",
"minimum": 0
}
}
},
"SentTip": {
"type": "object",
"required": [
"id",
"sender_user_id",
"asset",
"amount",
"is_public",
"status",
"created_at",
"funding_confirmations",
"confirmations_required",
"is_claimable"
],
"properties": {
"amount": {
"type": "integer",
"format": "int64"
},
"asset": {
"type": "string"
},
"claimed_at": {
"type": [
"string",
"null"
]
},
"clawed_back_at": {
"type": [
"string",
"null"
]
},
"confirmations_required": {
"type": "integer",
"format": "int32"
},
"created_at": {
"type": "string"
},
"funding_confirmations": {
"type": "integer",
"format": "int32"
},
"id": {
"type": "string"
},
"is_claimable": {
"type": "boolean"
},
"is_public": {
"type": "boolean"
},
"recipient_platform": {
"type": [
"string",
"null"
],
"description": "Always null in the public-only port (no targeted recipients)."
},
"recipient_username": {
"type": [
"string",
"null"
]
},
"sender_user_id": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"SetUsernameRequest": {
"type": "object",
"required": [
"username"
],
"properties": {
"username": {
"type": "string",
"description": "Desired username (3-32 chars, lowercase `[a-z0-9_]`, no leading/trailing\nunderscore). Submitted values are lowercased before validation."
}
}
},
"SetUsernameResponse": {
"type": "object",
"required": [
"username"
],
"properties": {
"username": {
"type": "string",
"description": "The username as stored (lowercased)."
}
}
},
"SlateIdRequest": {
"type": "object",
"description": "Identify a relay by its slate id (poll / cancel).",
"required": [
"slate_id"
],
"properties": {
"slate_id": {
"type": "string"
}
}
},
"SocialTipsResponse": {
"type": "object",
"required": [
"tips"
],
"properties": {
"tips": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SentTip"
}
}
}
},
"SpentOutputDto": {
"type": "object",
"description": "A candidate spent output (verify with the spend key before trusting).",
"required": [
"amount",
"key_image",
"tx_pub_key",
"out_index",
"mixin",
"subaddr_index"
],
"properties": {
"amount": {
"type": "string"
},
"key_image": {
"type": "string"
},
"mixin": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"out_index": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"subaddr_index": {
"$ref": "#/components/schemas/SubaddrIndexDto",
"description": "Subaddress index of the output BEING SPENT (`(0, 0)` = primary address),\ntaken from the spend record itself.\n\nLoad-bearing for the balance: the wallet recomputes this output's key\nimage to tell a real spend from a ring decoy, and the key image depends\non the subaddress index. Without it a subaddress spend is recomputed\nagainst the primary index, the key images never match, the spend is\ndismissed as a decoy, and the amount is never subtracted - the balance\nover-reports forever. It is deliberately NOT the enclosing transaction's\n`subaddr_index`, which is the change index and would be just as wrong."
},
"tx_pub_key": {
"type": "string"
}
}
},
"StatusResp": {
"type": "object",
"required": [
"active"
],
"properties": {
"active": {
"type": "boolean",
"description": "Whether the caller currently holds active premium."
},
"premium_until": {
"type": [
"string",
"null"
],
"description": "Premium expiry (RFC3339), or null if never premium / lapsed."
}
}
},
"SubaddrIndexDto": {
"type": "object",
"description": "A subaddress index `(major, minor)` an output/tx was received at. Nested to\nmatch the wallet's wasm `LwsOutput` shape (`subaddr_index: {major, minor}`);\nmapped from the LWS `recipient` field. `(0, 0)` is the primary address.",
"required": [
"major",
"minor"
],
"properties": {
"major": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"minor": {
"type": "integer",
"format": "int32",
"minimum": 0
}
}
},
"SubmitRequest": {
"type": "object",
"description": "Submit a finalized, signed transaction.",
"required": [
"asset",
"tx_hex"
],
"properties": {
"asset": {
"type": "string"
},
"tx_hex": {
"type": "string",
"description": "Hex-encoded signed transaction blob."
}
}
},
"TaggedHistoryEntry": {
"type": "object",
"description": "A transaction-history entry TAGGED with its owning address.",
"required": [
"address",
"txid",
"height"
],
"properties": {
"address": {
"type": "string"
},
"fee": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "Fee in satoshis (mempool entries only).",
"minimum": 0
},
"height": {
"type": "integer",
"format": "int64",
"description": "Block height (`0`/negative for unconfirmed)."
},
"txid": {
"type": "string"
}
}
},
"TaggedUtxo": {
"type": "object",
"description": "A single unspent output TAGGED with the address it belongs to. The tag is\nload-bearing: the wallet never re-derives which address owns a UTXO.",
"required": [
"address",
"txid",
"vout",
"value",
"height"
],
"properties": {
"address": {
"type": "string"
},
"height": {
"type": "integer",
"format": "int64",
"description": "Block height; `0` if unconfirmed.",
"minimum": 0
},
"txid": {
"type": "string"
},
"value": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"vout": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},
"TipResponse": {
"type": "object",
"required": [
"asset",
"height"
],
"properties": {
"asset": {
"type": "string"
},
"height": {
"type": "integer",
"format": "int64",
"description": "Best-chain tip height."
}
}
},
"TxDto": {
"type": "object",
"description": "A transaction in the account's history.",
"required": [
"hash",
"height",
"timestamp",
"total_received",
"total_sent",
"mempool",
"unlock_time",
"spent_outputs",
"subaddr_index"
],
"properties": {
"hash": {
"type": "string"
},
"height": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"mempool": {
"type": "boolean"
},
"payment_id": {
"type": [
"string",
"null"
]
},
"spent_outputs": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SpentOutputDto"
}
},
"subaddr_index": {
"$ref": "#/components/schemas/SubaddrIndexDto",
"description": "Subaddress index this tx was received at (`(0, 0)` = primary address)."
},
"timestamp": {
"type": "string"
},
"total_received": {
"type": "string"
},
"total_sent": {
"type": "string",
"description": "\"Possible\" sent — candidate spends, not authoritative."
},
"unlock_time": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},
"UnspentOutputDto": {
"type": "object",
"description": "An unspent output for spend construction.",
"required": [
"amount",
"public_key",
"tx_pub_key",
"index",
"global_index",
"height",
"timestamp",
"tx_hash",
"rct",
"spend_key_images",
"subaddr_index"
],
"properties": {
"amount": {
"type": "string"
},
"global_index": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"height": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"index": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"public_key": {
"type": "string"
},
"rct": {
"type": "string"
},
"spend_key_images": {
"type": "array",
"items": {
"type": "string"
},
"description": "Key images seen on-chain that may correspond to this output being spent."
},
"subaddr_index": {
"$ref": "#/components/schemas/SubaddrIndexDto",
"description": "Subaddress index this output was received at (`(0, 0)` = primary address)."
},
"timestamp": {
"type": "string"
},
"tx_hash": {
"type": "string"
},
"tx_pub_key": {
"type": "string"
}
}
},
"UnspentOutsResponse": {
"type": "object",
"required": [
"asset",
"outputs",
"per_byte_fee",
"fee_mask",
"fork_version"
],
"properties": {
"asset": {
"type": "string"
},
"fee_mask": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"fork_version": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"outputs": {
"type": "array",
"items": {
"$ref": "#/components/schemas/UnspentOutputDto"
}
},
"per_byte_fee": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},
"UserInfo": {
"type": "object",
"description": "Minimal, non-enumerable user info returned to a signed-in client.",
"required": [
"id"
],
"properties": {
"id": {
"type": "string"
},
"nostr_pubkey": {
"type": [
"string",
"null"
],
"description": "Linked Nostr pubkey (x-only hex), if any."
},
"username": {
"type": [
"string",
"null"
]
}
}
},
"UserKeyInfo": {
"type": "object",
"description": "A single per-asset public key.",
"required": [
"asset",
"public_key"
],
"properties": {
"asset": {
"type": "string",
"description": "Asset the key is for (lowercase: `btc`, `ltc`, `xmr`, `wow`, `grin`)."
},
"public_key": {
"type": "string"
},
"public_spend_key": {
"type": [
"string",
"null"
],
"description": "XMR/WOW only: the public spend key."
}
}
},
"UserKeysResponse": {
"type": "object",
"description": "A user's full set of per-asset public keys.",
"required": [
"keys"
],
"properties": {
"keys": {
"type": "array",
"items": {
"$ref": "#/components/schemas/UserKeyInfo"
}
}
}
},
"Utxo": {
"type": "object",
"description": "A single unspent output.",
"required": [
"txid",
"vout",
"value",
"height"
],
"properties": {
"height": {
"type": "integer",
"format": "int64",
"description": "Block height; `0` if unconfirmed.",
"minimum": 0
},
"txid": {
"type": "string"
},
"value": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"vout": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},
"UtxosResponse": {
"type": "object",
"required": [
"asset",
"address",
"utxos"
],
"properties": {
"address": {
"type": "string"
},
"asset": {
"type": "string"
},
"utxos": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Utxo"
}
}
}
},
"ViewRequest": {
"type": "object",
"description": "Address + private view key for a per-account query.",
"required": [
"asset",
"address",
"view_key"
],
"properties": {
"address": {
"type": "string"
},
"asset": {
"type": "string",
"description": "`xmr` or `wow`."
},
"view_key": {
"type": "string",
"description": "Private view key (64 hex). Forwarded to the LWS; never stored or logged."
}
}
},
"WebsiteChallengeRequest": {
"type": "object",
"description": "Request a website-auth challenge for a calling origin.",
"required": [
"origin"
],
"properties": {
"origin": {
"type": "string",
"description": "Origin of the requesting website (e.g. `https://smirk.cash`). Bound into\nthe challenge message so a signature for one origin is not reusable at\nanother."
}
}
},
"WebsiteChallengeResponse": {
"type": "object",
"description": "The challenge to sign, with the handle and expiry the wallet echoes back.",
"required": [
"challenge",
"challenge_id",
"expires_at"
],
"properties": {
"challenge": {
"type": "string",
"description": "The exact message string to sign with the wallet's key."
},
"challenge_id": {
"type": "string",
"description": "Opaque handle to present at verify time (this is the challenge nonce)."
},
"expires_at": {
"type": "string",
"description": "When the challenge expires (RFC 3339)."
}
}
},
"WebsiteVerifyRequest": {
"type": "object",
"description": "Verify a website-auth challenge with a single asset signature.",
"required": [
"challenge_id",
"signature"
],
"properties": {
"challenge_id": {
"type": "string",
"description": "The `challenge_id` returned by [`website_challenge`] (the challenge nonce)."
},
"signature": {
"$ref": "#/components/schemas/AssetSignature",
"description": "The wallet's signature over the challenge message."
}
}
},
"WellKnownResponse": {
"type": "object",
"description": "NIP-05 well-known document.\n\n* `names` maps each resolved local part to its x-only Nostr pubkey (hex).\n* `relays` maps each returned pubkey to a list of recommended relay URLs.\n\nBoth maps are empty when the name is unknown, unlinked, or omitted.",
"required": [
"names",
"relays"
],
"properties": {
"names": {
"type": "object",
"description": "`{ local_part: nostr_pubkey_hex }`. Empty when nothing resolved.",
"additionalProperties": {
"type": "string"
},
"propertyNames": {
"type": "string"
}
},
"relays": {
"type": "object",
"description": "`{ nostr_pubkey_hex: [relay_url, ...] }`. Empty when nothing resolved.",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
},
"propertyNames": {
"type": "string"
}
}
}
}
},
"securitySchemes": {
"bearer_auth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
"description": "Session access token issued by the /auth/* endpoints."
}
}
},
"tags": [
{
"name": "system",
"description": "Service health and metadata."
},
{
"name": "prices",
"description": "Fiat price feed (cached; per-feed configurable; feature-flagged)."
},
{
"name": "auth",
"description": "Wallet authentication: registration, restore, refresh, sessions, Nostr identity."
},
{
"name": "btc_ltc",
"description": "Bitcoin/Litecoin chain access via Electrum (balance, UTXOs, history, tip, fee, broadcast)."
},
{
"name": "xmr_wow",
"description": "Monero/Wownero chain access via LWS (register, balance, history, unspent/random outs, submit, height, confirmations)."
},
{
"name": "grin",
"description": "Grin view-only chain access (rewind_hash scan, tip height, broadcast)."
},
{
"name": "grin_relay",
"description": "Grin slatepack relay: async non-custodial mailbox for interactive Grin transfers (feature-flagged)."
},
{
"name": "premium",
"description": "Premium subscription: mint a tier invoice, activate on settlement, read status (feature-flagged)."
},
{
"name": "tips",
"description": "Public social tips: create/fund/claim/clawback share-URL tips (feature-flagged)."
}
]
}