Skip to content

About

A minting bot for the Circularity NFT, running as a Cloudflare Worker on a monthly Cron Trigger

Resources

Stars

3 stars

Watchers

1 watching

Forks

Repository files navigation

circularity-minter

A minting bot for the Circularity NFT, running as a Cloudflare Worker on a Cron Trigger. It's managed with the Cloudflare CLI (cf) and configured in cloudflare.config.ts. It previously ran on Vercel and sent transactions through Syndicate's Transaction Cloud.

On the first of every month at 1 AM UTC it:

  1. Calls burn() on Circularity, which burns the prior month's NFT and mints the current month's NFT to its recorded owner.
  2. Calls mint(amount) on Circularity - Burn Ceremony, minting every Burn Ceremony NFT that is owed to the artist.

It runs every day at 1 AM UTC, so if the run on the 1st fails, the next day's run retries it. Runs on other days find nothing due and send nothing. 1 AM is used instead of midnight to give humans an hour on the 1st to burn and mint themselves for on-chain provenance. Contract source: WillPapper/circularity.

How it behaves

  • Idempotent. Each run reads on-chain state first and sends only the transactions that are due. Re-running after a success, or after a human already burned and minted, sends nothing.
  • Retries daily. A failed run is retried the next day. This matters for Circularity: burn() can only mint the current month's NFT, so a month that's never burned is skipped for good.
  • Catches up. mint uses amountEligibleToMint() rather than a fixed 1, so Burn Ceremony NFTs for missed months are all minted on the next successful run.
  • No wasted gas on reverts. Every call is simulated before it is sent. Nothing is sent while an earlier transaction from the bot wallet is still pending (for example, stuck during a gas spike), since a duplicate behind it would be rejected. To make getting stuck unlikely, the max fee is set to 2x the current base fee instead of viem's default 1.2x; transactions still pay only the actual fee.
  • Independent steps. A burn that fails or reverts doesn't stop the mint, or the reverse. The exception is a pending transaction: if one is waiting to be mined, including one sent earlier in the same run, both steps wait for the next run. If either step fails, the invocation is marked as failed in the Worker's Cron Events.

Both contract functions are permissionless, so any wallet with ETH for gas can run this. The wallet never holds the NFTs.

Endpoints

Route Description
GET / Read-only status: the current Circularity month, Burn Ceremony NFTs owed, and the minter wallet's address, balance, and pending transactions.
POST /run Runs the same job as the cron immediately. Requires Authorization: Bearer <RUN_TOKEN>. It waits for receipts, so keep the connection open until it responds (usually under a minute). Avoid calling it while the 1 AM UTC run is in progress: two overlapping runs can each send a transaction, and the contract rejects the second, wasting a little gas.

Setup

Install dependencies and sign in to Cloudflare:

npm install
npx cf auth login

If your login can access more than one Cloudflare account, choose one with a gitignored .env file in the project root:

echo "CLOUDFLARE_ACCOUNT_ID=<account id>" > .env

Create a dedicated wallet for the bot and fund it with a small amount of ETH. A normal month uses about 145k to 165k gas: 77k to 97k for the burn, plus 68k for minting one Burn Ceremony NFT.

First deploy

cf can't set secrets on a Worker that doesn't exist yet, so the first deploy uploads them with the code:

npm run deploy:secrets

It prompts, with hidden input, for:

  • PRIVATE_KEY: the bot wallet's 0x-prefixed private key
  • RPC_URL: an Ethereum mainnet JSON-RPC URL, for example from Alchemy or Infura
  • RUN_TOKEN: any long random string, which you'll need for POST /run

The values are piped straight to cf deploy --secrets-file, so they never touch disk or shell history. Run it again any time to rotate secrets.

If the deploy runs somewhere you can't type into (for example, a remote agent's machine), set the secrets yourself first:

  1. Create a Worker named circularity-minter, with any placeholder code (for example, the dashboard's Hello World template).

  2. Add the secrets to it, either in the dashboard under Settings > Variables and Secrets (type Secret), or with cf on your own machine. This zsh loop prompts for each value with hidden input. In bash, use read -rsp "$name: " value instead:

    for name in PRIVATE_KEY RPC_URL RUN_TOKEN; do read -rs "value?$name: " && echo && cf workers secrets update "$name" --worker circularity-minter --type secret_text --text "$value"; done; unset value
  3. Run npm run deploy. It replaces the placeholder code and keeps the secrets. If it reports a configuration conflict with the placeholder, review the diff and rerun with --force.

Later deploys

npm run deploy

Existing secrets carry over to new versions.

To catch up immediately instead of waiting for the next cron:

curl -X POST -H "Authorization: Bearer $RUN_TOKEN" https://circularity-minter.<your-subdomain>.workers.dev/run

Workers plan

The Workers Paid plan is recommended. Deriving the signing key takes about 9ms of CPU on a cold start, and the Free plan allows 10ms per invocation. Free plan subrequest limits (50 per invocation) are fine: RPC reads are batched, and receipts are polled once per block.

Monitoring

  • Each run's report is logged as JSON. View it in the dashboard under the Worker's Observability tab, or stream live logs with wrangler tail circularity-minter. cf can't tail logs yet, so this is the one task that falls back to Wrangler (installed globally, not as a project dependency). It reads the same .env account.
  • Failed runs appear under the Worker's Settings > Triggers > Cron Events. A failed run on the 1st is retried each day at 1 AM UTC until it succeeds.
  • GET / reports pendingTransactions for the bot wallet. If it stays above 0, a transaction is stuck and runs won't send until it's mined or dropped. It's null if the RPC doesn't report pending transactions, in which case runs send normally and the contracts reject any duplicate.
  • GET / shows the minter wallet's balance, so you can top it up before it runs dry.

Local development

Copy .dev.vars.example to .dev.vars and fill it in. .dev.vars is gitignored. Then:

npm run dev
curl "http://localhost:5173/cdn-cgi/local/scheduled?cron=0+1+1+*+*&format=json"

The second command triggers the scheduled handler. Local runs send real transactions to whatever RPC_URL points at, so use the fork test below to try changes without spending ETH.

npm run typecheck regenerates the binding types in .cloudflare/types and type-checks the project.

Testing

npm run test:fork

This runs an end-to-end test against a local mainnet fork in about 15 seconds. It starts a Hardhat fork of Ethereum mainnet, runs the Worker with cf dev against it, and moves the fork's clock through each scenario:

  1. A scheduled run that catches up every missed month
  2. Re-runs, by cron and POST /run, and a mid-month daily run, all sending nothing
  3. POST /run rejecting missing and wrong tokens
  4. Humans burning and minting before the 1 AM run, which then sends nothing
  5. Missed months up to December, then a year rollover to January
  6. Exactly 12 missed months, where burn() only updates the year
  7. A transaction stuck pending blocking new sends, then the next day's run proceeding once it's mined
  8. An empty wallet failing loudly without changing anything, then the next day's run catching up once it's funded
  9. The RPC URL's key never appearing in responses or logs, including while the RPC is down

Expectations are computed from the contracts' state at the fork, so the test works whichever month mainnet is in.

The test never touches mainnet or your .dev.vars. It writes its own .dev.vars.forktest holding Hardhat's public test keys, removes it afterwards, and aborts unless the Worker is signing with the test key on the fork.

Variable Default Description
FORK_RPC_URL A public mainnet RPC The RPC to fork. Use your own for reliability.
FORK_BLOCK Latest block Pins the fork to a block. Requires an archive RPC.

About

A minting bot for the Circularity NFT, running as a Cloudflare Worker on a monthly Cron Trigger

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages