Skip to main content

Authentication

Trading

Orders

Cancelling never answers with an HTTP error for an unknown or already ended order: POST /api/orders/cancel returns 200 with status: "not_found" or status: "already_terminal". A cancel that is recorded but has no recorded answer yet returns 202 with status: "cancel_pending", code: "cancel_pending" and Retry-After: 5. Send the same cancel again after Retry-After until it ends. See Cancelling orders.

Portfolio

Events

Returned by GET /api/events, /api/events/featured and /api/events/{slug}. On most of these message is a generic string, so switch on code. The crypto filters (invalid_asset, invalid_cadence, invalid_frequency) are listed under Crypto markets.

Crypto markets

Returned by the crypto read surfaces — GET /api/markets/{symbol}/crypto-price-history, GET /api/crypto-series/{id}/windows, and the crypto filters on GET /api/events (invalid_asset, invalid_cadence, invalid_frequency; the other GET /api/events codes are under Events).

Withdrawals

Withdrawal security (2FA & whitelist)

Gate codes returned by POST /api/withdrawals/request when the platform enforces withdrawal security (SSO partners exempt). See 2FA and Address whitelist. Codes returned by the management endpoints under /api/me/withdrawal-security/*:

Rate limits

Service-level

Handling strategy

  • 4xx codes — fix the client.
  • 2xx with code — business logic reject; surface to user. The order endpoints (POST /api/orders/place, POST /api/orders/place-batch) answer 201 both for accepted orders and for these business rejects, so branch on the body (status: "REJECTED", or success: false per batch entry), never on the HTTP code alone.
  • 429 — wait retryAfterSec then retry.
  • 5xx — exponential backoff with jitter; 3 attempts max for idempotent calls. To retry a write, resend the identical signed request and never re-sign it: orders under the same clientOrderId, withdrawals with the same salt. A resent order or withdrawal is never executed twice.

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:
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 platform’s values are authoritative — it builds every order’s struct the same way, so sign exactly these values:
  • maker — your vault, VaultFactory.vaultOf(signer). Never your EOA, even when the request body’s maker field carries your EOA.
  • signatureType — always 1 (VAULT). 0 (EOA) is not supported: the platform verifies every order with 1, so a signature over 0 recovers a different address, and the exchange contract rejects any other value at settlement.
  • signer — your authenticated wallet (expected_signer).
  • taker — the zero address.
  • tokenId — the token of the outcome you are trading.
  • salt — the request’s nonce (decimal uint256 string).
  • expiration — the request’s expiry (Unix seconds, 0 = no expiry).
  • feeRateBps — the market’s feeTakerBps (GET /api/markets/{slug}), signed verbatim. Signing 0n while the platform reconstructs with 140 gives different digests and recovers a different address.
  • side — 0 for BUY, 1 for SELL.
  • makerAmount / takerAmount — derived from price × quantity (see the rounding rule).
If every field matches, check the domain: name PredictStreet, version 1, the chain’s chainId. verifyingContract is the neg-risk exchange for markets with negRiskEligible: true and the binary exchange for all other markets. recovered: null ⇒ signature wasn’t a valid EIP-712 sig at all (missing, malformed hex, wrong length). Re-sign and resend. recovered === expected_signer but still bad_signature ⇒ your struct and signature are correct, but the signed non-zero expiration is at or before the platform’s current time. There is no separate expired code: an order that is already expired at placement is rejected as bad_signature. Sign with expiration = 0 (no expiry) or a timestamp safely in the future, and send the same value in the request’s expiry field. (EXPIRED also appears as an order status, for a resting order that reaches its expiration after placement.) 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 request body’s maker belongs to the authenticated wallet: the wallet itself or the on-chain vault that will be debited at settlement. Specifically:
This check covers only the request body’s maker field. It is separate from what you sign: the EIP-712 Order.maker must always be the vault, with signatureType = 1. When the body carries your wallet, the platform substitutes the vault before it verifies the signature. A struct signed with maker = wallet therefore fails with bad_signature, not maker_mismatch. Three patterns produce this rejection in the wild: 1. Body maker belongs to a different wallet. The request body’s maker is wallet A (or A’s vault) while you authenticate as wallet B — your key’s associatedWallet, or the X-User-Wallet header for multi_wallet keys. The check runs per request and compares only the body maker, which must be B or VaultFactory.vaultOf(B). Placing orders for another user’s vault from an operator’s session is rejected by design — each end-user vault must be addressed under its own authenticated session. This check never inspects the signature: if the body maker is right but a different EOA signed the struct, you get bad_signature instead, with recovered ≠ expected_signer. 2. Stale local vaultOf cache during the deploy/observe race. If you send your vault as the request body’s maker, 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 VaultCreated 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 deployed yet. Until VaultFactory.vaultOf(wallet) returns a vault, the only body maker that passes this check is the authenticated wallet itself; any other address is rejected with maker_mismatch. A wallet maker passes, but the order is then rejected with vault_not_deployed, because trading requires a deployed vault. The platform deploys vaults automatically (see Backend auto-deploy) — there is no endpoint to request one. Poll GET /api/me/vault until it reports deployed: true (deployStatus shows progress), then retry. The response shape differs by endpoint:
  • POST /api/orders/place returns 401 maker_mismatch. The gateway compares the request body’s maker with your authenticated wallet and its bound vault before the order is sent for signature verification or matching.
  • POST /api/orders/place-batch returns the same code as a per-entry REJECTED result (success: false) inside the normal response array. The other entries in the batch are still processed.
  • wallet_mismatch is a legacy alias with the same meaning. Where it still appears, it is returned as 403.
Treat both codes 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 last fill of a multi-fill match, the summed per-fill amounts can exceed 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:
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):
The +1 wei vs FLOOR is sub-cent and economically meaningless, but the chain math cares about every wei. See Rounding rule section 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.