> ## Documentation Index
> Fetch the complete documentation index at: https://docs.calibri.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Signed orders (EIP-712)

> Build, sign, and place a non-custodial order — the EIP-712 Order domain and struct, 6-decimal unit rules, and the signed-order endpoint.

A non-custodial order is a standard limit order **authorized by an EIP-712
signature**. You build the typed `Order`, sign it with your EOA, and post it to
Calibri, which validates the signature and places the order on the book. The
resting order locks your [`locked_safe`](/non-custodial/balances) pool.

<Note>
  Orders are **placed** at `POST /api/v2/atlas/account/orders` so the signature can be validated and attached, and **read** back at `GET /api/v2/pythia/orders`. There is no unsigned placement path — every order on Calibri carries your signature.
</Note>

## 1. Read the market's CTF config

**`GET /api/v2/atlas/account/wallet/markets/:id`**

Returns your funding config plus the market's `condition_id`, `yes_token_id`, and `no_token_id`. Returns `404` if the market is custodial-only (not CTF-registered).

The config gives you every value needed to build and sign the order:
`exchange_address`, `chain_id`, `domain_name`, `proxy_address` (your Safe / the
maker), `owner_address` (what owns the Safe), `signature_type`,
`session_keys_enabled`, `safe_status`, and the token ids.

<Note>
  The YES/NO token ids are also published on the **public** market payload as `ctf_yes_token_id` / `ctf_no_token_id`, so a client that already holds a market can build an order without a per-market authenticated call.
</Note>

<Warning>
  Check `safe_status` before signing. `setup_required` means approvals are still outstanding: an order placed then will **rest on the book and then fail to settle**, which is worse than being rejected. Only `ready` is safe to trade on.
</Warning>

<Warning>
  `domain_name` is **served by the API** — it is the EIP-712 domain name of Calibri's exchange. You **must** sign with the value the API returns, not a copied constant, or the signature will not recover.
</Warning>

## 2. The EIP-712 domain

```
name:              domain_name       (from GET /account/wallet)
version:           "1"
chainId:           chain_id          (from GET /account/wallet)
verifyingContract: exchange_address  (from GET /account/wallet)
```

## 3. The Order struct

EIP-712 field names are camelCase:

```
Order(
  uint256 salt,
  address maker,          // your Safe
  address signer,         // your wallet EOA — or the Safe itself, for POLY_1271
  address taker,          // 0x0 (public)
  uint256 tokenId,        // YES or NO ERC-1155 token id for the order side
  uint256 makerAmount,    // USDC collateral in, 6-decimal base units
  uint256 takerAmount,    // outcome tokens out, 6-decimal base units
  uint256 expiration,     // unix seconds, non-zero
  uint256 nonce,
  uint256 feeRateBps,     // must be 0
  uint8   side,           // 0 = BUY (both YES and NO legs are BUYs that mint)
  uint8   signatureType   // 2 = POLY_GNOSIS_SAFE, 3 = POLY_1271
)
```

<Info>
  YES vs NO is carried **only** by `tokenId`. `side` is always `0` (BUY) — both legs are buys that mint a complete set.
</Info>

### Signature types

Your signature type determines how you sign an order. Read it from
`signature_type` on `GET /api/v2/atlas/account/wallet` and sign the way your
type requires — an order signed the wrong way is rejected.

* **`2`** — sign the order with your wallet key. Set `maker` to your Safe and
  `signer` to your wallet address.
* **`3`** — sign the [SafeMessage](/api-reference/authentication), not the order
  hash, using a [signing key](/non-custodial/signing-keys) you added. Set both
  `maker` and `signer` to your Safe. In the browser your passkey does this for
  you.
  Your type follows from what owns your Safe: a wallet you hold the key for
  (`2`) or a passkey (`3`) — so it is fixed by [how you set your wallet
  up](/non-custodial/wallets#which-signature-type-your-wallet-gets). The names
  below are the exchange contract's enum and mean the same as the numbers:

| Type | Owner of the Safe | `maker` / `signer` | How it is signed |
| - | - | - | - |
| `2` — `POLY_GNOSIS_SAFE` | Your wallet address (EOA) | `maker` = Safe, `signer` = your EOA | Your wallet signs the typed data; the exchange ECDSA-recovers `signer` and requires `getSafeAddress(signer) == maker` |
| `3` — `POLY_1271` | A passkey signer contract | `maker` = `signer` = **the Safe** | Anything the Safe accepts: a WebAuthn assertion, a session-key envelope, or a plain ECDSA signature from a second signing key you added. Validated on-chain, not by recovery |

<Warning>
  For `POLY_1271` the exchange does **no address derivation** — it requires `signer == maker`, that `maker` has code, and that `isValidSignatureNow(maker, hash, sig)` passes. That is exactly what allows a *contract* to own the Safe. A passkey order sent as type `2` cannot work: that path ECDSA-recovers a key which does not exist.
</Warning>

For type `2` the signature is your **EOA** signing the typed data above.

Type `3` has no EOA key *by default* — a passkey owns the Safe. Adding your own
wallet as a [signing key](/non-custodial/signing-keys) gives it one, and that is
what makes type `3` scriptable: the type does not change, only the signature.
See [Your wallet options](/non-custodial/wallets).

### Type 3: three signature shapes

A `POLY_1271` order carries one of **three** signature shapes, and the server
tells them apart by length alone — never by a client-supplied flag:

| Shape | Content |
| - | - |
| **Passkey** | The WebAuthn assertion, wrapped as a Safe contract signature. Long |
| **Session key** | `abi.encode(uint256 keyId, uint256 r, uint256 s)` — exactly **96 bytes** (`0x` + 192 hex chars) |
| **Safe owner** | A plain ECDSA signature — **65 bytes** (`0x` + 130 hex chars) — from a [second signing key](/non-custodial/signing-keys) you added |

The third is the only one a server-side client can produce: a passkey signs
only in the browser that registered it, and session keys are generated
non-extractable. Add your own wallet as a signing key and your code signs as an
owner of the Safe.

It signs the **SafeMessage**, not the order hash. Safe re-wraps the hash before
checking it, so signing `orderHash` directly recovers to a different address and
the Safe rejects it — reported as `422 signed_order signature was not accepted
by the maker wallet`, on a signature that verifies perfectly if you check it
yourself. The full recipe, computable without a node, is under **type 3** in
[Authentication](/api-reference/authentication).

The grant id is read out of the envelope's first word, so a client cannot name a
live grant while signing with an expired key. A session-signed order is
additionally checked against its grant's expiry at intake; an expired or revoked
grant is rejected with `422 Your trading key has expired or been revoked —
approve with your passkey`.

See [Trading without a prompt per order](/non-custodial/session-keys).

## 4. Amount and unit rules

`CTF_UNIT_SCALE = 1_000_000`. USDC and CTF outcome tokens are **both
6-decimal** — 1 contract = 1 outcome token = `1e6` base units.

* `takerAmount == round(volume * 1e6)` exactly.
* `makerAmount == round(legPrice * volume * 1e6)`, where **`legPrice` is the
  order's own side price** — a NO order signs the NO's own price, **not** an
  inverted YES probability.
* The implied price `makerAmount / takerAmount` must equal `price` within
  `1e-4`.

**Example** — buy 100 YES contracts at `0.60`:

| Field | Value |
| - | - |
| `takerAmount` | `100000000` (100 × 1e6) |
| `makerAmount` | `60000000` (0.60 × 100 × 1e6) |
| implied price | `60000000 / 100000000 = 0.60` ✓ |

## 5. Sign and build the wire object

Sign the typed data with your EOA key
(`wallet.signTypedData(domain, { Order: [...] }, order)`), then assemble the
`signed_order` wire object (snake\_case). **Every big-integer field is a decimal
string** — including `fee_rate_bps`; `side` and `signature_type` are numbers:

```json theme={null}
{
  "salt": "3040…",
  "maker": "0x… (Safe)",
  "signer": "0x… (EOA)",
  "taker": "0x0000000000000000000000000000000000000000",
  "token_id": "123…",
  "maker_amount": "60000000",
  "taker_amount": "100000000",
  "expiration": "1795000000",
  "nonce": "0",
  "fee_rate_bps": "0",
  "side": 0,
  "signature_type": 2,
  "signature": "0x…"
}
```

## 6. Place the order

**`POST /api/v2/atlas/account/orders`**

Places a non-custodial (signed) order. Calibri validates the `signed_order`, places it as a self-custody order (`custody = "self"`), and attaches the signature to the order record. Returns the order object, `201`.

Request body:

```json theme={null}
{
  "market": "will-x-happen",
  "side": "yes",
  "ord_type": "limit",
  "price": "0.60",
  "volume": "100",
  "post_only": false,
  "ioc": false,
  "signed_order": { "…the object from step 5…": "" }
}
```

`ioc` (immediate-or-cancel) is for a **market-style** order: a limit at a price
ceiling, signed with a short `expiration`. With `ioc: true`, whatever the book
can't fill when the order arrives is refunded instead of resting at the ceiling.
It is recorded on the order, so a wallet's market orders can be told apart from its
limit orders (both are `ord_type: "limit"`). Ignored if `post_only` is set.

If attaching the signature fails, the order is cancelled so no unsignable orphan
is left behind.

### Server-side validation

Calibri fully validates the signed order before anything is placed, rejecting with
`422` unless:

* it is a **limit** order with a valid side and `price ∈ (0, 1)`;
* the market is CTF-registered and `token_id` matches the side's token;
* `side == 0`, `taker == 0x0`, `fee_rate_bps == 0`;
* `expiration` is non-zero, unexpired, and within 365 days;
* amounts match the volume and price (see the unit rules above);
* the maker is a Safe **registered to you** and settlement-ready;
* for type `2`, `signer` equals your member EOA and the **signature
  ECDSA-recovers to `signer`**;
* for type `3`, `signer == maker` and the **Safe itself verifies the signature**
  (EIP-1271) — plus, for a session-key envelope, the grant is still within its
  expiry.

### Expiry is enforced

The engine keeps each order's `expiration` and never lets a signature lapse
between a match and its on-chain settlement:

* an order is never matched within **30 seconds** of its `expiration`;
* a resting order reaching that point is **cancelled and refunded** (its order
  update arrives as `cancel`);
* an order that arrives with less than 30 seconds left is rejected with
  `market.order.expired`.

Sign resting limit orders with a long `expiration` (up to 365 days) and market-style
orders with a short one — the app uses 120 seconds.

A check that cannot be completed — blockchain service unreachable, Safe
readiness unknown — **fails closed**: the order is rejected rather than placed
unverified.

Common errors: `422 signed_order with signature is required for non-custodial
orders`, `422 Member safe is not settlement-ready`, `422 signed_order signature
does not match the signer`. Engine `4xx` responses are passed through; upstream
failures return `502`.

## Related

* [The Safe](/non-custodial/the-safe) — make the Safe trade-ready before signing
* [Redeeming winnings](/non-custodial/redeeming-winnings) — claim payouts after a market resolves


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