> For the complete documentation index, see [llms.txt](https://docs.aftermath.finance/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.aftermath.finance/perpetuals/architecture/market-makers.md).

# Market Makers

There is no designated market maker program, special rebates, or latency advantages. Anyone is welcome to market make on Aftermath Perpetuals.

For technical integration questions, join our [Discord](https://discord.gg/VFqMUqKHF3) or message us on [X](https://x.com/AftermathFi).

## API Base

| Resource     | URL                                                                                                  |
| ------------ | ---------------------------------------------------------------------------------------------------- |
| API prefix   | `https://aftermath.finance/api/...`                                                                  |
| Swagger UI   | [`https://aftermath.finance/docs`](https://aftermath.finance/docs)                                   |
| OpenAPI spec | [`https://aftermath.finance/api/openapi/spec.json`](https://aftermath.finance/api/openapi/spec.json) |
| Agent skills | [`github.com/AftermathFinance/skills`](https://github.com/AftermathFinance/skills)                   |

The OpenAPI spec is the source of truth for field names and types. Generate your types from it rather than hand-writing request bodies.

{% hint style="info" %}
The agent skills repo packages these integration patterns for coding agents. If you build with Claude Code, Codex, or Cursor, point the agent at it before it writes any request bodies.
{% endhint %}

## Recommended Integration

We recommend using the native Perpetuals REST endpoints (`/api/perpetuals/account/transactions/*`) rather than the CCXT layer. The native endpoints give you full control over gas management and access to Sui-native features like Programmable Transaction Blocks (PTBs).

All native endpoints return a `TxKindResponse` containing a base64-encoded `TransactionKind`. You decode it, wrap it in a full `Transaction`, sign with your wallet, and submit via your Sui client.

### Key Endpoints

**Cancel and Place Orders** (`POST /api/perpetuals/account/transactions/cancel-and-place-orders`)

This is the primary endpoint for market makers. It atomically cancels existing orders and places new ones in a single PTB, which is significantly cheaper on gas than separate cancel and place transactions. There is no intermediate state between the two operations, so your book depth never drops to zero mid-requote. If you are refreshing quotes or managing order grids, this is the endpoint to use.

**Place Limit Order** (`POST /api/perpetuals/account/transactions/place-limit-order`)

For individual order placement. Supports inline SL/TP attachment in the same transaction.

**Place Market Order** (`POST /api/perpetuals/account/transactions/place-market-order`)

For immediate execution at market price. Also supports inline SL/TP.

**Place Scale Order** (`POST /api/perpetuals/account/transactions/place-scale-order`)

Distributes size across a price range in a single transaction. Useful for DCA-style entries, liquidity ladders, or spreading risk across multiple price levels.

**TWAP Orders** (`POST /api/perpetuals/account/transactions/create-twap-orders`, `edit-twap-orders`, `cancel-twap-orders`)

Schedules execution of a size over time. Read scheduled and processed amounts back from `POST /api/perpetuals/account/twap-order-datas`. Useful for unwinding accumulated inventory without paying the spread all at once.

{% hint style="info" %}
Every account transaction route above has a vault equivalent under `/api/perpetuals/vault/transactions/*`, including `cancel-and-place-orders`. If you quote from a vault rather than a personal account, the request shapes are the same — swap `accountId` for `vaultId`.
{% endhint %}

## The Requote Loop

`cancel-and-place-orders` is the endpoint you will call thousands of times a day, so it is worth knowing every field.

| Field                            | Notes                                                                                                                                                     |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `accountId`                      | Numeric account ID, sent as a BigInt string (`"123n"`). Use `vaultId` instead when quoting from a vault.                                                  |
| `accountCapId`                   | Optional object ID of the account capability authorizing the transaction.                                                                                 |
| `walletAddress`                  | Required. Receives any refunded gas coins.                                                                                                                |
| `marketId`                       | Required. The clearing house being quoted.                                                                                                                |
| `orderIdsToCancel`               | Chain-assigned order IDs, as BigInt strings. Omit to cancel nothing.                                                                                      |
| `clientOrderIdsToCancel`         | Cancel by **your own** IDs instead of chain IDs. Can be combined with `orderIdsToCancel`.                                                                 |
| `shouldAbortOnMissingId`         | Defaults to `false`. See below — this default is what you want.                                                                                           |
| `ordersToPlace`                  | Array of `{ side, price, size, clientOrderId? }`. Omit to place nothing.                                                                                  |
| `orderType`                      | Execution type for the whole batch. See the table below.                                                                                                  |
| `reduceOnly`                     | If `true`, placed orders can never increase position size.                                                                                                |
| `hasPosition`                    | Required. Whether the account currently holds a position in this market.                                                                                  |
| `expiryTimestamp`                | Optional ms-epoch expiry for the placed orders.                                                                                                           |
| `leverage`                       | Optional leverage override for the placed orders.                                                                                                         |
| `shouldDeallocateFreeCollateral` | If `true`, sweeps margin not backing a position back to the wallet in the same transaction.                                                               |
| `sponsor`                        | `{ walletAddress, bytes, signature, gasBudget? }` for GasPool sponsorship. `bytes` is base64 of `Aftermath Terms and Conditions`; `gasBudget` is in MIST. |
| `builderCode`                    | `{ integratorId, integratorFee }` — only relevant if you route flow as an integrator.                                                                     |
| `txKind`                         | An existing base64 `TransactionKind` to extend, so you can compose the requote into a larger PTB of your own.                                             |

### Order Types

`orderType` applies to every order in `ordersToPlace`:

| Value | Type      | Behavior                                                                                       |
| ----- | --------- | ---------------------------------------------------------------------------------------------- |
| `0`   | GTC       | Rests on the book until filled or cancelled.                                                   |
| `1`   | FOK       | Must fill entirely in one match, or the whole order is cancelled.                              |
| `2`   | Post-Only | Added to the book only if it would not immediately match. Rejected if it would take liquidity. |
| `3`   | IOC       | Fills what it can immediately, cancels the remainder.                                          |

Market makers should quote with `2`. It guarantees you never cross and never pay a taker fee.

### Client Order IDs

Every placed order accepts an optional `clientOrderId` — a `u64` you choose, sent as a BigInt string. Tag your quotes with your own IDs and you can cancel them via `clientOrderIdsToCancel` without ever reading the chain-assigned ID back.

This removes a round trip from the requote loop. Instead of place → read back order IDs → cancel by chain ID → place again, you keep quoting against IDs your strategy already knows. Client order IDs also come back on `pendingOrders` in position state and on the order streams, so you can reconcile fills against your own book directly.

`clientOrderId` is accepted on `place-limit-order` and, as `clientOrderIds`, on `place-scale-order`.

### Missing IDs Are Not an Error by Default

An order you are about to cancel may fill or expire between your decision and your transaction landing. With `shouldAbortOnMissingId: false` (the default) the transaction tolerates the missing ID: the remaining cancels and all of your new orders still go through.

Set it to `true` only if a partially-applied requote is worse for you than no requote at all. For most quoting strategies it is not — an aborted transaction leaves stale quotes on the book, which is the exact situation you were trying to fix.

### Example Request

```json
POST /api/perpetuals/account/transactions/cancel-and-place-orders

{
  "accountId": "123n",
  "accountCapId": "0x...",
  "walletAddress": "0x...",
  "marketId": "0x...",
  "clientOrderIdsToCancel": ["1001n", "1002n", "1003n"],
  "ordersToPlace": [
    { "side": 0, "price": "7250000000000n", "size": "100000000n", "clientOrderId": "1004n" },
    { "side": 0, "price": "7249000000000n", "size": "100000000n", "clientOrderId": "1005n" },
    { "side": 1, "price": "7260000000000n", "size": "100000000n", "clientOrderId": "1006n" },
    { "side": 1, "price": "7261000000000n", "size": "100000000n", "clientOrderId": "1007n" },
    { "side": 1, "price": "7262000000000n", "size": "100000000n", "clientOrderId": "1008n" }
  ],
  "orderType": 2,
  "reduceOnly": false,
  "hasPosition": true,
  "shouldAbortOnMissingId": false
}
```

`side` is `0` for bid and `1` for ask.

## Wire Format Rules

These four rules account for most failed first integrations.

**BigInt fields need the trailing `n`.** Native BigInt fields are JSON strings with an `n` suffix — `"123n"`. Plain `123` and `"123"` both fail. Responses come back the same way. Not every number is a BigInt: ordinary counters and millisecond timestamps stay plain JSON numbers, so follow each endpoint's schema rather than converting globally.

**`accountId` is numeric, not an object ID.** Native endpoints take the numeric account ID. CCXT write endpoints take an account capability object ID (`0x...`) under the same name, and CCXT read endpoints use `accountNumber`. Passing a `0x...` capability ID where a numeric `accountId` belongs is the single most common integration failure.

**Preview responses are success-or-error unions.** `/api/perpetuals/account/previews/*` can return HTTP `200` with a body of `{ "error": "..." }` and the header `X-Error-Message: true`. Check for the error shape before reading the success fields — a `200` alone does not mean the preview succeeded.

**Responses are deterministically ordered.** Markets sort by symbol, positions by market ID, and pending bids and asks each sort by order ID. You can diff successive account snapshots positionally instead of rebuilding a map on every poll.

## Real-Time Market Data

All live streams — orderbook, oracle, market state, account updates, and candles — multiplex over a single WebSocket:

`wss://aftermath.finance/api/perpetuals/ws/updates`

You subscribe per stream by sending a JSON frame after `open`:

```json
{
  "action": "subscribe",
  "subscriptionType": {
    "topOfOrderbook": {
      "marketId": "0x...",
      "priceBucketSize": 0.0001,
      "bucketsNumber": 10
    }
  }
}
```

The streams a quoting bot usually wants:

| Subscription/helper              | Use                                                                                                    |
| -------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `topOfOrderbook`                 | Bucketed top-N snapshots. Each frame is a complete snapshot, so no REST seed and no delta bookkeeping. |
| `orderbook`                      | Full depth deltas, when bucketed top-of-book is not enough.                                            |
| `oracle`                         | Oracle price updates for the market.                                                                   |
| `subscribeUserOrders`            | SDK helper for account order updates, including `clientOrderId`.                                       |
| `subscribeUserCollateralChanges` | SDK helper for account collateral movements.                                                           |
| `marketCandles`                  | OHLCV updates at `1m` … `1mo`.                                                                         |

Full frame schemas, error frames, and client examples are on the [WebSockets](/for-developers/api/websockets.md) page.

{% hint style="warning" %}
**On reconnect, resync before you resume.** Re-fetch snapshots, replace local state atomically, then resume applying deltas. Never continue from in-memory state that predates a disconnect.
{% endhint %}

If you prefer server-sent events, SSE streams are available under `/api/ccxt/stream/*`.

## Risk Controls

**Check size before you send it.** `POST /api/perpetuals/account/max-order-size` returns the largest order the account can place given current risk limits, for a given `marketId` and `side` (`0` bid, `1` ask). Call it before scaling up quote size or changing leverage rather than discovering the limit through a failed transaction.

**There is no dead man's switch.** The API has no scheduled-cancel or auto-expiry-on-disconnect endpoint. If your process dies, your quotes stay on the book. Build a heartbeat kill switch that cancels all open orders when your strategy loop stalls, and wire it to `SIGINT`/`SIGTERM` as well as to the heartbeat timeout. Verify the cancels landed before you exit.

**Account state goes stale immediately.** After any fill, cancel, deposit, withdrawal, or leverage change, refresh account and position state before computing the next quote.

**Serialize anything that touches the same coin object.** Concurrent signed transactions can race on the same USDC coin or gas coin and fail with version or equivocation errors. Serialize collateral operations, and give parallel submitters their own gas coins.

## Gas Optimization

The biggest gas savings come from how you structure your transactions. Using `cancel-and-place-orders` atomically avoids paying for separate transactions and removes the gap between cancelling stale quotes and placing replacements.

**Inefficient: Separate Transactions**

```
TX 1: cancel_orders              ← cancel stale quotes
TX 2: place_limit_order (×1)     ← place the replacement
────────────────────────────────────────────────
Result: two transactions and a gap between them
```

**Efficient: Atomic Cancel-and-Place with Batching**

```
TX 1:
  cancel_orders                  ← cancel stale quotes
  place_limit_order (×5)         ← 5 new orders
────────────────────────────────────────────────
Result: one atomic transaction for the entire quote refresh
```

In a sample of 17 successful mainnet transactions, cancel-and-place PTBs with two new limit orders used roughly **0.0006–0.0007 SUI total** (about **0.0003 SUI per new order**). Treat this as an example rather than a fixed fee: net gas is computation plus storage cost minus the storage rebate, and it changes with the PTB's contents and the reference gas price.

### Tips

* **Always use `cancel-and-place-orders`** — never send cancel and place as separate transactions.
* **Batch 5+ orders per call** — the `ordersToPlace` array accepts multiple orders. More orders per tx = lower gas per order.
* **Quote both sides in one tx** — place bids and asks in the same `ordersToPlace` array.
* **Use Post-Only (`orderType: 2`)** — guarantees maker execution and avoids taker fees.
* **Tag orders with `clientOrderId`** — cancel by your own IDs and skip the read-back round trip.
* **Use agent wallets** — delegate signing to an agent wallet via `grant-agent-wallet` so your main key stays cold. Agent wallets can trade but cannot withdraw collateral.
* **Use gas pool sponsorship** — pre-fund a gas pool (`/api/gas-pool/*`) and pass the sponsor wallet plus its reusable terms signature for predictable gas costs.
* **Compose with `txKind`** — pass an existing `TransactionKind` to fold the requote into a larger PTB instead of sending two transactions.
* **Don't add your own oracle updates** — the API handles oracle freshness internally.
* **Don't overpay gas to jump the queue** — priority-gas transactions carry an extra taker fee (`priorityTakerFee` in market params) and are rejected outright when that parameter is unset. See [Reference Gas Price](/perpetuals/architecture/fees/reference-gas-price.md).

### CCXT

If your firm already runs on CCXT infrastructure, we support the CCXT standard interface as well. See the [CCXT Docs](/for-developers/api/ccxt.md). Note that CCXT endpoints return `transactionBytes` and `signingDigest` rather than raw `TransactionKind`, so you have less control over gas and PTB composition. Sign the `signingDigest`, not the `transactionBytes`, and submit `signatures[]` — when sender and gas owner differ, both signatures go in that array.

CCXT's `OrderRequest` is also narrower than the native surface: `timeInForce` and `postOnly` are not supported by `/api/ccxt/build/createOrders` and are ignored if sent. If post-only quoting matters to you — and for a market maker it should — use the native endpoints.

For full API reference, see the [Swagger documentation](https://aftermath.finance/docs).
