Skip to content

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


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.

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)

Each pool engine can be in one of three states:

StateMeaningEntry
PAUSEDNo quoting. Orders already placed are canceled on transition.Startup default; operator call
ACTIVEQuoting and order placement running.POST /v1/pools/{symbol}/enable or /resume
HALTEDEmergency 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 manual enable or resume call.


Before performing any operation:

  1. Confirm the service is healthy: GET http://mm-service:8095/health
  2. Know the symbol exactly as registered in the Metadata Service (e.g. BTCUSDT-PERP)
  3. Have the user UUID and account UUID of the participant ready (from the User Service)
  4. Use the Admin Panel (/market-maker/pools) or curl against port 8095

This is the first thing you do when onboarding a new symbol for market making.

Terminal window
curl http://metadata-service:8080/v1/instruments/XYZUSDT-PERP

The 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.

Admin Panel: go to Pool Engines → Create Pool

REST:

Terminal window
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:

FieldConservative startWhen to increase
gamma0.1Lower for more aggressive quoting
kappa1.5Higher for thinner markets
tau1.0Increase for longer-horizon strategy
vol_lookback_seconds300 (5 min)Increase for more stable σ estimate
num_levels1Increase after verifying fills
min_spread_bps5Never below exchange tick size
max_spread_bps50Cap to avoid quoting at absurd prices
order_size_usdt500Scale with available capital
max_pool_position_usdt50000Sum of all member position_limit_usdt
max_pool_loss_usdt2000~4% of pool size is a reasonable default

Set enabled: false on creation. Enable only after members are assigned.

Terminal window
curl http://mm-service:8095/v1/pools/XYZUSDT-PERP

The 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.


Participants are global entities — they exist once and can join multiple pools.

Terminal window
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:

ModelBehaviour
KEEP_ALLParticipant keeps 100% of gross realised PnL
PERCENTAGE_SPLITPlatform 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.)

A fresh participant is PENDING and cannot join pools until activated.

Terminal window
curl -X POST http://mm-service:8095/v1/participants/{PARTICIPANT_ID}/activate

Admin Panel: MM Participants → row actions → Activate

State transitions:
PENDING ──activate──► ACTIVE ──suspend──► SUSPENDED
terminate
TERMINATED (irreversible)
Terminal window
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%.

Terminal window
curl http://mm-service:8095/v1/pools/BTCUSDT-PERP/members
# Review the capital_weight_pct of each existing member
Terminal window
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:

FieldMeaningConstraint
capital_weight_pctShare of pool order size allocated to this memberSum of all members ≤ 100
position_limit_usdtMember-level position limit (per-member kill switch)Must be ≤ pool max_pool_position_usdt
max_loss_usdtMember-level loss limitShould 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.

Terminal window
curl http://mm-service:8095/v1/pools/BTCUSDT-PERP/status

The response includes a members array. The new participant should appear with "active": true.

With three members at weights 50%, 30%, 20% and a pool order_size_usdt of $1000:

MemberWeightOrder size per level
Acme50%$500
Beta30%$300
Gamma20%$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.

Pool Engines → symbol row → click “Enable”

Terminal window
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:

Terminal window
curl -X POST http://mm-service:8095/v1/pools/BTCUSDT-PERP/resume
Terminal window
curl http://mm-service:8095/v1/pools/BTCUSDT-PERP/status
# state should be "ACTIVE"
# open_orders count should grow within quote_interval_ms

Check Prometheus for order placement:

mm_pool_member_open_orders{symbol="BTCUSDT-PERP"}

Configuration changes take effect on the next quoting cycle without requiring a restart (provided the pool engine is already running).

Terminal window
# Example: widen max spread and increase order size
curl -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”
Terminal window
# 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%.


Pausing stops all quoting and cancels all open MM orders for that symbol.

Terminal window
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”

A suspended participant’s pool memberships remain but their orders are halted. Use this when investigating risk or a performance anomaly.

Terminal window
curl -X POST http://mm-service:8095/v1/participants/{PARTICIPANT_ID}/suspend

To re-activate after review:

Terminal window
curl -X POST http://mm-service:8095/v1/participants/{PARTICIPANT_ID}/activate

Termination removes the participant from all active pools, cancels their live engine membership, and permanently closes their record.

Terminal window
curl -X POST http://mm-service:8095/v1/participants/{PARTICIPANT_ID}/terminate

Warning: Termination cannot be undone. A new participant record must be created if re-onboarding.

To remove a participant from one symbol without affecting other memberships:

Terminal window
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.


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.

Terminal window
curl -X POST http://mm-service:8095/v1/mm/emergency/flatten

Identical to flatten in the current implementation — pauses all engines.

Terminal window
curl -X POST http://mm-service:8095/v1/mm/emergency/kill

After a kill/flatten, each pool must be individually reviewed and resumed:

Terminal window
# 1. Review what triggered the halt
curl http://mm-service:8095/v1/mm/status
# check each symbol's "reason" field
# 2. Review fill history and P&L for affected symbols
curl http://mm-service:8095/v1/mm-analytics/BTCUSDT-PERP/metrics
# 3. Resume each symbol individually after review
curl -X POST http://mm-service:8095/v1/pools/BTCUSDT-PERP/resume
curl -X POST http://mm-service:8095/v1/pools/ETHUSDT-PERP/resume

Never resume a HALTED pool without understanding why it halted.
Check reason in the status response and logs before resuming.


Settlement records are created automatically by a monthly batch job. Ops reviews and approves or rejects each record.

Terminal window
curl http://mm-service:8095/v1/settlements/pending

Admin Panel: Settlements → filter by PENDING

Each record shows:

  • gross_pnl_usdt — total realised PnL for the period
  • platform_share_pct — platform’s cut (from participant’s revenue_share_pct)
  • platform_amount_usdt — amount retained by platform
  • net_payout_usdt — amount owed to participant
  • period_start / period_end

Cross-check with the Performance Leaderboard and fill history:

Terminal window
# Q-score and performance for the settlement period
curl "http://mm-service:8095/v1/performance/{PARTICIPANT_ID}"
# Raw fill data
curl "http://mm-service:8095/v1/mm-fills?participant_id={PARTICIPANT_ID}"
# Analytics overview
curl http://mm-service:8095/v1/mm-analytics/overview
Terminal window
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.

Terminal window
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.

After approval, the Finance team executes the wallet transfer and updates the record to SETTLED through the Wallet Service. Failed transfers become FAILED.

PENDING → APPROVED → SETTLED
↘ ↘ FAILED
REJECTED

Only possible when the pool engine is not ACTIVE.

Terminal window
# 1. Pause the pool
curl -X POST http://mm-service:8095/v1/pools/XYZUSDT-PERP/pause
# 2. Optionally remove all members first
curl -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 config
curl -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.


Perform this check at the start of each business day:

Terminal window
# 1. Overall health
curl 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 settlements
curl 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 symbol
curl http://mm-service:8095/v1/mm-orders/{SYMBOL}/breakdown

On Grafana, verify:

  • mm_pool_member_open_orders — should be non-zero for ACTIVE pools
  • mm_pool_net_position — should be within expected bands
  • mm_pool_member_pnl — monitor for unusual drawdown

Market typegammakappataumin_spread_bpsorder_size_usdt
High-liquidity perp (BTC, ETH)0.052.01.031000
Mid-cap perp0.11.51.05500
Low-liquidity / new listing0.21.00.510100
  • 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
  • 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
  • min_spread_bps must be at least 1 tick size (check instrument metadata)
  • max_spread_bps prevents quoting at nonsensical prices during low-data periods
  • Start wide and narrow as you gain confidence in market data quality
  • 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

Cause: All pools start PAUSED on boot; the engine exists in the process but has no running goroutine until enabled.

Fix:

Terminal window
curl -X POST http://mm-service:8095/v1/pools/{SYMBOL}/enable

Pool 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:

  1. Check current positions:
    Terminal window
    curl http://mm-service:8095/v1/pools/{SYMBOL}/status
  2. Review fill history to understand the directional build-up:
    Terminal window
    curl "http://mm-service:8095/v1/mm-fills/{SYMBOL}?limit=100"
  3. Consider raising max_pool_position_usdt if the limit was set too tight, or reduce member position_limit_usdt values to redistribute risk.
  4. 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:

  1. 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"
  2. Investigate fills during the loss window — look for adversarial flow patterns.
  3. If the limit was appropriate and the loss was genuine, do not resume until risk team signs off.
  4. Consider tightening max_pool_loss_usdt or reducing order_size_usdt before resuming.

Pool shows 0 open orders despite ACTIVE state

Section titled “Pool shows 0 open orders despite ACTIVE state”

Possible causes and fixes:

CauseCheckFix
No active membersGET /pools/{symbol}/membersAdd or re-activate a member
Stale market dataLogs: "stale market data"Check Kafka consumer lag; restart consumer if needed
All quotes within price thresholdprice_threshold_bps too highLower to 25 bps
Order Service unreachableService logs: HTTP errorsCheck ORDER_SERVICE_URL and network connectivity
Shadow modeMM_SHADOW_MODE=trueSet MM_SHADOW_MODE=false and restart

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:

Terminal window
# See current weights
curl http://mm-service:8095/v1/pools/{SYMBOL}/members
# Reduce an existing member
curl -X PUT http://mm-service:8095/v1/pools/{SYMBOL}/members/{EXISTING_PARTICIPANT_ID} \
-d '{"capital_weight_pct": "30"}'
# Now add the new member
curl -X POST http://mm-service:8095/v1/pools/{SYMBOL}/members \
-d '{"participant_id": "...", "capital_weight_pct": "20", ...}'

Cause: Wallet Service transaction failed during payout.

Steps:

  1. Retrieve the settlement:
    Terminal window
    curl http://mm-service:8095/v1/settlements/{ID}
    # check wallet_txn_id and notes for error detail
  2. Retry the wallet transfer manually via the Wallet Service admin API.
  3. Once the transfer succeeds, update the settlement status to SETTLED directly in the database (requires ops DB access — use an Atlas migration or direct SQL with audit).

Terminal window
curl http://mm-service:8095/v1/mm/status
# "shadow_mode": true → orders are NOT being placed

To disable shadow mode, set MM_SHADOW_MODE=false in environment and redeploy.


Add symbol: POST /v1/pools (create config, enabled:false)
Add participant: POST /v1/participants (creates PENDING)
Activate: POST /v1/participants/{id}/activate
Assign to pool: POST /v1/pools/{symbol}/members
Enable quoting: POST /v1/pools/{symbol}/enable
Pause: POST /v1/pools/{symbol}/pause
Resume: POST /v1/pools/{symbol}/resume
Emergency stop: POST /v1/mm/emergency/flatten
Status: GET /v1/mm/status
Pool detail: GET /v1/pools/{symbol}/status
Pending payouts: GET /v1/settlements/pending
Approve payout: POST /v1/settlements/{id}/approve body: {approved_by}
Reject payout: POST /v1/settlements/{id}/reject body: {rejected_by, reason}