19 KiB
maker
Automated market maker engine for Lux Network exchanges. Aggregates orderbook depth from multiple external venues, calculates a fair mid price, and generates limit order quotes on both sides. Also seeds and maintains liquidity directly on the venue D-Chain over its FROZEN clob_* CLOB ZAP surface — it is the V4 liquidity seeder.
Mainnet 0x9999 fill — PROVEN (2026-07-14)
First real DEX settlement proven on Lux mainnet 96369 post-reimport, on the C-Chain
0x9999 embedded-book precompile (NOT the native D-Chain path — D is unmounted on
mainnet). Driver: the one-shot cmd/provefill (since deleted; maker rests a BID, a distinct taker SELLs to cross;
uses Pool9999, the same money-path calldata coherence uses).
- Settlement tx
0xb3c561bb530d0c758f84df9df2d86009d49894b43a693e87ede58b227b309861, block 1087057, status 1. Market LSOL/WLUX (poolId0xeb8c1af7c9dd79282bb04a82fd2d0e02545d9be1f38ef02e8b673f16d7e27bda), maker0x5806b0e0…, taker0xcD0cDEd2…(the coherence maker/taker keys, secret a k8s Secret since deleted, keys now in KMS at /bots/retired). Emitted DEXFill (amountOut 500000000600072843) + 2 real ERC-20 Transfers. Conservation holds (0.5 LSOL ↔ 0.5 WLUX). Indexed by the explorer dex subgraph (explorer:8090/v1/graph/cchain/dex/graphql, entitiesmarkets/orders/fills).
Root cause of the prior "maker up but 0 fills" (two independent causes, both found live):
-
coherence RPC was dead:
COHERENCE_RPCpointed at a host/path pair that 404s. The endpoint that works is/v1/chain/C/rpc— everywhere, on both hops. Re-measured 2026-07-27 against the live mainnet fleet (luxdv1.34.0, LuxNetwork CRspec.ports.http = 9630):target /v1/chain/C/rpc/v1/chain/C/rpccontrol /v1/chain/ZZZ/rpcluxd-1:9630direct200 0x17871404 404 api-gateway-gateway.lux-mainnet.svc:80200 0x17871404 404 https://api.lux.network200 0x17871404 404 So the earlier claim in this file — "luxd serves the C-Chain RPC ONLY through the api-gateway; direct
:9630404s BOTH prefixes" — was wrong on both halves. Direct:9630works fine on/v1, and the gateway 404s/ext. luxd has exactly one prefix (node/server/http/server.go,baseURL = "/v1");/ext/is gone, not deprecated. Never generalise the port either: mainnet is 9630, devnet 9650, testnet 9640 — readspec.ports.httpfrom that net's LuxNetwork CR. -
taker probe disabled: coherence logs
probe=0s— the taker-probe (ProbeInterval) is off, so coherence only rests a book + arbs; it never self-crosses. Fills need an external taker ORCOHERENCE_PROBE_INTERVAL/ProbeSizeset (owner decision).
CLOB conventions that matter (precompile/dex v0.15.0 embedded book, the deployed ABI):
- base = currency0, quote = currency1 (PoolKey auto-orders by address).
swapPlaceisBid=BUY locks quote(c1);!isBid=SELL locks base(c0). swapexact-input BUY is refused without a price limit (ErrBuyRequiresLimit); a SELL needs a min-out floor (carried in hookData).Pool9999.Swaphardcodes sqrtPriceLimit=0, so it only drives SELLs — the price-limit-free settle path.- amounts are raw token wei, capped at uint64 (~18.4 tokens at 18 dec per op).
- TWO distinct ledgers: the V4 ERC-6909 balance
balanceOf(account,asset)(0xf7888aec) is NOT the swap-custody CLOB available — reading the former for aswapDepositshows 0 and looks like a dropped-credit bug; it isn't. The precompile's own revert/return is truth. - Build:
GOWORK=off(the repo go.workuses the localprecompile= v0.19.1 settle-only, which removed the embedded book; the maker pins v0.15.0 = the deployed embedded-book ABI) andCGO_ENABLED=0(skip pkg/lx GPU cgo).
Pool9999.Withdraw (swapWithdraw) was added alongside Deposit so the client can round-trip
custody (deposit → place/swap → withdraw).
Module
github.com/luxfi/maker — Go library + CLI.
Key Types
Maker— quote loop (start/stop, position tracking, requote)Aggregator— merges orderbooks from NProviderSourcein parallelStrategy— interface:MidSpreadStrategy(symmetric),InventorySkewStrategy(position-aware)OMAProviderSource— reads on-chain OracleMirroredAMM prices via EVM RPCConfig— spread, levels, qty, refresh interval, phantom depth, position limits,Pairs,DEXEndpoint,MakerUserDEXClient(dex.go) — venue D-Chain seeding path; speaks the venue's FROZENclob_*surface (clob_ensure_market/clob_place/clob_cancel/clob_submit) overgithub.com/luxfi/rpc, keyed by 32-byte poolId. The frozen frame (poolId[32], place=65, cancel=40, submit=66, ack=24, fill=17) is re-declared locally byte-identical togithub.com/luxfi/dex/pkg/zapwire(the same pattern chains/dexvm/relay.go and the precompile use — each client home re-declares the frozen frame rather than importing the package). The venue is poolId-keyed and symbol-agnostic (its stored "symbol" is just the poolId hex); the maker, as the gateway-level seeder, owns the symbol→poolId map:SymbolPoolID(sym)= keccak256(sym) by default, overridable per pair viaPair.PoolIDto seed the exact book a deployed V4 PoolKey (key.ID()) resolves to. Book reads (BestBid/BestAsk) are NOT on the clob_* surface (write-only: reads are chain-state queries) → they returnErrReadUnsupported; the seeder requotes around its configured reference mid.Seeder(seeder.go) — creates markets and maintains a 2-sided book perPair:Seedplaces initial quotes,Maintaincancels+re-quotes around the live mid,Rundoes both onRefreshInterval.
Usage
Quote-only (aggregate external venues):
go run ./cmd/maker -symbols BTC-USD,ETH-USD -spread 10 -levels 5 -interval 5s
Seed + maintain the DEX (creates LUX/LUSD and LUX/LETH, places 2-sided quotes):
go run ./cmd/maker -dex 127.0.0.1:5000 -user maker -spread 10 -levels 5 -interval 5s
Wire real venues by implementing ProviderSource (see README.md).
Deploy (container + k8s)
The maker ships as ghcr.io/luxfi/maker — a long-running CLOB liquidity seeder
deployed in-cluster ALONGSIDE luxd so it can reach the native D-Chain CLOB route
group. It creates the canonical custody markets (LUX/LUSD, LUX/LETH) and maintains
a 2-sided book around a reference mid, which is the orderbook the exchange Trade
page renders (the exchange-api reads the SAME D-Chain over dex_get_*).
- Image (
Dockerfile): CPU, linux/amd64,CGO_ENABLED=0static binary on luxfi/crypto's pure-Go path — same build shape as the siblingghcr.io/luxfi/dex. Built by.github/workflows/docker.ymlvia the sharedhanzoai/.github/.github/workflows/docker-build.yml@mainreusable (secrets: inheritpasses thegh_tokenBuildKit secret for private luxfi modules). Semver-only tags. - Config is 12-factor (env) — every flag's default reads an env var
(
cmd/maker/main.go):DEX_HTTP_URL(the.../v1/chain/Droute prefix; setting it puts the binary in seed mode),MAKER_USER,MAKER_SPREAD_BPS,MAKER_LEVELS,MAKER_INTERVAL,MAKER_SKEW_BPS,MAKER_DEBUG. CLI flags still win. - k8s (
k8s/):kubectl kustomize k8s | kubectl apply -f -. AConfigMap(maker-config) carries the env; a single-replicaDeployment(Recreatestrategy — two seeders sharingMAKER_USERwould double-deposit and fight over the book). Base targetslux-devnet; for testnet/mainnet change the namespace and theDEX_HTTP_URLport (devnet 9650 / testnet 9640 / mainnet 9630). - compose (
compose.yml): local seeding —DEX_HTTP_URL=http://host.docker.internal:9650/v1/chain/D docker compose up --build.
Live seeding requires a reachable luxd serving the native D-Chain VM at
<DEX_HTTP_URL>/dex/* (chain alias D); the D-Chain is not publicly exposed
(api.lux.network/v1/chain/D 404s), so this is an in-cluster surface.
Coherence — continuous Kraken-oracle maker on the C-Chain 0x9999
MAKER_MODE=coherence runs the continuous price-coherence service on the C-Chain
0x9999 PoolManager (NOT the D-Chain CLOB above — orthogonal venue, orthogonal
transport). It pegs a 2-sided book to the live Kraken mid for each configured
market every COHERENCE_REQUOTE, an arb loop corrects drift past the band, and an
optional probe taker exercises the book so the realized price is MEASURED. It is
the continuous, persistent-market production form of the trader-pkg coherence
study (~/work/lux-private/memos/dex-price-coherence.md); the strategy is private,
hence it lives here in luxfi/maker (private), never in public luxfi/trader.
- Files:
pool9999.go(the 0x9999 EVM client — initialize/swapDeposit/ swapPlace/swapCancel/swap + getBestBidAsk/getOpenOrders + DEXFill reads, multi- endpoint broadcast for nets that don't gossip the mempool),coherence.go(the service: setup → requote → arb → probe → metrics),coherence_config.go(market spec parse + composite oracle),coherence_http.go(Prometheus/metrics+/healthz). Reusesprovider_kraken.goas the live oracle (no new oracle code). - Architecture: V2/V3/V4 are three liquidity modes of the ONE 0x9999 pool (one pool id, one fee, one maker peg), so coherence = one maker pegging one price.
- Why the maker pegs the book to ~Kraken: bid = mid·(1−s), ask = mid·(1+s); the
book MID ≈ Kraken (the "right price"). The half-spread
sis the maker's income, not tracking error. A market order realizes ≈ −s bps (it crosses the spread). - CLOB units: price grid = price·1e8 (
priceGridMul); size + deposits are uint64 units that map 1:1 to ERC-20 wei. Size ≫ 1 (default 100000) so grid quantization is sub-bps. The maker tops its vault once at setup (COHERENCE_DEPOSIT_BUFFER); cancels refund locks, so per-cycle deposits are unnecessary. - Config (12-factor):
COHERENCE_RPC(comma-separated for multi-broadcast),COHERENCE_MARKETS("SYMBOL=SOURCE:BASE:QUOTE[:SIZE]"),COHERENCE_SPREAD_BPS,COHERENCE_REQUOTE(~60–120 s optimum),COHERENCE_ARB_BAND_BPS,COHERENCE_PROBE(taker cadence; 0=off),COHERENCE_PROBE_SIZE,COHERENCE_{MAKER,TAKER}_IDXorCOHERENCE_{MAKER,TAKER}_KEY,COHERENCE_MNEMONIC/LUX_MNEMONIC,COHERENCE_METRICS_ADDR(:2112). The maker and taker MUST be distinct accounts (matcher self-trade prevention). - Reference SOURCE forms (the three orthogonal pricing realities, one per market):
a Kraken query pair (
ETHUSD) → live CEX mid (USD-quoted major);fixed:<price>→ a configured constant (a non-Kraken asset like LUX);ratio:<pair>/<denom>→Kraken(pair)/denom, the cross-rate that prices a base in a numeraire the CEX does not list. An L/LUX book uses ratio*:LETH/LUX=ratio:ETHUSD/12.5peg-mid = (ETH/USD)/(LUX/USD) ≈ Kraken ÷ 12.5. One shared Kraken feed serves both direct majors and every ratio leg — no second oracle, no fabricated price (unknown → error). - Bootstrap (one-shot, dev/test):
MAKER_MODE=coherence-initdeploys ONE shared quote + a base per market, opens the 0x9999 markets, funds maker+taker, and prints theCOHERENCE_MARKETSto bake into the ConfigMap. The quote token is env-selected (COHERENCE_INIT_QUOTE/_NAME, defaultLUSD): set it toWLUXand drive the market list with ratio sources to seed a LUX-quoted book (L* bases vs one WLUX quote). The init'sCOHERENCE_MAKER_KEYMUST equal the service's, so the funded inventory lands where the running maker's vault deposit pulls from (else Setup's swapDeposit reverts — the classic exit-1). Pure restart-safe service: env in, peg out, no token redeploy. - Testnet luxd read-surface caveat (chain 96368, as of 2026-07): the deployed
testnet luxd's 0x9999 precompile serves the WRITE surface (swapDeposit/swapPlace
land) but predates the READ selectors —
getBestBidAsk/getOpenOrdersreturn "unknown 0x9999 selector". So the maker quotes fine (logsmsg=requotewith bid/ask; 2-sided places confirmed on-chain by decoding swapPlace calldata) butorders=shows 0 andcoherence_peg_error_bps=0(BestBidAsk unavailable). Proving the book on this luxd = decode the successful swapPlace txs, not an eth_call read. Bumping testnet luxd to a precompile ≥ v0.15.0 restores the read path. - Deploy: no overlay ships here. The
k8s/coherenceandk8s/humoverlays were three Deployments of one program quoting a venue that cannot settle, and are deleted. Per-net an overlay must set the overlay sets the in-cluster RPC, theCOHERENCE_MARKETSaddresses, and the image tag; the maker/taker keys come from KMS (/bots), synced by a KMSSecret, on mainnet) out-of-band. Metrics:coherence_peg_error_bps/_p99_bps(book-mid vs Kraken — the "right price"),coherence_realized_error_bps(probe fill vs Kraken),coherence_burn_lux(gas·25 gwei), requote/arb/probe/fill counters.
Coherence go-live (gated — mainnet is REAL VALUE)
- Build + publish the image (CI, never local): tag
luxfi/maker→ CI buildsghcr.io/luxfi/maker:vX.Y.Z. - Supply REAL mainnet ERC-20 addresses (WETH/WBTC/WLUX + a USD stable) in the
mainnet overlay's
COHERENCE_MARKETS— NEVER throwaway deploys on real value. - Fund two ops accounts (maker + taker) from the
lux-deployertreasury viacast, store their keys in KMS, sync via aKMSSecretasCOHERENCE_{MAKER,TAKER}_KEY. COHERENCE_PROBEstays UNSET on mainnet (external flow trades; never wash-trade).- Deploy ONLY after explicit go-live sign-off; watch
coherence_peg_error_p99_bpsand inventory; start with one market (LUX/USD).
The AMM the maker can actually trade — V3 only (measured 2026-08-08)
Read from a PINNED node (luxd-4, chain 96369) — never the public LB, which
round-robins across heights.
The constant-product stack this repo once carried a V2Venue for is gone. Every
address from standard/deployments/mainnet-96369-amm.json (deployed 2026-07-28)
returns 0 bytes of code: WLUX 0x634a2398, LETH 0x2702d8f8, LBTC 0x9858086e,
LUSDC 0x6b23efd9, AMMV2Factory 0xcf3ccd95, AMMV2Router 0x80ee1653, and all three
pools. The RLP restore's restore point sits below that deploy, so the restore silently
un-deployed it — the same failure mode that un-deployed governance. ammv2.go and its
V2Venue are therefore deleted: a corrector whose factory has no code can only skip.
hanzo (36963) and zoo (200200) have no AMM and no tokens at all.
What IS live on 96369 is the pre-existing V3 stack: factory
0x80bBc7C4C7a59C899D1B37BC14539A22D5830a84, router02
0x939bC0Bca6F9B9c52E6e3AD8A3C590b5d9B9D10E, position manager
0x7a4C48B9dae0b7c396569b34042fcA604150Ee28. (V3_QUOTER 0x12e2B76F is 0 bytes —
quote by simulating the swap, not by calling the quoter.) Six pools hold liquidity:
| market | pool | spot (t1/t0) | pool holds |
|---|---|---|---|
| WLUX/LZOO 0.3% | 0x1c000d5dbe1246fb84ad431e933e5563f212a62b |
21.749 | 131.9M WLUX / 2.87B LZOO |
| LSOL/WLUX 0.3% | 0xa9330ee37bba7139c7697d82253b3d5fbb430004 |
275,920 | 1.038 LSOL / 2.17M WLUX |
| WLUX/LUSD 0.3% | 0x37011bb281676f85962fb35c674f7e9eb7584452 |
0.000613 | 12,091 WLUX / 6.26 LUSD |
| LPOL/WLUX 0.3% | 0xe0128e0fb36a1dedb69ba8a62084074dc4be50e0 |
2,697.9 | 2.647 LPOL / 7,003 WLUX |
| WLUX/LETH 0.3% | 0xfafbf1a0c5d6604f48378872c534545612a17aec |
2.207e-07 | 4.81M WLUX / 0.00635 LETH |
| LBTC/WLUX 0.3% | 0xa06e94ed014034e804c674f64c6f4c9313f2a6d7 |
1.163e08 | 4.18e-05 LBTC / 10.0M WLUX |
Six more pools exist at zero liquidity (WLUX/LUSD 0.05%, LBTC/LUSD, LETH/LUSD, LPOL/LUSD, LZOO/LUSD, LBTC/LETH) holding 1e-18 dust.
The tokens themselves barely exist. totalSupply() on 96369, priced off Kraken:
| asset | total supply | worth | in an AMM pool | pooled worth |
|---|---|---|---|---|
| LSOL | 18.11 | $1,361.61 | 1.038 | $78.03 |
| LETH | 1.808 | $3,469.05 | 0.00635 | $12.19 |
| LUSD | 355.89 | $355.89 | 6.260 | $6.26 |
| LBTC | 0.0057 | $370.22 | 0.0000418 | $2.72 |
| LPOL | 17.08 | $1.29 | 2.647 | $0.20 |
| TOTAL | $5,558 | $99 |
Every reference-bearing asset on the chain, summed, is worth $5,558, and $99 of it sits where a price can be corrected. So this is not a treasury-funding problem the 2T LUX could solve: there is no LBTC to add to a pool, because 0.0057 LBTC is the entire supply. It is a bridge/issuance problem. WLUX (150.1M) and LZOO (10.86B) are the only real supplies, and their pool is the only real market.
And nothing external trades. Over the last 60 blocks: 155 txs, 140 from the traffic bot, 15 from the treasury, 2 distinct senders on the whole chain, 0 empty blocks. The arbitrage that normally drags a pool back to reference has no participants.
Why no bot can converge these markets, and what would. Price the non-LUX leg off Kraken and each pool implies a LUX/USD: LUSD $0.000613, LBTC $0.000558, LETH $0.000423, LSOL $0.000272, LPOL $0.0000279. They span 21.9x. That dispersion is the entire arbitrage, and it is uncorrectable, because correcting it means buying or selling the scarce leg — and the scarce leg across all five reference-bearing pools is worth $99 in total (LSOL $78.03, LETH $12.19, LUSD $6.26, LBTC $2.72, LPOL $0.20). A maker pointed at these pools drains the short side in a handful of trades and leaves the price further off than it found it. Convergence here is a FUNDING problem: the pools need real two-sided inventory before a reconciler has anything to reconcile. WLUX/LZOO is the one deep pool and neither leg lists anywhere, so it has no external reference to converge to — its price is definitionally whatever the pool says.
The reconciler that would do the work already exists and needs no new code:
decideReconcile (reconcile.go) is a pure two-sided decision under a no-trade band,
a per-trade notional cap, inventory floors and a daily realized-loss bound, with
V3Venue (v3router.go) as its live corrector.
CI builds this repo (corrected 2026-08-08)
The prior note here said ghcr.io/luxfi/maker "cannot currently be built" because all
47 luxfi runners were offline. That is stale. The workflow moved to
.hanzo/workflows/docker.yml and builds on the forge: ghcr.io/luxfi/maker:sha-0b6c660
was published 2026-08-07T00:50:37Z from main. CI watches git.hanzo.ai, not GitHub —
git push hanzo <ref> creates the build; pushing only to origin creates nothing and
looks like broken CI.
go.sum: luxfi/zap v0.8.10 moved tag
Its recorded module-zip hash no longer matched the published tag, so go build failed
closed with SECURITY ERROR ... checksum mismatch on any machine with a cold cache —
the same class of breakage as the earlier luxfi/pq v1.0.3 fix. Corrected in place
(h1:QKNTAsen… → h1:4prCYVqL…); the /go.mod hash was already right and is
unchanged.