Back to blog

Open-Sourcing Our Error Detector Arbiter

An open-source arbiter that guarantees your customers never pay for error messages or garbage responses. Automatic escrow release on valid content, instant refund on failure.

We at x402r are excited to announce and open source an error detector arbiter. You can use it to provide the guarantee to your customers that the response you give them after they pay will not be an error message or garbage.

When a customer pays for an API response, their funds go into an on-chain escrow instead of directly to you. The arbiter evaluates what you sent back, and if it's real content, releases the funds to you. If it's garbage, the customer gets an automatic refund. No dispute process, no waiting, no trust required.

The evaluation is two-tiered:

  • Heuristic pass (instant, no LLM call): catches empty bodies, error JSON (500s, 404s, "Internal Server Error"), HTML error pages, and placeholder text like "lorem ipsum" or "coming soon"
  • AI pass: for everything else, an AI model reads the response body with a single instruction: does this look like real, substantive content?

The prompt is intentionally broad. It's not checking if the response is correct or high quality, just that it's not obviously broken. A weather API returning weather data passes. A weather API returning a stack trace fails.

The arbiter also stores every response body it evaluates. If a malicious merchant sends good content to the arbiter but garbage to the customer, the customer can detect the mismatch by comparing hashes and retrieve the real payload directly from the arbiter using their wallet signature. The customer always gets what they paid for.

Everything is enforced on-chain. The escrow rules are immutable: only the arbiter or the customer can release funds, the arbiter can refund immediately on failure, and if everyone goes offline, funds automatically refund after the escrow period expires. The customer can verify all of this before paying by reading the operator contract.

Integration

import express from "express";
import cors from "cors";
import {
  paymentMiddleware,
  x402ResourceServer,
} from "@x402/express";
import { HTTPFacilitatorClient } from "@x402/core/server";
import { CommerceServerScheme } from "@x402r/evm/commerce/server";
import {
  createAttestationExtension,
  declareAttestationExtension,
} from "@x402r/evm/extensions/attestation";
import {
  authCaptureEscrow,
  tokenCollector,
  forwardToArbiter,
} from "@x402r/helpers";

const account = /* your wallet account */;
const FACILITATOR_URL = "https://facilitator.ultravioletadao.xyz";
const ARBITER_URL = "https://ai-arbiter.up.railway.app";
const networkId = "eip155:84532";
const operatorAddress = /* your deployed operator address */;

const facilitatorClient = new HTTPFacilitatorClient({
  url: FACILITATOR_URL,
});
const resourceServer = new x402ResourceServer(facilitatorClient)
  .register(networkId, new CommerceServerScheme())
  .registerExtension(createAttestationExtension(ARBITER_URL))
  .onAfterSettle(forwardToArbiter(ARBITER_URL));

const app = express();
app.use(cors());

app.use(paymentMiddleware({
  "GET /weather": {
    accepts: [{
      scheme: "commerce",
      network: networkId,
      price: "$0.01",
      payTo: account.address,
      extra: {
        escrowAddress: authCaptureEscrow,
        operatorAddress,
        tokenCollector,
        feeReceiver: operatorAddress,
        maxFeeBps: 500,
      },
    }],
    extensions: declareAttestationExtension(),
  },
}, resourceServer));

app.get("/weather", (_req, res) => {
  res.json({ temperature: 68, conditions: "Partly cloudy" });
});

app.listen(4021);

Here is what every line is doing

A vanilla x402 merchant server looks like this:

const facilitatorClient = new HTTPFacilitatorClient({
  url: FACILITATOR_URL,
});
const resourceServer = new x402ResourceServer(facilitatorClient)
  .register("eip155:84532", new ExactEvmScheme());

app.use(paymentMiddleware({
  "GET /weather": {
    accepts: [{
      scheme: "exact",
      price: "$0.001",
      network: "eip155:84532",
      payTo: evmAddress,
    }],
  },
}, resourceServer));

Here is every change to turn it into an x402r arbiter-protected merchant:

1. CommerceServerScheme instead of ExactEvmScheme

.register(networkId, new CommerceServerScheme())

The vanilla x402 uses ExactEvmScheme which does a direct token transfer: pay and done. CommerceServerScheme uses the commerce-payments escrow flow instead: funds go into escrow first, and are only released to the merchant after the arbiter confirms the response was valid. This is the foundation that makes refunds possible.

2. scheme: "commerce" instead of scheme: "exact"

accepts: [{
  scheme: "commerce",
  ...
}]

Tells the client to use the commerce payment scheme (ERC-3009 authorization into escrow) rather than the exact scheme (direct transfer).

3. extra block: escrow configuration

extra: {
  escrowAddress: authCaptureEscrow,
  operatorAddress,
  tokenCollector,
  feeReceiver: operatorAddress,
  maxFeeBps: 500,
},

These are the commerce-specific fields that don't exist in vanilla x402:

  • escrowAddress: The AuthCaptureEscrow contract that holds funds during the escrow period. authCaptureEscrow is a protocol-wide singleton. Every x402r merchant uses the same escrow contract.
  • operatorAddress: The PaymentOperator contract that gates who can release, refund, and record payments. Use the arbiter's pre-deployed operator (0x673Dd1f107cDa8E992b4DBd5cB61f5b6c017f3B7, deployed on Base and Base Sepolia).
  • tokenCollector: The contract that receives the ERC-3009 transferWithAuthorization and deposits into escrow. Paired with escrowAddress. Always use them together.
  • feeReceiver: Address that receives operator fees when funds are released. Set to operatorAddress here so fees accumulate in the operator (can be distributed later).
  • maxFeeBps: Maximum fee the merchant allows (500 = 5%). The actual fee is determined by the operator's fee calculator at release time.

4. Attestation extension: arbiter identity in the 402 response

.registerExtension(createAttestationExtension(ARBITER_URL))

// On each route:
extensions: declareAttestationExtension(),

This is the first arbiter-specific addition. When the client gets a 402 "Payment Required" response, the attestation extension calls the arbiter's /attest/identity endpoint and includes the result in the 402 response under extensions.attestation. This tells the client before they pay: "an independent arbiter at this URL is watching this merchant."

The attestation includes the arbiter's identity, supported chains, a description of what it does, its available endpoints (skills), and a full explanation of the commerce payment scheme flow. AI agents can read this to understand how to pay and what protection they get.

The client can then verify the arbiter is real, check its operator address, and decide whether the protection is sufficient before sending money.

5. forwardToArbiter: post-settlement hook

.onAfterSettle(forwardToArbiter(ARBITER_URL))

This is the second arbiter-specific addition, and the most important one. After the facilitator settles the payment (authorizes funds into escrow), this hook fires and sends the response body + payment details to the arbiter's /verify endpoint.

The arbiter then:

  • Evaluates the response body (is it garbage? an error? placeholder text?)
  • On PASS: calls release() on the operator to send funds to the merchant
  • On FAIL: calls refundInEscrow() immediately to return funds to the payer

Without this line, funds would sit in escrow until the escrow period expires and someone triggers a refund. With it, the arbiter automates the release/refund decision in seconds.

What if the merchant cheats?

The arbiter stores every response body it evaluates, keyed by settlement transaction hash. If a malicious merchant sends good content to the arbiter (getting a PASS and release) but garbage to the client, the client can detect and recover:

  1. Detect: the client hashes the response they received and compares it against the responseBodyHash from GET /verdict/:tx (public endpoint). If the hashes differ, the merchant forwarded different content to the arbiter.

  2. Recover: the client signs the message "x402r:payload:{txHash}" with their wallet and calls GET /verdict/:tx/payload. The arbiter verifies the signature matches the payer address from the payment, then returns the actual content. Only the wallet that paid can retrieve the payload.

This means the arbiter doubles as a fallback delivery mechanism. The client always gets what they paid for, either from the merchant directly or from the arbiter if the merchant cheated.

Live now

The error detector arbiter is live on Base Sepolia and Base mainnet (beta). Payment settlement is handled by Ultravioleta DAO, our facilitator partner, who runs the commerce scheme infrastructure that verifies and settles escrow payments on-chain. If you're a facilitator interested in integrating the commerce scheme, reach out to us on Discord or at hi@x402r.org.

We'd love for you to test it out and would appreciate feedback! Check out the documentation for more information or join our Discord to chat with us.