> ## 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.

# Orders and vault operations

> Sign orders per market generation, and withdraw, redeem or split / merge from a chosen vault with the vault field.

## Orders

The struct, domain `name` and `version` are the same for both
generations, binary and neg-risk:

```solidity theme={null}
struct Order {
    uint256 salt;
    address maker;
    address signer;
    address taker;
    uint256 tokenId;
    uint256 makerAmount;
    uint256 takerAmount;
    uint256 expiration;
    uint256 feeRateBps;
    uint8   side;             // 0=BUY, 1=SELL
    uint8   signatureType;    // always 1 (VAULT); 0 (EOA) is rejected
}
```

Two values depend on the market's generation
([how to detect it](https://docs.testnet.dev.adipredictstreet.com/concepts/vault-gen2/market-generation.md)):

* `maker`, in the struct and in the request body: your vault of that
  generation;
* `domain.verifyingContract`: the market's exchange.

```typescript theme={null}
// marketExchange / marketGeneration: see Detect a market's generation
const exchange = marketExchange(market);
const maker    = vaults.find(v => v.generation === marketGeneration(market))!.vaultAddress;
const domain   = { name: 'PredictStreet', version: '1', chainId: contracts.chainId, verifyingContract: exchange };

const order = {
  salt, maker, signer: eoa.address,
  taker: '0x0000000000000000000000000000000000000000',
  tokenId: BigInt(tokenId),
  makerAmount, takerAmount, expiration: 0n,
  feeRateBps: BigInt(market.feeTakerBps),
  side: 0, signatureType: 1,
};
const signature = await eoa.signTypedData(domain, types, order);   // `types`: the Order type from EIP-712 signing
// POST /api/orders/place with `maker` = the same vault, as on Placing orders
```

Everything else is as in [Placing orders](https://docs.testnet.dev.adipredictstreet.com/concepts/trading/placing-orders.md)
and [EIP-712 signing](https://docs.testnet.dev.adipredictstreet.com/concepts/trading/eip712-signing.md).

| You send | Answer |
| - | - |
| The other generation's vault as `maker` | `409 vault_generation_mismatch`. `error.details.vaultAddress` is the vault to use; also `marketGeneration`, `exchangeAddress`, `vaults[]`. Re-sign with that maker |
| A generation-2 market when your generation-2 vault comes from a retired testnet factory | `201`, `status: REJECTED`, `code: vault_factory_retired` (`RND-2026`, testnet/dev only; checked before `vault_generation_mismatch`, not replay-maskable; never fires on mainnet) |
| A generation-2 market before you upgraded | `409 vault_generation_missing`. [Upgrade](https://docs.testnet.dev.adipredictstreet.com/concepts/vault-gen2/upgrade.md) first |
| The right maker, signed against the other generation's exchange | `201`, `status: REJECTED`, `code: bad_signature` |
| Body `maker` that is neither the authenticated wallet nor one of your vaults | `401 maker_mismatch` |
| A canary market while your wallet is not allowlisted | `409 vault_gen2_not_enabled` or `400 market_not_tradeable` |
| An order that increases a position after the canary was closed | `409 vault_gen2_canary_closed`. Only position-reducing orders are accepted |

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`.

| `vault` | Used vault |
| - | - |
| Omitted, withdraw | The active vault |
| Omitted, redeem or split / merge / convert | The vault that holds the market's tokens |
| Not one of yours | `404 vault_not_found` |
| Malformed | `400 invalid_vault` |

## Withdraw

`POST /api/withdrawals/request` (`withdraw:write` or `vault:write`), the
[usual request](https://docs.testnet.dev.adipredictstreet.com/concepts/withdrawals/eip712.md) plus `vault`. `userSig` is
`WithdrawERC20` signed in the domain of that vault:

```typescript theme={null}
const userSig = await eoa.signTypedData(
  { name: 'PredictStreetVault', version: '1', chainId: contracts.chainId, verifyingContract: vault },
  WITHDRAW_ERC20_TYPES,   // the WithdrawERC20 type from WithdrawERC20 EIP-712
  { token: contracts.usdc, to, amount: parseUnits(amount, 6), salt, deadline },
);
await fetch(`${BASE}/api/withdrawals/request`, {
  method: 'POST', headers: auth,
  body: JSON.stringify({ amount, destination: to, salt: salt.toString(), expiry: Number(deadline), userSig, vault, totpCode }),
});
```

| Vault | Allowed `destination` |
| - | - |
| Generation 1 | As today: any address your whitelist allows |
| Generation 2 | Your own wallet, or an address the platform has listed (on chain: `VaultFactoryGen2.isWithdrawDestination(address)`). You cannot list one yourself |

* 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):

```typescript theme={null}
const sig = await eoa.signTypedData(
  { name: 'PredictStreetRedeem', version: '1', chainId: contracts.chainId },   // no verifyingContract
  { RedeemAuthorization: [
      { name: 'owner', type: 'address' }, { name: 'marketId', type: 'string' },
      { name: 'outcomeIndex', type: 'uint8' }, { name: 'deadline', type: 'uint256' } ] },
  { owner: eoa.address, marketId, outcomeIndex, deadline },
);
const { v, r, s } = Signature.from(sig);
const body = { marketId, outcomeIndex, vault, auth: { deadline, v, r, s } };
```

Refusals: `400 redeem_signature_required`, `400 redeem_expired`,
`403 redeem_signer_mismatch`. Sign a fresh authorization.

## Split / merge / convert

The [existing flow](https://docs.testnet.dev.adipredictstreet.com/concepts/portfolio/positions.md) 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

| Request | Answer |
| - | - |
| `GET /api/me/balances?vaults=1` | Adds `vaults: [{ vaultAddress, generation, active, balances, usdc?, balanceSource }]`, and (without `cashOnly`) `positionsValue`; `usdc` is `{ available, locked, pendingWithdrawal, inTransit }` (omitted on every vault while an atomic vault move is settling, during which `portfolioValue` is `unavailable` and `positionsValue` stays populated; `RND-2082`). See [Per-vault USDC](https://docs.testnet.dev.adipredictstreet.com/concepts/portfolio/balances.md#per-vault-usdc) |
| `GET /api/me/balances?vault=0x…` | One vault |
| `GET /api/me/positions` | Positions across both vaults. On a two-vault wallet each position carries `vaultAddress`, `vaultGeneration` and `vaultHoldings[]` |

## 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:

| Request | Answer | Where the halt fields are |
| - | - | - |
| `POST /api/orders/place`, `place-batch` | `201`, `status: REJECTED`, `code: market_generation_halted` | `details` of the order result |
| `POST /api/vault/split-signature`, `/merge-signature`, `/convert-signature` | `409 market_generation_halted` | `error.details.details` (also `entry`: `split` / `merge` / `convert`) |
| `POST /api/me/positions/op-submit` | `409 market_generation_halted` | `error.details` |

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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.