Market Maker Service — Operations SOP
Document type: Standard Operating Procedure (SOP)
Audience: Platform engineers, operations, risk management
Scope: Market Maker Service — Pool Engine architecture
Service port: 8095 (REST) · 9090 (Prometheus)
Admin panel: http://localhost:3001/market-maker/pools
1. Conceptual overview
Section titled “1. Conceptual overview”The Market Maker Service operates through Pool Engines — one per trading symbol. Each engine aggregates positions across multiple enrolled market-maker participants, runs the Avellaneda-Stoikov (A-S) optimal-quoting model on the pooled inventory, then splits the resulting order sizes among members proportionally to their capital weights (pro-rata allocation).
Orders are placed under each member’s own exchange account — fills, P&L, and performance scores are tracked per participant. Revenue is shared monthly via the Settlement workflow.
Entity map
Section titled “Entity map”Participant (global identity — user_id / account_id) │ └── PoolMember ──► symbol BTCUSDT-PERP ──► PoolEngine └── PoolMember ──► symbol ETHUSDT-PERP ──► PoolEngine
PoolEngine (per symbol) ├── PoolConfig (A-S parameters, risk limits) ├── MemberRuntime × N (live position, PnL, open orders) ├── ASEngine (reservation price + optimal spread) ├── VolatilityTracker (rolling σ) └── ProRataAllocator (splits order sizes by capital weight)State machine
Section titled “State machine”Each pool engine can be in one of three states:
| State | Meaning | Entry |
|---|---|---|
| PAUSED | No quoting. Orders already placed are canceled on transition. | Startup default; operator call |
| ACTIVE | Quoting and order placement running. | POST /v1/pools/{symbol}/enable or /resume |
| HALTED | Emergency stop. Manual review required before reset. | Risk guard triggers kill switch |
The service ALWAYS starts all pools in PAUSED.
Quoting never auto-resumes after a restart without a manualenableorresumecall.
2. Prerequisites
Section titled “2. Prerequisites”Before performing any operation:
- Confirm the service is healthy:
GET http://mm-service:8095/health - Know the symbol exactly as registered in the Metadata Service (e.g.
BTCUSDT-PERP) - Have the user UUID and account UUID of the participant ready (from the User Service)
- Use the Admin Panel (
/market-maker/pools) orcurlagainst port8095
3. Runbook — Add a new trading pool
Section titled “3. Runbook — Add a new trading pool”This is the first thing you do when onboarding a new symbol for market making.
Step 1 — Confirm the instrument exists
Section titled “Step 1 — Confirm the instrument exists”curl http://metadata-service:8080/v1/instruments/XYZUSDT-PERPThe symbol must exist in the Metadata Service before creating a pool config. The pool engine uses instrument metadata for tick size and lot size rounding.
Step 2 — Create the pool config
Section titled “Step 2 — Create the pool config”Admin Panel: go to Pool Engines → Create Pool
REST:
curl -X POST http://mm-service:8095/v1/pools \ -H "Content-Type: application/json" \ -d '{ "symbol": "XYZUSDT-PERP", "enabled": false, "gamma": "0.1", "kappa": "1.5", "vol_lookback_seconds": 300, "tau": "1.0", "num_levels": 3, "level_spacing_bps": "5", "min_spread_bps": "5", "max_spread_bps": "50", "order_size_usdt": "500", "max_pool_position_usdt": "50000", "max_pool_loss_usdt": "2000", "check_staleness": true, "staleness_threshold_ms": "2000", "quote_interval_ms": "100", "price_threshold_bps": "5" }'Field guidance:
| Field | Conservative start | When to increase |
|---|---|---|
gamma | 0.1 | Lower for more aggressive quoting |
kappa | 1.5 | Higher for thinner markets |
tau | 1.0 | Increase for longer-horizon strategy |
vol_lookback_seconds | 300 (5 min) | Increase for more stable σ estimate |
num_levels | 1 | Increase after verifying fills |
min_spread_bps | 5 | Never below exchange tick size |
max_spread_bps | 50 | Cap to avoid quoting at absurd prices |
order_size_usdt | 500 | Scale with available capital |
max_pool_position_usdt | 50000 | Sum of all member position_limit_usdt |
max_pool_loss_usdt | 2000 | ~4% of pool size is a reasonable default |
Set
enabled: falseon creation. Enable only after members are assigned.
Step 3 — Verify the config was saved
Section titled “Step 3 — Verify the config was saved”curl http://mm-service:8095/v1/pools/XYZUSDT-PERPThe response includes an active_members count (will be 0) and state: "NOT_RUNNING" until the
service is restarted or a member is added to an already-running engine.
4. Runbook — Enroll a new participant
Section titled “4. Runbook — Enroll a new participant”Participants are global entities — they exist once and can join multiple pools.
Step 1 — Create the participant record
Section titled “Step 1 — Create the participant record”curl -X POST http://mm-service:8095/v1/participants \ -H "Content-Type: application/json" \ -d '{ "user_id": "3f4a8b2c-0000-0000-0000-000000000001", "account_id": "5d6e9f1a-0000-0000-0000-000000000002", "display_name": "Acme Trading Desk", "initial_margin_usdt": "10000", "max_drawdown_pct": "20", "revenue_model": "PERCENTAGE_SPLIT", "revenue_share_pct": "20", "tier": "TIER_1", "enrolled_by": "ops-team", "notes": "Onboarded 2026-04" }'The participant is created with status PENDING.
Revenue model options:
| Model | Behaviour |
|---|---|
KEEP_ALL | Participant keeps 100% of gross realised PnL |
PERCENTAGE_SPLIT | Platform retains revenue_share_pct% — e.g. 20 means participant gets 80% |
Tier options: TIER_1, TIER_2, TIER_3
(Tier affects Q-score weight in leaderboard rankings.)
Step 2 — Activate the participant
Section titled “Step 2 — Activate the participant”A fresh participant is PENDING and cannot join pools until activated.
curl -X POST http://mm-service:8095/v1/participants/{PARTICIPANT_ID}/activateAdmin Panel: MM Participants → row actions → Activate
State transitions: PENDING ──activate──► ACTIVE ──suspend──► SUSPENDED │ terminate │ ▼ TERMINATED (irreversible)Step 3 — Verify
Section titled “Step 3 — Verify”curl http://mm-service:8095/v1/participants/{PARTICIPANT_ID}# status should be "ACTIVE"5. Runbook — Assign a participant to a pool
Section titled “5. Runbook — Assign a participant to a pool”Before assigning, the participant must be ACTIVE and a pool config must exist for the symbol.
Step 1 — Check current capital weight allocation
Section titled “Step 1 — Check current capital weight allocation”Capital weights across all members of a pool must not exceed 100%.
curl http://mm-service:8095/v1/pools/BTCUSDT-PERP/members# Review the capital_weight_pct of each existing memberStep 2 — Add the member
Section titled “Step 2 — Add the member”curl -X POST http://mm-service:8095/v1/pools/BTCUSDT-PERP/members \ -H "Content-Type: application/json" \ -d '{ "participant_id": "{PARTICIPANT_ID}", "capital_weight_pct": "40", "position_limit_usdt": "20000", "max_loss_usdt": "800" }'Field guidance:
| Field | Meaning | Constraint |
|---|---|---|
capital_weight_pct | Share of pool order size allocated to this member | Sum of all members ≤ 100 |
position_limit_usdt | Member-level position limit (per-member kill switch) | Must be ≤ pool max_pool_position_usdt |
max_loss_usdt | Member-level loss limit | Should be proportional to their capital weight |
If the pool engine is already running, the member is hot-added to the live engine immediately — no restart required.
Step 3 — Verify the assignment
Section titled “Step 3 — Verify the assignment”curl http://mm-service:8095/v1/pools/BTCUSDT-PERP/statusThe response includes a members array. The new participant should appear with "active": true.
Capital weight example
Section titled “Capital weight example”With three members at weights 50%, 30%, 20% and a pool order_size_usdt of $1000:
| Member | Weight | Order size per level |
|---|---|---|
| Acme | 50% | $500 |
| Beta | 30% | $300 |
| Gamma | 20% | $200 |
6. Runbook — Enable a pool (start quoting)
Section titled “6. Runbook — Enable a pool (start quoting)”Only call this after at least one active member is assigned.
Via Admin Panel
Section titled “Via Admin Panel”Pool Engines → symbol row → click “Enable”
Via REST
Section titled “Via REST”curl -X POST http://mm-service:8095/v1/pools/BTCUSDT-PERP/enable# → {"symbol":"BTCUSDT-PERP","state":"ACTIVE"}If the pool engine is already running in PAUSED state, use /resume instead:
curl -X POST http://mm-service:8095/v1/pools/BTCUSDT-PERP/resumeVerify quoting is live
Section titled “Verify quoting is live”curl http://mm-service:8095/v1/pools/BTCUSDT-PERP/status# state should be "ACTIVE"# open_orders count should grow within quote_interval_msCheck Prometheus for order placement:
mm_pool_member_open_orders{symbol="BTCUSDT-PERP"}7. Runbook — Update pool config
Section titled “7. Runbook — Update pool config”Configuration changes take effect on the next quoting cycle without requiring a restart (provided the pool engine is already running).
# Example: widen max spread and increase order sizecurl -X PUT http://mm-service:8095/v1/pools/BTCUSDT-PERP \ -H "Content-Type: application/json" \ -d '{ "symbol": "BTCUSDT-PERP", "enabled": true, "gamma": "0.1", "kappa": "1.5", "vol_lookback_seconds": 300, "tau": "1.0", "num_levels": 3, "level_spacing_bps": "5", "min_spread_bps": "5", "max_spread_bps": "80", "order_size_usdt": "1000", "max_pool_position_usdt": "50000", "max_pool_loss_usdt": "2500", "check_staleness": true, "staleness_threshold_ms": "2000", "quote_interval_ms": "100", "price_threshold_bps": "5" }'Upsert is a full replacement — always include all fields even if only changing one.
8. Runbook — Adjust a member’s capital weight or limits
Section titled “8. Runbook — Adjust a member’s capital weight or limits”# Increase Acme's weight from 40% to 50%curl -X PUT http://mm-service:8095/v1/pools/BTCUSDT-PERP/members/{PARTICIPANT_ID} \ -H "Content-Type: application/json" \ -d '{ "capital_weight_pct": "50", "position_limit_usdt": "25000", "max_loss_usdt": "1000" }'The service checks that the new total weight across all members does not exceed 100%. If it would,
the request is rejected with 400 Total capital weight would exceed 100%.
9. Runbook — Pause a pool
Section titled “9. Runbook — Pause a pool”Pausing stops all quoting and cancels all open MM orders for that symbol.
curl -X POST http://mm-service:8095/v1/pools/BTCUSDT-PERP/pause# → {"symbol":"BTCUSDT-PERP","state":"PAUSED"}Use this when:
- Performing config maintenance
- A symbol is undergoing settlement
- Volatility is extreme and you want to step back manually
10. Runbook — Suspend or terminate a participant
Section titled “10. Runbook — Suspend or terminate a participant”Suspend (reversible)
Section titled “Suspend (reversible)”A suspended participant’s pool memberships remain but their orders are halted. Use this when investigating risk or a performance anomaly.
curl -X POST http://mm-service:8095/v1/participants/{PARTICIPANT_ID}/suspendTo re-activate after review:
curl -X POST http://mm-service:8095/v1/participants/{PARTICIPANT_ID}/activateTerminate (irreversible)
Section titled “Terminate (irreversible)”Termination removes the participant from all active pools, cancels their live engine membership, and permanently closes their record.
curl -X POST http://mm-service:8095/v1/participants/{PARTICIPANT_ID}/terminateWarning: Termination cannot be undone. A new participant record must be created if re-onboarding.
Remove from a single pool only
Section titled “Remove from a single pool only”To remove a participant from one symbol without affecting other memberships:
curl -X DELETE http://mm-service:8095/v1/pools/BTCUSDT-PERP/members/{PARTICIPANT_ID}This is a soft-remove (state set to REMOVED) and removes the member from the live engine.
11. Runbook — Emergency controls
Section titled “11. Runbook — Emergency controls”Flatten all positions (ordered wind-down)
Section titled “Flatten all positions (ordered wind-down)”Pauses all pool engines, triggering order cancellation across every symbol. Use when you need to halt all MM activity immediately but want a clean shutdown.
curl -X POST http://mm-service:8095/v1/mm/emergency/flattenKill all (hard halt)
Section titled “Kill all (hard halt)”Identical to flatten in the current implementation — pauses all engines.
curl -X POST http://mm-service:8095/v1/mm/emergency/killRecovery after emergency
Section titled “Recovery after emergency”After a kill/flatten, each pool must be individually reviewed and resumed:
# 1. Review what triggered the haltcurl http://mm-service:8095/v1/mm/status# check each symbol's "reason" field
# 2. Review fill history and P&L for affected symbolscurl http://mm-service:8095/v1/mm-analytics/BTCUSDT-PERP/metrics
# 3. Resume each symbol individually after reviewcurl -X POST http://mm-service:8095/v1/pools/BTCUSDT-PERP/resumecurl -X POST http://mm-service:8095/v1/pools/ETHUSDT-PERP/resumeNever resume a HALTED pool without understanding why it halted.
Checkreasonin the status response and logs before resuming.
12. Runbook — Monthly settlement
Section titled “12. Runbook — Monthly settlement”Settlement records are created automatically by a monthly batch job. Ops reviews and approves or rejects each record.
Step 1 — View pending settlements
Section titled “Step 1 — View pending settlements”curl http://mm-service:8095/v1/settlements/pendingAdmin Panel: Settlements → filter by PENDING
Each record shows:
gross_pnl_usdt— total realised PnL for the periodplatform_share_pct— platform’s cut (from participant’srevenue_share_pct)platform_amount_usdt— amount retained by platformnet_payout_usdt— amount owed to participantperiod_start/period_end
Step 2 — Verify the numbers
Section titled “Step 2 — Verify the numbers”Cross-check with the Performance Leaderboard and fill history:
# Q-score and performance for the settlement periodcurl "http://mm-service:8095/v1/performance/{PARTICIPANT_ID}"
# Raw fill datacurl "http://mm-service:8095/v1/mm-fills?participant_id={PARTICIPANT_ID}"
# Analytics overviewcurl http://mm-service:8095/v1/mm-analytics/overviewStep 3 — Approve
Section titled “Step 3 — Approve”curl -X POST http://mm-service:8095/v1/settlements/{SETTLEMENT_ID}/approve \ -H "Content-Type: application/json" \ -d '{"approved_by": "ops-jane"}'Status transitions to APPROVED.
Step 4 — Reject (if needed)
Section titled “Step 4 — Reject (if needed)”curl -X POST http://mm-service:8095/v1/settlements/{SETTLEMENT_ID}/reject \ -H "Content-Type: application/json" \ -d '{ "rejected_by": "ops-jane", "reason": "Abnormal fill pattern detected between 2026-03-14 08:00 and 10:00 UTC. Pending risk review." }'Status transitions to REJECTED. The settlement record is preserved with the rejection reason.
Step 5 — Execute the payout
Section titled “Step 5 — Execute the payout”After approval, the Finance team executes the wallet transfer and updates the record to SETTLED
through the Wallet Service. Failed transfers become FAILED.
Settlement state machine
Section titled “Settlement state machine”PENDING → APPROVED → SETTLED ↘ ↘ FAILED REJECTED13. Runbook — Delete a pool
Section titled “13. Runbook — Delete a pool”Only possible when the pool engine is not ACTIVE.
# 1. Pause the poolcurl -X POST http://mm-service:8095/v1/pools/XYZUSDT-PERP/pause
# 2. Optionally remove all members firstcurl -X DELETE http://mm-service:8095/v1/pools/XYZUSDT-PERP/members/{PARTICIPANT_ID_1}curl -X DELETE http://mm-service:8095/v1/pools/XYZUSDT-PERP/members/{PARTICIPANT_ID_2}
# 3. Delete the configcurl -X DELETE http://mm-service:8095/v1/pools/XYZUSDT-PERP# → {"symbol":"XYZUSDT-PERP","deleted":true}Fill history and settlement records for this symbol are preserved in the database even after the pool config is deleted.
14. Daily operations checklist
Section titled “14. Daily operations checklist”Perform this check at the start of each business day:
# 1. Overall healthcurl http://mm-service:8095/health
# 2. Pool engine states — any unexpected PAUSED or HALTED?curl http://mm-service:8095/v1/mm/status
# 3. Check for any pending settlementscurl http://mm-service:8095/v1/settlements/pending
# 4. 24h analytics overview — any unusual PnL or volume?curl http://mm-service:8095/v1/mm-analytics/overview
# 5. Open orders sanity check per symbolcurl http://mm-service:8095/v1/mm-orders/{SYMBOL}/breakdownOn Grafana, verify:
mm_pool_member_open_orders— should be non-zero for ACTIVE poolsmm_pool_net_position— should be within expected bandsmm_pool_member_pnl— monitor for unusual drawdown
15. Configuration tuning guide
Section titled “15. Configuration tuning guide”Starting parameters for a new symbol
Section titled “Starting parameters for a new symbol”| Market type | gamma | kappa | tau | min_spread_bps | order_size_usdt |
|---|---|---|---|---|---|
| High-liquidity perp (BTC, ETH) | 0.05 | 2.0 | 1.0 | 3 | 1000 |
| Mid-cap perp | 0.1 | 1.5 | 1.0 | 5 | 500 |
| Low-liquidity / new listing | 0.2 | 1.0 | 0.5 | 10 | 100 |
Tuning γ (gamma — risk aversion)
Section titled “Tuning γ (gamma — risk aversion)”- Too high: Quotes pull too far from mid — misses fills, very little volume
- Too low: Takes excessive inventory risk, drawdown increases
- Rule of thumb: Start at 0.1, reduce toward 0.05 if volume is too low
Tuning κ (kappa — depth parameter)
Section titled “Tuning κ (kappa — depth parameter)”- Higher kappa: Assumes thick order book, quotes tighter relative to inventory
- Lower kappa: Spreads wider to account for thin market
- Check: If the engine is frequently at min_spread, increase kappa
Tuning spread floor / ceiling
Section titled “Tuning spread floor / ceiling”min_spread_bpsmust be at least 1 tick size (check instrument metadata)max_spread_bpsprevents quoting at nonsensical prices during low-data periods- Start wide and narrow as you gain confidence in market data quality
Tuning num_levels
Section titled “Tuning num_levels”- Start at
1(one bid, one ask) - Only increase after the level-0 quotes are filling consistently
- More levels = more open orders = higher capital requirement per member
16. Troubleshooting
Section titled “16. Troubleshooting”Pool is NOT_RUNNING after restart
Section titled “Pool is NOT_RUNNING after restart”Cause: All pools start PAUSED on boot; the engine exists in the process but has no running goroutine until enabled.
Fix:
curl -X POST http://mm-service:8095/v1/pools/{SYMBOL}/enablePool state is HALTED — reason: “position limit exceeded”
Section titled “Pool state is HALTED — reason: “position limit exceeded””Cause: Aggregate pool position exceeded max_pool_position_usdt.
Steps:
- Check current positions:
Terminal window curl http://mm-service:8095/v1/pools/{SYMBOL}/status - Review fill history to understand the directional build-up:
Terminal window curl "http://mm-service:8095/v1/mm-fills/{SYMBOL}?limit=100" - Consider raising
max_pool_position_usdtif the limit was set too tight, or reduce memberposition_limit_usdtvalues to redistribute risk. - Resume only after the current net position is within bounds:
Terminal window curl -X POST http://mm-service:8095/v1/pools/{SYMBOL}/resume
Pool state is HALTED — reason: “loss limit exceeded”
Section titled “Pool state is HALTED — reason: “loss limit exceeded””Cause: Aggregate realised PnL fell below −max_pool_loss_usdt.
Steps:
- Pull analytics to understand the loss period:
Terminal window curl "http://mm-service:8095/v1/mm-analytics/hourly?symbol={SYMBOL}&since=2026-04-01T00:00:00Z" - Investigate fills during the loss window — look for adversarial flow patterns.
- If the limit was appropriate and the loss was genuine, do not resume until risk team signs off.
- Consider tightening
max_pool_loss_usdtor reducingorder_size_usdtbefore resuming.
Pool shows 0 open orders despite ACTIVE state
Section titled “Pool shows 0 open orders despite ACTIVE state”Possible causes and fixes:
| Cause | Check | Fix |
|---|---|---|
| No active members | GET /pools/{symbol}/members | Add or re-activate a member |
| Stale market data | Logs: "stale market data" | Check Kafka consumer lag; restart consumer if needed |
| All quotes within price threshold | price_threshold_bps too high | Lower to 2–5 bps |
| Order Service unreachable | Service logs: HTTP errors | Check ORDER_SERVICE_URL and network connectivity |
| Shadow mode | MM_SHADOW_MODE=true | Set MM_SHADOW_MODE=false and restart |
New member not receiving orders
Section titled “New member not receiving orders”Cause: Capital weight was added but the member was not hot-added to the engine (race condition on startup, or engine restarted after member was added).
Fix: Restart the Market Maker Service. On startup it loads all DB members and rebuilds the pool. Alternatively, remove and re-add the member via the API — this will hot-add them again.
Weight validation error when adding member
Section titled “Weight validation error when adding member”400 Total capital weight would exceed 100%Fix: Review existing members and reduce one before adding the new one:
# See current weightscurl http://mm-service:8095/v1/pools/{SYMBOL}/members
# Reduce an existing membercurl -X PUT http://mm-service:8095/v1/pools/{SYMBOL}/members/{EXISTING_PARTICIPANT_ID} \ -d '{"capital_weight_pct": "30"}'
# Now add the new membercurl -X POST http://mm-service:8095/v1/pools/{SYMBOL}/members \ -d '{"participant_id": "...", "capital_weight_pct": "20", ...}'Settlement shows FAILED status
Section titled “Settlement shows FAILED status”Cause: Wallet Service transaction failed during payout.
Steps:
- Retrieve the settlement:
Terminal window curl http://mm-service:8095/v1/settlements/{ID}# check wallet_txn_id and notes for error detail - Retry the wallet transfer manually via the Wallet Service admin API.
- Once the transfer succeeds, update the settlement status to
SETTLEDdirectly in the database (requires ops DB access — use an Atlas migration or direct SQL with audit).
How to check if shadow mode is active
Section titled “How to check if shadow mode is active”curl http://mm-service:8095/v1/mm/status# "shadow_mode": true → orders are NOT being placedTo disable shadow mode, set MM_SHADOW_MODE=false in environment and redeploy.
17. Quick reference card
Section titled “17. Quick reference card”Add symbol: POST /v1/pools (create config, enabled:false)Add participant: POST /v1/participants (creates PENDING)Activate: POST /v1/participants/{id}/activateAssign to pool: POST /v1/pools/{symbol}/membersEnable quoting: POST /v1/pools/{symbol}/enablePause: POST /v1/pools/{symbol}/pauseResume: POST /v1/pools/{symbol}/resumeEmergency stop: POST /v1/mm/emergency/flattenStatus: GET /v1/mm/statusPool detail: GET /v1/pools/{symbol}/statusPending payouts: GET /v1/settlements/pendingApprove payout: POST /v1/settlements/{id}/approve body: {approved_by}Reject payout: POST /v1/settlements/{id}/reject body: {rejected_by, reason}18. Related documentation
Section titled “18. Related documentation”- Market Maker Service Reference — Architecture, formulas, full API list
- Market Maker Engineering Guide — Conceptual deep-dive for engineers
- Market Maker API (Swagger)
- Order Service
- Kafka Integration