Skip to Content
THULI

Limits and non-goals

What Thuli does not catch, does not defend against, has not built, and has not measured.

The "not built" items describe the code on 13 November 2026.

What Thuli does not catch

  • A farm that funds each wallet from its own exchange withdrawal, waits, and never sweeps will pass. Thuli makes farming dearer and explains its calls; it does not prove a wallet belongs to one person.
  • Farms funded through a batch-send contract (a router) are not grouped. A sweep into a contract wallet (a Safe, a forwarder) never links, so one contract collector avoids rule A3.
  • A farm running its own high fan-out funder is missed by the shared-funder rule.
  • A farm that pays its collector from enough throwaway wallets first makes it look public, so A3 stops firing there. Shared funding still catches it.
  • A funding burst split across UTC midnight, or kept under the minimum, is not caught.
  • Only native ETH funding is traced, not tokens. The replay, the free check and the paid check read Ethereum only; a campaign ask is read on Ethereum or Base mainnet. In the replay, transfers past a wallet's first 100 are not read.
  • One wallet screened alone cannot show a group, a shared deposit address, a funding burst or a claim paid elsewhere.

It can also be wrong the other way:

  • A new public address used first mostly by list wallets links them.
  • Wallets on one list could sweep into an address a victim once used and turn the victim's payment into an A3 exclude.
  • Someone who knew the list before the snapshot could fund listed wallets and cause holds, which a person reviews.
  • The shared-funder rule gave up holds that were sometimes right: on Hop's eliminated A8 and A9 sample, holds fell from 11.2% to 3.0%.
  • A funding burst cuts real users funded in one to partial too.

What Thuli does not defend against

  • Prompt injection inside the model. The protection is structural: a ceiling set by code, no stranger-written text in the input, and appeals that need an owner.
  • The campaign owner. An owner can set unfair settings, override any decision and withdraw after the end. The vault does not protect claimants from the project that funds it. A stolen agent key is covered on the vault page.
  • A lying data source. Chain data is not verified cryptographically; Thuli trusts its configured RPC endpoints and explorer and fails closed only on missing or inconsistent data.
  • Outside parties failing: the x402 facilitator, Kite Passport, the model providers, Circle freezing or pausing USDC, the Base sequencer.
  • Hiding claimants from the screening services. SERV sees full addresses.
  • A stolen claimant key. Whoever holds it can appeal and claim as that wallet.
  • Verdicts about people and scraping. A decision assesses on-chain behaviour from evidence and is not an accusation. Published decisions are public by design.
  • Farmers reading the rulebook. It is public so the process is fair and appealable. Farmers reading it is a stated non-goal.
  • Payment edge cases. Without a database, a restart can let a copy inside its window be answered from a facilitator's pending cache. A facilitator that fails after broadcasting leaves a payment unserved unless a later try of the same header is answered with its transaction. If the server copy screening a wallet stops part way, that wallet answers 202 for up to 330 seconds, until its place frees itself.
  • The paid route's two named limits. A crash in the instant between the facilitator settling a payment and the store recording it leaves that payment unredeemable: the money is on chain and is refunded by hand. To fix it, read settlements from the chain on a schedule and record any that have no claim. A redeem does not check the signature again, so anyone who rebuilds the payment header from the settlement on chain can fetch that wallet's stored result. It buys no screening the payment did not already buy. To fix it, check the signature on every redeem, at the cost of one more verification for each fetch.

What the campaign worker does not do

The campaign worker is the script that screens each ask, posts the approved wallets' list to the vault, submits their claims and sends appealed decisions to an outside check. It runs against Base Sepolia with test money. One copy is hosted for the demo campaign, as a single process on its own host, sharing a hosted database with the site; a project with its own campaign runs it from this repository. Its limits, each with what it would take to fix:

  • It judges each ask as it arrives, so it sees a group of linked wallets only as the group grows. Until a group reaches the campaign's group size (5 unless the campaign sets another), each wallet in it looks alone and can be approved, and a wallet that has been decided is not looked at again. Screening a whole claim list at once, as the replay does, has no such gap. To fix it, hold approvals until a campaign closes and screen the whole list at once, which makes every wallet wait.
  • It links wallets through the first wallet that funded each one, and only that one. If the money passed through in-between wallets first, the worker does not follow it back, where the offline replay follows up to 6 such wallets. To fix it, run the same trace on each ask, which costs more chain reads for every ask against the same ceilings.
  • A first funder that has sent 10,000 or more transactions, or is a contract, is treated as an exchange or a service and links nobody. Wallets funded straight from such an address are not linked by their funding. To fix it, a rule that tells a busy funder's real customers from a farm it feeds, which Thuli does not have.
  • A campaign ask is read on one chain, Ethereum or Base, whichever the wallet was active on first. A wallet used mostly on a third chain looks new, which can cut it to a partial reward. The free check, the paid API and the replay read Ethereum only. To fix it, read the same facts on each other chain, which needs a reader and a rule for every chain.
  • Only native ETH counts toward a wallet's first activity and its first funder. A wallet that has only ever held tokens such as USDC, and never sent a transaction, reads as never used. To fix it, read token transfers too, which needs a list of tokens that count as money.
  • Every threshold (30 days, 5 transactions, an exchange at 10,000 sends) and the false-hold rate on the control sample were measured on Ethereum history. Nothing has been measured on Base history, where transactions are cheaper and wallets are busier. To fix it, a labelled sample of Base wallets and a second measurement.
  • The two-chain read cannot date some wallets and holds them for a person: one whose first money arrived with no transaction (a staking payout or a block reward), one that delegated its code before its first own transaction, one paid in more than 24 separate blocks before it first sent anything, and one with more than 4 blocks of failed transactions before its first that succeeded. To fix it, an indexed history for those cases.
  • A group whose wallets share a funder on one chain is not linked when each wallet is older on the other chain, where its funder differs. To fix it, read the funder on both chains for every ask.
  • The two-chain read needs RPC endpoints that serve old state and traces, about 220 calls for one ask, with a ceiling of 1,000 calls for each of its three reads. If the endpoint is down, every try fails until it is back, and an ask that fails ten times is held until an operator puts it back in the queue. A failed read is never stored as a decision. Without such endpoints the worker reads Ethereum only, and decides only a wallet that two plain calls show was never used on Base: an ask from a wallet used on Base ends held for review too, and in that mode a stranger can cause this with 1 wei sent to the wallet on Base. The free public endpoints in .env.example are such endpoints. An endpoint that gives no answer to a plain read when the worker starts stops the worker, so an outage never chooses the mode.
  • A wallet whose history the reader cannot read whole is not decided either: more than 24 blocks that paid it before its first send, money that arrived with no transaction (a staking withdrawal, a block's fees), or a first block on its older chain that someone filled with more calls than the reader takes. Its ask ends held for review. For a wallet that has never sent, a stranger can cause the first of these, or fill a block that pays it with more calls than the reader takes, for cents on Base. To fix it, an indexed source of traces, which costs money for each ask.
  • Which chain is the older one can be arranged: 1 wei sent to a wallet on the other chain before its real funding makes that chain the one screened, where the wallet has done almost nothing. A group funded on one chain is then not linked by that funding. To fix it, read and link the funding on both chains for every ask.
  • The single-wallet reader used by the free check, the paid API and the replay fetches a wallet's internal transfers only when its ordinary transactions name no funder. For a wallet whose first money came through a contract and whose larger funding came later, it counts the age from the later transfer. A live comparison found this on 1 of 29 wallets (272 days against 283). To fix it, always read both lists, which would move the replay's measured numbers and so needs a new replay.
  • It uses only free public data: the wallet's own history on the chain it was screened on and the campaign's own asks. It does not measure a shared deposit address, a sweep to one shared collector, a funding burst, a claim paid to another address or funding through in-between wallets. Every decision says these were not checked.
  • It is meant to run as one copy. Two copies for a few seconds cannot pay anyone twice, because the vault pays a wallet only what it has not yet received. They can post the same list twice, which restarts that list's waiting time.
  • If the worker stops after it has sent a list, then restarts, the earlier transaction lands, and the owner cancels that list, all before the next run, the worker can post the cancelled list again. The owner's pause stops it. To fix it, read the owner's cancel from the vault's logs, not only from its state.
  • A resend of a lost post takes fresh fees with no bump, so a node that still holds the lost transaction at the same fee may refuse it. The worker tries again 10 minutes later, at most 5 times. To fix it, raise the fee on each resend by the amount nodes ask to replace a transaction.
  • If the worker cannot submit a wallet's claim after 10 tries, spread over about an hour, it stops trying. The wallet is still owed. It can claim for itself with the claim ticket the campaign route gives it, and pays its own gas. A pause of the vault uses up none of those tries.
  • If the project cancels a waiting list, the worker records that only after 2 minutes, and does not post that list again until a new wallet takes a slot. A project that wants no further lists pauses the vault, which also stops every claim until it unpauses.
  • Locking a campaign with fake asks costs an attacker at least 1,000 wallets that each hold a balance or have sent a transaction, and one set of funded wallets can fill every campaign's cap. A wallet that has never been used on Ethereum or on Base is turned away at the door and nothing is stored. To fix it, a higher bar for an ask, such as a minimum age or balance, which would also turn away real new wallets.
  • Model screening is capped at 2,000 a day across every campaign. An attacker with many addresses and aged wallets can use the day up, and real asks then wait until after midnight UTC. To fix it, a larger budget, or a small cost for each ask.
  • Owner sign-in is not built, so an operator's command settles appeals: npm run appeal:settle, a dry run unless --write is given. To fix it, owner sign-in and an owner route that calls the same settlement.
  • The scorecard does not score appeals yet. npm run appeal:export writes checked appeals as input for the next rulebook change, and nothing reads that file. To fix it, add the exported appeals to the scorecard's input.
  • Two appeals have been checked by Reppo live, both on 13 November 2026 and both against the same test wallet's partial decision: one came back review and one ship, each cost 440 credits, and the owner upheld both. The route block, a live reversal and the failure paths have run only in tests, against a stand-in for the outside check. To fix it, run more appeals, one of them reversed, and record each result. See Live campaign.

What Thuli has not built

  • Owner sign-in and an owner dashboard. There are no owner routes: no route creates a campaign or changes one. A project can connect its wallet on a setup page to create and fund a vault on Base Sepolia, and an operator then registers the campaign from the command line with npm run campaign:create. The public campaign routes take a signed ask and a signed appeal, show the board and serve a claim ticket, and the worker screens the asks and posts the lists (see above).
  • A second copy of the database. One hosted Postgres holds payment claims, rate limits, cached checks and the campaign tables (campaigns, asks with their decisions, list versions, claim submissions and appeals) for the site and the worker. No restore from a backup has been rehearsed. A server started with no database keeps payment claims, rate limits and checks in memory. Only the older offline appeal demo and the replay's run state are local files.
  • A general job queue. Only campaign asks, claim submissions and appeal checks are queued, each in its own table, and the worker takes them one at a time. The paid route screens inside the request, and the replays run as scripts.
  • Paid data through Kite Passport, Nansen or Allium, and Dolphin summaries. No code calls them.
  • Worker hosting as code, and a second worker. The worker that screens asks, posts lists and submits claims runs as one process on a host, set up by hand in the host's settings; the repository holds no container file or host configuration. If that process stops and the host does not restart it, asks wait in the queue and nothing is posted or paid until it is back.
  • A campaign on Base mainnet. The vault factory is deployed and verified on Base mainnet, and one proof vault there paid 1 real USDC to a listed wallet and returned the rest (see Contracts and addresses). That is one vault, one list and one wallet. The public campaign and every other address stay on Base Sepolia with test money.
  • The paid route has taken two real payments, 5 cents each on Base mainnet: one in a run on one machine and one on the live site (see Check a wallet and the paid API). Both came from Thuli's own wallet, so there is no paying customer yet. Its limits and its behaviour under many buyers have been run only in tests.
  • Owner sign-in for appeals. A wallet files an appeal through a public route, the outside check reads Thuli's decision, and an operator's command settles it. Contract wallets cannot appeal, and neither can a wallet whose type could not be read. A reversal gives the wallet the next free slot, and the next list the worker posts carries it, so the vault pays it only after that list's delay.
  • An external audit.

What Thuli has not measured

  • How a model answers under a different rulebook text. The versions comparison checks one stored reply per wallet under both rulebooks.
  • Reppo judged decisions made before the shared-funder rule, so no verdict in that sample is on a v2 decision. The two appeals Reppo has checked are v2 decisions on one wallet and are not part of the sample.
  • The false-hold rate on real users. Hop's probably-real wallets are not proved real, and the rate comes from a sample. Holds outside Bitquery's key are an upper bound on false holds, because the key may miss farms. Catching Hop's eliminated wallets is partly by construction, not a recall claim.
  • Dollar figures on the site are unrealised: they value ROBO at the claim-day price, nothing was sold or paid, and a real sale would move the price.
  • The cost of paid data lookups, because they are not built.
  • Vault edge cases. The random call-sequence tests use one vault, lists of up to five wallets and plain tokens. The fork tests do not cover Circle blacklisting USDC.