
yohaku
Agents never sleep. People do. Yohaku routes their requests, so only what needs a human reaches one.
Demo
About
Scraping the open web is free and contaminated. A model can invent an answer to "what made you put it back on the shelf." It cannot invent a true one. So the scarce thing is no longer information — it is an answer only a person who lived it can give. That is a market with a problem: the buyer never sleeps and the seller does. Open a counter in the agent economy and about fifty requests arrive overnight. Answer them by hand and it does not scale. Approve them automatically and you have lost control of your own data. Yohaku is the layer in between. Ten ordered rules sort every incoming request into auto, human or deny — first match wins. What reaches the person is bundled, ranked, and held to a daily cap she sets. Across twenty runs the arrivals wander between 46 and 54, and the number reaching her is 2 in every single one. That is the output of npm run verify, not a claim. Three properties are the product. Everything fails to deny. Engine down, screening unreachable, identity check failed, deadline passed with no answer — all of them refuse. Silence is not consent. The guarantee lives in the schema. A model is consulted on exactly one rule, and the only values it can return are ask and drop. There is no value meaning pass, so a prompt injection that fully convinces the model still cannot widen access — widening is not expressible. A refusal you cannot inspect is the same as never having been asked. Agents really do ask for a home address and for a wallet. Those requests are in the demo, refused under a rule the owner set, and she can open /dropped and see what each one wanted beside the rule that stopped it. We charge for the decision, never a share of what she earns: one unit per request routed, offered as an optional extension on the payment the buyer was already making. Her proceeds never enter that ledger, and its summary is typed so it cannot report collected money. MIT licensed, because a surface that decides what a person never sees has to be one they can read.
How it's made
TypeScript, no framework. src/core is pure — the router takes a request, a policy and a clock and returns a verdict; no I/O, so the same code runs in the tests, in the server and in a single-file console that opens from a USB stick with no network. Ports and adapters: ClassifierPort, IdentityPort, ScreeningPort, PermissionsPort, SettlementPort. The guarantee is in the port's schema, not in the adapter, so swapping an implementation cannot weaken it. World ID (OIDC). Discovery read at run time — no endpoint is written in our source. PKCE with S256, which turned out to be mandatory and undocumented in the sandbox; we found it by probing eight parameter combinations against a bare invalid_request with no error_description. The ID token is verified against the issuer's JWKS with jose, acr is required to be the orb level, and auth_time is checked against this server's clock with a 120-second ceiling. We store only verifiedAt and acr — never the subject identifier. Measured caveat, stated because a summary that omits it is wrong: sending both prompt=login and max_age=0, the sandbox answers amr: ["pop"] with auth_time re-stamped one second before iat, and never hands off to the World ID app — through Safari, Safari private and Chrome alike. So what is true is "a credential only an orb-verified person holds is presented at that moment", not "she re-proves personhood in the app". The product screen says exactly that, and FreshnessPolicy.acceptedAmr exists so a deployment that needs more can refuse pop. x402 (protocol v2, Base Sepolia). The published x402 npm package still speaks v1 (X-PAYMENT); the deployed facilitator answers x402Version 2 (PAYMENT-SIGNATURE). We build on @x402/core, whose only dependency is zod, and accept both header names. The server confirms at startup that the facilitator actually supports the configured scheme and network. The design decision worth pointing at: an EIP-3009 authorization carries validBefore, and we set it to the request's deadline. The agent signs before she wakes up; the authorization moves nothing while it waits; settlement runs from exactly one place in the code, after a verified yes. If she never answers, the authorization expires and nobody can settle it — not the agent, not us. "Silence is not consent" stops being a server behaviour and becomes a property of the money. Two transfers on Base Sepolia, verified by reading the chain rather than trusting the facilitator's response: 0x79c1e323… (120 atomic, settled inside the request) and 0x5c79fddf… (4,200 atomic, which moved only when a person pressed Yes — the seller's balance was 120 before and 4,320 after). The buyer's ETH balance is still zero; the facilitator paid 102,844 gas. ENSv2 (Ethereum Sepolia — a different chain from the settlement, which costs an hour if you confuse them). A name is registered and resolved through a permissioned resolver: yohaku-minta-2026.eth, receipt 0x8e13e0ad…, resolver a proxy of PermissionedResolverImpl, paid 8.000021 MockUSDC for 365 days. Role bits confirmed from the chain via decodeSetter — all five text keys at role 16 — after direct eth_call to the implementation reverted for every selector. The delegate boundary runs on chain, not against a mock. A delegated account was granted the setter role for yh:proposal only; it wrote yh:proposal and succeeded (0x16873dfa…), wrote yh:policy and reverted with EACUnauthorizedAccountRoles (0xb459618b…), and after the owner revoked, the proposal write reverted too (0x2380d0fe…). Six receipts, in order, on Ethereum Sepolia. Simulating the same write today still reverts with the same error, so it is not a historical artefact — the contract refuses it right now. Anti-guessing is enforced rather than remembered. npm run verify re-derives every number in the documents by running the code, and refuses any hardcoded model id, API endpoint or contract address in src/ or scripts/. It also fails if any published page quotes a test or claim count the test run did not produce. We added that last check today and it immediately found seventeen stale counts, three of them on our own published site. It then caught its own author: adding an eleventh claim made the count in eight other files wrong, and the check failed until those were fixed too. The claims are published as machine-readable JSON, because the reviewer we are most likely to get is an agent. 343 tests. 12 verified claims. Two numbers in these documents were measured, found false, and withdrawn in place — the retraction is still visible in ASSUMPTIONS.md.