Skip to main content
@faremeter/test-harness provides an in-process test environment that connects client, middleware, and facilitator using function adapters instead of HTTP. No real blockchain interaction, no network calls, no running servers.
pnpm add -D @faremeter/test-harness

TestHarness

The TestHarness class wires together a client, middleware, and facilitator in memory. It creates a fetch function that runs the full x402 payment flow — 402 response, payment construction, settlement — entirely in-process.
import {
  TestHarness,
  accepts,
  createTestPaymentHandler,
  createTestFacilitatorHandler,
} from "@faremeter/test-harness"

const harness = new TestHarness({
  accepts: [accepts({ maxAmountRequired: "10000", asset: "USDC" })],
  clientHandlers: [createTestPaymentHandler()],
  facilitatorHandlers: [createTestFacilitatorHandler({ payTo: "test-receiver" })],
})

const fetch = harness.createFetch()
const response = await fetch("http://test.local/resource")

Configuration

OptionTypeDescription
acceptsx402PaymentRequirements[]Payment requirements the test server returns. Use the accepts() helper for defaults.
clientHandlersPaymentHandlerV1[]Required. Client-side payment handlers (v1, internally adapted to v2). Use createTestPaymentHandler() for tests.
facilitatorHandlersFacilitatorHandler[]Required. Facilitator handlers. Use createTestFacilitatorHandler({ payTo }) for tests.
clientInterceptorsInterceptor[]Interceptors between client and middleware.
middlewareInterceptorsInterceptor[]Interceptors between middleware and facilitator.
settleMode"settle-only" | "verify-then-settle"Whether to verify before settling. Default: "settle-only".

Resource handler

Set a custom response for the protected resource:
harness.setResourceHandler(async (ctx) => {
  return new Response(JSON.stringify({ data: "protected content" }), {
    status: 200,
    headers: { "Content-Type": "application/json" },
  })
})

Reset

Call harness.reset() between tests to clear interceptors and restore defaults.

Test helpers

The accepts() and acceptsV2() helpers create payment requirements with sensible test defaults so you don’t need to specify every field:
import { accepts, acceptsV2 } from "@faremeter/test-harness"

// v1 requirements with test defaults
const reqs = accepts({ maxAmountRequired: "5000", asset: "USDC" })

// v2 requirements
const reqs = acceptsV2({ amount: "5000", asset: "USDC" })

Interceptors

Interceptors wrap the fetch pipeline to observe, modify, or block requests. They sit between the client and middleware, or between middleware and facilitator.
harness.addClientInterceptor(interceptor)      // between test code and middleware
harness.addMiddlewareInterceptor(interceptor)   // between middleware and facilitator

Logging

Capture all requests and responses for inspection:
import { createLoggingInterceptor, createEventCollector } from "@faremeter/test-harness"

const events = createEventCollector()
const interceptor = createLoggingInterceptor(events.collect)

harness.addClientInterceptor(interceptor)

const fetch = harness.createFetch()
await fetch("http://test.local/resource")

console.log(events.events) // array of request/response log events
Or log to the console during debugging:
import { createConsoleLoggingInterceptor } from "@faremeter/test-harness"

harness.addClientInterceptor(createConsoleLoggingInterceptor("client"))
harness.addMiddlewareInterceptor(createConsoleLoggingInterceptor("facilitator"))

Capturing requests

Inspect specific requests matching a pattern:
import { createCaptureInterceptor, matchFacilitatorSettle } from "@faremeter/test-harness"

const capture = createCaptureInterceptor(matchFacilitatorSettle)
harness.addMiddlewareInterceptor(capture.interceptor)

const fetch = harness.createFetch()
await fetch("http://test.local/resource")

console.log(capture.captured) // array of captured settle requests

Simulating failures

Test error handling by making specific requests fail:
import { createFailureInterceptor, failOnce, failNTimes, matchFacilitatorSettle } from "@faremeter/test-harness"

// Fail all settle requests
harness.addMiddlewareInterceptor(
  createFailureInterceptor(matchFacilitatorSettle, new Error("network error"))
)

// Fail only the first settle request (retry succeeds)
harness.addMiddlewareInterceptor(
  failOnce(matchFacilitatorSettle, new Error("transient error"))
)

// Fail the first 3 attempts
harness.addMiddlewareInterceptor(
  failNTimes(matchFacilitatorSettle, 3, new Error("flaky"))
)

Simulating latency

import { createDelayInterceptor, matchFacilitatorSettle } from "@faremeter/test-harness"

// Add 500ms delay to settle requests
harness.addMiddlewareInterceptor(
  createDelayInterceptor(matchFacilitatorSettle, 500)
)

Composing interceptors

Combine multiple interceptors into one:
import { composeInterceptors } from "@faremeter/test-harness"

const combined = composeInterceptors(loggingInterceptor, delayInterceptor, captureInterceptor)
harness.addMiddlewareInterceptor(combined)

Request matchers

Matchers are predicate functions that determine which requests an interceptor acts on:
MatcherMatches
matchAllEvery request
matchNoneNo requests
matchFacilitatorAny facilitator endpoint
matchFacilitatorAcceptsPOST /accepts
matchFacilitatorSettlePOST /settle
matchFacilitatorVerifyPOST /verify
matchFacilitatorSupportedGET /supported
matchResourceThe protected resource request
Combine matchers with and(), or(), and not():
import { and, or, not, matchFacilitator, matchFacilitatorSettle } from "@faremeter/test-harness"

const matcher = and(matchFacilitator, not(matchFacilitatorSettle))

Payer choosers

When multiple payment options are available, the payer chooser selects which one to use. The test harness provides choosers for testing different selection strategies:
ChooserBehavior
chooseFirstPicks the first available option
chooseCheapestPicks the lowest amount
chooseMostExpensivePicks the highest amount
chooseByScheme(scheme)Returns a chooser that matches by payment scheme
chooseByNetwork(network)Returns a chooser that matches by network name
chooseByAsset(asset)Returns a chooser that matches by asset name
chooseByIndex(n)Returns a chooser that picks by array index
chooseNoneAlways throws — tests “no option” paths
Wrap choosers with additional behavior:
import { chooseFirst, chooseWithInspection } from "@faremeter/test-harness"

const chooser = chooseWithInspection((execers) => {
  console.log(`${execers.length} options available`)
}, chooseFirst)

Test handlers

For testing specific facilitator behaviors, use the pre-built handler factories:
FactoryBehavior
createTestPaymentHandler()Standard test handler (no crypto)
createTestFacilitatorHandler({ payTo })Standard facilitator handler (no crypto). payTo is required.
createWorkingHandler()Always succeeds
createNonMatchingHandler()Returns no matching execers
createThrowingHandler(message)Always throws with the given message
createEmptyPayloadHandler()Returns empty payload
createNullPayloadHandler()Returns null payload
import { TestHarness, createThrowingHandler } from "@faremeter/test-harness"

const harness = new TestHarness({
  accepts: [accepts({ maxAmountRequired: "10000" })],
  clientHandlers: [createTestPaymentHandler()],
  facilitatorHandlers: [createThrowingHandler("simulated failure")],
})

const fetch = harness.createFetch()
const response = await fetch("http://test.local/resource")
// response will reflect the facilitator error

Example: full test

import { describe, it, expect, beforeEach } from "vitest"
import {
  TestHarness,
  accepts,
  createTestPaymentHandler,
  createTestFacilitatorHandler,
  createCaptureInterceptor,
  matchFacilitatorSettle,
} from "@faremeter/test-harness"

describe("payment flow", () => {
  let harness: TestHarness

  beforeEach(() => {
    harness = new TestHarness({
      accepts: [accepts({ maxAmountRequired: "10000", asset: "USDC" })],
      clientHandlers: [createTestPaymentHandler()],
      facilitatorHandlers: [createTestFacilitatorHandler({ payTo: "test-receiver" })],
    })
    harness.setResourceHandler(async () =>
      new Response(JSON.stringify({ ok: true }), { status: 200 })
    )
  })

  it("completes payment and returns resource", async () => {
    const fetch = harness.createFetch()
    const response = await fetch("http://test.local/api/data")

    expect(response.status).toBe(200)
    expect(await response.json()).toEqual({ ok: true })
  })

  it("sends settle request to facilitator", async () => {
    const capture = createCaptureInterceptor(matchFacilitatorSettle)
    harness.addMiddlewareInterceptor(capture.interceptor)

    const fetch = harness.createFetch()
    await fetch("http://test.local/api/data")

    expect(capture.captured).toHaveLength(1)
  })
})

Further reading