What It Demonstrates
In the happy path,fetchResourceCredentials issues a JWT and the challenge transitions from PAID to DELIVERED in the same request. But if that callback throws — or the server crashes mid-delivery — the challenge gets stuck in PAID state. The buyer has paid, but received nothing.
The refund cron is the safety net. It periodically scans for PAID records older than a grace period and refunds the USDC on-chain.
In this example, every payment triggers a refund because
fetchResourceCredentials always throws. In production, refunds only happen when delivery genuinely fails.Code Walkthrough
The intentional failure
The seller is configured with afetchResourceCredentials callback that always throws. This simulates a downstream failure — a database outage, a rate limit, a misconfigured token issuer — anything that prevents credential delivery after payment:
PAID state instead of transitioning it to DELIVERED.
Redis storage
Both the challenge store and the seen-tx store share a single Redis connection. The challenge store tracks the full state machine; the seen-tx store prevents double-spend of the same transaction hash:BullMQ cron setup
The refund cron uses BullMQ’s repeatable job feature. On startup, it clears any stale repeatable jobs from a previous run, registers a new one at the configured interval, then starts a worker:The queue is closed after registration because only the Worker needs an active connection. BullMQ stores the repeat schedule in Redis, so the worker picks it up independently.
The refund function
Each cron tick callsprocessRefunds() from the SDK. This function handles the entire refund lifecycle atomically:
processRefunds() does four things:
- Queries the store for challenges in
PAIDstate older thanminAgeMs - Atomically transitions each to
REFUND_PENDING(prevents duplicate refunds across replicas) - Sends USDC back to the buyer’s wallet on-chain
- Transitions to
REFUNDEDon success, orREFUND_FAILEDif the on-chain transaction reverts
Running the Example
Prerequisites
- Redis running locally (or a remote instance)
- A wallet address and its private key (the seller wallet)
- Bun installed
Setup
Configure environment variables
.env:| Variable | Required | Default | Description |
|---|---|---|---|
KEY0_WALLET_ADDRESS | Yes | — | Seller wallet address (receives payments) |
KEY0_WALLET_PRIVATE_KEY | Yes | — | Seller wallet private key (signs refund transactions) |
KEY0_ACCESS_TOKEN_SECRET | Yes | — | Secret for JWT signing (min 32 characters) |
KEY0_NETWORK | No | testnet | testnet (Base Sepolia) or mainnet (Base) |
REDIS_URL | No | redis://localhost:6379 | Redis connection URL |
PORT | No | 3000 | HTTP server port |
PUBLIC_URL | No | http://localhost:3000 | Public URL for agent card and resource endpoints |
REFUND_INTERVAL_MS | No | 15000 | How often the cron checks for stuck payments (ms) |
REFUND_MIN_AGE_MS | No | 30000 | Grace period before a PAID record is eligible for refund (ms) |
Expected Output
On startup, the server prints its configuration:fetchResourceCredentials throws and the payment gets stuck. Once the grace period elapses, the next cron tick picks it up:
REFUNDED state and will not be processed again.

