A minimal, event-driven keeper for the Liquidity Party AMM. It watches the
PartyConcierge singleton for queued mint requests and drives them to completion by calling
executeMints(pool, maxCount), earning the protocol's keeper fees. We run it as a free service for
our users and do not gate submission on profitability — we are willing to subsidize gas.
- Idle: subscribed only to the
MintQueued(andMintRequestCanceled) events on the Concierge. No polling, near-zero RPC traffic. - Wake: a
MintQueuedevent marks its pool active and kicks the work loop. On startup, on a WS reconnect, and on a user cancel (which leaves a profitable-to-sweep tombstone), the keeper rescans all pools (PartyPlanner.poolCount+getAllPools, thenqueueLengthper pool) to rebuild the active set. - Work loop (single in-flight tx): for each active pool it simulates
executeMints; if the simulation does not revert and would execute work, it signs and broadcasts, waits for the receipt, then measures real progress (queue length shrank or the receipt carried fill/cancel logs — aexecuted > 0return alone is not progress, since a transient gate just rotates the queue head). It keeps flushing a pool until its queue is empty, then drops it. When all pools are drained it returns to idle. - Anti-spin: a pool that makes no real progress is retried after
RETRY_DELAY_MS, then parked afterNO_PROGRESS_RETRIESconsecutive no-progress sends until its next event/rescan.
The queue is per-pool — there is no global keep(); executeMints and queueLength both take a
pool address. The event the keeper waits on is MintQueued (carries an indexed pool).
All via environment variables (see .env.example):
| Var | Required | Default | Purpose |
|---|---|---|---|
RPC_WS_URL |
yes | — | WebSocket RPC endpoint (ws:// / wss://) |
PRIVATE_KEY |
yes | — | keeper wallet key (32-byte hex) |
CHAIN_ID |
no | 1 |
selects entry in deployment/liqp-deployments.json |
CONCIERGE_ADDRESS |
no | from deployments | override |
PLANNER_ADDRESS |
no | from deployments | override |
MAX_COUNT |
no | 10 |
queue slots per executeMints |
NO_PROGRESS_RETRIES |
no | 3 |
no-progress sends before parking a pool |
RETRY_DELAY_MS |
no | 13000 |
backoff between no-progress retries |
Contract addresses default from the bundled deployment/liqp-deployments.json (a copy of
../lmsr-amm/deployment/liqp-deployments.json). Refresh it with npm run sync-deployments.
npm install
cp .env.example .env # then edit RPC_WS_URL and PRIVATE_KEY
npm run dev # tsx, no build step
# or
npm run build && npm startdocker build -t liqp-keeper .
docker run --rm \
-e RPC_WS_URL=wss://your-node/ws \
-e PRIVATE_KEY=0x... \
-e CHAIN_ID=1 \
liqp-keeperThe image is a multi-stage node:22-alpine build with production-only dependencies, running as the
non-root node user — small enough for the smallest VM.
On a single-tenant box the keeper runs directly under systemd — no container needed. bin/deploy
builds locally, ships the artifacts over SSH, and installs/restarts the service. It is idempotent:
the first run bootstraps a fresh Debian box (Node 22, the keeper service user, the unit, an env
file), later runs just push a new build.
bin/deploy user@your-vmThe login user must be able to sudo. The unit is deployment/liqp-keeper.service; code lands in
/opt/liqp-keeper owned by an unprivileged keeper user. Secrets live only on the VM in
/etc/liqp-keeper.env (root-owned, chmod 600) — the script never copies your local .env. On the
first deploy that file is seeded from .env.example and the service is left stopped; edit it and
sudo systemctl restart liqp-keeper to go live.
sudo systemctl status liqp-keeper
sudo journalctl -u liqp-keeper -f- Start anvil and deploy via the scripts in
../lmsr-amm/bin/; note the deployedPartyConciergeandPartyPlanneraddresses (or chain31337in../lmsr-amm/liqp-deployments.json). - Create a pool and enqueue a mint (
mint(..., useQueue=true)withmsg.value >= NATIVE_KEEPER_FEE) so aMintQueuedevent fires. - Run the keeper:
RPC_WS_URL=ws://127.0.0.1:8545 CHAIN_ID=31337 \ CONCIERGE_ADDRESS=0x... PLANNER_ADDRESS=0x... \ PRIVATE_KEY=0x<anvil-dev-key> npm run dev
- Confirm the keeper finds the queued pool, submits
executeMints, drains the queue to zero, and returns to idle with no further RPC traffic.