Skip to main content

Orders

The struct, domain name and version are the same for both generations, binary and neg-risk:
Two values depend on the market’s generation (how to detect it):
  • maker, in the struct and in the request body: your vault of that generation;
  • domain.verifyingContract: the market’s exchange.
Everything else is as in Placing orders and EIP-712 signing. Cancels (POST /api/orders/cancel, cancel-batch, cancel-all) are keyed by your wallet: they cover orders of both vaults, before and after the upgrade. GET /api/orders/open lists the orders of both vaults; each order’s maker shows which vault it rests on.

The vault field

Withdraw, redeem and split / merge / convert take an optional vault: one of your vaults[].vaultAddress.

Withdraw

POST /api/withdrawals/request (withdraw:write or vault:write), the usual request plus vault. userSig is WithdrawERC20 signed in the domain of that vault:
  • Any other destination from a generation-2 vault: 400 withdrawal_destination_not_allowed (details: { destination, vaultAddress, generation }). It is checked before the 2FA code, so the code is not spent. Withdraw to your wallet and transfer from there.
  • A listed destination can be removed at any time. A refused destination is final for that request.
  • 2FA and whitelist rules are unchanged. A TOTP code is single-use per 30-second step: a second withdrawal in the same step needs the next code or a backup code.
  • 503 withdrawal_unknown: the withdrawal may have been created. Do not sign a new one; a new salt is a second withdrawal. Re-send the same body (same salt, expiry, userSig, totpCode) for up to about 60 seconds, then check GET /api/me/withdrawals. A stored request answers with its withdrawal (details.withdrawalId).
  • 503 withdrawal_temporarily_unavailable: nothing was created. Retry later, with a new 2FA code if 2FA is on.
  • Cancel (POST /api/me/withdrawals/{id}/cancel) now also accepts withdraw:write.

Return stray USDC (RND-2019)

USDC sent directly to a vault contract address by a plain ERC-20 transfer (not deposited via POST /api/deposits/relay) is never credited to trading balances. GET /api/me/vault reports this as strayUsdc on the active vault and inside vaults[].strayUsdc for each deployed vault. strayUsdcReturnEnabled: true means a return would be queued now: it is enabled on the environment, neither the platform nor the withdrawals freeze is on, and the relay can take it. When it is false although returns are enabled, strayUsdcReturnBlockedBy names the code the POST would answer 503 with: platform_frozen, withdrawals_frozen or stray_usdc_unavailable (otherwise null). The freezes are read only when one of your vaults holds stray USDC. To return stray USDC to your owning wallet without user gas costs: POST /api/me/vault/stray-usdc/return (withdraw:write or vault:write; optional ?vault=0x... query parameter to target an inactive vault). The platform relay executes VaultLedger.sweepOffLedgerERC20(USDC):
  • Returns 202 Accepted (or 200 OK if a return job just finished) with a StrayUsdcReturnRespDto containing jobId, status, and enqueued.
  • Idempotent: while a return job is in flight, repeating the call returns the existing job (enqueued: false).
  • Poll status via GET /api/me/vault/stray-usdc/return/{jobId} until status: "confirmed".
  • When confirmed, /api/me/activity shows a corresponding stray_usdc_return transfer entry.
  • Returns 409 no_stray_usdc when nothing is stray; 409 stray_usdc_return_disabled if feature is disabled on the environment; 422 stray_usdc_return_reverted if simulation reverts on chain.
  • Returns 503 platform_frozen / 503 withdrawals_frozen (retryAfterSec) while the platform or the withdrawals freeze is on: nothing is queued, so show a paused state rather than an error. A return queued before the freeze stays pending (held, not sent) and goes ahead once the freeze lifts; keep polling its jobId. If the relay has not sent it in time, it ends expired and you ask again.
  • Returns 503 stray_usdc_unavailable (retryAfterSec) when the chain could not be read or returns are paused on the platform side; nothing is queued, retry later.

Redeem

POST /api/me/positions/redeem (positions:write or vault:write), then poll GET /api/me/positions/redeem/{jobId} until confirmed. Redeem a legacy market on the generation-1 vault and a generation-2 market on the generation-2 vault; the payout lands in that vault. API-key callers sign a RedeemAuthorization (the relay is gasless):
Refusals: 400 redeem_signature_required, 400 redeem_expired, 403 redeem_signer_mismatch. Sign a fresh authorization.

Split / merge / convert

The existing flow with vault added at each step:
  1. POST /api/vault/split-signature (or merge-signature, convert-signature) with { marketId, amount, vault } and an Idempotency-Key header. For neg-risk markets, supply outcomeIndex (uint8, 0..254) to specify the sub-question. The answer carries vaultAddress, the payload and backendSig.
  2. Sign the typed data in the domain of vaultAddress (PredictStreetVault / 1). Check it hashes to digest.
  3. POST /api/me/positions/op-submit with { signedOpId, ownerSig, backendSig, vault }, then poll GET /api/me/positions/op-submit/{jobId} until confirmed.

Balances and positions

Legacy markets after the upgrade

  • Quote and trade them as before, with the generation-1 vault as maker and the generation-1 exchange as verifyingContract, until the generation-1 trading halt (below).
  • Resting generation-1 orders stay live and cancel as usual. During the halt they stay on the book but cannot match, since no new order is accepted on the market. When the platform then retires the market (status CLOSED), they are cancelled and their unfilled part is refunded; a partial fill keeps its filled position.
  • When a legacy market resolves, redeem with vault = the generation-1 vault, then withdraw or move what is left.

Generation-1 trading halt

At generation-2 activation the platform halts trading on generation-1 markets. From then on, on a generation-1 market: The halt fields are generation (1), verdict (halted, or unknown when the halt could not be confirmed after activation) and haltedAt (ISO-8601 or null). The refusal is not retryable. A re-send of an order or op the platform accepted before the halt is answered as it was then, not refused. 503 vault_generation_unavailable means the market’s generation could not be read and nothing was done; retry it. The signature endpoints can answer it at any time, op-submit only while the halt is in force. Generation-2 markets are never refused, and cancel, redeem, withdrawal and USDC moves are not affected. On the market read surface (GET /api/markets, GET /api/markets/{slug}, and markets[] in GET /api/events and /featured):
  • Every generation-1 market sets generationRetired: true and generationHaltedAt (ISO-8601 timestamp).
  • A generation-1 market in PRE_MARKET, OPEN, PAUSED, SUSPENDED, POSTPONED, or CLOSED with lifecycle.closedAt >= generationHaltedAt reads tradable: false, tradingMode: "closed", and notTradableReason: "generation_retired" (GET /api/events list rows omit tradingMode and notTradableReason, which can be derived with the same rule). A market that had already closed on its schedule before the halt (lifecycle.closedAt < generationHaltedAt) keeps notTradableReason: "closed_at_cutoff". From DELAYED onward, its settlement state (awaiting_data, awaiting_settlement, under_review, resolved, voided) is kept.
  • rfq.available is false on every generation-1 market, and GET /api/markets/{symbol}/executable-price reports tradable: false.
  • When the retire walk closes a generation-1 market, market_lifecycle pushes status_changed with toStatus: "CLOSED" and action: "market.retire_gen1" (a normal cutoff close sends action: "market.close"), and each cancelled resting order’s order_cancelled frame carries publicMessage explaining that trading ended with the platform upgrade.