Buyback and burn

An engine for Robinhood on Pons, on Robinhood Chain. Every three minutes it claims the creator rewards Pons holds for the dev wallet in its fee escrow, spends every basis point of them buying the token, and has the buy deliver the tokens straight to the burn address.

Two things live in the repository. engine/ is the engine: TypeScript, one runtime dependency (ethers, for signing), and a test suite that runs it against an in-memory chain and against a real EVM node. site/ is this website: a static directory with no build step, rendering the ledger the engine writes.

Two invariants are enforced in code rather than promised in copy. assertPolicyBalanced() throws unless the routing policy allocates exactly 10,000 basis points, and it runs on import. After every buy the engine checks the wallet moved by exactly the amount sent plus the gas paid, and refuses to record the cycle otherwise.

The cycle

Pons fee escrow ── claim() ──→ dev wallet
  balanceOf(dev wallet)           ├─ keep the gas reserve
  accrues on every trade          └─ spend the rest ──→ buy, recipient = 0x000…dEaD
                                                          before graduation:  curve.buy(amountIn, minOut, 0x…dEaD)
                                                          after graduation:   UniversalRouter.execute(V4_SWAP → take(0x…dEaD))
                                                        ──→ receipt: Transfer(ROBIN → 0x…dEaD)
                                                        ──→ balance check, to the wei
                                                        ──→ ledger entry

claim reads the escrow's balanceOf for the dev wallet and, if it is above the minimum, sends claim() and checks the wallet received exactly that amount less gas. plan reads the wallet, subtracts the gas reserve, runs the remainder through the routing policy (one bucket, but the same code path a multi-bucket policy would use), reads the launch record to pick the venue, quotes it, and derives the minimum output. Nothing is signed. execute takes a plan and sends exactly that. A dry run and a live run compute the same plan; the only difference is whether execute is called.

Quiet cycles

If nothing is claimable, or the spendable amount is under the minimum buyback (0.002 ETH by default), the cycle does nothing. Spending most of a small reward on gas would be worse than waiting, so the rewards stay where they are and the next cycle sees them.

The claim

Pons does not pay creator rewards to the wallet; it credits them to the creator's balance in its fee escrow contract, in ETH, on every trade. balanceOf(creator) reads that balance and claim() pays it to the caller. The escrow reverts on an empty claim, so the engine reads first and only sends when there is something above the minimum (0.0005 ETH) worth the gas.

The escrow credits the launch's creator fee recipient, which is recorded in the factory. faucet doctor checks that recipient is the configured dev wallet; if it were not, the engine could never claim.

The buy

A Pons v2 launch trades on its bonding curve until it graduates, then on a Uniswap v4 pool guarded by the Pons hook. The engine reads the launch record each cycle and buys wherever the token trades now.

  • On the curve. buy(amountIn, minOut, recipient) with the ETH as the transaction value and the burn address as the recipient. The curve delivers the tokens to the recipient and returns the amount. There is no separate quote function; the engine simulates the buy with eth_call, which is the exact answer.
  • On the pool. One execute on the Universal Router with a single V4_SWAP command: SWAP_EXACT_IN_SINGLE on the pool key {ETH, token, fee 0, tick spacing, Pons hook}, SETTLE_ALL for the ETH, and TAKE with the burn address as the recipient and amount OPEN_DELTA, so the router hands the whole output to 0x…dEaD. The V4 quoter gives the expected output.
  • Slippage. minOut is the quote less the configured bound (3%). Below that the venue reverts. The node's gas estimate catches this before anything is signed, so a refused buy costs nothing.
  • Deadline. A signed buy not mined within 180 seconds is void.
  • Reading the burn. The engine parses the receipt's Transfer logs for the token and sums the amounts whose recipient is the burn address. That, not the quote, is what the ledger records as burned.

Conservation

Before recording a cycle the engine checks, in wei:

balance_before − eth_spent − gas_cost == balance_after
tokens_burned ≥ minOut
gas_cost ≤ gas_reserve
claimed == escrow.balanceOf(wallet) before the claim

If any of those fails, the cycle is not recorded and the error names the numbers. The ledger loader re-checks the first line on every receipt it reads, so a hand-edited ledger is rejected too.

Verified on-chain

Every address and call was checked against Robinhood Chain mainnet (chain id 4663) by simulation, with no key involved, before it was written into engine/src/pons.ts:

ContractAddressWhat was confirmed
Pons fee escrow0xd3AFEB2a57f70eF218Aa82451c51B2fb0416Ac9ebalanceOf is the claimable ETH; claim() pays exactly that to the caller; an empty claim reverts.
Pons factory0x7eD598BcEf8bd9Edd8C97A195C6d13f40801EC7egetLaunchedToken gives the curve, the creator fee recipient, the pair token and the v4 tick spacing.
Pons hook0xE5e702641Ea86F4ae6cC3cDaeD2B886f976Be044The pool key with this hook reproduces the pool id the PoolManager emitted.
Bonding curveper launchbuy delivers to the recipient, returns the amount, reverts under minOut.
Universal Router0x8876789976decbfcbbbe364623c63652db8c0904A V4_SWAP with TAKE to the burn address delivers exactly the V4 quoter's amount; reverts under minOut.

The test suite also stands up a real EVM node and runs the live code path (real signing) against a bonding curve and fee escrow with the same external shape and the same revert selectors.

Configuration

Limits live in engine/src/config.ts and can be overridden in wei from the environment. Chain and venue addresses default to Robinhood Chain and the Pons v2 contracts. Only the token and the dev wallet are launch-specific.

SettingDefaultMeaning
FAUCET_TOKENThe token contract, as launched on Pons v2.
FAUCET_DEV_WALLETThe wallet that launched it; the escrow credits its rewards here. Claims and signs the buys.
FAUCET_DEV_WALLET_KEYPrivate key. Needed only to claim and buy. Never committed.
FAUCET_RPC_URL, FAUCET_CHAIN_IDRobinhood Chain, 4663Pinned; the engine refuses an RPC that reports a different id.
FAUCET_MIN_CLAIM_WEI0.0005 ETHBelow this, rewards stay in the escrow.
FAUCET_GAS_RESERVE_WEI0.001 ETHLeft in the wallet every cycle.
FAUCET_MIN_BUYBACK_WEI0.002 ETHBelow this, nothing is bought.
limits.slippageBps300Maximum drop from the quote before the buy reverts.
limits.deadlineSeconds180Validity of a signed buy.
limits.intervalSeconds180How often the runner cycles.

CLI

faucet policy                       show the routing policy and prove it sums to 100%
faucet claim [--execute] [--mock]   claim creator rewards from the Pons fee escrow
faucet plan [--mock]                read the wallet and quote the buyback (no spend)
faucet burn [--execute] [--mock]    one cycle: claim, buy, burn; --execute sends
            [--rounds N] [--write-site]
faucet run [--every S] [--mock]     cycle forever (default every 180s)
           [--commit-every M]       commit + push the ledger at most every M minutes
faucet status                       totals from the burn ledger
faucet verify <txhash>              confirm a tx burned the token
faucet doctor                       check config, RPC, launch record; simulate a buy
faucet reset --yes                  empty the ledger and the site data

--mock runs against an in-memory chain and writes under .faucet-mock/, never into the shipped data. --execute is the only flag that spends anything, and only with a signer that matches the configured dev wallet.

Going live

  1. Export FAUCET_TOKEN and FAUCET_DEV_WALLET.
  2. npm run doctor. Every line must be ok: chain id, gas price against the reserve, the token, the launch record and its creator fee recipient, the claimable balance, and a simulated buy to the burn address that matches the quote.
  3. npm run plan to see what one cycle would do, with nothing sent.
  4. Export FAUCET_DEV_WALLET_KEY in the shell that runs the engine, never in a file, then npm run run:live.
  5. The runner commits and pushes the ledger; the site redeploys with each burn.

The engine will not send if the signer derived from the key is not the configured dev wallet, if the buy recipient is not the burn address, or if the plan does not account for the whole balance. Those checks run before the transaction is built.

Runbook

Reading a receipt

received is what the claim pulled from the escrow. spent is the ETH sent as the buy's value. burned is the token amount that reached the burn address, read from the receipt, with the quote and realised slippage beside it. wallet shows before and after, and conserved is the wei check.

When a buy is refused

FAIL   buy rejected before sending: execution reverted (bonding curve: output below minOut). Nothing was signed and no gas was spent

The market moved more than 3% between quote and fill. The ETH is still in the wallet; the next tick quotes again. Do not loosen the bound to force it through.

When conservation fails

The cycle is not recorded. Nothing further will be sent until the numbers are explained. A mismatch usually means something else moved ETH in the wallet between the plan and the receipt; the receipt's own numbers will show which.

Scheduling

faucet run is the scheduler: claim, plan, execute, record, sleep, with a lock file, backoff on failure and an optional commit-and-push. Keep it up with deploy/faucet.service or the Dockerfile, with the key only in that process's environment.

The site

Static files. It reads data/faucet.json, the ledger summary the engine writes after every cycle; data/burns.json, the full receipts; and data/build.json, the commit and build time stamped by npm run build:info. The footer of every page shows that stamp. Opened straight off disk it falls back to the snapshot in scripts/data.js, which npm run sync:fallback regenerates, and says so. The ledger ships empty and fills only with real transactions.

Deploying

The repository is Vercel-ready. vercel.json serves site/ and uses the build step as a guard: it typechecks the engine and runs faucet policy, which throws unless the policy sums to 100%. Headers include a Content-Security-Policy with no inline scripts, and data/ is served uncached so a fresh burn is visible on the next load.

npm i -g vercel
vercel --prod