Slice
API

Errors

One error shape everywhere, a correct status code, and a message written for a person. Nothing internal is ever sent to a client.

The envelope

Every error, without exception
{ "error": "That is not a Solana wallet address. Paste the address of a wallet you control." }

One field. No error codes, no nested detail object, no stack trace, no SQL, and no environment variable names. When something unexpected happens, the detail goes to the server log and the client gets a sentence it can show a user.

Match on the status code, not on the message text. Messages are written for people and can be reworded; statuses are the contract.

Status codes

StatusMeaningRetry
200Success. An empty list is a success, not an error.Not applicable
202Accepted, not final. Only /api/launch/confirm, while the launch transaction is not yet confirmed on chain.Yes, poll again
400The request is malformed: a bad address, handle, cursor or limit, a body that is not a JSON object, an amount out of range, a launch field that breaks a rule.No, fix the request
401Not signed in with X, or the session expired, on /api/claim and /api/routing. Also a launch message signature that does not match the wallet. /api/me never returns it: signed out is a normal answer there.After signing in again
403A claim, a routing change or an /api/rpc request sent from another site.No
404No such token or account, or no stored image for a token. Also what the admin and cron routes return when they are not configured, so they do not advertise themselves.No
409Well formed, but in conflict with the current state: a payout already in flight, a token address already used, a launch step taken out of order, a handle that changed hands.Only after the conflict is resolved
410A launch whose signing window expired. Prepare it again.No, start over
413, 415A launch image that is too large, or not PNG, JPEG, GIF or WebP.No, fix the file
429Rate limited. The response carries retry-after in seconds.Yes, after retry-after
500An unexpected failure. Details are in the server log, never in the response.Yes, with backoff
502An upstream we depend on failed: the IPFS upload of a launch, or the RPC provider.Yes, with backoff
503Not available right now: the service is not configured, claims or launches are paused, the treasury balance could not be read for /api/proof, X could not be asked who owns a handle that has been paid here before, or a payout wallet could not be checked on chain.Yes, later

Handling them

A correct client
async function getJson<T>(url: string): Promise<T> {
  const res  = await fetch(url, {headers: {accept: "application/json"}});
  const body = await res.json().catch(() => null);

  if (!res.ok) {
    // The envelope is guaranteed on every error status.
    const message = body && typeof body === "object" && "error" in body
      ? String(body.error)
      : `Request failed with status ${res.status}`;

    if (res.status === 429) {
      const wait = Number(res.headers.get("retry-after") ?? 5);
      throw new RetryableError(message, wait);
    }
    throw new Error(message);
  }

  return body as T;
}

Retry 429, 500, 502 and 503 with exponential backoff. Do not retry 400, 401, 403 or 404, because nothing about the request will have changed.

Rate limit responses

429
HTTP/1.1 429 Too Many Requests
x-ratelimit-limit:     120
x-ratelimit-remaining: 0
x-ratelimit-reset:     1790000060
retry-after:           23

{ "error": "Too many requests. Slow down and try again shortly." }

The three x-ratelimit- headers are on every answer from a limited route, success or failure, so a client can slow down before it is refused. retry-after appears only on a 429. x-ratelimit-reset is unix seconds, not a duration. Windows are fixed, one minute on every route but the launch metadata upload, so the allowance refills all at once. The per-route limits are on the API overview.

On-chain failures are not API errors

A sweep, payout or burn that fails on chain is not an error from this API. It is a state: the record says failed, and what happens next is on Sweeps and Claiming with X. A queued claim returns 200 even though the payout has not been sent yet; follow its status on GET /api/me.

The launch transaction is signed and sent by your own wallet, so its failures come back from your wallet and from the simulation the launch page runs first, not from this API. /api/launch/confirm reports a transaction that failed on chain as a 200 with {"status": "failed", "error": "…"}.

Empty is not an error

  • No tokens yet: {"tokens": [], "nextCursor": null} with status 200.
  • An empty ledger: {"events": [], "nextBefore": null} with status 200.
  • /api/stats on a fresh deployment returns zeroes, not an error.
  • Unknown values are null, never a placeholder. A market cap nobody has measured is null, not 0.

Interfaces built on this API should say "No data yet" in those cases, which is what the charts throughout these docs do.