Welcome to API Academy
Module 15 · Production · ~25 min
Market making.
By the end of this module, you’ll be the one collecting fees on Limitless instead of paying them, your bot quoting both sides of the book and getting paid for the privilege of waiting.
To get there, you’ll quote both sides of the book, manage inventory, and earn the spread, the first real strategy in the Production tier.
Production tier · Reference cardHow does a market-making bot work on Limitless?
It posts two passive limit orders, a bid below the mid and an ask above it, and collects the spread whenever someone crosses them; you’re renting your balance sheet to impatient traders, not betting on direction. The loop: pull a fresh mid from the orderbook, place a pair of OrderType.GTC orders with postOnly: true (rejected if they would cross, which guarantees maker fees), and when the mid drifts past a reposition threshold (0.5% in the example), cancelAll(marketSlug) and re-quote. A smart maker also skews: when inventory builds up long, it widens the bid and tightens the ask to lean the next fill back toward flat, reading live balances from PortfolioFetcher.getPositions(). The risk you’re paid for is inventory risk, being left holding a position when the mid moves against you.
Endpoints verified 2026-06-09 against the OpenAPI spec.
Section 01
The market maker’s job.
A market maker posts two passive limit orders, a bid below the mid and an ask above it, and collects the spread every time someone crosses them. You’re not betting on direction. You’re renting your balance sheet to impatient traders who want to move right now.
Bid
Willing to buy YES slightly below mid. Filled when sellers hit you.
Spread
The gap between your bid and ask. Wider = safer but slower. Tighter = faster but more inventory risk.
Ask
Willing to sell YES slightly above mid. Filled when buyers lift you.
The risk you earn the spread for is inventory risk, being left holding a position when the mid moves against you.
Section 02
Quote management.
A basic quote placer: pull the orderbook mid, place a pair of OrderType.GTC orders on the YES token with postOnly: true so you always earn maker fees, and cancelAll(marketSlug) before re-quoting when the mid drifts past a reposition threshold. Everything else is tuning. Wrap the loop in the SDK’s retry helper so transient 429s and 5xxs don’t kill your maker.
How to run this
- Set LIMITLESS_API_KEY and PRIVATE_KEY in your environment, this loop places real CLOB orders. Swap btc-100k-weekly at the bottom for a low-volume slug you’re comfortable quoting.
- Save the snippet above as quote-maker.ts, then run npx tsx quote-maker.ts.
- Save the snippet above as quote_maker.py, then run python quote_maker.py.
- Save the snippet above as main.go inside a Go module (go mod init example), then run go run main.go.
- Every two seconds, your process cancels existing orders and posts a fresh maker pair at mid × (1 ± 0.02) whenever the mid has drifted more than 0.5%. Check the Limitless UI, you should see your bid and ask sitting on the book with post-only status.
// Module 15, Basic quote placer with reposition on mid drift.
import {
HttpClient,
MarketFetcher,
OrderClient,
Side,
OrderType,
withRetry,
} from '@limitless-exchange/sdk';
import { Wallet } from 'ethers';
const SPREAD = 0.02; // quote ±2% of mid
const REPOSITION = 0.005; // re-quote if mid moves > 0.5%
const SIZE = 100; // shares per quote
const httpClient = new HttpClient({ apiKey: process.env.LIMITLESS_API_KEY });
const marketFetcher = new MarketFetcher(httpClient);
const wallet = new Wallet(process.env.PRIVATE_KEY!);
const orderClient = new OrderClient({ httpClient, wallet, marketFetcher });
let quotedMid = 0;
async function requote(slug: string) {
// 1. Pull orderbook for a fresh mid.
// GET /markets/{slug}/orderbook
const book = await marketFetcher.getOrderBook(slug);
const bestBid = Number(book.bids[0]?.price ?? 0);
const bestAsk = Number(book.asks[0]?.price ?? 0);
if (!bestBid || !bestAsk) return;
const mid = (bestBid + bestAsk) / 2;
// 2. Skip if drift is below the reposition threshold.
if (quotedMid && Math.abs(mid - quotedMid) / quotedMid < REPOSITION) return;
// 3. Cancel every live order on this market before re-quoting.
// DELETE /orders/all/{slug}
await orderClient.cancelAll(slug);
// 4. Place a fresh maker-only pair on the YES token.
const market = await marketFetcher.getMarket(slug);
const yesTokenId = market.positionIds[0];
await withRetry(
() => orderClient.createOrder({
marketSlug: slug,
tokenId: yesTokenId,
side: Side.BUY,
price: mid * (1 - SPREAD),
size: SIZE,
orderType: OrderType.GTC,
postOnly: true, // rejects if it would cross, guarantees maker fees
}),
{ statusCodes: [429, 500, 502, 503, 504], maxRetries: 3, delays: [1000, 2000, 4000] },
);
await withRetry(
() => orderClient.createOrder({
marketSlug: slug,
tokenId: yesTokenId,
side: Side.SELL,
price: mid * (1 + SPREAD),
size: SIZE,
orderType: OrderType.GTC,
postOnly: true,
}),
{ statusCodes: [429, 500, 502, 503, 504], maxRetries: 3, delays: [1000, 2000, 4000] },
);
quotedMid = mid;
}
setInterval(() => {
requote('btc-100k-weekly').catch(console.error);
}, 2_000);
# Module 15, Basic quote placer with reposition on mid drift.
import asyncio
import os
from eth_account import Account
from limitless_sdk.api import HttpClient, retry_on_errors
from limitless_sdk.markets import MarketFetcher
from limitless_sdk.orders import OrderClient
from limitless_sdk.types import Side, OrderType
SPREAD = 0.02 # quote ±2% of mid
REPOSITION = 0.005 # re-quote if mid moves > 0.5%
SIZE = 100.0 # shares per quote
http_client = HttpClient()
market_fetcher = MarketFetcher(http_client)
wallet = Account.from_key(os.environ["PRIVATE_KEY"])
order_client = OrderClient(http_client, wallet=wallet, market_fetcher=market_fetcher)
state = {"mid": 0.0}
@retry_on_errors(status_codes={429, 500, 502, 503, 504}, max_retries=3, delays=[1, 2, 4])
async def place(slug: str, token_id: str, side: Side, price: float) -> None:
await order_client.create_order(
token_id=token_id,
price=price,
size=SIZE,
side=side,
order_type=OrderType.GTC,
market_slug=slug,
post_only=True, # rejects if it would cross, guarantees maker fees
)
async def requote(slug: str) -> None:
# 1. Pull orderbook for a fresh mid.
# GET /markets/{slug}/orderbook
book = await market_fetcher.get_orderbook(slug)
bids, asks = book.get("bids") or [], book.get("asks") or []
if not bids or not asks:
return
best_bid = float(bids[0]["price"])
best_ask = float(asks[0]["price"])
mid = (best_bid + best_ask) / 2
# 2. Skip if drift is below the reposition threshold.
if state["mid"] and abs(mid - state["mid"]) / state["mid"] < REPOSITION:
return
# 3. Cancel every live order on this market before re-quoting.
# DELETE /orders/all/{slug}
await order_client.cancel_all(slug)
# 4. Place a fresh maker-only pair on the YES token.
market = await market_fetcher.get_market(slug)
yes_token_id = market["positionIds"][0]
await place(slug, yes_token_id, Side.BUY, mid * (1 - SPREAD))
await place(slug, yes_token_id, Side.SELL, mid * (1 + SPREAD))
state["mid"] = mid
async def main() -> None:
while True:
await requote("btc-100k-weekly")
await asyncio.sleep(2)
if __name__ == "__main__":
asyncio.run(main())
// Module 15, Basic quote placer with reposition on mid drift.
package main
import (
"context"
"log"
"math"
"os"
"time"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
const (
Spread = 0.02 // quote ±2% of mid
Reposition = 0.005 // re-quote if mid moves > 0.5%
Size = 100.0 // shares per quote
)
func main() {
ctx := context.Background()
client := limitless.NewHttpClient()
marketFetcher := limitless.NewMarketFetcher(client)
orderClient, err := limitless.NewOrderClient(client, os.Getenv("PRIVATE_KEY"))
if err != nil { log.Fatal(err) }
retryCfg := limitless.RetryConfig{
StatusCodes: []int{429, 500, 502, 503, 504},
MaxRetries: 3,
ExponentialBase: 2.0,
MaxDelay: 60 * time.Second,
}
slug := "btc-100k-weekly"
var quotedMid float64
tick := time.NewTicker(2 * time.Second)
defer tick.Stop()
for range tick.C {
// 1. Pull orderbook for a fresh mid.
book, err := marketFetcher.GetOrderBook(ctx, slug)
if err != nil {
log.Println(err); continue
}
if len(book.Bids) == 0 || len(book.Asks) == 0 { continue }
bestBid := book.Bids[0].Price
bestAsk := book.Asks[0].Price
mid := (bestBid + bestAsk) / 2
// 2. Skip if drift is below reposition threshold.
if quotedMid != 0 && math.Abs(mid-quotedMid)/quotedMid < Reposition {
continue
}
// 3. Cancel every live order on this market.
if _, err := orderClient.CancelAll(ctx, slug); err != nil {
log.Println(err); continue
}
// 4. Place a maker-only pair on the YES token.
market, err := marketFetcher.GetMarket(ctx, slug)
if err != nil { log.Println(err); continue }
place := func(side limitless.Side, price float64) error {
return limitless.WithRetry(ctx, func() error {
_, err := orderClient.CreateOrder(ctx, limitless.CreateOrderParams{
OrderType: limitless.OrderTypeGTC,
MarketSlug: slug,
Args: limitless.GTCOrderArgs{
TokenID: market.Tokens.Yes,
Side: side,
Price: price,
Size: Size,
PostOnly: true,
},
})
return err
}, retryCfg)
}
if err := place(limitless.SideBuy, mid*(1-Spread)); err != nil { log.Println(err) }
if err := place(limitless.SideSell, mid*(1+Spread)); err != nil { log.Println(err) }
quotedMid = mid
}
}
Section 03
Inventory skew.
A naive maker quotes symmetric spreads. A smart one skews, when inventory gets long, it tightens the ask and widens the bid to lean the next fill flat. Pull your current CLOB balances from PortfolioFetcher.getPositions() and feed them into the skew helper every loop.
// Module 15, Inventory-skewed quotes, driven by real CLOB balances.
import { HttpClient, PortfolioFetcher } from '@limitless-exchange/sdk';
interface SkewParams {
mid: number;
baseSpread: number; // e.g. 0.02
inventory: number; // signed units; >0 long YES, <0 short
inventoryCap: number; // hard ceiling
skewStrength: number; // 0..1
}
export function skewedQuotes(p: SkewParams): { bid: number; ask: number } {
const ratio = Math.max(-1, Math.min(1, p.inventory / p.inventoryCap));
const skew = ratio * p.skewStrength * p.baseSpread;
// long (ratio > 0): widen bid, tighten ask
const bid = p.mid * (1 - p.baseSpread - skew);
const ask = p.mid * (1 + p.baseSpread - skew);
return { bid, ask };
}
const httpClient = new HttpClient({ apiKey: process.env.LIMITLESS_API_KEY });
const portfolio = new PortfolioFetcher(httpClient);
async function quotesForMarket(slug: string, mid: number) {
// GET /portfolio/positions
const positions = await portfolio.getPositions();
const clobEntry = positions.clob.find((p) => p.market.slug === slug);
// marketValue is the canonical inventory signal on PositionDataDto
// (5 required fields: cost, fillPrice, realisedPnl, unrealizedPnl, marketValue).
// It's the current USDC-denominated value of each side in token decimals.
const yesValue = Number(clobEntry?.positions?.yes?.marketValue ?? 0);
const noValue = Number(clobEntry?.positions?.no?.marketValue ?? 0);
const inventory = yesValue - noValue;
return skewedQuotes({
mid,
baseSpread: 0.02,
inventory,
inventoryCap: 100,
skewStrength: 0.5,
});
}
quotesForMarket('btc-100k-weekly', 0.52).then(console.log).catch(console.error);
# Module 15, Inventory-skewed quotes, driven by real CLOB balances.
import asyncio
from dataclasses import dataclass
from limitless_sdk.api import HttpClient
from limitless_sdk.portfolio import PortfolioFetcher
@dataclass
class SkewParams:
mid: float
base_spread: float # e.g. 0.02
inventory: float # signed units; >0 long YES, <0 short
inventory_cap: float # hard ceiling
skew_strength: float # 0..1
def skewed_quotes(p: SkewParams) -> dict[str, float]:
ratio = max(-1.0, min(1.0, p.inventory / p.inventory_cap))
skew = ratio * p.skew_strength * p.base_spread
# long (ratio > 0): widen bid, tighten ask
bid = p.mid * (1 - p.base_spread - skew)
ask = p.mid * (1 + p.base_spread - skew)
return {"bid": bid, "ask": ask}
http_client = HttpClient()
portfolio = PortfolioFetcher(http_client)
async def quotes_for_market(slug: str, mid: float) -> dict[str, float]:
# GET /portfolio/positions
positions = await portfolio.get_positions()
clob_entry = next(
(p for p in positions.get("clob", []) if p["market"]["slug"] == slug),
None,
)
# marketValue is the canonical inventory signal on PositionDataDto
# (5 required fields: cost, fillPrice, realisedPnl, unrealizedPnl, marketValue).
yes_value = float((clob_entry or {}).get("positions", {}).get("yes", {}).get("marketValue", 0))
no_value = float((clob_entry or {}).get("positions", {}).get("no", {}).get("marketValue", 0))
inventory = yes_value - no_value
return skewed_quotes(SkewParams(
mid=mid,
base_spread=0.02,
inventory=inventory,
inventory_cap=100,
skew_strength=0.5,
))
async def main() -> None:
print(await quotes_for_market("btc-100k-weekly", 0.52))
await http_client.close()
if __name__ == "__main__":
asyncio.run(main())
// Module 15, Inventory-skewed quotes, driven by real CLOB balances.
package main
import (
"context"
"fmt"
"log"
"math"
"strconv"
limitless "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"
)
type SkewParams struct {
Mid float64
BaseSpread float64
Inventory float64
InventoryCap float64
SkewStrength float64
}
func SkewedQuotes(p SkewParams) (bid, ask float64) {
ratio := math.Max(-1, math.Min(1, p.Inventory/p.InventoryCap))
skew := ratio * p.SkewStrength * p.BaseSpread
// long (ratio > 0): widen bid, tighten ask
bid = p.Mid * (1 - p.BaseSpread - skew)
ask = p.Mid * (1 + p.BaseSpread - skew)
return
}
func main() {
ctx := context.Background()
client := limitless.NewHttpClient()
portfolio := limitless.NewPortfolioFetcher(client)
// GET /portfolio/positions
positions, err := portfolio.GetPositions(ctx)
if err != nil { log.Fatal(err) }
var inventory float64
slug := "btc-100k-weekly"
for _, p := range positions.Clob {
if p.Market.Slug != slug { continue }
yes, _ := strconv.ParseFloat(p.Positions.Yes.MarketValue, 64)
no, _ := strconv.ParseFloat(p.Positions.No.MarketValue, 64)
inventory = yes - no
break
}
bid, ask := SkewedQuotes(SkewParams{
Mid: 0.52,
BaseSpread: 0.02,
Inventory: inventory,
InventoryCap: 100,
SkewStrength: 0.5,
})
fmt.Printf("bid=%.4f ask=%.4f (inventory=%.2f)\n", bid, ask, inventory)
}
How to run this
- Set LIMITLESS_API_KEY, this helper only reads positions, it doesn’t place orders. Already have a position in btc-100k-weekly? Good. No position? Point the slug at a market you’re in so the skew isn’t zero.
- Save the snippet above as inventory-skew.ts, then run npx tsx inventory-skew.ts.
- Save the snippet above as inventory_skew.py, then run python inventory_skew.py.
- Save the snippet above as main.go inside a Go module, then run go run main.go.
- Terminal prints a { bid, ask } pair for a mid of 0.52. If you’re long YES, the bid is pulled lower and the ask is pulled lower too, the whole fence leans against your inventory. Flip the sign of the position and both quotes shift up.
Section 04
Adverse selection & toxic flow.
When not to make markets
Market makers lose money when the people crossing them know something they don’t. That’s adverse selection, you’re getting filled exactly because the price is about to move against you. These are the three environments where a maker should stop quoting immediately.
Event windows
Don’t quote into scheduled resolutions, court decisions, election calls, earnings prints, or macro data releases. The flow that hits you seconds before an event is almost always informed.
Thin books
If total depth around the mid is less than a few multiples of your quote size, a single market order can move the mid through both your quotes. You’ll take two fills in the wrong direction before you can even cancel.
Informed counterparties
Large repeat flow from a single wallet that always seems to pick the right side isn’t luck. Track your per-counterparty hit rate; back off anyone you keep losing to.
A profitable maker widens or withdraws faster than they cancel. Your event calendar, book-depth check, and counterparty-score check should all be able to pull quotes inside a single loop iteration.
Market making on Limitless: what people ask
Each answer also ships invisibly as schema.org FAQ data for search engines and AI assistants. Tap a question to expand.
-
What does postOnly do on a Limitless order?
postOnly: truemakes the order strictly passive: if it would cross the book and fill immediately, it’s rejected instead of executed. For a market maker that’s the point, every fill you do get is a maker fill, so you always earn maker fees rather than paying taker. The quote placer pairs it withOrderType.GTCon both the bid and the ask of the YES token. -
When should a market-making bot re-quote?
When the mid drifts past a reposition threshold, not on every tick. The example loop checks every two seconds and re-quotes only when the mid has moved more than 0.5% from the last quoted mid, which prevents thrashing. The sequence matters:cancelAll(marketSlug)(theDELETE /orders/all/{slug}path) first, then post the fresh maker pair atmid × (1 ± spread), then record the new quoted mid. Wrap the calls in the SDK retry helper so a transient 429 can’t kill the maker. -
What is inventory skew in market making?
Tilting both quotes against your current position so the next fill leans you back toward flat. Compute signed inventory as the YESmarketValueminus the NOmarketValuefromPortfolioFetcher.getPositions(); when you’re long, widen the bid and tighten the ask. Skew is a gradient, not a fence: hard-cap inventory in code on every quote emit, before any spread or skew math, because in a strong run the market can trade through your bids faster than skew keeps up. -
What risk is a market maker actually paid for?
Inventory risk: being left holding a position when the mid moves against you. A maker isn’t betting on direction; the spread is rent for providing immediacy to traders who want to move right now. The tuning knob is the spread itself: wider is safer but fills slower, tighter fills faster but carries more inventory risk. The spread you collect is your compensation for bearing that risk. -
When should a market maker stop quoting?
Three environments: event windows, scheduled resolutions, earnings prints, election calls, macro releases, where the flow that hits you seconds before the event is almost always informed; thin books, where one market order can move the mid through both your quotes before you can cancel; and informed counterparties, a single wallet that keeps picking the right side, so track per-counterparty hit rate and back off anyone you keep losing to. All three checks should be able to pull quotes inside a single loop iteration.
Section 05
Module checklist.
Tick each item once you’ve actually done it. The Continue button unlocks at 5/5.
I understand that a market maker is paid for bearing inventory risk, not direction
I can place a pair of limit quotes at a configurable spread around the mid
My quotes reposition when the mid drifts past a threshold, without thrashing
I skew bids and asks based on current inventory to lean back toward flat
I have rules that pull my quotes during event windows, thin books, or toxic flow
Module 15 complete
Quoting live.
You’re a maker, not a taker. Other traders cross your spread to get filled, and the difference is your fee, the same business model that funds entire trading firms, running on your code instead of theirs.
Concretely, you’re posting both sides of the book and collecting the spread. Three things you walk away with:
A two-sided quote loop that cancels and repositions on every 0.5% mid drift, posts postOnly GTC orders so you always earn maker fees, and wraps every call in withRetry so a transient 429 can’t kill the bot.
A skewedQuotes() helper that reads your live PortfolioFetcher.getPositions() inventory and leans both sides of the fence against your current YES−NO imbalance.
A concrete list of conditions under which a maker should widen or withdraw, event windows, thin books, and informed counterparty flow, plus the mental model that a profitable maker pulls faster than they cancel.
Next up: hunting parity gaps, YES + NO ≠ $1, sibling markets out of sync, and the execution code that captures the difference before it closes.
Complete the checklist above to unlock