> ## Documentation Index
> Fetch the complete documentation index at: https://blank.build/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshooting

> Symptom-first fixes for Blank API v2 authentication, scopes, pagination, rate limits, idempotency, predictions, transaction intents, and webhooks.

# Troubleshooting

Every failure carries a stable `code` and a `requestId`. Start there: read `code` from the [Problem Details](/docs/reference/errors) body and log `X-Request-Id`.

## Authentication and scopes

<AccordionGroup>
  <Accordion title="401 invalid_api_key">
    The credential is missing, malformed, unknown, expired, revoked, issued for
    a different environment, or its wallet is not verified. The API
    deliberately does not tell you which.

    Check in this order: the header is `Authorization: Bearer <key>`; the key
    matches `blank_live_...` for production or `blank_test_...` for staging and
    development; `expiresAt` is in the future. Then rotate the key in
    **Settings** and redeploy. See
    [Authentication](/docs/for-developers/authentication).
  </Accordion>

  <Accordion title="403 insufficient_scope">
    The key is valid but does not carry every scope the operation declares. Scopes
    **cannot be widened after creation** — create a new key with the right scope
    set and revoke the old one. Check what a key actually grants with
    `blank.identity.me()`. See [Scopes](/docs/for-developers/scopes).
  </Accordion>

  <Accordion title="The SDK throws when I construct a client">
    `BlankClient` refuses an `apiKey` in a browser runtime, and rejects a
    `baseUrl` that carries credentials, a query, or a fragment, or that is not
    HTTPS outside `localhost`. Move the client to server code. If a browser needs
    private data, proxy it through your own server.
  </Accordion>

  <Accordion title="A browser request fails with a CORS error">
    Only anonymous read operations are browser-reachable. Authenticated operations
    receive no `Access-Control-Allow-Origin`, by design — an API key must never
    reach a browser. Call them from your server.
  </Accordion>

  <Accordion title="I cannot create an API key">
    Key creation requires a username on the Blank account, and each account can
    hold at most **5 active keys per environment**. Set a username, or revoke an
    unused key first.
  </Accordion>
</AccordionGroup>

## Requests and responses

<AccordionGroup>
  <Accordion title="404 not_found on a path that looks right">
    No route matched the method and path. Confirm both against the [HTTP
    reference](/docs/reference/http-reference), and confirm the origin is
    `https://api.blank.build/api/v2`. There is no v1 contract and no
    unversioned alias — a path outside `/api/v2` will never serve the public
    contract.
  </Accordion>

  <Accordion title="422 validation_failed">
    The request failed schema validation. Read `errors[]`: each entry has a `path`
    such as `body.predictedPriceInSol`, a `code`, and a message. Request schemas
    are strict, so an unexpected extra property is a validation failure, not a
    silently ignored field.
  </Accordion>

  <Accordion title="415 unsupported_content_type or 400 malformed_json">
    Requests with a body must send `Content-Type: application/json` and
    well-formed JSON. `413 request_body_too_large` means the body exceeded
    1,048,576 bytes.
  </Accordion>

  <Accordion title="Numbers look wrong or lose precision">
    Prices and SOL amounts are canonical decimal strings; raw on-chain amounts and
    lamports are unsigned integer strings. They are strings because doubles cannot
    represent them exactly. Parse with `BigInt` or a decimal library — never
    `Number`.
  </Accordion>

  <Accordion title="The SDK throws BlankNetworkError on a successful-looking call">
    The SDK validates every response against the contract schema and refuses to
    hand back data that violates it. It also raises `BlankNetworkError` for
    aborts (`request_aborted`), per-attempt timeouts (`request_timeout`), and
    transport failures (`network_error`). A `BlankNetworkError` means the
    outcome is **unknown** — for a mutation, retry with the same
    `idempotencyKey` exposed on the error.
  </Accordion>
</AccordionGroup>

## Pagination

<AccordionGroup>
  <Accordion title="400 cursor_invalid">
    Cursors are opaque, signed, and bound to the query that produced them. This
    error means the cursor was hand-built, decoded and re-encoded, truncated, or
    reused after you changed a filter. Restart the traversal without a cursor
    and keep the query identical across pages.
  </Accordion>

  <Accordion title="Pagination never finishes">
    Stop when `page.hasMore` is `false` **or** `page.nextCursor` is `null`. If you
    use `iterate()`, it throws once `maxPages` (default 100, maximum 10,000) is
    exceeded rather than looping forever — raise the bound deliberately or narrow
    the query.
  </Accordion>

  <Accordion title="A list looks truncated at 100 entries">
    The staking leaderboard and prediction standings are hard-capped at 100
    entries and are not paginated. That is the full response.
  </Accordion>
</AccordionGroup>

## Rate limits

<AccordionGroup>
  <Accordion title="429 rate_limit_exceeded">
    Read `Retry-After` and wait. `X-RateLimit-Policy` names the policy that
    tripped, and `X-RateLimit-Limit` / `X-RateLimit-Remaining` /
    `X-RateLimit-Reset` describe the tightest dimension applying to you.

    Anonymous traffic is limited globally and per IP; authenticated traffic is
    additionally limited per API key, per account, and per wallet — so more keys
    on the same account will not raise an account-level ceiling. Limits are
    environment configuration: read the headers, do not hardcode numbers.
  </Accordion>

  <Accordion title="503 rate_limit_unavailable">
    The limiter itself could not be reached, and requests fail closed rather
    than passing unmetered. It is transient — retry with backoff.
  </Accordion>
</AccordionGroup>

## Idempotency and concurrency

<AccordionGroup>
  <Accordion title="400 idempotency_key_invalid">
    Every mutation requires an `Idempotency-Key` of 8 to 128 printable ASCII
    characters. The SDK generates one when you omit `idempotencyKey`, so this
    usually means a hand-built HTTP request omitted the header.
  </Accordion>

  <Accordion title="409 idempotency_key_reused">
    The same key was replayed with a **different** request body. Keys are scoped
    to the API key identity, method, and route, and retained for 24 hours. Either
    resend the byte-identical request, or use a new key.
  </Accordion>

  <Accordion title="409 idempotency_request_in_progress">
    Your first attempt is still running. Wait for the `Retry-After: 1` window and
    retry with the same key. Do not switch keys — that would create a second
    mutation.
  </Accordion>

  <Accordion title="I do not know whether my mutation succeeded">
    That is exactly what idempotency keys are for. Retry the same request with the
    same key: a completed attempt replays its original response with
    `Idempotent-Replayed: true`. Generate and persist the key **before** the
    request, so a crash mid-call still leaves you able to reconcile.
  </Accordion>

  <Accordion title="412 precondition_failed">
    `If-Match` did not match the resource's current `version` — someone else
    changed it first. Re-read the resource, re-apply your change on top of the
    new state, and retry with the new version.
  </Accordion>
</AccordionGroup>

## Price predictions

<AccordionGroup>
  <Accordion title="403 prediction_authorization_invalid">
    The end user must sign the exact UTF-8 `message` returned by
    `createDelegatedPredictionIntent` with the wallet named in that intent.
    Do not rebuild, trim, prefix, or otherwise rewrite the message before
    calling the wallet.
  </Accordion>

  <Accordion title="404 prediction_intent_not_found">
    Submit the intent with the same API key that created it. Blank deliberately
    returns the same result for an unknown ID and an intent owned by another key.
  </Accordion>

  <Accordion title="409 prediction_intent_expired or prediction_intent_consumed">
    Intents expire after five minutes or at round lock and can be used once. Retry
    an uncertain submission with the original idempotency key; otherwise prepare a
    fresh intent.
  </Accordion>

  <Accordion title="403 prediction_wallet_mismatch">
    `walletAddress` in the body must equal the wallet pinned to the API key. Read
    it from `blank.identity.me()` rather than configuring it separately.
  </Accordion>

  <Accordion title="409 prediction_round_not_open">
    Predictions are only accepted while the round is `open`, before `locksAt`.
    Re-read the round; if it has moved to `locked`, wait for the next one.
  </Accordion>

  <Accordion title="409 prediction_already_submitted">
    One prediction per wallet per round, and predictions are immutable — they
    cannot be edited or withdrawn. Read yours back with
    `blank.predictions.get(predictionId)`.
  </Accordion>

  <Accordion title="409 prediction_username_required or prediction_wallet_not_verified">
    The Blank account behind the key needs a username, and the pinned wallet must
    be verified. Fix both in the Blank app, then re-check with
    `blank.predictions.eligibility(mint, roundId)`.
  </Accordion>

  <Accordion title="409 prediction_insufficient_balance">
    Eligibility is evaluated against finalized chain state. A transfer that has
    not finalized yet will not count. Check `balanceRaw` and `evaluationSlot` on
    the eligibility response.
  </Accordion>

  <Accordion title="409 prediction_token_not_enabled">
    The token has no Price Prediction allocation. That allocation is fixed at
    launch and cannot be added later. See [Price
    Prediction](/docs/launching-tokens/price-prediction).
  </Accordion>

  <Accordion title="422 prediction_price_out_of_range">
    `predictedPriceInSol` must be a positive canonical decimal string — not `"0"`
    — with at most 12 integer digits and 18 fractional digits. Send a string, not
    a number.
  </Accordion>

  <Accordion title="409 prediction_exact_values_locked, or entries show no values">
    Exact predictions are withheld until the round durably locks, so nobody can
    copy live entries. Before lock, `listRoundPredictions` returns
    `{ visibility: "aggregate", participantCount }` and standings return this
    error. After lock, entries return `visibility: "exact"`. Your own prediction
    is always readable via `getOwnedPrediction`.
  </Accordion>

  <Accordion title="503 prediction_balance_unavailable">
    Finalized balance data is temporarily unreadable. Retry with backoff; it is
    not a rejection.
  </Accordion>
</AccordionGroup>

## Transaction intents

<AccordionGroup>
  <Accordion title="422 transaction_intent_tampered">
    The signed message differs from the message Blank prepared. This is almost
    always a wallet rewriting the fee payer, blockhash, compute-budget
    instructions, or lookup tables before signing. Disable transaction
    "enhancement" features, or sign the exact bytes from
    `serializedTransaction`.
  </Accordion>

  <Accordion title="422 transaction_intent_wrong_signer">
    The transaction was signed by a wallet other than `expectedSigner`. That field
    is the wallet pinned to the API key.
  </Accordion>

  <Accordion title="409 transaction_intent_expired">
    The blockhash window closed before a valid submission arrived. Blockhashes are
    short-lived: fetch, inspect, sign, and submit in one pass. Request a fresh
    intent — a signature cannot be moved to a new intent.
  </Accordion>

  <Accordion title="412 transaction_intent_version_conflict">
    The `If-Match` version was stale. Re-read the intent and submit with its
    current `version`.
  </Accordion>

  <Accordion title="I got 202 but do not know whether the transaction landed">
    `202` means durably queued, not confirmed. Poll
    `blank.transactionIntents.get(intentId)` honouring `Retry-After` until
    `status` is `confirmed`, `failed`, or `expired`. See [Operations and
    transaction intents](/docs/for-developers/operations).
  </Accordion>
</AccordionGroup>

## Webhooks

<AccordionGroup>
  <Accordion title="422 webhook_url_disallowed">
    Endpoint URLs must be public HTTPS on the default port, with no userinfo and
    no fragment, and must not resolve to a private, loopback, link-local, or
    reserved address. Hostnames ending in `.localhost`, `.local`, `.internal`,
    `.home.arpa`, or `.onion` are rejected. The policy is re-applied to every
    redirect hop.
  </Accordion>

  <Accordion title="Signature verification always fails">
    Verify the **raw request body** before parsing JSON. Re-serializing changes
    the bytes and therefore the HMAC. Also check that you are within the
    300-second timestamp tolerance and that your framework has not consumed or
    rewritten the body.
  </Accordion>

  <Accordion title="Deliveries fail during a secret rotation">
    During the overlap window Blank signs with both secrets: `v1` with the new
    secret and `v0` with the previous one. Pass both to `verifyWebhookSignature`
    via `secret` and `previousSecret` until the window closes.
  </Accordion>

  <Accordion title="Deliveries land in dead_letter">
    All 7 attempts failed. Inspect `responseStatus` and `lastErrorCode` on the
    delivery: `http_<status>` is your endpoint rejecting the request,
    `request_timeout` means you exceeded the 10-second delivery timeout, and
    `url_disallowed` or `redirect_limit_exceeded` are URL policy failures. Fix
    the endpoint, then drain with `blank.webhooks.replay(deliveryId)`.
  </Accordion>

  <Accordion title="I received the same event twice">
    Delivery is at-least-once. Deduplicate on `Blank-Event-Id`, which is stable
    across retries and replays; `Blank-Delivery-Id` changes each attempt.
  </Accordion>

  <Accordion title="Deliveries time out under load">
    The delivery timeout is 10 seconds. Return `2xx` as soon as the event is
    durably written to a queue or table, and do the real work asynchronously.
  </Accordion>
</AccordionGroup>

## Still stuck

Collect the `requestId`, the `code`, and the operation ID, and report them together. Never share an API key, a webhook signing secret, or a signed transaction.
