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 byGET /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 byPOST /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) answer201both for accepted orders and for these business rejects, so branch on the body (status: "REJECTED", orsuccess: falseper batch entry), never on the HTTP code alone. - 429 — wait
retryAfterSecthen 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 samesalt. 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’smakerfield carries your EOA.signatureType— always1(VAULT).0(EOA) is not supported: the platform verifies every order with1, so a signature over0recovers 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’snonce(decimal uint256 string).expiration— the request’sexpiry(Unix seconds,0= no expiry).feeRateBps— the market’sfeeTakerBps(GET /api/markets/{slug}), signed verbatim. Signing0nwhile the platform reconstructs with140gives different digests and recovers a different address.side—0for BUY,1for SELL.makerAmount/takerAmount— derived fromprice×quantity(see the rounding rule).
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:
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/placereturns 401maker_mismatch. The gateway compares the request body’smakerwith your authenticated wallet and its bound vault before the order is sent for signature verification or matching.POST /api/orders/place-batchreturns the same code as a per-entryREJECTEDresult (success: false) inside the normal response array. The other entries in the batch are still processed.wallet_mismatchis a legacy alias with the same meaning. Where it still appears, it is returned as 403.
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:
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):
SELL.makerAmount is the exact
outcome-token quantity (qtyWei), no rounding involved. Only BUY
hits this gate.