Skip to main content

Prerequisites

  • A wallet with ETH on Base Sepolia for gas (faucet)
  • Node.js 18+ and npm
  • Operator and escrow addresses from the Merchant Setup
The AI garbage detector example implements this pattern end-to-end with heuristic plus LLM evaluation.
1

Install dependencies

npm install @x402r/sdk @x402r/helpers
2

Create the arbiter client

The role-narrowed createArbiterClient exposes payment.voidPayment, payment.getState, and payment.getAmounts. Capturing requires the full surface, so use createX402r() directly:
import { createPublicClient, createWalletClient, http } from 'viem'
import { baseSepolia } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
import { createX402r } from '@x402r/sdk'

const account = privateKeyToAccount(process.env.ARBITER_PRIVATE_KEY as `0x${string}`)

const arbiter = createX402r({
  publicClient: createPublicClient({ chain: baseSepolia, transport: http() }),
  walletClient: createWalletClient({
    account,
    chain: baseSepolia,
    transport: http(),
  }),
  operatorAddress: process.env.OPERATOR_ADDRESS as `0x${string}`,
  escrowPeriodAddress: process.env.ESCROW_PERIOD_ADDRESS as `0x${string}`,
})
3

Handle the verify endpoint

The merchant’s forwardToArbiter() hook POSTs { responseBody, transaction, paymentInfoWire } to /verify. The paymentInfoWire is the JSON-safe wire form of PaymentInfo; call PaymentInfo.fromWire(...) to recover the bigint-typed struct expected by SDK actions.
import { PaymentInfo } from '@x402r/sdk'
import express from 'express'

const app = express()
app.use(express.json())

app.post('/verify', async (req, res) => {
  const { responseBody, transaction, paymentInfoWire } = req.body

  if (!paymentInfoWire) {
    res.status(400).json({ error: 'missing_payment_info' })
    return
  }

  const paymentInfo = PaymentInfo.fromWire(paymentInfoWire)

  const passed = await evaluate(responseBody)

  if (passed) {
    const amounts = await arbiter.payment.getAmounts(paymentInfo)
    await arbiter.payment.capture(paymentInfo, amounts.capturableAmount)
    res.json({ verdict: 'PASS' })
  } else {
    // Arbiter can refund immediately without waiting for escrow expiry.
    await arbiter.payment.voidPayment(paymentInfo)
    res.json({ verdict: 'FAIL' })
  }
})

app.listen(3001)
4

Implement your evaluation logic

The evaluate() function is where your logic lives. It can run:
  • Heuristic checks: HTTP status code, response size, content-type validation
  • AI judgment: send response body to an LLM and ask “is this a valid response?”
  • Schema validation: check if the response matches an expected JSON schema
async function evaluate(responseBody: string): Promise<boolean> {
  // Reject empty or error responses
  if (!responseBody || responseBody.length < 10) return false
  if (responseBody.includes('"error"')) return false

  // LLM evaluation
  // const result = await llm.evaluate(responseBody)
  // return result.verdict === 'PASS'

  return true
}
5

What happens on failure

With delivery protection, the arbiter can call voidPayment() immediately on a FAIL verdict. You do not need to wait for escrow expiry. The receiver (merchant) can also trigger a voluntary refund at any time.
If your service goes down, no payments get evaluated and funds stay in escrow until timeout. The escrow period protects payers, but add uptime monitoring and alerting.

Next Steps

Merchant Setup

Deploy the operator and configure forwardToArbiter().

Examples

Runnable examples for every SDK operation.