cron402

cron402

Local
@userdefault13TypeScriptMITUpdated 2w ago

Schedule webhook calls, pay per run in USDC on Base via x402. Ten tools, built for small models.

cron402

x402-native cron-as-a-service for AI agents. Agents schedule webhook calls and pay per run in USDC on Base via the x402 protocol — no accounts, no API keys. The wallet is the credential.

  • Price: $0.008 flat per run
  • Network: Base mainnet (eip155:8453) — validated end-to-end on Sepolia first (settlement tx 0x01f7bed4…)
  • Wallets: built for Coinbase Agentic Wallets / any x402 client

Architecture

┌────────────────────────────┐        ┌─────────────────────────────────────────┐
│  apps/web — SvelteKit      │        │  apps/api — Cloudflare Worker (Hono)    │
│  deployed to Vercel        │──────▶ │                                         │
│  - landing page            │        │  x402 paywall (@x402/hono v2)           │
│  - agent integration docs  │        │  ├─ POST /v1/crons            $0.008    │
│                            │        │  ├─ POST /v1/crons/topup/1|10|100       │
│                            │        │  ├─ GET  /v1/crons/:id        (free)    │
│                            │        │  ├─ POST /v1/crons/:id/pause|resume     │
│                            │        │  └─ DELETE /v1/crons/:id   (EIP-712)    │
└────────────────────────────┘        │                                         │
                                      │  CronJobDO (Durable Object per job)     │
                                      │  alarm() → dispatch webhook             │
                                      │  D1: jobs, executions, payments         │
                                      └─────────────────────────────────────────┘
                                                │ settle via facilitator
                                                ▼
                                         USDC on Base (Sepolia/mainnet)

How scheduling works

  • Every job gets its own Durable Object (CronJobDO). A cron-parser-computed next-run time is set as the DO's alarm() — exact to the minute for arbitrary cron expressions.
  • Each fire: dispatch the target webhook (10s timeout) → up to 3 attempts with backoff → log the execution to D1 (kept: last 100 runs / 30 days per job) → consume 1 credit.
  • Auto-pause rules: 3 consecutive failures → paused; zero credits → exhausted.
  • A global Worker Cron Trigger (* * * * *) sweeps for missed alarms as a safety net.
  • Top-ups reactivate paused/exhausted jobs automatically.

How payments work

Paid endpoints are gated by @x402/hono v2 middleware:

  1. Agent calls the endpoint with no auth.
  2. Server responds 402 Payment Required with instructions (price, network, payTo).
  3. Agent signs an EIP-3009 USDC transfer from its wallet and retries.
  4. The facilitator verifies + settles; settlement receipts land in D1.

Because the payer isn't online when an alarm fires, billing is prepaid run credits: creation includes 1 credit; packs of 1 / 10 / 100 cost $0.008 / $0.08 / $0.80. Credits never expire and top-ups are permissionless (anyone can fund any job).

Management actions (pause/resume/delete) require an EIP-712 signature over ManageJob { action, jobId, timestamp } from the payer key (±5 min validity).


Repository layout

dev/ai-cron-site/
├── package.json               # pnpm workspaces + turbo tasks
├── turbo.json                 # build/check/lint pipeline
├── biome.json                 # Biome lint/format config
├── .env.example               # all env vars documented
├── packages/shared/
│   └── src/                   # zod schemas, constants, types, JSON Schemas
├── packages/mcp/
│   ├── src/index.ts           # cron402-mcp: MCP server (10 agent tools)
│   ├── src/schedule.ts        # plain-English → cron parsing, validation, next-run preview
│   └── src/store.ts           # remembers created job ids for list_crons
├── skills/cron402/SKILL.md    # drop-in agent skill for using the MCP server
└── apps/
    ├── api/                   # Cloudflare Worker
    │   ├── wrangler.jsonc     # DO bindings, D1 binding, cron trigger
    │   ├── .dev.vars.example  # local dev vars template
    │   ├── migrations/        # D1 schema (0001 init, 0002 notify_url)
    │   └── src/
    │       ├── index.ts       # Hono app, routes, lazy x402 middleware
    │       ├── scheduler.ts   # CronJobDO (alarm loop, retries, retention, notify)
    │       ├── auth.ts        # EIP-712 management-signature verification
    │       ├── payments.ts    # payer extraction from x402 headers
    │       ├── facilitator.ts # CDP hosted facilitator client
    │       ├── cdp-auth.ts    # CDP JWT signing (jose/WebCrypto)
    │       ├── openapi.ts     # OpenAPI 3.1 document builder
    │       └── types.ts       # Env bindings + typed DO stub interface
    └── web/                   # SvelteKit 5 → adapter-vercel (MS-DOS theme)
        └── src/routes/
            ├── +page.svelte          # landing
            ├── docs/+page.svelte     # agent integration guide
            ├── dashboard/+page.svelte # wallet-connected job console
            ├── llms.txt/+server.ts   # agent-readable site summary
            └── robots.txt/+server.ts # crawler policy (AI bots allowed)

API reference

EndpointAuthPriceDescription
POST /v1/cronsx402$0.008Create job (body: { schedule, target: { url, method?, headers?, body? }, maxRuns? }). Includes 1 run credit.
POST /v1/crons/topup/1x402$0.008Add 1 credit (body: { jobId })
POST /v1/crons/topup/10x402$0.08Add 10 credits
POST /v1/crons/topup/100x402$0.80Add 100 credits
GET /v1/crons/:idfree—Job status, credits, last 20 executions
POST /v1/crons/:id/pausewallet-signed—Pause job
POST /v1/crons/:id/resumewallet-signed—Resume job (requires credits)
DELETE /v1/crons/:idwallet-signed—Delete job
GET /healthfree—Liveness probe
POST /v1/echofree—Test target for smoke tests

Signed requests add two headers:

  • x-cron402-timestamp: unix ms (valid ±5 min)
  • x-cron402-signature: EIP-712 signature, domain { name: "cron402", version: "1" }, primaryType ManageJob

Limits & policies (v1)

  • Min interval: 1 minute · Max 1,000 active jobs per payer wallet
  • Retry policy: 3× with 1s/3s/8s backoff; non-retryable 4xx (except 429) fails immediately
  • Log retention: last 100 executions or 30 days per job, whichever trims first

Deployment status (live)

Network: Base mainnet (eip155:8453) — flipped from Sepolia after a fully verified paid run. Receiving wallet secret PAY_TO_ADDRESS is set on the Worker. Facilitator: Coinbase CDP hosted (CDP_API_KEY_ID/CDP_API_KEY_SECRET secrets set; JWT auth implemented in src/cdp-auth.ts). D1 database cron402 (id b7e05d96-4737-4aa4-ab52-badcfacb3a22) holds the schema from both migrations.

Bazaar: bazaar extensions are declared via @x402/extensions/bazaar (info.input.type: "http"); listing appears after a successful CDP settle that echoes the extension — do not claim currently indexed until validated.

Execution webhooks

Pass optional notifyUrl when creating a job. After every fire, cron402 POSTs to it:

{
  "type": "cron402.execution",
  "jobId": "…",
  "runAt": 1755000000000,
  "ok": true,
  "attempts": 1,
  "statusCode": 200,
  "error": null,
  "durationMs": 42
}

Notifications are best-effort (10s timeout, never affect scheduling).

Human dashboard

/dashboard connects any injected EIP-1193 wallet (MetaMask etc.). Load a job by id to see status/credits/executions; pause/resume/delete request an eth_signTypedData_v4 signature over ManageJob — exactly what agents sign, just through a browser wallet.

MCP server (cron402-mcp)

Ten tools for any MCP client, designed so a small model can schedule a cron without burning money on mistakes:

ToolCostPurpose
cron402_guidefreeThe full procedure, prices and rules — what a model reads first
check_walletfreeAddress, network, USDC/ETH balance, runs affordable
preview_schedulefreeValidate a schedule, show the next 5 UTC run times
create_cron$0.008Create a job (includes 1 credit)
topup_cron$0.008–$0.80Add 1 / 10 / 100 credits
get_cronfreeStatus, credits, last 20 executions
list_cronsfreeJobs created from this machine, with live status
pause_cron · resume_cron · delete_cronfreeEIP-712 signed management

What makes it usable by weak models:

  • Plain-English schedules — "every 15 minutes", "every weekday at 9am", "every monday at 5pm", "weekly", or raw cron. Ambiguous input ("every 2 weeks", "at teatime") fails loudly with a suggested fix instead of silently scheduling the wrong time.
  • Timezone conversion — pass an IANA timezone and the time-of-day is converted to the UTC cron the API stores, with the DST caveat spelled out in the response.
  • Free dry run before any spend — preview_schedule costs nothing, and create_cron re-validates locally so a malformed schedule never reaches a paid request.
  • Money guardrails — paid tools are labelled PAID with the exact amount, and every response carries a next_step telling the model not to call them twice.
  • Actionable errors — { ok, error, next_step } for insufficient funds, an unreachable API, a job owned by another wallet, or a localhost target.
  • Job memory — the API has no list endpoint, so created ids are written to ~/.cron402/jobs.json (override with CRON402_STATE_DIR) and list_crons reads them back.
  • Read-only start — a missing CRON402_PRIVATE_KEY no longer kills the server; free tools keep working and the paid ones explain what to configure.

Paid calls flow through x402 automatically; management actions are signed locally.

Agent skill (skills/cron402)

skills/cron402/SKILL.md teaches an agent the whole workflow — check wallet → preview → confirm → create → top up — plus the rules that keep it from double-charging. Drop it into any project:

mkdir -p .claude/skills/cron402
curl -sL https://raw.githubusercontent.com/userdefault13/ai-cron-site/main/skills/cron402/SKILL.md \
  -o .claude/skills/cron402/SKILL.md

See skills/README.md for other clients.

Install from npm:

export CRON402_PRIVATE_KEY=0x...   # fund with USDC on Base
npx -y cron402-mcp

MCP client config (Cursor / Claude Desktop):

{
  "mcpServers": {
    "cron402": {
      "command": "npx",
      "args": ["-y", "cron402-mcp"],
      "env": { "CRON402_PRIVATE_KEY": "0x..." }
    }
  }
}

Local dev from repo:

pnpm --filter cron402-mcp build
packages/mcp/abra-start.sh   # key from abracadabra vault (Touch ID gated)

See packages/mcp/PUBLISHING.md for release instructions.

Env (optional overrides): CRON402_NETWORK (eip155:8453 mainnet default, eip155:84532 for Sepolia), CRON402_API_URL, CRON402_ABRA_PROJECT (default ai-cron-site). If CRON402_PRIVATE_KEY is already set in the environment, the vault lookup is skipped.

Agent wallet: 0xfD4AE5BD56d9BB939B95CCb870E0c97B62E0dC30 — stored encrypted in the abracadabra vault as ai-cron-site / EVM_PRIVATE_KEY. (It replaced 0x269B9678…, which was rotated out after its key was briefly committed to this repo and swept; never hardcode the key, read it from the vault.) On mainnet it needs mainnet USDC (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) + a little ETH for gas. This is the payer; revenue flows to the Worker's PAY_TO_ADDRESS secret.


Local development

Prereqs: Node 22+, pnpm 9+, Wrangler (bundled).

pnpm install

# API — apply schema locally and start dev server (:8787)
pnpm --filter @cron402/api db:local
cp apps/api/.dev.vars.example apps/api/.dev.vars   # SKIP_PAYMENTS=1 by default
pnpm dev:api

# Web — SvelteKit dev server (:5173)
pnpm dev:web

With SKIP_PAYMENTS=1 (local only), paid routes skip x402 verification and identify the payer via the x-payer header.

E2E smoke test

# create an every-minute job targeting the built-in echo endpoint
curl -X POST http://127.0.0.1:8787/v1/crons \
  -H 'content-type: application/json' \
  -H 'x-payer: 0x1111111111111111111111111111111111111111' \
  -d '{"schedule":"* * * * *","target":{"url":"http://127.0.0.1:8787/v1/echo","method":"POST"}}'

# wait ~70s for the next minute boundary, then check the execution log
curl http://127.0.0.1:8787/v1/crons/<jobId>
# expect: credits decremented, executions[0].ok = 1, status flips to "exhausted" at 0 credits

# top up and watch it reactivate
curl -X POST http://127.0.0.1:8787/v1/crons/topup/10 \
  -H 'content-type: application/json' \
  -H 'x-payer: 0x1111111111111111111111111111111111111111' \
  -d '{"jobId":"<jobId>"}'

Quality gates: pnpm lint (Biome), pnpm check (tsc + svelte-check), pnpm build.


Go-live instructions

1. Provision Cloudflare resources

cd apps/api
npx wrangler login

# create production D1 database and note the returned id
npx wrangler d1 create cron402
npx wrangler d1 execute cron402 --remote --file=migrations/0001_init.sql

Edit apps/api/wrangler.jsonc: replace the placeholder database_id with the real one.

⚠️ Durable Object class CronJobDO uses new_sqlite_classes — if you rename it or add classes later, bump the migration tag.

2. Set secrets & vars (API)

cd apps/api

# your receiving address on the chosen network (REQUIRED)
npx wrangler secret put PAY_TO_ADDRESS

# optional: CDP-hosted facilitator for production settlement
# default is the open https://x402.org/facilitator
npx wrangler secret put FACILITATOR_URL

Vars live in wrangler.jsonc / dashboard:

VarValuesNotes
X402_NETWORKeip155:84532 (sepolia) · eip155:8453 (mainnet)must match payTo network
SKIP_PAYMENTSunset in prod!only for local testing
FACILITATOR_URLfacilitator base URLsee below

Facilitator options

  • Default: https://x402.org/facilitator — open, no auth, testnet only.
  • Production: Coinbase CDP hosted facilitator (recommended). Set secrets CDP_API_KEY_ID + CDP_API_KEY_SECRET; the Worker then automatically switches to CdpFacilitatorClient (apps/api/src/facilitator.ts), which generates a per-request CDP JWT bound to each endpoint (/verify, /settle, /supported) via the uris claim. JWT signing is implemented on jose/WebCrypto in apps/api/src/cdp-auth.ts and supports both Ed25519 and EC (ES256) CDP keys.
    • Get keys at https://portal.cdp.coinbase.com → API Keys → Secret API Key.
    • Optionally override FACILITATOR_URL if CDP exposes a different base path; default is https://api.cdp.coinbase.com/platform/v2/x402.

3. Deploy

# API
pnpm --filter @cron402/api deploy
curl https://cron402-api.<your-subdomain>.workers.dev/health

# Web (first time: vercel link inside apps/web)
cd apps/web && npx vercel --prod
# set PUBLIC_API_URL to the deployed Worker URL in Vercel project settings,
# then update the apiUrl constant in src/routes/docs/+page.svelte

4. Fund & verify end-to-end on Sepolia

  1. Get free test USDC: https://faucet.circle.com/ (Base Sepolia).
  2. Run the buyer snippet from /docs on the site (uses @x402/fetch + Coinbase Agentic Wallet) pointed at your deployed Worker URL.
  3. Confirm: 402 → sign → retry succeeds, job appears, first fire lands within one minute, settlement logged in D1:
npx wrangler d1 execute cron402 --remote --command "SELECT * FROM payments ORDER BY created_at DESC LIMIT 5"

5. Mainnet flip checklist

  • PAY_TO_ADDRESS is a mainnet-controlled address
  • X402_NETWORK=eip155:8453
  • Facilitator upgraded (CDP recommended; set spend controls on the payer side too)
  • Re-test a full paid cycle before announcing
  • Review rate limits / pricing in packages/shared/src/index.ts (PRICE_PER_RUN_USD, pack prices in apps/api/src/index.ts TOPUP_PACKS)

Operational notes

  • Monitoring: poll GET /v1/crons/:id; execution rows include status code, latency, error text.
  • Sweeper: the every-minute Cron Trigger re-kicks DOs whose alarms were missed (>90s overdue). Check Worker metrics if jobs stall.
  • Changing price: update PRICE_PER_RUN_USD in shared + TOPUP_PACKS in the API together.
  • Secrets hygiene: .dev.vars, .env* are gitignored; never commit keys.