| 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 |
| Split / merge / convert positions | PredictStreetVault | per-user vault clone address (VaultFactory.vaultOf(signer)), NOT the factory | SplitPosition / MergePositions — see Contracts → Vaults |
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
Order struct
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 | Set to 0 — taker fees are computed and charged on-chain by the exchange via the quadratic curve, independent of this field. |
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
- SignatureType.VAULT (production default)
- SignatureType.EOA (integrator / MM)
signer= your wallet EOAmaker=VaultFactory.vaultOf(signer)(must match on-chain)- Funds source: user’s vault contract
- On-chain check:
vaultFactory.vaultOf(signer) == makerandecrecover(digest, signature) == signer.
Amount semantics
BothmakerAmount 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) | outcome qty you receive (6-dec) |
SELL (1) | outcome qty you sell (6-dec) | USDC notional you receive (6-dec) |
BUY at price 0.42 and qty 2.0: makerAmount = 840_000,
takerAmount = 2_000_000.
Signing example — TypeScript
Signing example — Python
Deriving the orderId
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.
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.
Common signing mistakes
- Wrong
verifyingContractfor the market — binary vs neg-risk. Both exchanges sharename+versionbut have distinct addresses, so the domain separators are distinct. ReadnegRiskEligibleoffGET /api/markets/{symbol}to pick. - Using the Order domain for split / merge / convertPositions —
those operations sign under the
PredictStreetVaultdomain withverifyingContract= the user’s vault clone, not the exchange. See the section above. signer≠ your API key’sassociatedWallet— backend impersonation check rejects.- VAULT mode with wrong
maker—makermust equalVaultFactory.vaultOf(signer). The frontend always pre-resolves the vault viaVaultFactory.vaultOf(eoa)before signing; if the user has no vault yet,vaultOfreturns0x0— callVaultFactory.createVault(eoa)first. feeRateBps != 0— taker fees are charged on-chain by the exchange’s quadratic curve; the field on the order is ignored beyond the signature digest, so any non-zero value just makes your signature non-portable to other systems.expiration > 0with a tight TTL — settlement is async, so a “5 minute TTL” easily expires between off-chain match and on-chain submit, surfacing asMatchFailed(OrderExpired)(selector0xc56873ba). Use0unless you specifically need a hard TTL well above worst-case settlement latency (~30s on testnet).- Reused
salt— every order’s hash is single-use on-chain.
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
makeryou 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._verifyVaultboth perform the samevaultOf(signer) == makercheck — 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.