Market Maker Guide

Why Aptos for Market Making

The short version: Block-STM v2 removes the serialization constraint that makes market making expensive on Sealevel (Solana). Multiple market makers can operate on the same market in the same block without forcing sequential execution.

On Solana, every order update serializes through the market PDA. At 5 makers each submitting 10 quote updates per second, the effective throughput is 5×10 = 50 sequential writes per second through a single bottleneck. Practical HFT market making requires either the drop-and-set optimization (Dropset) or accepting severe gas costs from contention.

On Aptos with Block-STM v2, those 50 writes execute speculatively in parallel. Conflicts are detected and re-executed, but the re-execution rate is proportional to how often two makers happen to touch the same price level in the same block. Under typical conditions, most updates don't conflict, and true parallelism provides meaningful throughput headroom.

QuoteIntent's two-step design reduces the conflict surface further: submit-intent writes only to the maker's own address (no conflict with anyone). execute-own-intent is the only shared-state write, and it's atomic. This means the effective conflict window for any maker is the minimum possible.

The QuoteIntent Workflow

Standard Ladder Update Cycle

1. Submit-intent (private write, no book contention)
   Writes to: your seat's pending_intent only
   Gas: minimal. Single resource write
   Conflict risk: zero

2. Execute-own-intent (shared write. B+tree update)
   Writes to: BigOrderedMap (shared) + your seat
   Gas: O(1) per order in ladder. Flat via B+tree leaf split
   Conflict risk: low, only if another maker updates same price level simultaneously

The practical workflow for a market maker running a two-sided BTC/USDT book with 10 levels each side:

// Step 1: compute new quotes based on current mid + spread
const newQuotes = computeLadder(midPrice, spreadBps, numLevels);

// Step 2: prepare order entries for each new resting order
const orderEntries = prepareOrderEntries(newQuotes);

// Step 3: submit-intent (no book lock acquired)
await submitIntent(newQuotes, orderEntries, nonce++);

// Step 4: execute-own-intent (atomic cancel+replace)
// This is the only operation that touches the shared book
await executeOwnIntent(cancelNullifiers, cancelProof, placeProof);

The window between step 3 and step 4 can be milliseconds. There is no cancel-then-replace race condition. execute-own-intent is atomic. Competitors cannot fill your just-canceled orders before the replacements land.

Handling Concurrent Updates

If multiple execute-own-intent transactions from different makers land in the same block and conflict on the same price level, Block-STM resolves the conflict by re-executing the lower-priority transaction (by gas price). Both makers' ladders update successfully. The second maker's transaction re-executes against the state left by the first maker's settlement. No orders are lost, and both updates complete within the same block.

For makers who need deterministic ordering within a block: submit execute-own-intent at a gas price premium during high-volatility periods when you specifically need your update to take priority.

Seat Management

Your seat is the per-user data structure that tracks your active orders independently of the shared book. It's the foundation for efficient cancel-all.

Seat Initialization

On first interaction with the Whop CLOB, a seat is automatically created at your address:

public entry fun initialize_seat(trader: &signer) {
    let seat = Seat {
        active_orders: Table::new(),
        order_count: 0,
    };
    move_to(trader, seat);
}

Gas: one-time initialization cost. After this, all order operations read from your seat, not from the global book state.

Seat Data Structure

For each active order, your seat stores:

  • Price level (the BigOrderedMap key)
  • Time priority within that level (position in the queue)
  • Order quantity
  • Order reference (pointer into BigOrderedMap for efficient cancel)

The seat is your per-user state. It enables efficient cancel-all without iterating the shared book.

Cancel-all Operation

public entry fun cancel_all(
    maker: &signer,
) acquires Seat, Market {
    let seat = borrow_global_mut<Seat>(signer::address_of(maker));
    let n = seat.order_count;
    
    // O(n). Iterates seat.active_orders, NOT the shared book
    for i in 0..n {
        let order_ref = Table::remove(&mut seat.active_orders, i);
        BigOrderedMap::remove(&market.book, order_ref.price_key);
    }
    
    // Return locked collateral to maker
    coin::deposit(signer::address_of(maker), released_collateral);
    
    seat.order_count = 0;
    Table::destroy_empty(seat.active_orders);
    seat.active_orders = Table::new();
}

At 20 active orders: 20 book removals. At 10,000 resting orders in the market: irrelevant to your cancel-all cost.

Latency target: A cancel-all covering 20 orders should land in the same block as submission. One block = 10ms at Archon speeds. The time from "market event triggers cancel-all" to "all orders removed from book" is one block time.

Privacy Options for Market Makers

Resting orders on the BigOrderedMap are public. Competitors can see your quotes, sizes, and price levels. This is inherent to a transparent orderbook and necessary for price discovery. Privacy for market makers operates at two layers:

Pre-execution privacy (encrypted mempool): The encrypted mempool (pending governance) hides your incoming quote updates before they land on the book. Competitors cannot see your ladder refresh before it executes, preventing front-running of your quote changes.

Post-settlement privacy (CA and UTT for transfers): When your orders fill, settlement proceeds can use CA or UTT for transfer privacy:

  • Zone 1 (CA): Transfer amounts hidden, wallet addresses visible. Competitors can see which address received a fill but cannot see the transfer amount or your updated balance. This is the practical choice for institutional market makers, no budget constraint, adequate privacy.
  • Zone 2 (UTT): Transfer amounts AND wallet identity hidden. Full graph privacy on settlements. However, UTT's monthly anonymity budget cap makes this impractical at institutional market-making scale. A $5M notional market maker settling frequently would exhaust their budget rapidly.

The practical recommendation: Large market makers use Zone 1 (CA) for settlement privacy. The budget cap is designed for end users and smaller traders. Institutional market makers get adequate protection from CA. Competitors cannot see fill sizes or inventory levels, even though the maker's address is visible.

Zone 1 (CA) properties for market makers:

  • Your resting bid/ask sizes: visible on the book (public orderbook)
  • Your wallet address: visible
  • Your settlement transfer amounts: hidden (Twisted ElGamal)
  • Your total inventory across positions: hidden
  • Your profit/loss per trade: hidden

Gas Budgeting

Target Gas Costs

Based on Decibel's measured figure of 26 gas per Bulk Order (the equivalent operation), and the QuoteIntent's O(1) B+tree property:

OperationEstimated gasNotes
submit-intent5-10 unitsSingle resource write, no book contact
execute-own-intent (20 orders)40-60 unitsB+tree ops × 20, flat per-op cost
cancel-all (20 orders)30-50 unitsSeat iteration + book removals
Single taker sweep (1 fill)20-35 unitsB+tree pop + UTT settle
Seat initialization50-100 unitsOne-time

These are estimates based on AIP-120 performance data. Actual gas costs will be measured against the deployed contract and updated here.

Gas Price Strategy

During normal market conditions: submit at base gas price. Block-STM's parallel execution means you're not competing with other makers for block inclusion speed.

During high-volatility events when you need priority: submit execute-own-intent at 2-5× base gas price. This ensures your ladder update takes priority over others in Block-STM's conflict resolution ordering.

During extreme events (cancel-all urgency): submit cancel-all at maximum reasonable gas. You want this landing in the next block. 10ms Archon block times means worst-case 10ms from submission to settlement.

Dead Man's Switch Configuration

For market makers running automated systems, the Dead Man's Switch is a critical safety mechanism. Borrowed from Decibel's AIP-120 primitives, it automatically cancels your entire ladder if you fail to refresh within a configured window.

public entry fun configure_dms(
    maker: &signer,
    max_staleness_blocks: u64, // cancel-all fires if no refresh in this many blocks
) acquires Seat {
    let seat = borrow_global_mut<Seat>(signer::address_of(maker));
    seat.dms_config = DeadMansSwitch {
        max_staleness: max_staleness_blocks,
        last_refresh: block::get_current_block_height(),
    };
}

With 10ms block times: 100 blocks = 1 second. A DMS set to 1,000 blocks fires after 10 seconds of inactivity. Configure based on your connectivity SLA and risk tolerance.

The DMS is executed by any keeper bot. A third party calls trigger_dms(maker_address) and earns a small bounty. This keeps the mechanism decentralized: even if Whop's infrastructure goes offline, keeper bots will still trigger your DMS if your system goes silent.

Market-making Profitability Framework

The basic spread P&L model for a two-sided market maker:

Daily P&L = (trades_per_day × avg_fill_size × spread_captured)
           - (num_quote_updates × gas_per_update × gas_price)
           - (inventory_risk_cost)

For a BTC/USDT market maker:

  • Spread target: 5bps (0.05%) on $80,000 BTC = $40 per round-trip
  • Average fill size: $50,000 notional
  • Spread captured per round-trip: $50,000 × 0.0005 = $25 (half the spread, one side)
  • At 100 round-trips per day: $2,500 gross spread revenue

Gas cost at 5 ladder refreshes per minute:

  • 300 refreshes/hour × 8 active hours = 2,400 refreshes/day
  • At 50 gas per refresh × $0.001/gas unit: $0.12/refresh
  • 2,400 × $0.12 = $288/day in gas

Net before inventory risk: $2,500 - $288 = $2,212/day

Inventory risk varies with volatility and position size. The key property: gas costs on Aptos are predictable (O(1) per operation) and measurable (not variable like Ethereum or the pre-AIP-120 Aptos). A market-making bot can budget gas costs with confidence.

Integration Checklist for Market Makers

  • Initialize seat on first connection
  • Configure Dead Man's Switch appropriate to your connectivity SLA
  • Implement submit-intent + execute-own-intent as atomic two-step
  • Implement cancel-all from seat state (not book state)
  • Implement CA settlement for Zone 1 transfers (default) or UTT settlement for Zone 2 transfers
  • Implement budget monitoring. Alert when Zone 2 monthly budget drops below threshold
  • Implement gas price escalation for high-urgency operations
  • Set up keeper monitoring for DMS health
  • Register for chart replay events on your fills (if you want fill attribution in Explorer)
  • Connect .whop name to your market maker address for attribution