Error Codes Reference

Error Codes Reference

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.

Every error response uses the standard envelope:

{
  "error": {
    "code": "machine-readable code",
    "message": "human-readable summary",
    "param": "optional offending field",
    "doc_url": "optional link to relevant doc"
  },
  "meta": { "request_id": "req_…" }
}

The seven codes:

codeHTTPOne-liner
bad_request400Payload, scope-mode mismatch, or business-rule violation
unauthorized401Bearer header missing or malformed
forbidden403Key invalid / revoked, or missing a required scope
not_found404The resource isn't on chain or in Supabase
conflict409Idempotency-Key reused with a different body
rate_limited429Per-minute or per-day quota exceeded
internal_error500Unhandled server fault — retriable

See Retry Policy for which of these to retry and how.

Auth & Permissions

codeHTTPWhen it firesFix
unauthorized401Authorization header missing, not Bearer …, or emptyAdd Authorization: Bearer $PIRATE_API_KEY
forbidden403Key hash not in api_keys, or status != 'active'Generate a new key at developer.piratecrew.fun
forbidden403Key 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.

codeHTTPWhen it firesFix
bad_request400Zod schema rejected the body — missing field, wrong type, regex mismatchCompare the body against /v2/openapi.json; check error.param for the failing field
bad_request400name not ^[a-zA-Z0-9 ]{1,20}$ (create-pool / create-token)Trim to 1-20 alphanumeric + space chars
bad_request400symbol not ^[A-Za-z0-9]{1,10}$Trim to 1-10 alphanumeric chars, no spaces
bad_request400A SolanaAddress field isn't valid base58 in length 32-44Re-derive or copy a real PublicKey
bad_request400bps outside 1-10000 on a kind: "split" fee claimUse a 1-10000 integer per entry
bad_request400splits[].bps doesn't sum to exactly 10000Rebalance so the entries total 10000
bad_request400splits has fewer than 2 or more than 10 entriesProvide 2-10 recipients
bad_request400NSFW moderation rejected the imageReplace the asset; the rejected category is in error.message
bad_request400Recent-ticker cooldown — same symbol used within the cooldown windowWait out the window or pick a fresh ticker

Idempotency

codeHTTPWhen it firesFix
conflict409Same Idempotency-Key sent with a body that hashes differently from the originalGenerate 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

codeHTTPWhen it firesFix
bad_request400pool_address or config_address is not valid base58Provide a real pool/config pubkey
not_found404GET /v2/pools/{address} — pool account doesn't exist on chainVerify the address; the row may not be created yet
not_found404Config account missing for a poolThe pool's config PDA hasn't been initialized; check the pool state response

Airdrop

codeHTTPWhen it firesFix
bad_request400entries array on POST /v2/merkle-trees is empty or has more than 50000 entriesChunk the recipient list into shards of <=50000
bad_request400airdrop_count is outside 0-65535Use a u16 — the on-chain instruction limit
bad_request400Proof endpoint called without a wallet path segmentUse GET /v2/merkle-trees/{id}/proofs/{wallet}
bad_request400mode=signed sent to POST /v2/airdrops/{id}/claimsSwitch to mode=unsigned — only the claimer wallet can sign
bad_request400Bytes-per-leaf mismatch between request and stored treeRebuild the tree; the stored layout doesn't match this entry shape
not_found404Proof lookup found no tree for that idBuild the tree first via POST /v2/merkle-trees
not_found404Proof requested but the wallet isn't in the tree leavesThe wallet is not eligible; double-check the entries you uploaded
forbidden403airdrop:write missing on POST /v2/merkle-treesRequest the scope on your key
forbidden403airdrop:admin missing on POST /v2/airdrops or mode=signed claimRequest the scope on your key

Fees

codeHTTPWhen it firesFix
bad_request400pool_address invalid base58 on any fee routeUse a real pool pubkey
bad_request400Validation rejected the splits array (see Validation row above)Fix the array to satisfy 2-10 entries summing to 10000
not_found404GET /v2/pools/{address}/fee-metrics couldn't read pool state or partner metricsThe pool may not exist yet, or it migrated to DAMM v2 — claim with kind: "damm_v2"
forbidden403mode=signed without fees:claim scopeRequest 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.

codeHTTPWhen it firesFix
bad_request400mode=signed on POST /v2/stakes or DELETE /v2/stakes/{asset}Use mode=unsigned and sign on the client
bad_request400mode=signed on POST /v2/nfts or DELETE /v2/nfts/{asset}Use mode=unsigned
bad_request400mode=signed on POST /v2/gold-locks or DELETE /v2/gold-locks/{user}Use mode=unsigned
bad_request400lock_duration_days outside 1-1460 on POST /v2/gold-locksStay within 4 years (1460 days)

Token

codeHTTPWhen it firesFix
bad_request400mode=signed on POST /v2/tokensThe payer wallet must sign — use mode=unsigned
bad_request400mode=signed on DELETE /v2/tokens/{mint}/authorities/{type}The current authority must sign — use mode=unsigned
bad_request400{mint} path on GET /v2/tokens/{mint} is invalid base58Provide a real SPL mint pubkey
bad_request400NSFW moderation rejected the token imageReplace the asset; the rejected category appears in error.message
bad_request400Recent-ticker cooldown — same symbol reusedWait out the cooldown or pick a new symbol

Transaction

codeHTTPWhen it firesFix
bad_request400transaction on POST /v2/transactions or POST /v2/transaction-simulations isn't a decodable base64 VersionedTransactionRe-serialize via tx.serialize() then Buffer.from(...).toString("base64")
bad_request400signed_transactions on POST /v2/transactions with bundle: true has 0 or more than 5 entriesSend 1-5 signed transactions per bundle
internal_error500JITO_BUNDLE_ENDPOINT not configured on the serverService config issue — file an incident
internal_error500Jito relay rejected the bundleInspect error.message; usually a stale blockhash or insufficient tip

Accounts

codeHTTPWhen it firesFix
bad_request400addresses on POST /v2/accounts/batch-fetch is empty or has more than 100 entriesSplit into batches of 100
bad_request400{kind} on POST /v2/pdas/{kind} is not a known PDA enumUse one of the names listed in /v2/docs
bad_request400args.<field> required by that PDA is null/missingProvide every required arg for the named PDA
bad_request400address path segment is not valid base58Provide a real pubkey
not_found404GET /v2/accounts/{address} — account doesn't exist on the clusterVerify the address and the cluster (mainnet vs devnet)

Storage

codeHTTPWhen it firesFix
bad_request400UCAN delegation request missing the agent DIDPass did:key:... for the agent receiving the delegation
internal_error500Storacha credentials not configured (STORACHA_DID / STORACHA_PROOF)Service config — file an incident

Rate Limiting

codeHTTPWhen it firesFix
rate_limited429Per-minute quota exceeded on your keyBack off and retry with exponential delay
rate_limited429Per-day quota exceededWait for UTC midnight rollover, or upgrade tier
rate_limited429Concurrent-request race lost the atomic counter updateSame as above — retry with jitter

See Retry Policy for the recommended backoff strategy.

Server

codeHTTPWhen it firesFix
internal_error500Anything not caught by AppError — unhandled exception, upstream Supabase / Helius / Privy failureRetry 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.