Skip to main content
A seller that intentionally fails during token issuance so you can observe the full refund lifecycle: a BullMQ cron job detects stuck payments and sends USDC back to the buyer on-chain. This example is a testing and development tool. It proves that your refund infrastructure works end-to-end before you deploy to production.

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 a fetchResourceCredentials 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:
Because this callback throws, the challenge engine catches the error and leaves the record in 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 calls processRefunds() from the SDK. This function handles the entire refund lifecycle atomically:
Under the hood, processRefunds() does four things:
  1. Queries the store for challenges in PAID state older than minAgeMs
  2. Atomically transitions each to REFUND_PENDING (prevents duplicate refunds across replicas)
  3. Sends USDC back to the buyer’s wallet on-chain
  4. Transitions to REFUNDED on success, or REFUND_FAILED if the on-chain transaction reverts
The KEY0_WALLET_PRIVATE_KEY environment variable is required for refunds. This is the private key of the seller’s wallet — the same wallet that received the original payment. Without it, the cron runs but skips all refund processing.

Running the Example

Prerequisites

  • Redis running locally (or a remote instance)
  • A wallet address and its private key (the seller wallet)
  • Bun installed

Setup

1

Navigate to the example

2

Configure environment variables

Set the following in .env:
3

Install dependencies

4

Start the server

Expected Output

On startup, the server prints its configuration:
While idle, the cron logs each tick:
After a client agent pays for a resource, fetchResourceCredentials throws and the payment gets stuck. Once the grace period elapses, the next cron tick picks it up:
The buyer’s USDC is returned on-chain. The challenge record is now in REFUNDED state and will not be processed again.

Testing the full cycle

To trigger a payment and observe the refund, run an agent against this seller. The agent will pay, the seller will fail to deliver, and within 30 seconds the cron will refund the payment.

Refunds reference

How the refund state machine, grace periods, and double-refund prevention work in production.

Source code

examples/refund-cron-example/server.ts