# Errors

Every error the API, the contract or the order flow can hand you, in one place, with what to do
about it.

## Error shape

API errors are JSON:

**Response**
```json
{ "error": "Order total is below the $1 minimum", "code": "BELOW_MIN_TOTAL" }
```

The two refusals that ask you to wait — `429 RATE_LIMITED` and `503 WARMING_UP` — carry
`retryAfterSec` and a `Retry-After` header instead of `code`. Order endpoints answer `400 { "error": "<message>" }` without a code.

## POST /api/quote

| HTTP | Code | Meaning | What to do |
| --- | --- | --- | --- |
| 400 | *(no code)* | The body is not valid JSON — rejected before validation, body `{ "error": "Unexpected token …" }`. A missing body counts as `{}` and fails with `BAD_FROM_CHAIN`. | Send `Content-Type: application/json` and a JSON object. |
| 400 | `BAD_FROM_CHAIN` | `chainId` is not an enabled FROM chain, or the chain is paused. | Pick a chain with `isTestnet: false`, `isEnabled: true` and no `isPaused` in `/api/chains`. |
| 400 | `BAD_TO_CHAIN` | `toChainId` is not an enabled TO chain, or paused. | Same check on the TO side. |
| 400 | `BAD_ADDRESS` | `fromToken`, `sender` or `toToken` is empty or malformed for its chain. | EVM `0x…`; Solana base58; TON any form. |
| 400 | `BAD_FROM_TOKEN` | `fromToken` is not listed for that FROM chain. | Use a token from `/api/chains`. |
| 400 | `BAD_TO_TOKEN` | `toToken` is not listed for that TO chain. | Use a token from `/api/chains` (`0x0…0` = native). |
| 400 | `BAD_ARRAYS` / `EMPTY` / `LENGTH_MISMATCH` | `recipients` / `toAmounts` not arrays, empty, or different lengths. | One amount per recipient. |
| 400 | `TOO_MANY` | More than 10 000 recipients. | Split into several quotes. |
| 400 | `BAD_RECIPIENT` | A recipient fails the TO chain's validation. | Check the address family (EVM / Solana / TON / Bitcoin testnet). |
| 400 | `BAD_TO_AMOUNT` | An amount is not a positive base-unit integer string. | `"5000000000000000000"`, not `5` or `5e18`. |
| 400 | `TO_TOO_SMALL` | A TO amount is below the per-recipient minimum. | Raise that amount. |
| 400 | `TO_TOO_PRECISE` | A TO amount is off the token's precision grid (4 decimals; whole sats for BTC). | Round it. |
| 400 | `FROM_ROUNDS_TO_ZERO` | A TO amount is too small to price in the FROM token. | Raise it. |
| 400 | `BELOW_SOL_RENT_MIN` | Solana TO: below the 0.001 SOL rent floor. | Send at least 0.001 SOL per recipient. |
| 400 | `BELOW_MIN_TOTAL` | Order total under $1 (single) / $5 (multisend). | Raise the amounts. |
| 400 | `NO_LIQUIDITY` | `Σ toAmounts` exceeds what can be dispensed right now. | Check `balance` in `/api/quotes`; lower the amounts or wait. |
| 500 | `NO_VAULT` | The FROM chain has no pay target configured. | Pick another FROM chain; tell us. |
| 503 | `NO_FROM_PRICE` | The FROM token's price feed is missing or stale (`400` with the same code if the feed reports a non-positive price). | Retry later or pay with another token. |
| 503 | `NO_TO_PRICE` | The TO token is unavailable (no rate or no liquidity data; `400` with the same code for a non-positive rate). | See `available` in `/api/quotes`; try later. |
| 503 | `SIGNING_DISABLED` | Quoting is paused by the operator. | Try later. |

## Rate limits and availability

| HTTP | Body | Meaning | What to do |
| --- | --- | --- | --- |
| 429 | `{ "error": "RATE_LIMITED", "retryAfterSec": n }` | Over 600 req/min per IP, or 10 quotes/min per IP, or 30 quotes/min per `sender`. | Sleep `Retry-After` seconds, then retry. Quote only when ready to pay. |
| 429 | *(empty body)* | Edge flood protection. | Back off harder. |
| 503 | `{ "error": "WARMING_UP", "retryAfterSec": 5, "waitingFor": [...] }` | Backend just restarted; prices / balances still loading. | Retry after `retryAfterSec`. |

Details: [Rate limits](https://docs.drip.tools/api/rate-limits.md).

## Other endpoints

| Endpoint | Error | Meaning |
| --- | --- | --- |
| `GET /api/quotes` | `400 Missing chainId or symbol` | Pass both or neither. |
| `GET /api/quotes` | `404 No quote found for symbol …` | No such TO token on that chain. |
| `GET /api/prices` | `400 Invalid address` | Malformed token address. |
| `GET /api/prices/at` | `400 chainId, address and ts are required` · `400 Invalid ts` · `404 No price recorded at or before ts` | Fix the parameters; history starts when the token was listed. |
| `GET /api/order` | `400 Provide order id or from_tx_hash` | Pass one of the two. |
| `GET /api/order?id=` | `200 null` | Not indexed yet — keep polling. Ids are case-sensitive. |
| `GET /api/orders` | `400 Incorrect address` | The payer identity is malformed (EVM `0x…`, Solana base58, TON). |
| `GET /api/orders` | `400 status must be one of: …` | Bad `status` filter. |
| `POST /api/orders/status` | `400 ids must be an array and is required` · `400 too many ids (max 500)` | Fix the body. |
| `GET /api/orders/stream` | `400 Incorrect address` · `503 Too many subscribers` | Fix the address · fall back to polling. |

## EVM contract reverts

Decode a revert against these custom errors (the ABI is on the
[contract page](https://docs.drip.tools/swap/contract.md#reverts-you-can-hit)):

| Error | Meaning | What to do |
| --- | --- | --- |
| `Expired` | Landed after `deadline`. | Request a fresh quote. |
| `MissingQuote` | `quoteId` is zero. | Pass the quote's `quoteId`. |
| `AmountTooLow` / `ZeroAmount` | Paid amount is zero or below the contract's per-token floor. | Pay `fromTotal`. |
| `TokenTransferMismatch` | The token delivered less than `amount` (fee-on-transfer / rebasing). | Use a listed token. |
| `TokenNotAllowed` | The token isn't accepted by the pay target. | Use a token from `/api/chains`. |
| `DirectPaymentNotAllowed` | A plain transfer to the contract, not a `pay` call. | Call `pay(...)`. |
| `EnforcedPause` | The contract is paused. | Try later. |

## Payments held for review

A payment that lands but does **not** match its quote — a different payer than `sender`, a
different token, or an amount other than `fromTotal` — does not become an order and is not
refunded automatically. It is flagged to the operator. If this happens to you, contact support on
[Telegram](https://t.me/drip_tools) or [Discord](https://discord.gg/Zn3M3gag2W) with the
transaction hash. On EVM, passing the quote's `fromTotal` makes this impossible to hit by
accident; on Solana / TON the transfer amount is yours to get right — see
[Payment rules](https://docs.drip.tools/reference/lifecycle.md#payment-rules).

The same applies to a **second payment of a quote that was already honoured**: it is held, and
which of the two payments to return is decided by the operator. Never retry a payment whose
transaction confirmed — track it instead.

## Refund reasons

A payment that can't become an order gets a refund record (`refund` on
[`GET /api/order`](https://docs.drip.tools/api/orders.md#refund-object), and a row in the payer's history). On TON look in
the history (`GET /api/orders?address=<UQ…>&view=batches`): a refused payment did not consume its
quote, so `from_tx_hash=<quoteId>` does not find it.

| Reason | Cause | Avoid it by |
| --- | --- | --- |
| `expired` | Paid after `deadline` (Solana / TON — on EVM the contract reverts). | Pay with plenty of the 180 s left; on TON set `validUntil = deadline`. |
| `unknown_quote` | The memo / comment carried a `quoteId` the backend never issued. | Put the `quoteId` from the quote response verbatim in the memo / comment. |
| `unattributed` | No readable `quoteId` at all in a transfer to the treasury (Solana / TON), above a small dust threshold. | Always send the memo / comment; never transfer to the treasury without a quote. |
| `ambiguous_memo` | Several different `quoteId`s in one Solana transaction. | One quote per transaction. |
| `bad_signature` | The stored quote's signature did not verify against a trusted signer key (operator-side). | Nothing to avoid; the payment comes back. |
| `failed_order` | Some recipients of a batch failed. | Nothing to avoid; their slice comes back (for a partial batch, capped so the delivered part still cost at least $1). |

Refunds are sent by the operator; the record shows `refund_pending` until the return transaction
is out, then `refunded` with `refundTxHash`.
