EIP-712 order signing
Every order on Conviction is signed as EIP-712 typed data. Regular users don't need to worry about this — the UI handles it automatically. This page is a reference for bots and market makers that want to post orders programmatically without the UI.
#Domain (EIP-712 Domain Separator)
{
name: 'Conviction CTF Exchange', // must match byte-for-byte
version: '1',
chainId: 56, // BSC mainnet
verifyingContract: CTF_EXCHANGE_ADDRESS, // address varies by environment
}A single wrong character in the domain name breaks it
Even with a typo, the ECDSA itself passes, so it looks fine at signing time on the client. But because it differs from the contract-side domain, it reverts at fill time.
verifyingContract is the CTFExchange address on BSC mainnet. Fetch it from a server response or an environment variable — don't hardcode it in the client.
#Order structure
| Field | Type | Meaning |
|---|---|---|
salt | uint256 | Random identifier (newly generated on every re-sign) |
maker | address | The user's address. Via EIP-7702 delegation the EOA is also the AA, so maker == signer. |
signer | address | The user's EOA — the same address as maker. |
taker | address | 0x0000... (public order) |
tokenId | uint256 | The outcome's position ID (= the outcome's position_id) |
makerAmount | uint256 | The wei amount of the asset the maker offers |
takerAmount | uint256 | The wei amount of the asset the maker wants to receive |
expiration | uint256 | Order expiration in unix seconds. 0 = no expiration |
nonce | uint256 | Fresh every time via CTFExchange.nonces(maker) |
feeRateBps | uint256 | Fee cap (basis points) |
side | uint8 | BUY = 0, SELL = 1 |
signatureType | uint8 | EOA = 0 (user order) |
#EIP-712 type definition
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: 'nonce', type: 'uint256' },
{ name: 'feeRateBps', type: 'uint256' },
{ name: 'side', type: 'uint8' },
{ name: 'signatureType', type: 'uint8' },
],
};#Single-order signing example (viem)
import { parseUnits } from 'viem';
const SHARES = parseUnits('100', 18);
const order = {
salt: BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)),
maker: eoaAddress, // EIP-7702: the EOA is also the AA
signer: eoaAddress, // the same address
taker: '0x0000000000000000000000000000000000000000',
tokenId: BigInt(outcome.position_id),
// BUY 100 shares at 0.5 USDT each
makerAmount: parseUnits('50', 18), // 100 × 0.5 = 50 USDT
takerAmount: SHARES, // 100 shares
expiration: BigInt(Math.floor(Date.now() / 1000) + 3600),
nonce: await ctfExchange.read.nonces([eoaAddress]),
feeRateBps: BigInt(market.platformFee),
side: 0n, // BUY
signatureType: 0, // EOA
};
const signature = await walletClient.signTypedData({
account: eoaAddress,
domain,
types: { Order: types.Order },
primaryType: 'Order',
message: order,
});
// POST /orders
await fetch('/api/orders', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
marketId: market.id,
outcomeId: outcome.id,
side: 'buy',
type: 'limit',
price: 0.5,
shares: 100,
userId: user.id,
salt: '0x' + order.salt.toString(16),
signature,
signerAddress: order.signer,
makerAddress: order.maker,
expiration: order.expiration.toString(),
signatureType: 0,
tokenId: order.tokenId.toString(),
makerAmount: order.makerAmount.toString(),
takerAmount: order.takerAmount.toString(),
feeRateBps: market.platformFee,
}),
});#makerAmount / takerAmount calculation rules
All amounts are 18-decimal wei integers.
| Side | What maker offers | What maker receives | makerAmount | takerAmount |
|---|---|---|---|---|
| BUY | USDT | Outcome tokens | shares × price (USDT wei) | shares (token wei) |
| SELL | Outcome tokens | USDT | shares (token wei) | shares × price (USDT wei) |
priceis a 0..1 float, but in wei-space it's used in multiplication asprice × 1e18.- Do all multiplication with bigint arithmetic — no JS
Number(1 USDT =10^18, which exceeds the safe-integer range).
#Common mistakes
These four come up most often
- Putting different addresses in
makerandsigner. In the EIP-7702 model, both must be the same — the user's EOA address. - Caching the nonce. Fetch fresh every time via
CTFExchange.nonces(maker). IfincrementNonce()is called, every unfilled order on the previous nonce reverts. - Sending
priceas a percent or bps. Thepricein the payload is a 0..1 float. - Sending makerAmount / takerAmount in human-readable units. Always a decimal string of 18-dec wei.
#Signatures are verified at fill time
- The backend does not verify signatures — it stores the signed payload as-is and hands it to the chain at fill time.
- A bad signature reverts at fill time because on-chain ECDSA recovery returns a different address → that order is marked failed.
- So the signing step must be exact — avoid the four mistakes above.
#Next
- REST → Markets — query the market data needed for orders, such as
feeRateBps - User wallet — the single-address (EOA == AA) model
Was this page helpful?