Market Making#
Market makers provide two-sided liquidity — they sit on both sides of the book simultaneously, earning the bid-ask spread when both sides fill. As a maker, you pay zero fee on every fill. Eligible fills also earn a maker rebate, in addition to the spread you quote.
How the spread works#
Bid @ 5400 bps | Ask @ 5600 bps
Spread = 5600 − 5400 = 200 bps (2%)
When both sides fill:
- Buy 100 shares @ 5400 bps = pay 54 USDT
- Sell 100 shares @ 5600 bps = receive 56 USDT
- Spread profit = 2 USDT before any eligible maker rebates
The shipped allocation is 50% protocol / 5% creator / 5% referral / 40% maker rebate. Eligible maker fills credit the maker's free quote balance; ineligible maker allocation and rounding dust go to protocol, as does the referral allocation without an eligible referrer. See fee constants and allocation details.
<!-- src: program/src/logic/fee.rs:168 FeeSplit4::TARGET --> <!-- F-01: update with builder fee allocation -->The shipped minimum resting age is 10 seconds. Book fills use the resting order timestamp; spline fills use an eligibility anchor that can reset on activation, qualifying mid-price moves or stale refreshes. Governance can retune this gate.
The resting-age governance ceiling is 3,600 seconds. The maker allocation
ceiling, MAX_MAKER_REBATE_SHARE_BPS = 4000, is 40% of the collected fee.
At the default 200-bps taker cap, an eligible fill's maker allocation can be
80 bps of notional before integer rounding. It is not an additional charge
to the maker or a rebate on every fill regardless of age.
Rebates credit your per-market TraderLedger.quote_free, not your wallet.
Use WithdrawFunds (0x09) to move free quote back to your settlement-token
account, or reuse it through supported free-funds instructions. Reconcile
credits with MakerRebateCredited events and the ledger; see
monitoring.
Post-Only orders: always be the maker#
Use the Post-Only order type to ensure you are never the taker:
| Order type | If would match | Result |
|---|---|---|
| Limit | Matches immediately | You become taker (pay fee) |
| Post-Only, reject mode | Rejects with WouldCross | No fill |
| Post-Only, slide mode | Moves to a non-crossing tick if possible | Rests at the adjusted price; otherwise rejects |
A Post-Only order never takes liquidity. The library default
reject_post_only_would_cross = 1 preserves your price or rejects. Setting
it to 0 permits sliding one tick beyond the best crossing complementary
maker. Read the confirmed resting price when reconciling a sliding order.
The PlaceMultiplePostOnlyOrders (0x0C) instruction lets you place up to
4 bids and 4 asks atomically in one instruction. The limit is per side;
eight bids are not a valid batch. Batching reduces transaction overhead.
Requoting without losing track of exposure#
There are no client order IDs and no dedicated atomic cancel-replace instruction in this source revision. Record program-assigned order IDs from confirmed events and reconcile them with the book. A transport timeout is not permission to submit a second equivalent quote.
You can compose cancellation and replacement in one Solana transaction:
- Add the compute-budget heap request for the total new batch size. The SDK's
batchPostOnlyHeapFrameBytes/computeBudgetPrefixForhelpers derive it; do not reuse a single-order heap assumption for an eight-entry batch. - Cancel at most six explicit IDs with
CancelMultipleOrdersById(0x11). - Place at most four new bids and four new asks with
PlaceMultiplePostOnlyOrders(0x0C). - Simulate the complete transaction, then sign and submit. If any instruction fails, both cancellations and new placements roll back.
Transaction composition is atomic, but it is not a stable-ID replace API:
replacement orders receive new IDs and time priority. ReduceOrder (0x14)
preserves priority when partially reducing an existing order at its existing
price; reducing its full remaining size cancels it. A price change requires
new placement.
Use self_trade_behavior = Abort when an unexpected surviving own quote
should reject the update. CancelProvide, the library default, can cancel
your crossed maker quote and continue. The full table is in
Placing orders.
Packet size and unique account count depend on the exact account tails, funding path, and any account-creation instructions. Serialize and simulate your actual transaction; an earlier 862-byte / 22-account design example is not a universal quote-update budget or a current measured guarantee.
<!-- src: program/src/constants.rs:184 batch_post_only_heap_frame_bytes --> <!-- src: program/src/processor/reduce_order.rs:12 partial reduce --> <!-- E-01: update reconciliation when client order IDs ship --> <!-- E-02: update requoting when atomic replace ships -->The maker price band#
All resting orders (Limit and Post-Only) are gated against the maker price band. IOC orders are exempt because they never rest.
Two-sided book (both bids and asks exist):
mid = (best_bid + best_ask) / 2
band = [max(100, mid − 1000), min(9900, mid + 1000)]
One-sided or empty book:
band = [100, 9900] (static fallback)
Constants (from program/src/constants.rs):
| Constant | Value | Meaning |
|---|---|---|
MAKER_BAND_BPS | 1000 | ±10% around mid |
STATIC_MIN_BPS_FOR_BAND | 100 | 1% dust floor |
STATIC_MAX_BPS_FOR_BAND | 9900 | 99% dust ceiling |
Example with mid at 5500 bps: your order must be in [4500, 6500]. Orders
outside this range are rejected with OrderPriceOutOfBand — nothing is
lost, just re-quote closer to the market.
Practical impact for makers: if the mid moves while your update transaction is in flight, your new quotes may land outside the band and be rejected. Design your update loop to handle this gracefully.
Minimum resting notional#
Each resting order must meet a minimum notional value (the stablecoin
amount locked). The default DEFAULT_MIN_RESTING_NOTIONAL is 2 base
units (effectively dust protection for 6-decimal USDT). This prevents
tiny junk orders from consuming order-book slots.
Orders below the minimum are rejected at placement. When a large order partially fills and the remaining quantity would fall below the minimum, the remainder is cancelled rather than left as a sub-minimum resting stub.
Order book capacity#
Each side of each market has a configured resting-order capacity. Current deep
books support tiers from 64 to 4096 resting orders per side. A sufficiently
better-priced incoming order can evict the least aggressive eligible order
on a full side. Otherwise placement rejects with OrderbookFull. Expired
orders at the eviction frontier must be reclaimed before competitive
eviction can proceed. Your resting quote can therefore disappear through
eviction as well as cancellation, expiry, or a fill. For market makers:
- Keep your resting order count below the market's configured capacity.
- Cancel stale quotes promptly to free slots.
- If you need atomic multi-order updates, use the batch post-only instruction.
Basic two-sided quoting strategy#
Example quote calculation (tick = 100 bps)#
Note that tick rounding happens after the NO-side conversion for any NO-side orders. Always verify your canonical price after rounding.
Spline quoting#
Check three preconditions before building a spline strategy on this source
revision: config must enable attachment through spline_flags; the spline
creator must match the market creator; and a taker must supply the required
spline accounts to consume its quotes. A curve existing in an account does
not by itself make that liquidity reachable by every client.
Each compiled level quotes a price offset from the published midpoint. Midpoint updates use the next sequence number and expected shape generation; a stale quote origin or expired quote age prevents use of the update/quote. Shape changes and midpoint changes are different operations. Midpoint jumps and stale refreshes can reset the maker-rebate eligibility anchor.
The default minimum commitment is 10,000,000 settlement-token base units, and actual commitment must also cover the curve's computed exposure floor. Read the live config and simulation rather than treating that default as a complete capital requirement. Current equal-price matching gives the discrete book priority over a spline. See the spline live surface and SDK spline guide for account grammar, generation, freshness, commitment, settlement, and recovery.
<!-- src: program/src/logic/spline_processor.rs:269 compute_attach_spline --> <!-- src: program/src/state/config.rs:332 DEFAULT_SPLINE_MIN_COMMITMENT --> <!-- src: program/src/logic/place_order_processor/matching.rs:87 spline price priority --> <!-- M-05: invert equal-price book priority when splines-first ships -->Multi-market cadence#
For a native series, each duration window has a new market_id; feed,
duration, and creator remain part of the identity. Discover the actual
created account and trading state before quoting. Calculating the next PDA
does not create a market or permit orders on an uninitialized address.
Prepare enough SOL and settlement funds for the next market and its ledger seat. A seat and free-funds balance belong to one market; they do not roll over with a bot's local strategy state. Withdraw or settle the old market's funds explicitly. A spline can be attached to only one market at a time in this revision: settle/detach the previous attachment before the next window's attachment, and publish a fresh quote. Keep old market IDs in the reconciliation ledger until their orders, claims, and pending signatures are resolved.
Inventory management#
Why inventory matters#
When your bid fills, you own YES shares. When your ask fills, you've sold YES shares. Over time, imbalanced fills accumulate directional exposure — if your bids fill faster than your asks (a falling market), you build a long YES position that loses value.
Skewing quotes to reduce inventory#
Adjust your mid price based on current inventory:
| Inventory | Skew direction | Effect |
|---|---|---|
| Long YES (too many) | Lower mid → lower bid + ask | Makes selling YES more attractive |
| Long NO (too many) | Raise mid → higher bid + ask | Makes buying YES more attractive |
| Neutral | No skew | Symmetric quotes |
inventory_skew_bps = delta_shares × skew_factor_bps_per_share
adjusted_mid = raw_mid − inventory_skew_bps
Delta exposure#
delta = yes_shares − no_shares
delta > 0 → long YES, profits if market resolves UP
delta < 0 → long NO, profits if market resolves DOWN
delta = 0 → neutral, profits from spread only
Pairs, token custody, and settlement#
MintShares converts collateral into equal YES and NO shares. Owning a pair
has a fixed combined terminal payout, but selling one side creates directional
exposure. WithdrawShares moves eligible internal share inventory to token
custody; it is a custody operation, not a sale or an external price hedge.
Track wallet tokens, position locks, and ledger balances separately so you do
not count a transferred share twice.
At settlement, remaining inventory pays according to the recorded outcome;
an expired neutral market uses its specified split. Normal Redeem with the
position/ledger surface also drains available quote_free to the owner's
settlement-token account. A zero-amount cleanup does not redeem nonzero share
inventory. Reconcile both share legs and free quote, then close eligible
accounts; do not assume the market timer or a cancelled order moved every
balance to the wallet. See claiming and
funding and free funds.
Risk management#
Time risk#
Seesaw markets have a fixed end time. Within the final minutes, a market
maker's position is exposed to binary resolution risk — whatever inventory
you hold settles at $0 or $1. Most strategies cancel all orders before
t_end and flat out remaining inventory.
CancelAllOrders and CancelUpTo (and their free-funds variants) are atomic
and bounded to 6 orders per call: if you have more than 6 resting orders in
the market, the whole transaction reverts instead of cancelling a partial
set. If your order count can exceed 6, build your time-based exit around
explicit-ID batch cancellation (CancelMultipleOrdersById). Submit bounded
batches and reconcile each confirmation. Repeating the same over-limit
CancelAllOrders call cannot reduce the count because each failed call
rolls back completely.
Hard limits#
| Metric | Suggested limit | Action when exceeded |
|---|---|---|
| Max absolute YES | 500 shares | Stop posting bids |
| Max absolute NO | 500 shares | Stop posting asks |
| Net delta | ±200 shares | Aggressive skew |
| Time remaining | < 2 min | Cancel all, exit |
Common pitfalls#
| Pitfall | Consequence | Prevention |
|---|---|---|
| Inventory build-up | Large directional loss at resolution | Aggressive skewing; delta limits |
| Slow quote updates | Stale quotes → adverse selection | Fast execution; use WebSocket subscriptions |
| Ignoring time | Trading into resolution with large inventory | Time-based cutoff rules |
| Tight spread at extremes | Negative EV after taker fee slippage | Check effective maker EV including costs |
| Over-posting | Fill the selected deep-orderbook tier | Keep order count headroom |
Fee economics#
Makers pay zero fee. Takers pay the capped-decay fee:
fee_bps(price) = min(200, floor(400 × (10000 − price) / 10000))
These are shipped fee defaults, not a live configuration reading. Read the current config when sizing orders.
<!-- src: program/src/logic/fee.rs:70 DEFAULT_FEE_CAP_BPS --> <!-- src: program/src/logic/fee.rs:96 DEFAULT_DECAY_RATE_BPS -->For an equal-quantity round-trip, gross proceeds before inventory losses are:
gross_proceeds = spread_bps × quantity / 10000 + credited_maker_rebates
The only fee you pay is when you must exit a position as a taker (e.g.,
closing inventory before t_end). Factor the taker fee into your minimum
spread when sizing exits.
Automation requirements#
Effective market making is hard to do manually. See Automation for implementation details including:
- Reading the order book via Solana RPC or WebSocket
- Building and sending
PlaceMultiplePostOnlyOrdersbatches - Tracking fills and adjusting quotes programmatically
Market making checklist#
Before starting
- Understand the asset and typical spread
- Set maximum inventory limits (YES and NO separately)
- Define minimum acceptable spread
- Decide on time-based exit cutoff (e.g., cancel all at t_end − 2 min)
During trading
- Monitor fills and inventory after each transaction
- Re-quote after fills using the latest book state
- Watch the price band — reject rejections gracefully
- Track unrealized inventory exposure
Before market end
- Cancel all resting orders before
t_end - Flatten remaining inventory via IOC or limit sells/buys
- Ensure no unfilled resting buy orders remain past resolution (they must be redeemed within 7 days — see Risks)
- Claim any accumulated creator fees if you also created the market
Next steps#
- Automation — running a maker programmatically with permissionless cranks and free-funds flows
- Placing Orders — all order types and parameters
- Risks — the 7-day rule and market-admin controls
Technical reference#
| Mechanism | Instruction |
|---|---|
| Single post-only order | PlaceOrder (0x0B) with order_type = PostOnly |
| Batch post-only orders | PlaceMultiplePostOnlyOrders (0x0C) |
| Batch post-only (free funds) | PlaceMultiplePostOnlyOrdersWithFreeFunds (0x0F) |
| Cancel by IDs | CancelMultipleOrdersById (0x11) |
| Cancel all | CancelAllOrders (0x12) |
| Cancel above/below price | CancelUpTo (0x13) |
| Post-only mode status | SetMarketEmergencyStatus (0x2C) / EnablePostOnlyMode (0x27) |