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

# EIP-712 signing

> The Order typed-data struct PredictStreet uses, domain parameters, SignatureType modes, and signing examples.

Every order, withdrawal, split, merge, and convert-positions request
requires an [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed-data
signature. Different operations sign under **different domains**:

| Operation                         | Domain name                     | `verifyingContract`                                                                | Struct                                                                                                   |
| --------------------------------- | ------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| Place order (binary market)       | `PredictStreet`                 | `CTFExchange` address                                                              | `Order` (this page)                                                                                      |
| Place order (neg-risk market)     | `PredictStreet`                 | `PredictStreetNegRiskCtfExchange` address                                          | `Order` (this page)                                                                                      |
| Withdraw USDC                     | `PredictStreet` (same as order) | `CTFExchange` address                                                              | `WithdrawERC20` — see [Withdrawals EIP-712](/concepts/withdrawals/eip712)                                |
| Split / merge / convert positions | **`PredictStreetVault`**        | **per-user vault clone address** (`VaultFactory.vaultOf(signer)`), NOT the factory | `SplitPosition` / `MergePositions` — see [Contracts → Vaults](/concepts/contracts/vaults#eip-712-domain) |

Using the wrong domain (name, version, or `verifyingContract`) produces
a signature that recovers a different address than your signer. The
backend returns `400 bad_signature`; on-chain it reverts at
`_verifyOrderSignature` / `_verifyVault`.

## Order-signing domain

```typescript theme={null}
const domain = {
  name: 'PredictStreet',
  version: '1',
  chainId: 99999,
  verifyingContract: '0x4074c225b296E1E556c565B0C3Ddba305E63E7c4', // CTFExchange (binary)
};
```

<Warning>
  **Binary vs neg-risk — pick the right `verifyingContract`.**

  Both exchanges share `name: "PredictStreet"` + `version: "1"` but live
  at different on-chain addresses, so the EIP-712 domain separator
  differs.

  * Binary markets → `CTFExchange` (`0x4074c225b296E1E556c565B0C3Ddba305E63E7c4`).
  * Neg-risk markets → `PredictStreetNegRiskCtfExchange` (`0x2eB97912c333963a21410Af1eF7E9a0aAB7631bf`).

  Read `negRiskEligible` on `GET /api/markets/{symbol}` — `true`
  means neg-risk, `false` (or null) means binary. The backend resolves
  the same flag server-side and verifies your signature against the
  matching domain; a mismatch fails with `400 bad_signature` even if
  the signature is otherwise valid.
</Warning>

## Order struct

```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;
    uint8   signatureType;
}
```

### Field semantics

| Field           | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `salt`          | Client-chosen uint256 for replay protection.                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `maker`         | Address of the funds source. For `SignatureType.EOA` this equals `signer`. For `SignatureType.VAULT` this is the user's vault address.                                                                                                                                                                                                                                                                                                                                                                 |
| `signer`        | Address that signs. Always an EOA.                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `taker`         | `0x0000…0000` for a public order; a specific address to restrict.                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `tokenId`       | ERC-1155 position ID.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `makerAmount`   | Maximum quantity of the maker asset sold.                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `takerAmount`   | Minimum quantity of the taker asset received.                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `expiration`    | Unix seconds after which the order is void. **Recommended: `0` (no expiry)** — settlement is asynchronous (off-chain match → on-chain batch), and a too-short TTL surfaces as `MatchFailed(OrderExpired)` (selector `0xc56873ba`) when the on-chain block timestamp passes the deadline mid-flight. The contract skips the expiry check entirely when `expiration == 0`.                                                                                                                               |
| `feeRateBps`    | **Must equal the live market's `feeTakerBps`** — read fresh from `GET /api/markets/{symbol}` on the same request that builds the digest. The matcher's [quadratic curve](/concepts/trading/fees) reads `feeRateBps` off the signed order, and the backend rejects with `bad_signature` when the value the client signed differs from `EffectiveFeeService.resolveForMarket(symbol)` (admin-published rate at settle time). Common bug: hard-coding `0` or a stale value across fee-period transitions. |
| `side`          | `0 = BUY`, `1 = SELL`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `signatureType` | `0 = EOA` (signer==maker, EOA holds USDC directly), `1 = VAULT` (signer is EOA, maker is their vault clone). **Production default: `1`** — every retail flow is vault-backed.                                                                                                                                                                                                                                                                                                                          |

## Signature type: EOA vs VAULT

<Tabs>
  <Tab title="SignatureType.VAULT (production default)">
    * `signer` = your wallet EOA
    * `maker` = `VaultFactory.vaultOf(signer)` (must match on-chain)
    * Funds source: user's vault contract
    * On-chain check: `vaultFactory.vaultOf(signer) == maker` **and**
      `ecrecover(digest, signature) == signer`.
  </Tab>

  <Tab title="SignatureType.EOA (integrator / MM)">
    * `signer` = `maker` = your wallet EOA
    * Funds source: the EOA itself (holds USDC directly)
    * On-chain check: `ecrecover(digest, signature) == signer == maker`.
  </Tab>
</Tabs>

## Amount semantics

Both `makerAmount` and `takerAmount` are 6-decimal wei (USDC-scale).
`tokenId`-denominated quantities use the same scale: 1 outcome token =
`1_000_000` wei.

| Side       | `makerAmount`                                                         | `takerAmount`                                      |
| ---------- | --------------------------------------------------------------------- | -------------------------------------------------- |
| `BUY`  (0) | USDC notional you spend (`⌈price × qty⌉`, 6-dec — see rounding below) | outcome qty you receive (6-dec)                    |
| `SELL` (1) | outcome qty you sell (6-dec)                                          | USDC notional you receive (`⌊price × qty⌋`, 6-dec) |

For `BUY` at price `0.42` and qty `2.0`: `makerAmount = 840_000`,
`takerAmount = 2_000_000`. (Clean-cent tuple — CEIL and FLOOR
coincide. See the next section for the boundary cases that matter.)

### Rounding rule — CEIL on BUY, FLOOR on SELL notional

The on-chain CTFExchange recomputes each side's price by an
INDEPENDENT floor-div:

```
calculatePrice(BUY)  = makerAmount × 1e6 / takerAmount   (floor)
calculatePrice(SELL) = takerAmount × 1e6 / makerAmount   (floor)
```

This is asymmetric and forces an asymmetric signing rule:

* **BUY** — round the USDC notional **UP** (`CEIL`). Adds at most
  1 wei extra USDC (sub-cent), guarantees the chain-side
  `calculatePrice(BUY) >= priceWei` and gives the matcher's CEIL'd
  per-fill math the +1 wei of headroom it needs against your
  signed cap. **FLOOR-signed BUY orders are rejected at placement**
  with [`order_signed_with_floor_notional`](/errors/codes#trading)
  — they sit on the book with ZERO headroom and overflow the chain
  invariant `MakingGtRemaining` on the closing tail fill, which
  forces a different taker to absorb the revert.
* **SELL** — round the USDC notional **DOWN** (`FLOOR`).
  CEIL'ing the SELL would push `calculatePrice(SELL)` up by 1 wei
  and trip the BURN-boundary `priceA + priceB <= ONE` check.
* **`makerAmount` itself on SELL** is the exact outcome-token
  quantity — no rounding (`makerAmount = qtyWei`).

The asymmetric ceil/floor closes both the MINT and BURN cross-order
boundary checks (LAO-LAT-007 incident reference) AND keeps the
matcher's per-fill CEIL math from overflowing the maker's signed
cap on the closing tail fill (MakingGtRemaining incident
WC26D-GRPA-MEX-URU). It is the correct shape, not a bug.

#### Worked example — boundary tuple

`BUY` at price `0.52`, qty `67.307692`:

```typescript theme={null}
const priceWei = parseUnits('0.52', 6);        //         520_000
const qtyWei   = parseUnits('67.307692', 6);   //      67_307_692
const product  = priceWei * qtyWei;            // 34_999_999_840_000

// WRONG — FLOOR. Placement REJECTS with order_signed_with_floor_notional.
const notionalFloor = product / 1_000_000n;              // 34_999_999

// CORRECT — CEIL.
const notionalCeil  = (product + 999_999n) / 1_000_000n; // 35_000_000
```

The 1-wei difference is sub-cent and economically meaningless, but
the chain math cares about every wei. Always sign `notionalCeil`
for `BUY.makerAmount`.

## Signing example — TypeScript

```typescript theme={null}
import { Wallet, randomBytes, parseUnits } from 'ethers';

const domain = {
  name: 'PredictStreet',
  version: '1',
  chainId: 99999,
  verifyingContract: '0x4074c225b296E1E556c565B0C3Ddba305E63E7c4',
};

const types = {
  Order: [
    { name: 'salt', type: 'uint256' },
    { name: 'maker', type: 'address' },
    { name: 'signer', type: 'address' },
    { name: 'taker', type: 'address' },
    { name: 'tokenId', type: 'uint256' },
    { name: 'makerAmount', type: 'uint256' },
    { name: 'takerAmount', type: 'uint256' },
    { name: 'expiration', type: 'uint256' },
    { name: 'feeRateBps', type: 'uint256' },
    { name: 'side', type: 'uint8' },
    { name: 'signatureType', type: 'uint8' },
  ],
};

// BUY 2 YES @ 0.42 USDC
const priceWei = parseUnits('0.42', 6);        //   420_000
const qtyWei   = parseUnits('2',    6);        // 2_000_000
// CEIL on BUY notional — sub-cent +1-wei guard against the
// matcher's per-fill CEIL math overflowing your signed cap. See
// "Rounding rule" above for the full rationale. FLOOR-signed BUY
// orders are rejected at placement with
// `order_signed_with_floor_notional`.
const product     = priceWei * qtyWei;
const notionalWei = (product + 999_999n) / 1_000_000n; //   840_000

// CRITICAL — feeRateBps MUST equal `market.feeTakerBps` from
// `GET /api/markets/{symbol}`. The platform reconstructs the canonical
// Order struct with the live taker fee, hashes it, and recovers the
// signer against THAT digest. Signing with `0n` (or any other constant)
// produces a different hash → recovery returns the wrong address →
// `POST /api/orders/place` rejects with `bad_signature`. There is no
// override; the fee is per-market and partner SDKs must thread it
// through to the signer.
const feeRateBps = BigInt(market.feeTakerBps);

const order = {
  salt:          BigInt('0x' + randomBytes(32).toString('hex')),
  maker:         userVaultAddress,         // VaultFactory.vaultOf(signer)
  signer:        wallet.address,           // EOA
  taker:         '0x0000000000000000000000000000000000000000',
  tokenId:       yesTokenId,               // from /api/markets/{symbol}.yesTokenId
  makerAmount:   notionalWei,              // BUY → USDC notional
  takerAmount:   qtyWei,                   // BUY → outcome qty
  expiration:    0n,                       // 0 = no on-chain expiry (recommended)
  feeRateBps,                              // = market.feeTakerBps (see above)
  side:          0,                        // BUY
  signatureType: 1,                        // VAULT
};

const signature = await wallet.signTypedData(domain, types, order);
```

## Signing example — Python

```python theme={null}
from eth_account import Account

domain = {
    "name": "PredictStreet",
    "version": "1",
    "chainId": 99999,
    "verifyingContract": "0x4074c225b296E1E556c565B0C3Ddba305E63E7c4",
}
types = {
    "EIP712Domain": [
        {"name": "name", "type": "string"},
        {"name": "version", "type": "string"},
        {"name": "chainId", "type": "uint256"},
        {"name": "verifyingContract", "type": "address"},
    ],
    "Order": [
        {"name": "salt", "type": "uint256"},
        {"name": "maker", "type": "address"},
        {"name": "signer", "type": "address"},
        {"name": "taker", "type": "address"},
        {"name": "tokenId", "type": "uint256"},
        {"name": "makerAmount", "type": "uint256"},
        {"name": "takerAmount", "type": "uint256"},
        {"name": "expiration", "type": "uint256"},
        {"name": "feeRateBps", "type": "uint256"},
        {"name": "side", "type": "uint8"},
        {"name": "signatureType", "type": "uint8"},
    ],
}

signed = Account.sign_typed_data(
    private_key,
    {"domain": domain, "primaryType": "Order", "types": types, "message": order_dict},
)
signature = signed.signature.hex()
```

## Deriving the `orderId`

```typescript theme={null}
import { keccak256 } from 'ethers';

const sigBytes = ethers.getBytes(signature);
const orderId = keccak256(sigBytes);
```

## Split / merge / convert-positions — different domain

Vault position operations (`splitPosition`, `mergePositions`,
`convertPositions`) sign under the **vault's own EIP-712 domain**, not
the exchange's. The domain name changes (`PredictStreetVault`) and
`verifyingContract` is the user's per-user `EIP-1167` clone — NOT the
factory, NOT an exchange.

```typescript theme={null}
const vaultDomain = {
  name: 'PredictStreetVault',
  version: '1',
  chainId: 99999,
  verifyingContract: userVaultAddress, // VaultFactory.vaultOf(signer)
};
```

The struct layout, `kind` field (0 = binary, 1 = neg-risk), and the
full dual-signature flow (owner + backend co-sig) are documented on
[Contracts → Vaults → EIP-712 domain](/concepts/contracts/vaults#eip-712-domain).

## Common signing mistakes

1. **Wrong `verifyingContract` for the market** — binary vs neg-risk.
   Both exchanges share `name` + `version` but have distinct
   addresses, so the domain separators are distinct. Read
   `negRiskEligible` off `GET /api/markets/{symbol}` to pick.
2. **Using the Order domain for split / merge / convertPositions** —
   those operations sign under the `PredictStreetVault` domain with
   `verifyingContract` = the user's vault clone, not the exchange. See
   the section above.
3. **`signer` ≠ your API key's `associatedWallet`** — backend
   impersonation check rejects.
4. **VAULT mode with wrong `maker`** — `maker` must equal
   `VaultFactory.vaultOf(signer)`. The frontend always pre-resolves the
   vault via `VaultFactory.vaultOf(eoa)` before signing; if the user has
   no vault yet, `vaultOf` returns `0x0` — call
   `VaultFactory.createVault(eoa)` first.
5. **`feeRateBps` ≠ live `market.feeTakerBps`** — the matcher's
   [quadratic curve](/concepts/trading/fees) reads `feeRateBps` off the
   signed order, and the backend rejects with `bad_signature` when the
   value differs from `EffectiveFeeService.resolveForMarket(symbol)`
   (admin-published rate at settle time). Hard-coding `0` or a stale
   value across fee-period transitions is the usual cause. Always re-fetch `GET /api/markets/{symbol}.feeTakerBps`
   on the same request that builds the digest, sign with that exact
   integer, and echo it as `feeRateBps` in the request body so the
   server can sanity-check against its own resolved value before
   verifying the signature.
6. **`expiration > 0` with a tight TTL** — settlement is async, so a "5
   minute TTL" easily expires between off-chain match and on-chain
   submit, surfacing as `MatchFailed(OrderExpired)` (selector
   `0xc56873ba`). Use `0` unless you specifically need a hard TTL well
   above worst-case settlement latency (\~30s on testnet).
7. **Reused `salt`** — every order's hash is single-use on-chain.
8. **FLOOR-rounded BUY `makerAmount`** — `(priceWei * qtyWei) /
   1_000_000n` in JS / Python uses **integer FLOOR division**. For
   boundary tuples (e.g. `0.52 × 67.307692`) FLOOR produces
   `34_999_999`, CEIL produces `35_000_000`. The platform rejects
   FLOOR-signed BUY orders at placement with
   [`order_signed_with_floor_notional`](/errors/codes#trading) so
   they never poison the book. **Fix:** use the CEIL formula
   `(product + 999_999n) / 1_000_000n`. See the [Rounding rule
   section](#rounding-rule-ceil-on-buy-floor-on-sell-notional)
   above. The reject envelope's `details` carries the exact
   `expectedCeilMakerAmountWei` you should re-sign with, so you
   don't need to recompute the formula client-side.

## Server-side EOA → vault resolution

When you POST `/api/orders/place`, the backend recomputes the vault for
your signer EOA via `VaultFactory.vaultOf(signer)` and overrides the
`maker` field of the on-chain order to that resolved address before
storage. This means:

* The `maker` you send in the REST body **must** be the same vault, or
  the EIP-712 digest will not match.
* The off-chain matcher and on-chain `CTFExchange._verifyVault` both
  perform the same `vaultOf(signer) == maker` check — passing one but
  not the other is impossible.
* For `SELL`, the position lookup is keyed by the **vault** address
  (since the ERC-1155 lives there), not the EOA. This is automatic.
