A hands-on guide to building an autonomous market-making agent for real-world assets with kcolbchain/meridian.
Traditional AMMs — constant-product (Uniswap v2), concentrated liquidity (v3), or weighted pools (Balancer) — were designed for liquid, fungible tokens that trade continuously across a deep global order book. Real-world assets break every one of those assumptions:
The result: passive LPs in RWA pools lose consistently to anyone with better real-world information. You need an active, oracle-driven agent that controls its own spread and inventory — which is exactly what meridian is.
Meridian is an open-source market-making agent, not a pool contract. It posts and cancels limit orders on any venue (on-chain CLOB, RFQ, or hybrid) using these core principles:
BaseStrategy interface, so you can replay historical prints through the backtest engine before going live.Open meridian.kcolbchain.com (or the local dashboard). In the sidebar:
adaptive_spread.Watch the mid-price chart and the KPI tiles update in real time. Here is what each KPI means:
| KPI | What it tells you |
|---|---|
| Mid (USD) | Current oracle mid-price of the asset. |
| Effective spread | Distance between the agent's live bid and ask, in basis points. Wider = more conservative. |
| Inventory | Net units the agent currently holds. Positive = long, negative = short. |
| Realised PnL | Cash profit from completed round-trips (buys matched by sells). |
| Inventory PnL | Mark-to-market value of the open position. This is unrealised. |
| Fill rate | Fraction of posted quotes that actually trade. Low fill rate means the spread is too wide for the venue. |
| Adverse selection | Fraction of fills where the mid moves against the agent immediately after. This is the core enemy. |
| Sharpe-ish | Rolling mean PnL change over its standard deviation. A quick proxy for risk-adjusted return. |
Experiment: crank Toxic flow share to 40%. Watch adverse selection spike and realised PnL decay. This simulates an informed counterparty — the exact scenario where passive AMMs hemorrhage value. Notice how adaptive_spread widens in response, limiting damage compared to constant_spread.
git clone https://github.com/kcolbchain/meridian.git
cd meridian
pip install -r requirements.txt
python -m src.agents.rwa_market_maker \
--config config/default.yaml \
--simulate
Sample output:
[meridian] strategy=adaptive_spread asset=rwa-credit mode=simulate
tick 001 mid=100.0000 bid=99.9010 ask=100.0990 inv=0.000 pnl=$0.00
tick 002 mid=100.0340 bid=99.9352 ask=100.1328 inv=0.482 pnl=$0.03
tick 003 mid= 99.9870 bid=99.8695 ask=100.1045 inv=0.482 pnl=-$0.02
...
tick 100 mid=100.1200 bid=100.0014 ask=100.2386 inv=-0.113 pnl=$4.17
[meridian] 100 ticks complete. Sharpe-ish: 0.41 Fill rate: 28% Adverse: 11%
The --simulate flag runs the agent against a synthetic price path. Drop it and provide venue credentials in config/default.yaml to quote on a live CLOB.
python -m src.backtest.engine \
--strategy adaptive_spread \
--data data/sample.csv
The backtest engine replays historical price prints through your strategy and reports the same KPIs as the simulator. Key parameters you can set in the config or override via CLI flags:
fill_probability — likelihood that a posted quote gets filled on each tick (default 0.3).fee_bps — venue trading fee deducted per fill (default 5 bps).ticks — number of time steps to replay (default: length of the data file).base_price — starting mid for synthetic mode if no data file is provided.volatility — annualised vol assumption for synthetic paths (bps/tick).Output is a JSON report plus optional CSV of the tick-by-tick state, which you can pipe into any plotting tool.
Every strategy extends BaseStrategy and overrides a single method:
def compute_quotes(self, mid: float, inventory: float, volatility: float)
-> tuple[float, float, float, float]:
"""Return (bid_price, ask_price, bid_size, ask_size)."""
Here is a minimal "wide spread when scared" strategy in about 20 lines:
# src/strategies/scared_spread.py
from src.strategies.base import BaseStrategy
class ScaredSpread(BaseStrategy):
"""Widens aggressively when vol or inventory is high."""
name = "scared_spread"
def __init__(self, config: dict):
super().__init__(config)
self.base_bps = config.get("base_spread_bps", 200)
self.vol_mult = config.get("vol_multiplier", 3.0)
self.inv_limit = config.get("inv_limit", 10.0)
self.order_size = config.get("order_size", 1.0)
def compute_quotes(self, mid, inventory, volatility):
fear = max(1.0, 1.0 + self.vol_mult * (volatility / 100))
if abs(inventory) > self.inv_limit * 0.5:
fear *= 2.0 # double the spread when half-full
half = (self.base_bps * fear) / 2 / 10_000
bid_px = mid * (1 - half)
ask_px = mid * (1 + half)
return bid_px, ask_px, self.order_size, self.order_size
Register the class in src/strategies/__init__.py, then run it in the backtest: --strategy scared_spread. If the Sharpe-ish holds up, promote it to the simulator dropdown and the live agent.
For ERC-3643 security tokens, the agent cannot blindly post quotes to any taker. Before quoting, it must verify that the counterparty holds valid claims on their ONCHAINID identity contract — jurisdiction, accreditation status, and KYC expiry.
The planned compliance_gated strategy wraps any inner strategy and adds a pre-quote check: query the token's identity registry, verify the taker's claims, and suppress the quote if verification fails. This keeps the agent (and its LPs) on the right side of transfer restrictions.
You can explore the identity and compliance contracts we are building at rwa-toolkit.kcolbchain.com. The strategy stub is already visible on the simulator dashboard under the compliance_gated card.
Issue #18 tracks the design for ERC-4626 vaults per strategy. The idea: depositors fund the agent's inventory by minting vault shares. The agent quotes using the pooled capital, and quoting profits (net of fees and adverse selection) accrue to the vault.
This is how meridian becomes investable — anyone can deposit USDC into, say, the adaptive_spread / rwa-credit vault and earn a share of market-making returns without running infrastructure. The vault's share price reflects cumulative PnL, and withdrawals are gated by an unwind period so the agent is not forced to close positions instantly.