docs three endpoints, one response shape

Documentation

Paid per call. Nothing to sign up for — payment is the authentication.

Base https://arbiter-hs23.onrender.com Network Algorand mainnet Asset USDC · ASA 31566704

01 payment

How paying works

Arbiter uses x402: an unpaid request is answered with 402 Payment Required and the terms; the client pays and retries. There are no API keys and no accounts.

the 402 handshake
# 1. Ask without paying
POST https://arbiter-hs23.onrender.com/v1/judge/transaction
→ 402 Payment Required
→ PAYMENT-REQUIRED: <base64 terms: amount, asset, payTo, network>

# 2. Client signs a USDC payment and retries with proof
POST https://arbiter-hs23.onrender.com/v1/judge/transaction
     X-PAYMENT: <signed payload>
→ 200 OK, verdict body

The SDK does both steps. You call one method and never see the 402.

SettingValue
NetworkAlgorand mainnet
AssetUSDC · ASA 31566704
Facilitatorhttps://facilitator.goplausible.xyz
Pays toGBRO5EM4JM57PDPS4A533V7JZNWFWB7S6IRT5UZNQYPPUN7XEEGJLEHW6I

Your paying account needs USDC and must be opted in to ASA 31566704. On Algorand a transfer to an account that has not opted in is rejected outright, so an un-opted-in payer simply cannot pay. Fees are sponsored by the facilitator, so the payer does not need ALGO.

02 surface

Endpoints

RoutePriceAnswers
POST /v1/judge/transaction $0.002 Is this transaction safe to sign?
POST /v1/judge/counterparty $0.01 Is this address who I think it is, and can it receive?
POST /v1/judge/human $0.25 What would a person say about this?
GET /v1/judge/human/:taskId free Collect a human verdict you already paid for

Transaction — Algorand

request
{
  "chain": "algorand",
  "transaction": "<base64 unsigned txn>",   // or an array for an atomic group
  "signer": "<58-char address>"          // optional; enables self-harm detection
}

Transaction — EVM

request
{
  "chain": "evm",
  "chainId": 1,                          // 1, 8453, 42161, 137, 11155111, 84532
  "transaction": {
    "to": "0x…",
    "data": "0x…",                      // omit for a plain value transfer
    "value": "0"
  }
}

Counterparty

request
{
  "address": "<58-char address>",
  "expectedAsset": "31566704",          // enables the opt-in check
  "amount": "250.00",
  "claimedIdentity": "acme.algo"          // detects a swapped payment address
}

Human

request
{
  "question": "Does this photo show a package at a front door?",
  "attachments": ["https://…"],
  "options": ["yes", "no", "unclear"],      // omit for free text
  "quorum": 3,
  "waitSeconds": 60                        // long-poll; 0 returns immediately
}

If reviewers do not finish inside waitSeconds you get a pending verdict and a taskId. Collect it later from the free retrieval endpoint — timing out never costs a second payment.

03 contract

The verdict

Every route returns this. Integrate the shape once.

response
{
  "decision": "block",        // allow | warn | block | escalate
  "risk": 100,                // 0–100
  "confidence": 1,            // 0–1
  "findings": [{
    "code": "txn.rekey_to_third_party",
    "severity": "critical",   // info | low | medium | high | critical
    "title": "…",
    "detail": "…",
    "source": "arbiter:decoder"
  }],
  "evidence": { /* route-specific: the decoded txn, the account record */ },
  "ttlSeconds": 60,
  "meta": { "latencyMs": 359, "degraded": false }
}
DecisionWhat to do
ALLOWNothing blocking was found. Proceed.
WARNProceed only if your operator accepts the listed risks.
BLOCKDo not proceed. Acting on this would likely cause loss.
ESCALATENot enough evidence to decide. Not an approval. Ask a human.

Two behaviours worth relying on. confidence below 0.4 returns escalate rather than allow, so a thin-evidence verdict can never be mistaken for a clean bill of health. And meta.degraded is true when an upstream was unreachable — the verdict is still useful, because the decode rules that catch the critical severities do not depend on the network, but it is partial.

04 clients

Clients

PackageFor
@arbiterlabs/sdkTypeScript. Pays x402 automatically.
@arbiterlabs/mcpAny MCP host.
@arbiterlabs/langchainLangChain.js tools.
@arbiterlabs/elizaElizaOS plugin.
@arbiterlabs/proxyLocal paying sidecar for non-TypeScript agents.
arbiter-crewaiCrewAI, via the sidecar.

Spend limits are part of the client

An agent stuck in a retry loop against a paid endpoint is a wallet-draining bug, so the cap is enforced in the payment selector — before anything is signed — rather than left to the integrator to remember.

client.ts
const arbiter = new ArbiterClient({
  baseUrl: "https://arbiter-hs23.onrender.com",
  privateKey: process.env.ALGO_KEY,
  maxPricePerCallUsd: 0.05,   // refuse any single call above this
  maxTotalSpendUsd: 10,       // lifetime cap for this client
});

Python and other languages

Paying x402 on Algorand requires an AVM scheme client, which today exists only in TypeScript — the published Python packages ship none. Run the sidecar instead; it holds the key, pays, and re-exposes the judgments unpriced on loopback.

shell
ARBITER_URL=https://arbiter-hs23.onrender.com \
ARBITER_PRIVATE_KEY=… \
npx arbiter-proxy

05 failure modes

Errors

StatusMeaning
402No payment attached, or the payment was not accepted. Usually the payer holds no USDC or is not opted in.
400Request body did not match the schema. The response lists the offending fields — you have already been charged, so it names what to fix rather than telling you to retry.
404Unknown route, or a human taskId that does not exist.
409Reviewer submission rejected — already answered, or the task closed.
500The verdict could not be produced. This call should not have been charged.

06 supply side

Reviewer API

Unpriced. Reviewers are the supply side and get paid, so charging them to see the queue would be backwards. Most people should just use the app.

RouteDoes
POST /v1/work/registerRegister; returns a bearer token, shown once.
GET /v1/work/queueOpen questions you have not answered.
POST /v1/work/:taskId/submitAnswer, with a rationale.
GET /v1/work/earningsUSDC owed and settled.