# Error codes (/docs/api/errors)

quarry's errors share one format. Handle them by `error.code`; the message is for
people and can change:

```json
{
  "error": {
    "code": "QUERY_TOO_LONG",
    "message": "'query' too long (max 2000 chars)",
    "details": { "field": "query", "max": 2000 }
  }
}
```

`details` is optional. Over HTTP, the error comes with the status below. Over MCP, it's
the text of a tool result with `isError: true`, and the HTTP status is 200. Two
exceptions: MCP arguments outside the documented limits get plain text starting with
`MCP error -32602`, not JSON, and the x402 `402` responses below have their own
format.

## Dataset routes [#dataset-routes]

`GET` and `POST` on `/{username}/{slug}`:

| Status | Code                    | Meaning                                                                              | What to do                                                                                                                      |
| ------ | ----------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `INVALID_JSON`          | The body isn't a JSON object                                                         | Send a JSON object                                                                                                              |
| 400    | `MISSING_QUERY`         | `query` is missing, empty, or not a string                                           | Send a non-empty `query`                                                                                                        |
| 400    | `QUERY_TOO_LONG`        | `query` is over 2,000 characters                                                     | Shorten it                                                                                                                      |
| 400    | `INVALID_TOP_K`         | `top_k` isn't a number from 1 to 20                                                  | Send an integer from 1 to 20                                                                                                    |
| 400    | `INVALID_ACCOUNT_TOKEN` | The `account_token` is expired or was made for another question. Nothing was charged | Call `ask` again and send the new `quarry.web_payment.body` as is                                                               |
| 402    | none                    | Payment required, or the payment failed                                              | See [Payment responses](#payment-responses)                                                                                     |
| 402    | `SUBSCRIPTION_REQUIRED` | The dataset only sells subscriptions                                                 | Use a subscription; `details` has the `contract` and `agent_mcp` links                                                          |
| 404    | `NOT_FOUND`             | No published dataset at this address                                                 | Find one with `search` or on [Explore](/explore)                                                                                |
| 404    | `NO_MATCH`              | No passage in this dataset answers the question. Nothing was charged                 | Rephrase the question, or try another dataset. The same signed payment can be sent again                                        |
| 409    | `PAYOUT_NOT_CONFIGURED` | The seller's payout wallet isn't set up                                              | Choose another dataset for now; the seller has to fix it                                                                        |
| 409    | `DATASET_EMPTY`         | The dataset has nothing to search yet                                                | Try later, or choose another dataset                                                                                            |
| 409    | `PAYMENT_IN_FLIGHT`     | Another request is using this signed payment                                         | Wait for its result before deciding to retry                                                                                    |
| 429    | `RATE_LIMITED`          | Too many requests                                                                    | Wait for the seconds in `Retry-After`: see [Limits](/docs/limits)                                                               |
| 500    | `SEARCH_FAILED`         | The search didn't complete                                                           | Retry after a delay; you weren't charged                                                                                        |
| 500    | `PAYMENT_FAILED`        | The payment service failed; settlement may have gone through                         | Don't sign a new payment yet. After 90 seconds, send the same signed payment again: if it already settled, it's refused as used |

## Payment responses [#payment-responses]

A paid `POST` can answer `402` in three ways. Read the headers, not only the body:

| Case                                    | Body                                       | Headers                                                          |
| --------------------------------------- | ------------------------------------------ | ---------------------------------------------------------------- |
| No payment attached                     | `"error": "payment_required"`, shown below | `PAYMENT-REQUIRED`: the requirements, base64 JSON                |
| The payment was refused at verification | `{}`                                       | `PAYMENT-REQUIRED`: the requirements, with the reason in `error` |
| Settlement failed                       | `{}`                                       | `PAYMENT-RESPONSE`: `"success": false`, with `errorReason`       |

The body of the first `402`, before any payment:

```json
{
  "error": "payment_required",
  "hint": "Payment requirements are in the PAYMENT-REQUIRED response header (base64 JSON). …",
  "contract": "https://quarry.market/alice/market-outlook",
  "terms": "https://quarry.market/terms"
}
```

Neither refusal charges you, and the signed payment stays unspent. The x402 library
behind the route can also answer `502` or `500` with `{ "error": "…" }` when the
facilitator fails: retry after a delay.

### When a payment keeps failing [#when-a-payment-keeps-failing]

* **Network:** sign for the network in `accepts`. A payment for Base can't pay on Base
  Sepolia, or the reverse.
* **Balance:** the wallet needs at least `amount` of the USDC contract in `accepts`. An
  unfunded wallet is refused at verification, and `error` names the balance or a
  reverted transfer.
* **Freshness:** sign a new payment for each question. A payment that already settled,
  or has expired, is refused.
* **Price:** if the price changed, sign for the requirements of the latest `402`.
* **Format:** sign the payment the x402 client makes for an `exact` USDC requirement,
  an EIP-3009 authorization. Other formats, such as Permit2, are refused at
  verification with `unsupported_payment_payload`.
* **Spend cap:** the x402 client refuses to sign over $1 by default: see
  [Paying with x402](/docs/x402#over-http).

## Subscription and agent APIs [#subscription-and-agent-apis]

The routes under `/api/agent`, and `query_dataset` on the sign-in address. `ask` uses
the same codes, except that a used-up month falls back to a payment request instead of
`QUOTA_EXHAUSTED`:

| Status | Code                    | What to do                                                                                                                        |
| ------ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `VALIDATION_ERROR`      | Check the fields against [Ask over HTTP](/docs/api/query#with-a-subscription)                                                     |
| 401    | `UNAUTHORIZED`          | Send a valid API key or OAuth token; a regular quarry login token isn't accepted. The response has a `WWW-Authenticate` challenge |
| 402    | `SUBSCRIPTION_REQUIRED` | No paid subscription for this dataset. A pending Checkout isn't paid access yet                                                   |
| 402    | `QUOTA_EXHAUSTED`       | No questions left this month. Wait for the renewal or pay per question with x402, if the dataset offers it                        |
| 404    | `NOT_FOUND`             | Check the dataset. A dataset the seller stopped selling stays available only to its subscribers                                   |
| 404    | `NO_MATCH`              | No passage answers the question, and no question was used. Rephrase it, or try another dataset                                    |
| 409    | `DATASET_EMPTY`         | The dataset has nothing to search yet                                                                                             |
| 409    | `REQUEST_IN_FLIGHT`     | This request ID is still running. Wait, then retry with the same ID                                                               |
| 409    | `IDEMPOTENCY_CONFLICT`  | The ID was used for a different question. Use the original fields to retry, or a new ID                                           |
| 409    | `RESERVATION_EXPIRED`   | Retry the same question with the same ID                                                                                          |
| 409    | `OFFER_UNAVAILABLE`     | Fetch the offer again with `quote_access`; it changed or the dataset is being retired                                             |
| 409    | `SELLER_UNAVAILABLE`    | The seller can't take subscription payments yet                                                                                   |
| 409    | `CHECKOUT_RECONCILING`  | quarry is checking a Stripe payment. Wait and try again                                                                           |
| 409    | `BILLING_IN_FLIGHT`     | Another billing operation is running. Retry after a delay                                                                         |
| 410    | `RESULT_EXPIRED`        | The saved answer is older than 24 hours. A new question needs a new ID and uses a question                                        |
| 429    | `RATE_LIMITED`          | Wait for `Retry-After`, or `details.retry_after` in a tool result                                                                 |
| 500    | `SEARCH_FAILED`         | The search failed and no question was used. Retry with the same ID                                                                |
| 500    | `BILLING_FAILED`        | The outcome is uncertain. Check your access, or retry with the same ID                                                            |
| 503    | `BILLING_UNAVAILABLE`   | Subscriptions aren't available on this deployment                                                                                 |

For `SUBSCRIPTION_REQUIRED` and `QUOTA_EXHAUSTED` on `POST /api/agent/query`,
`details` holds the `quote_access` result with the other ways to pay. Nothing is ever
bought for you.

## MCP tool results [#mcp-tool-results]

Tool errors come as `isError: true` with the format above. Besides the subscription
codes, `ask` and the other tools can return:

| Code                                     | Meaning                                                                                                                               |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `NOT_FOUND`                              | No published dataset at `username`/`slug`                                                                                             |
| `NO_MATCH`                               | No passage answers the question. Nothing was charged and no question was used                                                         |
| `RATE_LIMITED`                           | Too many calls; `details.retry_after` gives the wait (in the message only for an x402 payment)                                        |
| `SEARCH_FAILED`                          | The search didn't complete; nothing was charged                                                                                       |
| `SUBSCRIPTION_REQUIRED`                  | The dataset only sells subscriptions; the result also carries the `quarry` hints                                                      |
| `PAYMENT_UNAVAILABLE`                    | Paying per question is down for now; subscriptions still work                                                                         |
| `PAYOUT_NOT_CONFIGURED`, `DATASET_EMPTY` | As for the dataset routes                                                                                                             |
| `PAYMENT_IN_FLIGHT`                      | The signed payment is in use by another call                                                                                          |
| `PAYMENT_FAILED`                         | The payment service failed and settlement may have gone through. Wait 90 seconds, then send the same signed payment again             |
| `UNAUTHORIZED`                           | An account tool needs a signed-in account. On the free address, an anonymous call gets an HTTP 401 challenge instead of a tool result |
| `REQUEST_FAILED`                         | An unexpected error; retry after a delay                                                                                              |

An `ask` without payment isn't an error code: it's the `PaymentRequired` result
described in the [MCP server](/docs/mcp#ask) reference.
