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

# Error codes

> Complete list of error codes across the PredictStreet API.

## Authentication

| Code                           | HTTP | Meaning                                                                                                                |
| ------------------------------ | ---- | ---------------------------------------------------------------------------------------------------------------------- |
| `auth_required`                | 401  | `X-Api-Key` header missing on an authenticated endpoint                                                                |
| `api_key_bad_format`           | 401  | `X-Api-Key` header didn't match `ps_<env>_<keyId>_<secret>` shape                                                      |
| `api_key_unknown_key`          | 401  | `keyId` not found in the registry (typo, wrong environment, or revoked long ago and garbage-collected)                 |
| `api_key_bad_secret`           | 401  | Stored hash didn't match — wrong secret                                                                                |
| `api_key_revoked`              | 401  | Key was explicitly revoked via admin                                                                                   |
| `api_key_expired`              | 401  | Key's `expiresAt` has passed                                                                                           |
| `api_key_suspended`            | 401  | Partner was suspended — all their keys stop working until reactivation                                                 |
| `api_key_ip_denied`            | 401  | Caller IP not in the key's `ipAllowlist`                                                                               |
| `api_key_no_associated_wallet` | 401  | `single_wallet` partner has no `associatedWallet` attached — admin must attach one before authenticated endpoints work |
| `api_key_user_wallet_required` | 401  | `multi_wallet` partner: `X-User-Wallet` header missing on an authenticated request                                     |
| `api_key_user_wallet_invalid`  | 401  | `multi_wallet` partner: `X-User-Wallet` header value isn't a `0x` + 40-hex address                                     |
| `api_key_scope_missing`        | 403  | Key lacks the scope required by this endpoint; response body includes `requiredScope` + `have[]`                       |
| `wallet_banned`                | 403  | The request's effective wallet (associated or `X-User-Wallet`) is on the banned list                                   |

## Trading

| Code                               | HTTP           | Meaning                                                                                                                                                                                                                                                                                                  |
| ---------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_amounts`                  | 400            | `price` or `quantity` ≤ 0 or bad format                                                                                                                                                                                                                                                                  |
| `invalid_tif`                      | 400            | MARKET+GTC rejected                                                                                                                                                                                                                                                                                      |
| `bad_signature`                    | 400            | EIP-712 recover failed (see [diagnostic hints](#bad_signature-diagnostic-hints) below)                                                                                                                                                                                                                   |
| `order_signed_with_floor_notional` | 400            | `BUY.makerAmount` was FLOOR-rounded; CEIL is required to keep matcher math from overflowing the maker cap on chain. `details` carries `signedMakerAmountWei` + `expectedCeilMakerAmountWei` — re-sign with the latter. See [diagnostic hints](#order_signed_with_floor_notional-diagnostic-hints) below. |
| `fee_too_high`                     | 400            | `feeRateBps` > MAX\_FEE\_RATE\_BIPS (1000)                                                                                                                                                                                                                                                               |
| `expired`                          | 400            | Order `expiration` in the past                                                                                                                                                                                                                                                                           |
| `invalid_outcome`                  | 400            | Outcome index out of range                                                                                                                                                                                                                                                                               |
| `maker_mismatch`                   | 400            | `Order.maker` doesn't equal the authenticated wallet OR its bound vault — for `multi_wallet` partners using `X-User-Wallet`, `maker` MUST be the sub-account's wallet (or the vault auto-deployed for it). See [diagnostic hints](#maker_mismatch-diagnostic-hints) below                                |
| `wallet_mismatch`                  | 401            | Legacy alias of `maker_mismatch` still surfaced by some withdrawal / vault / position endpoints — same semantics. Treat identically; we are converging the order surface on `maker_mismatch` and the rest of the API will follow                                                                         |
| `insufficient_funds`               | 200 (envelope) | Balance overdraft                                                                                                                                                                                                                                                                                        |
| `market_not_open`                  | 409            | Market status ≠ OPEN                                                                                                                                                                                                                                                                                     |
| `idempotency_race`                 | 500            | Internal race on (wallet, clientOrderId)                                                                                                                                                                                                                                                                 |
| `matcher_error`                    | 200 (envelope) | Matcher returned a business error                                                                                                                                                                                                                                                                        |
| `exchange_unavailable`             | 503            | Exchange-service / matcher unreachable                                                                                                                                                                                                                                                                   |

## Orders

| Code              | HTTP | Meaning                                |
| ----------------- | ---- | -------------------------------------- |
| `order_not_found` | 404  | Order ID doesn't exist for this wallet |
| `not_cancellable` | 409  | Order already terminal                 |
| `forbidden`       | 403  | Order belongs to a different wallet    |

## Withdrawals

| Code                      | HTTP           | Meaning                                                                                                                                                              |
| ------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_amount`          | 400            | Amount ≤ 0                                                                                                                                                           |
| `bad_signature`           | 400            | User EIP-712 signature invalid                                                                                                                                       |
| `destination_not_cleared` | 200 (envelope) | New destination below EDD threshold                                                                                                                                  |
| `new_destination_edd`     | 200 (envelope) | New destination above EDD → MLRO review                                                                                                                              |
| `wallet_banned`           | 200 (envelope) | Destination on banned-wallets list                                                                                                                                   |
| `aml_blocked`             | 200 (envelope) | Destination failed AML screen                                                                                                                                        |
| `invalid_state`           | 409            | Tried to cancel after SUBMITTED                                                                                                                                      |
| `withdrawal_not_found`    | 404            | Withdrawal ID doesn't exist (or doesn't belong to the authenticated wallet). Same code on `GET /api/me/withdrawals/{id}` and `POST /api/me/withdrawals/{id}/cancel`. |

## Withdrawal security (2FA & whitelist)

Gate codes returned by `POST /api/withdrawals/request` when the platform
enforces withdrawal security (SSO partners exempt). See
[2FA](/concepts/withdrawals/two-factor) and
[Address whitelist](/concepts/withdrawals/address-whitelist).

| Code                                 | HTTP | Meaning                                                                              |
| ------------------------------------ | ---- | ------------------------------------------------------------------------------------ |
| `withdrawal_address_not_whitelisted` | 403  | Whitelist is configured (≥1 entry) and `destination` is not on it                    |
| `totp_required`                      | 403  | 2FA is active on the wallet but `totpCode` was omitted from the withdraw request     |
| `totp_invalid`                       | 403  | TOTP/backup code wrong, expired, or already used (replay)                            |
| `totp_not_configured`                | 403  | 2FA is not active (also returned by `confirm`/`disable` when there is no active 2FA) |

Codes returned by the management endpoints under
`/api/me/withdrawal-security/*`:

| Code                          | HTTP | Meaning                                                                                                                                                                                         |
| ----------------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `totp_already_configured`     | 409  | `POST …/totp/setup` while 2FA is already active — disable it first                                                                                                                              |
| `totp_setup_not_pending`      | 403  | `POST …/totp/confirm` with no pending setup (or 2FA already active)                                                                                                                             |
| `invalid_address`             | 400  | `POST …/whitelist` — `address` is not a `0x` + 40-hex EVM address                                                                                                                               |
| `address_already_whitelisted` | 409  | `POST …/whitelist` — `(wallet, address)` already exists                                                                                                                                         |
| `whitelist_limit_reached`     | 409  | `POST …/whitelist` — per-wallet cap (50) reached                                                                                                                                                |
| `whitelist_address_not_found` | 404  | `DELETE …/whitelist/{id}` — no such entry for this wallet                                                                                                                                       |
| `account_provisioning`        | 409  | The acting wallet is not yet provisioned in `core.users` (first request for a freshly-seen `multi_wallet` `X-User-Wallet`, before partner-backed onboarding commits). Transient — retry shortly |

## Faucet (testnet)

| Code               | HTTP | Meaning             |
| ------------------ | ---- | ------------------- |
| `faucet_disabled`  | 403  | Faucet not enabled  |
| `faucet_daily_cap` | 429  | Daily cap exceeded  |
| `invalid_amount`   | 400  | Amount out of range |

## Rate limits

| Code           | HTTP | Meaning                                       |
| -------------- | ---- | --------------------------------------------- |
| `rate_limited` | 429  | Bucket exhausted; retry after `retryAfterSec` |

## Service-level

| Code                          | HTTP | Meaning                                                       |
| ----------------------------- | ---- | ------------------------------------------------------------- |
| `exchange_unavailable`        | 503  | Upstream service temporarily unavailable — retry with backoff |
| `exchange_transport_error`    | 503  | Connection failed                                             |
| `exchange_malformed_response` | 503  | Non-JSON response                                             |
| `not_implemented`             | 501  | Feature disabled in this env                                  |

## Handling strategy

* **4xx codes** — fix the client.
* **200 with `code`** — business logic reject; surface to user.
* **429** — wait `retryAfterSec` then retry.
* **5xx** — exponential backoff with jitter; 3 attempts max for
  idempotent calls. Non-idempotent writes use `clientOrderId` / `nonce`
  * `deadline` for safe retries.

## `bad_signature` diagnostic hints

When `POST /api/orders/place` rejects with `code: bad_signature`, the
response body's `details` block carries enough context to diff your
client struct against what the platform reconstructed. This turns a
24-hour blind debug into a 30-second visual diff:

```json theme={null}
{
  "code": "bad_signature",
  "message": "EIP-712 signature invalid",
  "details": {
    "recovered":        "0xRecoveredFromYourSig",
    "expected_signer":  "0xYourAuthenticatedWallet",
    "platform_canonical_struct": {
      "salt":          "12345...",
      "maker":         "0xVaultAddr",
      "signer":        "0xYourEoa",
      "taker":         "0x0000000000000000000000000000000000000000",
      "tokenId":       "999...",
      "makerAmount":   "55000000",
      "takerAmount":   "100000000",
      "expiration":    "0",
      "feeRateBps":    "140",
      "side":          0,
      "signatureType": 1
    }
  }
}
```

`recovered ≠ expected_signer` ⇒ the digest you signed differs from
what the platform reconstructs. Compare your client-side struct
field-by-field against `platform_canonical_struct`. The most common
mismatch is **`feeRateBps`**: per-market value lives at
`market.feeTakerBps` (`GET /api/markets/{slug}`) and MUST be signed
verbatim — signing `0n` while the platform reconstructs with `140`
gives different digests and recovers a different address.

`recovered: null` ⇒ signature wasn't a valid EIP-712 sig at all
(missing, malformed hex, wrong length). Re-sign and resend.

Both addresses are checksum-cased — partner clients can compare with
`recovered === expected_signer` directly. Every value in
`platform_canonical_struct` is either user-supplied (already known to
the partner) or platform-public market metadata, so no privacy
concerns from logging the response.

## `maker_mismatch` diagnostic hints

`maker_mismatch` (and its legacy alias `wallet_mismatch` on the
non-order surfaces) is the platform's authorization check that the
off-chain order signer can actually instruct the on-chain custody
contract that will be debited at settlement. Specifically:

```
order.maker MUST equal EITHER
  - the authenticated wallet (X-User-Wallet, lowercased), OR
  - VaultFactory.vaultOf(authenticated wallet) on this chain
```

Three patterns produce this rejection in the wild:

**1. Signing-wallet ↔ authenticated-wallet drift.** The most common.
You're signing as EOA `A` while authenticating as wallet `B`.
This always fails — even when `A === VaultFactory.vaultOf(B)` on
chain. The check is per-request: `X-User-Wallet` is the principal,
`order.maker` is the counterparty in the signed payload, and the two
are compared after resolving the principal's vault. Reuse of an
operator's session to sign for another user's vault is rejected by
design — signer-of-record and authenticated-of-record are bound 1:1
even when the EOA is technically authorised on multiple vaults
on-chain. Each end-user vault must be addressed under its own
authenticated session.

**2. Stale local `vaultOf` cache during the deploy/observe race.**
If you've recently rotated to vault-direct signing
(`order.maker == vault`), make sure your local `vaultOf(wallet)`
cache is fresh. Order placement happens during a brief window where
the on-chain vault may already exist but the off-chain mirror hasn't
yet observed the `VaultDeployed` event. The placement handler
re-reads the chain on cache miss, so the platform-side resolution is
correct — but a stale value held by your client will pre-fail the
check on your side before the request even reaches us.

**3. Vault-not-yet-deployed.** Legitimate `maker_mismatch` —
`VaultFactory.vaultOf(wallet)` resolves to `null` and only the
plain-wallet form of `order.maker` is accepted. Deploy the vault
first, then retry. The vault-deploy `signedOp` flow lives at
`POST /api/me/vault/deploy/sign` → `POST /api/me/vault/deploy/submit`.

The HTTP status differs by surface:

* `POST /api/orders/place` returns **400** (current order endpoint) —
  rejected at the EIP-712 maker resolution step before the order ever
  reaches the matcher.
* Legacy withdrawal / vault / position endpoints still throw the
  alias `wallet_mismatch` as **401** (`UnauthorizedException`).

Treat both as the same condition. Diagnose with the three patterns
above; the fix in all three cases is on the client side.

## `order_signed_with_floor_notional` diagnostic hints

When `POST /api/orders/place` rejects with `code:
order_signed_with_floor_notional`, the user signed a `BUY` order
whose `makerAmount` was computed with **integer FLOOR division**
(`(priceWei * qtyWei) / 1_000_000n` in JS — BigInt division
truncates toward zero). For boundary tuples where `price × qty`
doesn't divide cleanly into 6-decimal wei, FLOOR underflows the
canonical CEIL by 1 wei.

Why we reject: FLOOR-signed BUY orders sit on the book with **zero
wei of headroom** against the matcher's per-fill CEIL math. On the
closing tail fill of a multi-leg cross-outcome batch, the cumulative
`maker_fill_amount_wei` overshoots the signed `makerAmount` by 1 wei
→ chain reverts `MakingGtRemaining` (selector `0xe2cc6ad6`) → the
**TAKER** absorbing this order (a different user, not the FLOOR
signer) sees the failure. Rejecting at placement prevents the poison
from reaching the book at all.

Response envelope shape:

```json theme={null}
{
  "status": "REJECTED",
  "code": "order_signed_with_floor_notional",
  "message": "Order signed with FLOOR-rounded BUY notional (makerAmount=34999999 wei). CEIL is required to keep matcher math from overflowing your maker cap on chain — re-sign with makerAmount=ceil(price × qty / 1e6) = 35000000. See predictstreet-frontend/src/lib/eip712-order.ts:170 for the canonical formula.",
  "details": {
    "signedMakerAmountWei": "34999999",
    "expectedCeilMakerAmountWei": "35000000",
    "signedRounding": "FLOOR",
    "expectedRounding": "CEIL"
  }
}
```

**Fix:** patch the `makerAmount` to `details.expectedCeilMakerAmountWei`
and re-sign + re-submit. The SDK shouldn't need to recompute the
formula client-side — `details` carries the exact integer to use.

Canonical CEIL formula (TypeScript / Python identical semantics):

```typescript theme={null}
const product     = priceWei * qtyWei;
const notionalWei = (product + 999_999n) / 1_000_000n; // CEIL
```

The +1 wei vs FLOOR is sub-cent and economically meaningless, but
the chain math cares about every wei. See [Rounding rule
section](/concepts/trading/eip712-signing#rounding-rule-ceil-on-buy-floor-on-sell-notional)
for the full asymmetric CEIL-BUY / FLOOR-SELL story.

**SELL orders are unaffected** — `SELL.makerAmount` is the exact
outcome-token quantity (`qtyWei`), no rounding involved. Only `BUY`
hits this gate.
