feat(arena): permissionless keeper for matchmaking + settlement#53
Merged
Conversation
The arena's runMatchmaking(tier) and settleMatch(id) are permissionless but the EVM can't self-trigger, so an off-chain heartbeat must tick them. Add scripts/keeper.mjs: an ethers-v5 process that, every tick, runs matchmaking for each tier whose cooldown has elapsed and settles any created-but-unsettled match. Talks straight to the chain (no MCP-server dependency) so it runs as a standalone systemd/pm2/launchd service. - Gates matchmaking on pool>=2 AND cooldown elapsed (mirrors the contract require) so it never burns gas on empty pools or eats rate-limit reverts. - Settlement walks an in-memory watermark, advancing past the leading run of settled matches so steady-state reads stay O(new), not O(all). - RPC + router are read from frontend/config/<NETWORK>.json (the same files the frontend uses), so addresses never drift and you don't pass them by hand. localhost.json carries no router, so local falls through to deployed-addresses.json. Env (RPC_URL/ROUTER_ADDRESS/ARENA_ADDRESS) still overrides the file when needed. - KEEPER_KEY can be any funded address — gas only; no role required. justfile recipes: keeper-start (NETWORK=localhost) and keeper-gravity (NETWORK=gravity) — the testnet recipe takes only the keeper key now.
63657d1 to
fd0b024
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds the arena keeper — the off-chain heartbeat that drives tier matchmaking and match settlement.
The contract's
runMatchmaking(tier)andsettleMatch(id)are permissionless, but the EVM can't trigger itself. Something off-chain has to call them on a schedule. That's this script.mcp-server/scripts/keeper.mjs— a standalone ethers-v5 process (no MCP-server dependency)justfile—keeper-start(local anvil) andkeeper-gravity(testnet) recipesWhat it does each tick
runMatchmaking(tier)only when the pool has ≥2 ghosts AND the tier's cooldown has elapsed. Both checks mirror the contract'srequire(last == 0 || now >= last + effectiveTierPeriod), so it never burns gas on an empty pool and never eats arate limitedrevert.settleCursor) that advances past the leading run of already-settled matches, so steady-state reads stayO(new matches)instead ofO(all matches)per tick.RPC + router come from
frontend/config/<NETWORK>.json— the same config files the frontend already reads — so the keeper never drifts from the deployed addresses and you don't pass them by hand.localhost.jsoncarries norouter_address, so local naturally falls through todeployed-addresses.json. Env vars (RPC_URL/ROUTER_ADDRESS/ARENA_ADDRESS) still override the file when needed. It recovers from transient RPC/tx errors per-tick without dying.Can the keeper be any address?
Yes — any funded address works.
runMatchmakingandsettleMatchhave no access control; the contract only checks the cooldown and match state. The keeper key needs gas and nothing else — no operator role, no owner, no agent registration.Implication: the keeper is a public heartbeat, not a privileged party. If it goes down, anyone (even the frontend) can call the same functions to unblock matchmaking. Use a dedicated key holding only a little gas — never the owner/deployer key.
How to run
No addresses are passed on the command line — the network name selects the config file.
Local (anvil): RPC from
config/localhost.json, router fromdeployed-addresses.json.Testnet (Gravity): RPC + router both read from
frontend/config/gravity.json.Direct / cron (one tick then exit):
Production (always-on): run under
pm2,systemd, orlaunchdwith auto-restart, e.g.Config (env)
NETWORKlocalhostfrontend/config/<NETWORK>.json(rpc + router)KEEPER_KEYRPC_URLrpc_urlROUTER_ADDRESSrouter_addressdeployed-addresses.jsonARENA_ADDRESSTICK_SECONDS15TIERS0,1,2ONCE1→ run a single tick then exit (cron)Note
The keeper only drives matchmaking + settlement. For a full self-running loop you still need a producer (the
agent-runner, or any client callingarena_submit) to put ghosts into the tier pools — otherwise the keeper just sees empty pools and does nothing.Test plan
ArenaEngine.sol(getMatchtuple order,nextMatchIdstarts at 1,lastTierMatchmakingAtmapping getter, cooldownrequire).ONCE=1smoke test, local (NETWORK=localhost, chain 31337): RPC fromlocalhost.json, arena resolved fromdeployed-addresses.json; readstierPopulation×3 +nextMatchIdwithout error; empty pools → no-op.ONCE=1smoke test, testnet (NETWORK=gravity, chain 7771625): RPC + router read fromgravity.json, arena resolved via Router with no addresses passed.matchmade+settledlog lines.