Documentation
How FluxPool works
Status: in testing
Overview
One hook. Any agent.
FluxPool lets a token's creator and its agent change how the pool behaves without ever changing the promise to holders. Agents tune the pool; what holders were promised stays locked. Not the agent, not the creator, not us.
A pool is made of a few parts, all part of an immutable layer with no proxy and no owner:
- Shell — the only contract Uniswap calls. It checks every swap against four fixed rules.
- Core — a cheap clone per pool holding its ordered block lists.
- Blocks — shared contracts that add a fee or reject a buy, within bounds.
- Registries — the agent, playbook and change registries that govern who may do what.
- Launcher — the launch adapter that seeds liquidity and makes the first buy atomically.
- Revenue router — splits creator fees and runs market-cap-scaled buybacks.
Behind the contracts, a backend indexes every pool, replays attacks, runs the block-writing pipeline and fires alerts. The app, agents and alerts all read the same events, so a change appears everywhere at once.
The shell
Four protections that never switch off
The shell wraps the core and checks four rules on every swap. If the core misbehaves, or fails outright, the shell still holds. These rules are the same in all three freedom modes.
How it keeps holding: there is no proxy, no owner and no admin function that can change a protection. The core is called as an external contract with a gas limit inside a try/catch, never with delegatecall. If the core reverts, runs out of gas or returns out-of-range values, the shell uses the base fee and lets the trade through. Permissions are fixed at deploy.
One deliberate detail: the shell does enable return deltas, but only so it can collect Programmable's 0.30% platform fee. Blocks are never given deltas, so no block can move tokens.
The core and blocks
A narrow sandbox for every block
Each pool gets its own core: one implementation cloned with OpenZeppelin Clones. The core stores an ordered list of block addresses for each trade moment — today the before-swap and after-swap lanes — and calls each block in order.
- Add a fee, which the shell clamps to the cap.
- Reject a buy, which is visible and reversible by removing the block.
- Keep its own state and read public pool data.
- Move tokens or touch liquidity.
- Block a sell or exceed the active fee cap.
- Change another block, the core or the shell.
Combining rules, deterministic so the composer can preview them: run blocks in lane order; the first block that rejects stops the lane and rejects the buy (sells still pass); fees add up and the shell clamps the total; a failing block is skipped, not fatal.
Who is the trader? In Uniswap v4 the sender is the router, not the person. The app passes the trader's address in hook data, but it is never relied on for protection. That is why anti-snipe limits are per swap and per block, not per wallet.
The starter blocks
Four blocks every pool can start with
Blocks are deployed once and shared by every pool; their per-pool state is keyed by the calling core. These four settle how money flows and how trading opens.
Freedom modes
Pick how much freedom the agent gets
The mode is chosen at launch and shown on the trust card. It decides how fast new settings and new code go live. The four protections hold in all three. Raising an agent's power and moving to a freer mode are always timelocked, so nobody can quietly turn a pool wild.
| Change | Guarded | Fast | Wild |
|---|---|---|---|
| Playbook fire or a safer setting | Instant | Instant | Instant |
| Riskier setting, within bounds | Timelocked | Instant | Instant |
| New block or lane change | Checks + timelock | Checks only | Instant |
| Raising an agent's level | Timelocked | Timelocked | Timelocked |
| Switching to a freer mode | Timelocked | Timelocked | — |
| Switching to a safer mode | — | Instant | Instant |
| Anything outside a setting's bounds | Rejected | Rejected | Rejected |
A failed check blocks a change for good. Switching to a safer mode also slows down changes already in the queue, because execute applies the stricter of the rules a change was queued under and the pool's current mode.
Timelock
A visible delay before risky changes
- The delay is fixed at launch, above a protocol minimum. The working default is 48 hours.
- Proposers are the creator, or the agent at its permission level.
- The creator can cancel anything queued.
- Anyone can execute a change once the delay has passed.
- Instant changes still emit a queued event and then an applied event, so every change has an id in the journal with a visible outcome.
Checks
New code must prove itself
A queued block change lists the checks required to activate it. Designated attesters record results, and execute reverts unless every required check has passed. A failed check is final; a pass can still be downgraded to a failure.
- The protocol minimum is compiled, interface, simulated and sourceVerified, plus programmableVerified when a pool opts in (one-way), plus any the proposer adds.
- A lane change needs checks only if it brings in a block that is not already in the pool's lanes. Reorders and removals follow the same timing without checks.
- In phase A the pipeline attester is a single trusted key. Every check's inputs and outputs are published with a manifest so anyone can reproduce them; several independent attesters come later.
Agents
Plug in any agent. Keep the off switch.
An agent is just an address, so a scoped smart account with a session key works. A pool has one active agent in the first version. The creator registers it and sets its level; the contracts enforce the rest.
| Level | What it can call |
|---|---|
| Fire | fire(playbookId) |
| Propose settings | Fire, plus proposeSetting(block, setting, value) |
| Propose code | Both above, plus proposeBlock(...) and proposeLaneChange(...) |
- No level can move funds, change caps, approve playbooks, change the freedom mode or touch the shell.
- Adding an agent, replacing one, or raising its level is timelocked in every mode. Lowering its level or removing it is instant.
- removeAgent is the off switch and works immediately. Every instant change bumps an epoch that voids queued agent changes, so a pending raise cannot re-add a removed agent.
- Rate limits default to one action a minute and 48 a day. Tightening them is instant; loosening them is timelocked.
- Agents cannot propose the fee split or change the mode, in any mode. Without that, a settings agent in a Fast pool could raise its own share instantly.
Playbooks
The response you approve before a bad day
A playbook is a response the community approves in advance. Firing one is instant in every mode, because it can only tighten settings.
- A playbook holds up to 8 actions, a cooldown of 5 minutes to 7 days, a daily fire limit of up to 24, and an optional trigger.
- Approval rejects any action outside its bounds or in its riskier direction, judged by the block's own setting bounds.
- Firing is instant, by the creator or by the agent at Fire level within its rate limits. Each action is re-checked against the current value, and one that no longer tightens is skipped. An active playbook cannot fire again, so its saved previous values cannot be overwritten.
- After the cooldown, anyone can expire it to restore the previous values — but only if the setting still holds the playbook's value. Overlapping playbooks can end tighter than before, never looser. An active playbook cannot be removed.
- An optional on-chain trigger checks a sustained price drop in ticks between the start-of-block prices the shell records. The drop must be present a set number of blocks ago and now, so an in-block dump, or one that holds for a single block, cannot fire it.
The block pipeline
From a problem description to live code
This is the part nobody else has. An agent describes a problem, and the pipeline decides whether a new block can go live, following the pool's mode.
- 1
Generate
A stronger model on OpenRouter writes a block implementing the block interface. The problem text is fenced as data under the rules, never as instructions.
- 2
Compile and interface check
Compile with a pinned solc version against an import allow-list. Reject anything that does not implement the exact selectors, uses delegatecall or selfdestruct, makes arbitrary external calls, or fails a runtime bytecode scan.
- 3
Simulate
Deploy on an Anvil fork with the pool's real state and run the crisis library plus the attack scripts. Fail if the block raises gas past its limit, rejects too many normal buys, or makes any scenario worse for holders.
- 4
Verify
Deploy the block, verify its source on Sourcify and submit it for Programmable's verification where required.
- 5
Record checks
After the agent queues the block, the attester records each passed check, with a link to the published results, and only for a change that adds the exact block it checked.
- 6
Queue and activate
The agent proposes the block; the change registry applies the pool's mode. Guarded adds checks plus the timelock, Fast checks only, Wild neither.
Failures feed the next attempt, and attempts and runs per agent are capped. Tokens are logged, and a model outage pauses the pipeline instead of shipping a bad block. An agent-written block may declare which lanes it belongs to.
The reference agent
One agent, built only on the public SDK
The reference agent shows what any agent can do, and it is built entirely on the public SDK, so it proves the interface works for outside agents too.
| Part | What it does | Model |
|---|---|---|
| Watcher | Subscribes to swaps and journal events; keeps rolling per-pool signals. | No |
| Rule engine | Checks signals against each approved playbook's trigger rule. | No |
| Executor | Fires the playbook when a rule matches, within rate limits and with retries. | No |
| Explainer | Writes plain-language journal and alert text after fires and big moves. | Cheap, on events |
| Proposer | Suggests setting changes after seeing repeated patterns, simulated first. | Cheap |
| Coder | Hands problems to the block-writing pipeline. | Stronger, rarely |
The watcher, rule engine and executor run as a long-lived process with a health check, aiming to fire within a few seconds of a trigger. If a model call fails, the playbook still fires and the explanation is filled in later. Stopping the agent never affects the shell's protections. A monthly spend cap, cached explanations and per-pool token logs keep the cost visible.
The deployed app runs the same explainer itself rather than waiting for the agent's process: a route in the app reads the journal events that have no text yet, asks a cheap OpenRouter model for one or two sentences — with the playbook's real trigger and cooldown read from the registry, so a fire can say why — and stores the result in its own database. The journal prefers that text over the deterministic summary and marks which entries carry it. Until the model key is set the pass is a no-op, and a failed call leaves the fire and its summary in place for the next pass.
Tests and attacks
Test the agent as a possible attacker
Four layers run in CI: unit tests at every bound, fuzz tests over random settings and orderings, invariant tests over long random sequences in every mode, and attack scenarios replayed on a fork in a shared format. The backend tests the off-chain side.
Asserted after every simulated call, in every mode:
- A sell never reverts because of the core, a block or a playbook.
- The fee never exceeds the active cap.
- Locked liquidity is never withdrawn early.
- Supply never changes.
- An agent never acts beyond its permission level.
- In Guarded and Fast, no block activates without every required check.
Every attack below has a passing scenario, on the contracts and, for the off-chain rows, in the backend:
| Attack | What must hold |
|---|---|
| Snipe at launch across many wallets | Per-block cap and opening fee hold |
| Panic sell | Every sell succeeds |
| Stolen agent key fires playbooks repeatedly | Rate limits cap it; the off switch stops it |
| Stolen agent key proposes a loosening or a new block | Mode rules hold; events fire at once |
| Fake dump to trigger a playbook | Multi-block signals and on-chain trigger checks resist it |
| Hostile block: rejects all buys, burns gas, max fees | Shell clamps fees, sells pass, block is removable |
| Wild pool with a hostile block | Shell protections still hold |
| Quiet switch to Wild | Timelock holds |
| Block change with a missing or failed check | Never activates |
| Fee griefing with small trades | Multi-block averaging makes it too costly |
Invariant runs use 10,000 sequences per mode in CI, about 400,000 calls each, and all six assertions pass with no reverts.
Off-chain safety
Untrusted data, scoped keys, watched attesters
- Untrusted data. Everything the MCP server reads from the chain or the indexer comes back labelled untrusted, with control and invisible characters stripped and strings capped. Inputs are validated before any call. Token names and social posts go to a model as data, never as instructions.
- Scoped sessions. A session key is limited to one pool and one level: that level's calls plus execute and expire on that pool, with no value, no swaps and no other contracts.
- Sponsored gas, bounded. The sponsor only sponsors in-scope calls, with a per-agent daily gas budget and an hourly rate limit. Every refusal is logged.
- One-time codes. Connecting an agent uses a single-use, expiring MCP code.
- Watched attesters. Every check recorded by the attester key must match an attestation the pipeline published first, so a compromised key is flagged.
- Fail-clean simulation. Simulator workers are health-checked before and after each run; dead forks are retired and requests fail fast, so a crash never records a check.
The FluxPool token
Our own pool runs on the same rules
FluxPool's token launches through the same shell as everyone else's, with its economics written into the contracts rather than a blog post. This applies to our token's own pool only; other pools keep their own settings within the shell's limits. Pools pair against native ETH on Ethereum.
5% → 3.2%
Our fee across the launch window, then flat. Programmable's 0.30% sits on top.
80 / 20
Fee split: 80% to the creator share (and its router) and 20% to the protocol treasury.
5–20%
Of the creator share goes to buybacks, scaling up with market cap.
30%
Goes to buybacks during a sustained drop. Everything bought back is burned.
- The pool config starts at a 3.2% base fee, a 5% launch-window cap, a buys-only opening fee of 1.8% and a 4% normal cap.
- The split sends the creator share to the revenue router and the protocol share to the treasury.
- The router sends buyback shares scaled linearly with on-chain market cap between a low and a high bound, and 30% during a significant drop. The rest goes to the treasury.
- Token buybacks are burned at once. ETH buybacks wait in the router until the treasury or its operator buys in the pool and burns everything bought; the treasury can withdraw at any time.
A few parameters are still open and must be set before deploy: the treasury Safe, the buyback operator, the market-cap bounds, the drop size and timing, and the launch window length.
Launching
Pools come through Programmable
FluxPool does not ship its own factory. Pools launch through Programmable, and FluxPool takes Path B: a Custom Launch on Ethereum with our shell as the hook, on Uniswap's official PoolManager, with a native ETH / token pool, following Programmable's Ethereum native-ETH launch. Programmable's 0.30% platform fee applies on top of ours and is enforced inside the hook, so no router can skip it.
- Every component, including agent-written blocks, needs Sourcify v2 exact-match verification bound by address, runtime hash and role.
- Programmable rebuilds and simulates the launch before approval.
- The shell's permission-bearing address can only be mined through a factory, so the plan deploys a minimal CREATE2 factory and mines the shell salt against a precomputed initcode hash.
- The plan is nonce-pinned: 14 ordered steps from deploying the registries and core to seeding liquidity and making the required first buy.
Launch-day rules, learned the hard way:
- The controller wallet is frozen for the whole launch. Every step is pinned to a nonce, so one manual transaction breaks the next step.
- Never replan after deployments start: a replan re-binds every step and changes addresses.
- The gas budget must cover every step plus the 6,000,000-gas stamp, with a 20% margin.
- Run the signer in the background and make it resumable. Proof posts use an idempotency key, and every step waits for finality.
- No mainnet transaction without an explicit approval, and keys never go into chat. Any key pasted anywhere is treated as compromised and rotated.
Migration
Existing tokens move to a new pool
A pool's hook is fixed at creation, so an existing token cannot be retrofitted. Instead it opens a new FluxPool pool and moves across.
- Open the new pool through Programmable.
- Pull the liquidity out of the old pool, which the owner must control.
- Add it to the new pool as shell-held liquidity.
- Announce the move to holders.
Pulling the old liquidity matters: if liquidity stays split across pools, the protections leak. It works best for tokens whose team controls the liquidity and whose community is active.
Deployment
Foundry, CREATE2, and the chains
- Mine the shell address once per chain and deploy with CREATE2.
- Order: blocks, core implementation, agent, playbook and change registries, launcher, shell, then any adapter Programmable needs.
- Deploy with a Foundry script and a key loaded from the environment; a missing key reverts rather than falling back.
- Ethereum mainnet, paired with native ETH. Sepolia and a mainnet fork come first; nothing touches mainnet without explicit approval.
- Pools pair with native ETH, which skips wrapping and costs less gas per swap than WETH. WETH pools remain supported.
- The simulator is hosted apart from the app. A crisis replay deploys the contracts on Anvil and runs forge, so it cannot live in a function: it runs in a Vercel Sandbox that the app reaches through /api/simulator. Until that sandbox is set up the panel says it cannot reach the simulator, never a guessed result.
Gas matters more on Ethereum, because every trader pays for the shell and its blocks on every swap. Measured per swap with native ETH: about 108k gas for a plain Uniswap v4 pool, about 181k to 208k for a FluxPool pool with no blocks, and about 252k to 284k with the two starter blocks. The first swap in each block costs a little more because the shell records that block's starting price. Keep lanes short on Ethereum: every block adds to every trade.
Still open before mainnet: compliance with Programmable's launch-protection policy and the Ethereum fee profile (the platform recipient is settled — it is the one Programmable publishes); the smart-account, bundler and paymaster stack for session keys; the agent, attester and pipeline keys plus model choices; where evidence is served publicly; and Programmable's verification API.
The simulator's host is switched on. /api/simulator/setup created a persistent sandbox, cloned the repository into it and installed Foundry, the contracts' submodules, the workspace and the contracts build. Requests resume that sandbox and reach the simulator by runningcurl inside it, because a function's own fetch to the sandbox's public port does not connect — and a port that answers the whole internet is a port anyone can spend this project's compute through, so the port is not exposed at all. Setup converges rather than runs once: re-running POST /api/simulator/setup moves the sandbox's checkout to the tip of the revision it is pinned to, rebuilds what changed and stops the server, so the next request starts the new code — which is how a change under services/ reaches the host. Both setup and health report the commit the checkout holds, so what the host is running is never a guess. Two things are worth knowing before relying on it. Starting the server is not the same as it being up: each worker deploys the whole stack on Anvil first — four minutes on the very first start with two workers, about fifteen seconds once the machine is warm — so the first request after a session has expired waits while that happens. And a draft names its blocks rather than addressing them, because the address a block has in the live deployment holds no code on the chain the simulator replays on: a setting's block travels by name, the way lanes already did, and the simulator resolves it against the chain it has. An address is still accepted, and one that chain does not have is reported as exactly that rather than as a failed launch. The next step for the host is to replay against a fork of the live chain, where the state a replay sees is the pool's real state.
Interfaces
What builders code against
Every block, including agent-written ones, implements this narrow interface:
interface IFluxBlock {
function onBeforeSwap(address trader, PoolKey calldata key, SwapParams calldata params)
external returns (uint24 feeAdd, bool allow);
function onAfterSwap(address trader, PoolKey calldata key, SwapParams calldata params, BalanceDelta delta)
external;
/// Bounds per setting; gates and playbooks use higherIsSafer to classify changes.
function settingBounds(bytes32 setting) external view returns (uint256 min, uint256 max, bool higherIsSafer);
}The agent interface is the table under agents. The check names are those under checks. The journal events share stable names and fields so the indexer, alerts, SDK and track records can all read them:
ChangeQueued(pool, changeId, proposer, kind, target, setting, oldValue, newValue, executableAt)
CheckRecorded(pool, changeId, check, attester, passed)
ChangeApplied(pool, changeId, instant)
ChangeCancelled(pool, changeId)
ModeChanged(pool, oldMode, newMode)
AgentSet(pool, agent, level)
PlaybookAdded(pool, playbookId)
PlaybookFired(pool, playbookId, firedBy)
PlaybookExpired(pool, playbookId)
BlockAdded(pool, lane, blockAddr, author)
FeesCollected(pool, currency, toCreator, toProtocol, toAgent)
Launched(pool, ...) / InitialBuy(...)Beyond those, the indexer also records rate-limit changes, playbook removals, attester and programmable-verification settings, pool registration, liquidity seeding and withdrawal, platform fee accrual and claims, core call failures, and the revenue router's Distributed and BoughtBack. The router's two events name no pool — one router is deployed per pool and neither event carries the id — so the indexer attributes them from the router's own immutable poolId(). Both the reference indexer and the copy that runs inside the app read them.
Attack scenarios are written as replayable JSON — a starting state, a swap sequence by block, and an expected outcome — and are replayed unchanged by both the Foundry runner and the simulator.
| Backend API | What it serves |
|---|---|
| GET /pools | Every pool as a summary: mode, agent and level, swaps, pending changes. |
| GET /pools/:id | The trust card (lock, holders, dev wallet, fees), live lanes and playbooks. |
| GET /journal | Every change, fire and new block, newest first, with tx and evidence links. |
| GET /agents/:address | A track record: pools served, fires, proposals and blocks written with check results. |
| GET /crises | The crisis library: starter and saved attack scenarios. |
| POST /simulate | A draft config or proposed block, replayed against the crisis library. Returns the comparison. |
| POST /pipeline/blocks | A problem description and pool id; runs generate, compile, simulate and verify. |
| POST /pipeline/blocks/:id/attest | The attester records the passed checks for the deployed block. |
The app
Four screens, one live journal
The app reads the contracts through the backend. Agents act through the SDK and MCP; holders watch on the token page. Rules for the app: always show what a change will do and whether it is instant or timelocked before anyone signs; link only the official contract; production never uses mock data.
| Screen | What it does |
|---|---|
| Token page | A price chart and the recent trades, read from the indexer's swap mirror and polled every 30 seconds; trust card with the mode, active blocks, lock status, dev-wallet activity, holder spread and pending changes with countdowns; agent panel with level, track record and playbooks; a journal with links to published check results; buying and selling through the pool's own router, quoted by simulating the swap before anything is signed, with the quote's price impact measured against the pool's own spot price, which is read from the pool's state rather than the last trade, and a Max on the ETH side that fills in everything except this swap's gas at the chain's current price; and alerts, which are described below and are not hosted yet. |
| Composer | Lanes per trade moment; drag blocks from a palette; sliders that cannot leave the bounds each block's contract enforces; conflict warnings; a tighten-only playbook builder; a mode picker with plain explanations; and a simulator panel that replays the shared attack scenarios. Agent-written blocks join the palette, marked with their author and checks. |
| Launch and migration | A new token's details, liquidity and lock type, composer, mode, a preview of the trust card as buyers will see it, and the launch through Programmable; or the migration flow for an existing token. The form and the preview are live and keep a draft in the browser. Submitting waits on Programmable's Ethereum fee recipient and fee profile: until they arrive the last step shows the exact submission it would post and names what is missing. |
| Creator dashboard | Your pools, read from the chain: fees waiting in the pool and what you can claim, with collect and claim as wallet transactions; the fee split and what has been collected so far; the agent, its level and its fee share, with set-agent (instant when removing or lowering, timelocked when adding or raising) and the instant off switch; queued proposals with their countdown and check state, each cancellable and applicable once ready; and the pool's freedom mode. Every write names what it does and whether it is instant or timelocked before you sign, and only the creator's wallet can sign one. The agent host and its one-time MCP handshake are not built yet, so setting an agent registers an address you already control. |
| Alerts | The token page's alerts panel, off and saying why instead of offering a button that cannot work. The service behind it is a worker in services/alerts that reads the same journal events the indexer does and pushes them to a Telegram bot and an X account. It is not deployed: it needs a bot token, X API credentials, durable storage for subscriptions, and a host that can run a process that never sleeps. Until then the panel promises nothing. |
Today the token page, the pools list, the composer, the launch form and the creator dashboard are live against this deployment. The indexer and the explainer both run inside this deployment, each as a route the platform calls on a schedule — this project is on Vercel Pro, so every minute; on Hobby the platform refuses anything more frequent than a daily pass. The pages read whatever the last pass wrote, so how fresh the numbers are follows that schedule, not the page's own refresh. There is no separate host to keep alive.
Trading is the pool's own router: an exact-input buy or sell sent through the shell, with the trader's slippage floor and deadline. The quote shown before signing is not a formula — it is the same swap simulated against the pool's current state, so the shell's dynamic fee and the price impact are already inside the number, and the floor is that number less the slippage the trader chose. A sell needs one approval first, because the router pulls the token from the wallet. The router holds no funds between calls, has no owner, no pause and no state to administer, and reports the trader to the pool's blocks rather than itself, so a per-block buy cap sees the holder. On Sepolia it is at 0x1e88ee14A8996007c578C183B7525A0CA7312e1a, set as NEXT_PUBLIC_FLUX_SWAP_ROUTER. With that unset, or with a pool whose key does not hash back to its id, the card says trading is off rather than pointing at a contract nobody deployed.
The dashboard reads money and the agent from the chain, not the indexer. The indexer knows what happened; only the shell knows what is claimable right now, and only the registries know whether a queued change can run. Collecting takes the whole pool key, which neither the shell nor the PoolManager will hand back, so the app rebuilds it and checks that it hashes back to the pool id before offering to collect — a key that does not match is refused rather than used against the wrong pool. Setting an agent is a plain wallet transaction from the creator, so it needs no bundler; the account-abstraction stack is for the agent's own session key, which is the part still to build.
The composer edits a draft, never a pool. It reads every block's bounds and defaults from the chain, so a value the contracts would reject cannot be chosen, and it reads the core's per-lane limit rather than assuming one. A block runs once per lane. AdaptiveFee is the one starter block that needs both lanes, because it measures selling after a swap and charges the fee before the next one. Playbooks are tighten-only by construction: an action's slider only offers values on the safer side of what the pool runs now, inside the registry's own limits (8 actions, a cooldown of 5 minutes to 7 days, at most 24 fires a day, and a window plus hold of at most 63 blocks). The crisis library the simulator replays is inlined from the contracts repository at build time, so this deployment serves the same scenarios the contracts are tested against. The draft lives in the browser until it is saved to a pool, and the simulator is reached over its own URL.
The launch form reads that same draft. It collects the token's details and the liquidity and lock choice, re-runs the composer's own layout and playbook checks so the two screens cannot disagree, and renders the trust card as buyers will see it — mode, lock, platform fee, the fee split read from the CreatorFee block's values, and the blocks each lane will run. Blocks are named, never addressed, in the launch review: the plan gives each one its own address when it deploys, so this deployment's addresses are not the ones that will be live. The ticker is collected and never rendered outside the form, and nothing is signed until the last step.
Keys and secrets
Separate keys for separate jobs
| Key | Can do | Can't do |
|---|---|---|
| Deployer | Deploy contracts | Anything on pools after deploy |
| Reference agent | Act on pools that registered it, at their level | Move funds, approve playbooks, change caps |
| Pipeline attester | Record check results for proposed blocks | Propose or apply changes |
| Test wallets | Simulations and attack scripts | Anything on mainnet |
- A pre-commit guard blocks any staged 0x + 64-hex string, which catches private keys and also transaction hashes and pool ids.
- Secrets load with an explicit error and never a fallback, so a missing key stops a build instead of silently using a default.
- Environment files are gitignored, and a committed example lists empty values only.
- In production, agent and attester keys live in a secrets manager, the model key has a monthly spend cap, and any key that might have leaked is rotated.
- No ticker and no contract address anywhere until launch day.