The API uses a small, stable set of machine-readable error codes — seven in total. The same code value can fire from many different call sites, so the tables below group triggers by domain. Look up your domain, find the row that matches the message, ship the fix.
Key is missing a scope the route requires (e.g. fees:claim, airdrop:write, airdrop:admin)
Request the scope on your key, or drop back to mode=unsigned
The scope-missing message includes the required scope names, e.g. API key missing required scope(s): fees:claim.
Validation
The catch-all bucket. Most 400s land here. See Common Validation Errors for snippets of the most frequent ones.
code
HTTP
When it fires
Fix
bad_request
400
Zod schema rejected the body — missing field, wrong type, regex mismatch
Compare the body against /v2/openapi.json; check error.param for the failing field
bad_request
400
name not ^[a-zA-Z0-9 ]{1,20}$ (create-pool / create-token)
Trim to 1-20 alphanumeric + space chars
bad_request
400
symbol not ^[A-Za-z0-9]{1,10}$
Trim to 1-10 alphanumeric chars, no spaces
bad_request
400
A SolanaAddress field isn't valid base58 in length 32-44
Re-derive or copy a real PublicKey
bad_request
400
bps outside 1-10000 on a kind: "split" fee claim
Use a 1-10000 integer per entry
bad_request
400
splits[].bps doesn't sum to exactly 10000
Rebalance so the entries total 10000
bad_request
400
splits has fewer than 2 or more than 10 entries
Provide 2-10 recipients
bad_request
400
NSFW moderation rejected the image
Replace the asset; the rejected category is in error.message
bad_request
400
Recent-ticker cooldown — same symbol used within the cooldown window
Wait out the window or pick a fresh ticker
Idempotency
code
HTTP
When it fires
Fix
conflict
409
Same Idempotency-Key sent with a body that hashes differently from the original
Generate a fresh v4 UUID, or resend with the original body to get the cached response
Replays with the same key and same body return the cached 2xx response from the original attempt — that's the happy path, not an error. See Idempotency.
Pool
code
HTTP
When it fires
Fix
bad_request
400
pool_address or config_address is not valid base58
Provide a real pool/config pubkey
not_found
404
GET /v2/pools/{address} — pool account doesn't exist on chain
Verify the address; the row may not be created yet
not_found
404
Config account missing for a pool
The pool's config PDA hasn't been initialized; check the pool state response
Airdrop
code
HTTP
When it fires
Fix
bad_request
400
entries array on POST /v2/merkle-trees is empty or has more than 50000 entries
Chunk the recipient list into shards of <=50000
bad_request
400
airdrop_count is outside 0-65535
Use a u16 — the on-chain instruction limit
bad_request
400
Proof endpoint called without a wallet path segment
Use GET /v2/merkle-trees/{id}/proofs/{wallet}
bad_request
400
mode=signed sent to POST /v2/airdrops/{id}/claims
Switch to mode=unsigned — only the claimer wallet can sign
bad_request
400
Bytes-per-leaf mismatch between request and stored tree
Rebuild the tree; the stored layout doesn't match this entry shape
not_found
404
Proof lookup found no tree for that id
Build the tree first via POST /v2/merkle-trees
not_found
404
Proof requested but the wallet isn't in the tree leaves
The wallet is not eligible; double-check the entries you uploaded
forbidden
403
airdrop:write missing on POST /v2/merkle-trees
Request the scope on your key
forbidden
403
airdrop:admin missing on POST /v2/airdrops or mode=signed claim
Request the scope on your key
Fees
code
HTTP
When it fires
Fix
bad_request
400
pool_address invalid base58 on any fee route
Use a real pool pubkey
bad_request
400
Validation rejected the splits array (see Validation row above)
Fix the array to satisfy 2-10 entries summing to 10000
not_found
404
GET /v2/pools/{address}/fee-metrics couldn't read pool state or partner metrics
The pool may not exist yet, or it migrated to DAMM v2 — claim with kind: "damm_v2"
forbidden
403
mode=signed without fees:claim scope
Request the scope, or call with mode=unsigned and sign client-side
Staking, NFT & Gold
These endpoints are user-owned. The server cannot sign them — the user's wallet must.
code
HTTP
When it fires
Fix
bad_request
400
mode=signed on POST /v2/stakes or DELETE /v2/stakes/{asset}
Use mode=unsigned and sign on the client
bad_request
400
mode=signed on POST /v2/nfts or DELETE /v2/nfts/{asset}
Use mode=unsigned
bad_request
400
mode=signed on POST /v2/gold-locks or DELETE /v2/gold-locks/{user}
Use mode=unsigned
bad_request
400
lock_duration_days outside 1-1460 on POST /v2/gold-locks
Stay within 4 years (1460 days)
Token
code
HTTP
When it fires
Fix
bad_request
400
mode=signed on POST /v2/tokens
The payer wallet must sign — use mode=unsigned
bad_request
400
mode=signed on DELETE /v2/tokens/{mint}/authorities/{type}
The current authority must sign — use mode=unsigned
bad_request
400
{mint} path on GET /v2/tokens/{mint} is invalid base58
Provide a real SPL mint pubkey
bad_request
400
NSFW moderation rejected the token image
Replace the asset; the rejected category appears in error.message
bad_request
400
Recent-ticker cooldown — same symbol reused
Wait out the cooldown or pick a new symbol
Transaction
code
HTTP
When it fires
Fix
bad_request
400
transaction on POST /v2/transactions or POST /v2/transaction-simulations isn't a decodable base64 VersionedTransaction
Re-serialize via tx.serialize() then Buffer.from(...).toString("base64")
bad_request
400
signed_transactions on POST /v2/transactions with bundle: true has 0 or more than 5 entries
Send 1-5 signed transactions per bundle
internal_error
500
JITO_BUNDLE_ENDPOINT not configured on the server
Service config issue — file an incident
internal_error
500
Jito relay rejected the bundle
Inspect error.message; usually a stale blockhash or insufficient tip
Accounts
code
HTTP
When it fires
Fix
bad_request
400
addresses on POST /v2/accounts/batch-fetch is empty or has more than 100 entries
Split into batches of 100
bad_request
400
{kind} on POST /v2/pdas/{kind} is not a known PDA enum
Use one of the names listed in /v2/docs
bad_request
400
args.<field> required by that PDA is null/missing
Provide every required arg for the named PDA
bad_request
400
address path segment is not valid base58
Provide a real pubkey
not_found
404
GET /v2/accounts/{address} — account doesn't exist on the cluster
Verify the address and the cluster (mainnet vs devnet)
Storage
code
HTTP
When it fires
Fix
bad_request
400
UCAN delegation request missing the agent DID
Pass did:key:... for the agent receiving the delegation
internal_error
500
Storacha credentials not configured (STORACHA_DID / STORACHA_PROOF)
Service config — file an incident
Rate Limiting
code
HTTP
When it fires
Fix
rate_limited
429
Per-minute quota exceeded on your key
Back off and retry with exponential delay
rate_limited
429
Per-day quota exceeded
Wait for UTC midnight rollover, or upgrade tier
rate_limited
429
Concurrent-request race lost the atomic counter update
Same as above — retry with jitter
See Retry Policy for the recommended backoff strategy.
Server
code
HTTP
When it fires
Fix
internal_error
500
Anything not caught by AppError — unhandled exception, upstream Supabase / Helius / Privy failure
Retry up to 3 times with jitter; if persistent, file an incident with the request ID and timestamp
In production error.message is sanitized to "Something went wrong". The full stack is in the service logs — never echo internal messages back to end users.