Skip to Content
THULI

Try it

What to do on the website, and the commands to run if you have the repository.

On the website

You need nothing installed for these four.

  1. Open the replay. Pick a family of wallets, then open one wallet. You will see its outcome, the rule that decided it, the reason in plain words, and the transactions the reason cites.
  2. Check a wallet at /check, or with the box on the home page. Paste any Ethereum address. Thuli reads its public history and applies the same rulebook to that one wallet, with no sign-in. You get the outcome, the rule, the reason, the transactions it cites and a list of what one wallet on its own cannot show. A new check can take up to two minutes. Each visitor gets 5 new checks an hour, and a result is kept for a day, so checking the same wallet again costs nothing.
  3. Read the versions page. It shows the rulebook before and after the one change outside feedback led to, scored on the same stored answers.
  4. Attack the system at /break. It has ten rows, each a real attack: five against the proof vault on Base Sepolia, four against this site's own routes, and one against how a reason is shown. A vault attack is a simulated call, so it sends nothing and needs no wallet. Press "Run all 10 attacks", or run one row, and read the answer. The four rows against the site meet the same per-caller limits as any visitor, and a row that meets one shows 429 and counts as refused. Once the proof vault's campaign has ended, the vault refuses every claim for that reason, and the page says so beside the answer.

Try the campaign

The campaign page shows the campaign the site is set up with. If the site has none open, the page says "No campaign is open right now." When one is open you need a wallet you control and nothing else. Asking costs no gas, and the reward is test USDC on Base Sepolia, which has no value. What happens behind each step is on Live campaign.

  1. Open the campaign page and connect a wallet. Pick one you do not mind showing in public. The board lists a wallet's address, its decision and the reason.
  2. Press Sign and ask. The page first shows the four-line message your wallet will sign. Signing costs nothing and moves nothing.
  3. Watch the page. It shows that your wallet is being screened, then your decision with its rule, its reason, the transactions the reason cites and the things the screening did not measure. The same decision appears on the board for everyone to read.
  4. If you are approved you hold a slot. Your reward goes on the next list Thuli posts to the vault, and the page shows that list and the time it starts paying. The vault waits its delay, at least 1 hour, before it pays any list. After that the worker submits your claim and the page shows the reward as paid.
  5. If you are held, excluded or cut to a partial reward, the page shows a form headed "DISAGREE?". Write one line, press Appeal, and your wallet signs a second message. The steps behind it, and what the outside check can and cannot do, are in the appeals part of Live campaign.

What to expect:

  • A wallet that has never been used is turned away at the door. If it has sent no transaction and holds no balance on Ethereum or on Base, the page says "This wallet has never been used" and stores nothing. It still counts toward the visitor's 5 signed asks an hour.
  • A wallet whose history shows no incoming transfer of value has no first funder to read. A fact that cannot be read means hold, under rule A1, and the board shows the rule and the reason.
  • A wallet with some history that is young or quiet gets a partial reward, not a hold. By default that is under 30 days old or fewer than 5 transactions sent, under rule A8.
  • A wallet used only on a third chain looks new, because only Ethereum and Base are read.
  • When every slot is taken, the page says "Every slot is taken" and offers no button. When the campaign has taken all the asks it will, it says "This campaign has stopped taking requests". When 200 undecided asks are already waiting, the route answers full and the page says "This campaign is full". Nothing is stored, and signing cost nothing.
  • Two appeals have been run live, one of them on this site's demo campaign. They are described on Live campaign.

With the repository, npm run rehearse:campaign runs the whole path on a local copy of Base Sepolia. Six signed asks are screened through the real decision path with live SERV, the approved wallets take the campaign's three slots, the list goes to a vault made by the real factory, the copy's clock moves past the delay, and the worker submits each claim. It needs Foundry's anvil, and the Base Sepolia, SERV and Ethereum settings from .env. It loads no key from .env: the owner, the agent and the six asking wallets are fresh keys made for the run, and every transaction stays inside the local copy.

With the repository

You need Node 26 or newer.

npm install
npx vitest run
npm run typecheck

npx vitest run runs every test under the packages folders. That covers the fixed checks, the facts engine, the output checks on model answers, claim lists and Merkle leaves, the appeal and ask checks, the replay scoring, the paid and free API routes with their limits, the store, and the web app's data loading. It does not run the Solidity contracts, and it is not the proof against live networks. The contract tests and the proof commands below cover those.

npm run typecheck runs the TypeScript compiler over the shared code, the worker, the API, the store and the web app, and writes no files.

The contracts have their own tests and need Foundry installed:

npm install --prefix packages/contracts
npm run contracts:test

On 11 November 2026 forge test reported 77 tests passed, none failed and 2 skipped with no Base RPC set, and 79 passed with BASE_RPC_URL set. The 2 skipped are the demo test and the fork test. The 79 are 64 unit tests, 1 demo test, 1 leaf fixture test, 11 fuzz tests at 10,000 runs each, 1 fork test against real Base USDC, and one invariant suite holding 7 invariants at 256 runs of depth 64. The demo test and the fork test run on a local copy of Base and are skipped unless BASE_RPC_URL is set.

Run the site yourself

The site serves the pages, these docs, the free check and the paid route. Copy .env.web.example to packages/web/.env.local, fill in the empty values, then run npm run web:dev and open http://localhost:3000. The values, by name:

  • X402_NETWORK: base for real USDC or base-sepolia for test USDC. It has no default, so testnet is a choice.
  • The address that receives payments: PAYTO_ADDRESS_TESTNET on Base Sepolia, PAYTO_ADDRESS on Base. It is an address, never a key.
  • SERV_API_KEY, SERV_BASE_URL and SERV_GRAPH_ID, from the SERV console. Every screening calls SERV with them.
  • ETH_RPC_URL and BLOCKSCOUT_ETH_API, where a screening reads the wallet's history. For a campaign ask the worker also reads BASE_RPC_URL, and when both RPC endpoints serve old state and traces it reads the history from them alone, on Ethereum or on Base.
  • On Base only: CDP_API_KEY_ID, CDP_API_KEY_SECRET, BASE_RPC_URL and the server's own https origin. The origin is PUBLIC_ORIGIN, or on Vercel the production host the platform names, so there it can stay empty. On Base Sepolia the origin is optional, and FACILITATOR_URL left empty means https://x402.org/facilitator.
  • DATABASE_URL and TRUSTED_CLIENT_HEADER, which a production server must have. Locally, leave DATABASE_URL empty and the site runs an in-process database whose contents are lost on restart.

No variable whose name ends in PRIVATE_KEY may be set in the site's environment. If one is, the standalone API refuses to start, and the site answers 503 to every request under /api/check and /api/x402, logging the variable's name and never its value. The refusal reads NAME is set, and this process must hold no private key, because it serves the paid API to the internet. Start it without NAME., with the variable names where it says NAME. Keep test keys in the files at the repository root, .env and .env.payer, never in packages/web/.env.local or .env.api.

To run the paid and free API on its own, copy .env.api.example to .env.api and run npm run api:dev. It listens on port 8787 unless PORT is set, and it takes the same network, payment, SERV and RPC settings. The scripts for the replay, the scoring and the vault proof read .env at the repository root, copied from .env.example.

Proof commands

These commands produce the numbers this documentation quotes. Some need data or keys that are not in the repository. The table says exactly what each one needs.

CommandWhat it doesWhat it needs
npm run prove:vault -- verifyChecks the committed vault proof record against Base Sepolia, line by line, in 14 checks. Sends nothing and loads no key. The checks are listed on Contracts and addresses.npm install, the two committed files in packages/contracts/deployments, and a Base Sepolia RPC URL. With BASE_SEPOLIA_RPC_URL unset it uses the public https://sepolia.base.org, which is rate limited. It does not check that the refused wallet's claim reverts, because that needs a private claim list.
npm run prove:vault -- recordReads the vault proof run back from Base Sepolia. Loads no key and sends nothing. Writes the public proof record only if every receipt, event and vault read agrees. Not for strangers.An https Base Sepolia RPC URL in BASE_SEPOLIA_RPC_URL, and the saved state file of the setup run. That file is private and stays on the machine that ran the setup, so nobody else can run record without running setup and claim first.
npm run versions:scoreScores rulebook version 1 against version 2 on the stored model answers. Refuses all network access. Stamps the scorecard with the day it ran, today in UTC unless --run-date YYYY-MM-DD is given.The stored replay data, described below. The script starts with --env-file=.env, so copy .env.example to .env first. No value in it is used.
npm run export:replay -- --spent-cents N --run-date YYYY-MM-DD [--out DIR]Rebuilds the replay offline, checks the stored answers under the current rulebook, and writes the files the site reads. The scorecard keeps the run date versions:score gave it.The stored replay data, and a finished versions:score run.
npm run prove:paid -- [--wallet ADDRESS] [--url ORIGIN] [--save FILE] [--save-terms FILE]Pays for one screening of one wallet, by default a ROBO farm wallet, fetches the stored result 10 more times with the same payment and expects each copy to be identical, expects a fresh header that never settled to buy nothing, then checks that exactly 5 cents left the paying wallet. With --url it pays a site that is already running instead of starting the API in its own process. --save writes the answer to a file and --save-terms the payment terms it was quoted. The API it starts itself is kept out of the facilitator's public catalogue, because nobody else can reach it.A wallet with at least 5 cents of USDC on the network X402_NETWORK names (test USDC on Base Sepolia, real USDC on Base), its key in .env.payer, and the paid API settings in .env.api, including a SERV key. A site named with --url must use the same network and payment address.

The stored replay data is the ROBO and Hop replay rows, the stored SERV answers, the saved Reppo results and the answer key files. It is not in the repository, because the source lists carry no licence that lets us republish them. The repository does contain what the export wrote: the published files under packages/web/public/replay, which hold the replay, scorecard and Hop numbers quoted in these pages.

You can check the vault proof without any keys or stored data. Run npm run prove:vault -- verify, or read the record in packages/contracts/deployments/base-sepolia-proof.json and look the transactions up on the Base Sepolia explorer.

prove:vault takes four phases on Base Sepolia: setup, claim, record and verify. With --network base --real-money it has six phases for the small Base mainnet proof, which was run on 12 November 2026 and is described on Contracts and addresses. Only verify is for strangers. To run setup yourself you need two different test keys, set as PAYER_PRIVATE_KEY and WORKER_PRIVATE_KEY in .env: an owner with at least 2 Base Sepolia USDC and some test ETH for gas, and an agent with test ETH. Free test USDC comes from the Circle faucet. The root waits one hour on chain, so claim only works an hour after setup.