# Knock docs
> Let one real human in, once. Add a Knock button to your site with door-sdk, and check each knock with a verifier you run.
Knock lets a site let in a real person, once, with no account, form or captcha. The person owns a **Wedgie**: a small USB device with a screen and two buttons, whose key is bound to their World ID on World Chain and named in ENS. When they knock on your site, the Wedgie shows your site's address, they press green, and your verifier checks what the Wedgie signed against ENS and World Chain.
You add two things to your site:
- **A Knock button**, with [`@knock-dev/door-sdk`](reference/door-sdk.md). Its one function, `knock()`, opens Knock's Door and hands back what the Wedgie signed.
- **A verifier**, which gives out challenges and checks each knock, eight checks in order: [`@knock-dev/verify`](guides/verify-in-your-server.md) inside your server, if it runs JavaScript, or [`knock-verifier`](guides/verifier.md), a small Rust service beside your site. If your server is in Rust, you can embed the same checks with the [`knock-verify`](reference/knock-verify.md) crate instead.
## What a knock tells you
A knock that passes your verifier tells you that, just now, on your site:
- someone pressed green on a Wedgie whose key is in ENS under a name, such as `wedgie.itzraj.eth`;
- the Wedgie signed your challenge, for your origin, with a counter higher than in any knock of its you've seen;
- the name is switched on, so its owner hasn't revoked it or killed it;
- that key is bound on World Chain to one human, proven with World ID;
- if you asked, this human hasn't done this action on your site before.
It doesn't tell you who the person is. You get the Wedgie's name and the human's **nullifier**: a number that is the same for one human everywhere Knock is used, and that says nothing else about them. [How a knock works](how-it-works.md) goes through each step.
## The parts
| Part | What it does | In the repo |
| --- | --- | --- |
| `@knock-dev/door-sdk` | The browser side. `knock()` opens the Door and resolves with the knock. One TypeScript file, no dependencies. | [`packages/door-sdk`](https://github.com/getknock/knock/blob/main/packages/door-sdk/src/index.ts) |
| `@knock-dev/verify` | The checks as a TypeScript package, for a server that runs JavaScript: Node, Bun, Deno, serverless. | [`packages/verify`](https://github.com/getknock/knock/blob/main/packages/verify/src/index.ts) |
| `knock-verifier` | Your verifier: `POST /challenge`, `POST /verify` and `GET /health`, with SQLite for what's been used. | [`server/verifier`](https://github.com/getknock/knock/blob/main/server/verifier/src/main.rs) |
| `knock-verify` | The checks themselves, as a Rust library, for your own server. | [`crates/knock-verify`](https://github.com/getknock/knock/blob/main/crates/knock-verify/src/lib.rs) |
| The Door | Knock's popup, at `https://door.getknock.dev/knock`. It talks to the Wedgie over USB, from the browser. | [`apps/door`](https://github.com/getknock/knock/blob/main/apps/door/app/knock/Door.tsx) |
| The Knock Console | Where a person sets up their Wedgie, at `https://getknock.dev`: its name, its binding, and its records in ENS; and the kill switch. Sites never call it. | [`apps/door`](https://github.com/getknock/knock/blob/main/apps/door/app/console/Console.tsx) |
| knockd | Relays a Wedgie's binding to World Chain, and signs World ID requests, while a person sets it up. Sites never call it. | [`server/knockd`](https://github.com/getknock/knock/blob/main/server/knockd/src/main.rs) |
| `@knock-dev/protocol` | The messages a Wedgie signs (EIP-712), key helpers, and test vectors. | [`packages/protocol`](https://github.com/getknock/knock/blob/main/packages/protocol/src/index.ts) |
| The Human Club | A demo site: petitions and fundraisers where each human signs once, checked with `@knock-dev/verify`. | [`apps/bouncer`](https://github.com/getknock/knock/blob/main/apps/bouncer/lib/club.ts) |
Every part is open source, in [`getknock/knock`](https://github.com/getknock/knock), and you can [host any of them yourself](self-host/overview.md).
## Where Knock is today
Knock is new. Read this before you plan around it:
- **Nothing is on a registry yet.** Every package is at 0.1.0: door-sdk and `@knock-dev/verify` aren't on npm, and knock-verify isn't on crates.io. The [quickstart](quickstart.md) uses a checkout of [`getknock/knock`](https://github.com/getknock/knock).
- **Knocking needs a desktop Chromium browser**, such as Chrome, Edge, Brave or Arc. The Door reaches the Wedgie over Web Serial, which Firefox, Safari and phones don't have.
- **Only one kind of binding counts**: those made on KnockBinding v3's production deployment on World Chain. Bindings from World ID's Simulator (staging) never count.
- **One mode.** Mode 1, Bound, takes the human from the Wedgie's binding. Mode 2, Fresh, a new World ID proof with each knock, isn't offered.
- **The nullifier is public.** It sits in the name's ENS records, so two sites can tell that the same human knocked on both. [Security](security.md) says what follows from that.
## Start here
- New to Knock? The [quickstart](quickstart.md) gets a button and a server that checks knocks working in about fifteen minutes.
- Adding Knock to a real site? Read [Add a Knock button](guides/knock-button.md), then [Check knocks in your server](guides/verify-in-your-server.md) or [Run a verifier](guides/verifier.md).
- Running Knock's parts yourself? [Host Knock yourself](self-host/overview.md).
- Want each human in once? [One human, once](guides/one-human-once.md).
- A knock bounced? [Troubleshooting](troubleshooting.md) and [Checks and refusals](reference/checks.md).
- Using a coding agent? Point it at [`SKILL.md`](agents.md).
# Quickstart
> A Knock button on a page, a server that checks each knock with @knock-dev/verify, and one human let in, in about fifteen minutes.
By the end, you'll have a page with a Knock button, a small server that checks each knock with `@knock-dev/verify`, and a Wedgie that gets in once and is turned away the second time.
## Before you start
- **Bun** 1.4 or later.
- **A checkout of [`getknock/knock`](https://github.com/getknock/knock)**, with `bun install` run in it. The packages aren't on npm yet, so you'll import them from the checkout, working in a folder beside it.
- **To knock:** a Wedgie that's set up in the [Knock Console](https://getknock.dev), meaning named and bound to a human, and Chrome or another Chromium browser on a desktop.
```bash
git clone https://github.com/getknock/knock
cd knock && bun install && cd ..
```
> [!NOTE]
> You can build all of this without a Wedgie. You need one only at step 3, to press green. [Test your integration](guides/testing.md) covers what you can check without one.
## 1. Check knocks in your server
Your server checks each knock with `@knock-dev/verify`, Knock's eight checks as a TypeScript package. It gives out a challenge before each knock, then checks what the Wedgie signed against ENS and World Chain.
Make a folder beside your checkout:
```bash
mkdir knock-quickstart && cd knock-quickstart
```
Then the server, in one file. It serves your page and answers two routes: one gives out challenges, the other checks knocks.
```ts title="server.ts"
import { Refusal, Verifier, memoryStore } from '../knock/packages/verify/src/index.ts'
import page from './index.html'
// Checks knocks for this page's origin. It keeps what it has seen in memory, which is fine for one process.
const verifier = new Verifier({ origin: 'http://localhost:5173', store: memoryStore() })
// A refusal as JSON, with the HTTP status knock-verifier would give it
function refused(e: unknown) {
if (e instanceof Refusal) return Response.json(e, { status: e.status })
throw e
}
Bun.serve({
port: 5173,
routes: {
'/': page,
'/api/challenge': {
POST: async () => {
try {
const { challenge } = await verifier.challenge()
return Response.json({ challenge })
} catch (e) {
return refused(e)
}
},
},
'/api/knock': {
POST: async (req) => {
const { assertion } = await req.json()
try {
// 'enter' lets each human in once. Leave the action out to let them in every time.
const k = await verifier.verify(assertion, { action: 'enter' })
return Response.json({ ok: true, name: k.name })
} catch (e) {
return refused(e)
}
},
},
},
})
console.log('Open http://localhost:5173')
```
The package isn't on npm yet, so the server imports it from your checkout.
The verifier checks knocks for one origin: the scheme, host and port your page is served from. Here that's `http://localhost:5173`, where this server serves the page. It reads both chains through public RPCs unless you give it your own, and it needs no keys.
`memoryStore()` keeps the challenges the verifier gave out, each Wedgie's counter and each human's entries in memory, and forgets them when the server stops. A real site keeps them in a database: [Check knocks in your server](guides/verify-in-your-server.md) shows how, with every setting.
If your server isn't JavaScript, run [`knock-verifier`](guides/verifier.md) beside it instead. It runs the same checks as a small Rust service, and your server asks it over HTTP.
## 2. Add the button
The page is two files beside `server.ts`. It imports door-sdk, the browser side of Knock, from your checkout too. door-sdk opens Knock's Door, at `https://door.getknock.dev`: the popup that talks to the Wedgie over USB. There's nothing to run for it.
```html title="index.html"
Knock
```
A knock is two requests to your server with one press in between: a challenge before, and the check after.
```ts title="main.ts"
import { KnockError, knock } from '../knock/packages/door-sdk/src/index.ts'
const button = document.querySelector('#knock')!
const said = document.querySelector('#said')!
/** A fresh challenge from your server: single use, good for 2 minutes. */
async function challenge(): Promise<`0x${string}`> {
const r = await fetch('/api/challenge', { method: 'POST' })
const j = await r.json()
if (!r.ok) throw new Error(j.why)
return j.challenge
}
button.addEventListener('click', async () => {
button.disabled = true
said.textContent = 'Waiting for your Wedgie…'
try {
// Call knock() in the click itself: browsers allow a popup only there.
// It opens the Door at once and sends the challenge when it arrives.
const assertion = await knock({ challenge: challenge() })
said.textContent = 'Checking…'
const r = await fetch('/api/knock', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ assertion }),
})
const v = await r.json()
said.textContent = v.ok ? `In: ${v.name}` : `Bounced at check ${v.check}: ${v.why}`
} catch (e) {
said.textContent = e instanceof KnockError ? `No knock: ${e.message}` : String(e)
} finally {
button.disabled = false
}
})
```
The page only shows what the server decided. Your server is what lets someone in, counts a vote or starts a session, so it never takes the page's word for who got in.
## 3. Knock
Start the server:
```bash
bun server.ts
```
Open `http://localhost:5173` and press the button.
1. The Door opens as a popup, from `door.getknock.dev`. The first time, it asks you to connect the Wedgie, and the browser asks which serial device to use.
2. The Wedgie shows **KNOCK**: the name it knocks as, and `http://localhost:5173`, the origin the Door saw.
3. Press green. The Door closes, and your page says `In: wedgie.itzraj.eth`, or whichever name your Wedgie has.
4. Press the button again and press green again. This time your server turns the knock away at check 8: `this human has done enter here already`.
That second answer is `action: 'enter'` at work: the verifier keeps one entry per human for each action. Leave `action` out, and the same human can come in as often as they knock.
## What you have
- A challenge per knock, so a knock can't be replayed or used on another site.
- A server that read the name's records from ENS and the binding from World Chain, and checked the Wedgie's signature, before letting anyone in.
- One entry per human for `enter`.
## Next
- [Add a Knock button](guides/knock-button.md): the button for a real site, every error, and React.
- [Check knocks in your server](guides/verify-in-your-server.md): a database for what the verifier remembers, serverless functions, and every setting.
- [Run a verifier](guides/verifier.md): the same checks as a service, for a server in another language.
- [Checks and refusals](reference/checks.md): what each check means, and what to tell people.
# How a knock works
> Who does what in a knock, from the click to the verifier's answer, and why your verifier needn't trust the browser, the Door or Knock.
## Who takes part
- **The person and their Wedgie.** The Wedgie makes its key inside its secure chip, an OPTIGA Trust M, and the key never leaves it. The Wedgie shows each request on its own screen and signs only after a press of its green button.
- **Your page**, with door-sdk.
- **The Door**: Knock's popup. It talks to the Wedgie over USB, through the browser's Web Serial, and hands the knock to the page that opened it. It holds no key, so it can carry a knock but never make one.
- **Your verifier**: your own service. It reads two public chains and trusts nothing it's handed.
- **ENS, on Ethereum mainnet**: the Wedgie's name, and the Knock records on it.
- **World Chain**: the binding. It's a transaction on Knock's KnockBinding contract, which checked a World ID proof and then tied the Wedgie's key to one human and to the name.
## Once: setting up a Wedgie
Before a Wedgie can knock, its owner sets it up in the Knock Console, in this order:
1. **The key.** The Wedgie makes it on the chip, after a green press.
2. **The binding.** The person proves with World ID that they're a human, and the Wedgie signs a request to bind its key. KnockBinding checks the proof on World Chain and logs `Bound(nullifier, keyHash, node, expiresAtMin)`: this key, this human, this name. The nullifier is the human's number for Knock's World ID action: the same for every Wedgie they bind.
3. **The name.** The owner's wallet sets the name's Knock records in ENS. `knock.poh` points at the binding's transaction, so the binding comes first.
| Record | Holds |
| --- | --- |
| `knock.v` | `1`, the version of these records |
| `knock.key` | The Wedgie's public key: compressed P-256, 33 bytes in hex |
| `knock.status` | `active`, or `revoked` when its owner turns it off |
| `knock.nullifier` | The human's nullifier, as a 32-byte word in hex |
| `knock.poh` | The binding: `eip155:480:tx:` and its transaction hash |
The owner can kill the name with the Console's kill switch, from the wallet that holds it: the name leaves ENS, and its knocks bounce at once. The Wedgie needn't be there, so a lost or stolen one can be stopped too. If it is there, it forgets the name and its binding, and can be set up again from scratch. The owner can also revoke the name in the [ENS app](https://app.ens.domains), by setting `knock.status` to `revoked`. One human can bind several Wedgies, each under its own name.
## A knock, step by step
```text
your page the Door the Wedgie your verifier ENS, World Chain
| challenge, please ------------------------------>|
| open ------->| |
| challenge -->| request ------->| |
| | KNOCK, green |
| |<------- signed | |
|<-- the knock | |
| check it ---------------------------------------->| records, binding ->|
|<------------------------------------- in, or why not |
```
1. **A challenge.** On the click, your page asks your verifier for a challenge: 32 random bytes for your origin, good once, for 120 seconds.
2. **The Door opens.** In the same click, `knock()` opens the Door as a popup. The Door says it's ready, and door-sdk sends it the challenge, addressed to the Door's origin only.
3. **Your origin, from the browser.** The Door reads your page's origin from the browser, as the origin of the message that carried the challenge. Your page never states it, so it can't claim to be another site. The Door takes `https://` origins only, and `http://localhost` or `http://127.0.0.1` for development.
4. **The Wedgie asks its owner.** Its KNOCK page shows the name it knocks as, and your origin, with *unverified: the Door says so* beneath. The Wedgie can't see the browser, so it shows whose word the origin is.
5. **The press.** On green, the Wedgie raises its counter and signs a `KnockPass` (EIP-712) over the challenge, the origin, its name's node, the counter, the mode and the human's nullifier. Red means no knock.
6. **The Door hands it over.** The Door checks the signature against the Wedgie's key, then posts the knock to your page's origin only, and closes.
7. **Your verifier checks it.** The knock goes to your verifier's `POST /verify`. It runs eight checks in order, and the first that fails gives the answer. If all pass, it uses up the challenge, records the counter and, if you named an action, the entry, all in one transaction. Then it answers `{ok: true, knock}`.
## The eight checks
| | Check | It reads |
| --- | --- | --- |
| 1 | The knock is whole and well formed | The knock |
| 2 | Its challenge is one this verifier gave, unused, under 2 minutes old, for this origin | Its own store |
| 3 | The name has Knock's records, switched on, and unchanged for the last 3 blocks | ENS on mainnet |
| 4 | The Wedgie whose key ENS holds signed it, for this site and challenge | ENS's key, and the knock |
| 5 | Its counter is higher than in any knock of this Wedgie's seen before | Its own store |
| 6 | The binding names this key, this human and this name, on KnockBinding v3, and isn't too old | World Chain |
| 7 | Its mode is one this verifier offers: mode 1 | The knock |
| 8 | This human hasn't done this action here before, when there is one | Its own store |
Check 7 runs just before check 6, and keeps its number. [Checks and refusals](reference/checks.md) has every answer each check can give.
## What your verifier trusts
It trusts four things:
- **Ethereum mainnet**, for ENS, through the RPC you give it.
- **World Chain**, for the binding, through the RPC you give it.
- **KnockBinding v3**, whose code checks a World ID proof before it logs a `Bound`.
- **The Wedgie**: its chip keeps the key in, and its firmware signs only after a green press.
It trusts nothing else:
- **Not the browser, and not your page.** The signature covers the challenge and the origin. The key, the human and the binding come from ENS and World Chain, never from the knock.
- **Not the Door.** It holds no key, so a knock it made up fails check 4.
- **Not Knock.** No Knock server takes part in a knock. Your verifier reads public chains only, so it keeps working as long as ENS and World Chain do.
## What each side learns
- **Your site** learns the name, the Wedgie's key, the human's nullifier, the Wedgie's counter and the binding's transaction. It doesn't learn who the person is.
- **Knock's servers** learn nothing about knocks. knockd, Knock's signer, takes part only when a Wedgie is bound.
- **Anyone** can read each name's records in ENS, including its key and nullifier. Only the sites a Wedgie knocks on see its knocks.
Because the nullifier is the same for one human on every site, sites that compare notes can tell it's the same human. [Security](security.md) covers this and the rest of the model.
# Add a Knock button
> The browser side of Knock. Call knock() from a click, hand it a challenge, and turn every answer into words people can act on.
The button does three things: it gets a challenge from your verifier, calls `knock()`, and sends the knock it gets back to be checked.
## Call knock() from the click
`knock()` opens the Door as a popup. Browsers allow a popup only while they're handling a click, and that permission doesn't last long. So call `knock()` straight away in the click handler, before any `await`, and hand it the challenge as a promise:
```ts
button.addEventListener('click', () => {
// The Door opens now, and the challenge goes to it when your verifier answers
const knocked = knock({ challenge: challenge() })
})
```
Don't wait for the challenge first. If your verifier is slow, the click's permission can run out, and the browser blocks the Door:
```ts
button.addEventListener('click', async () => {
const c = await challenge() // the click's permission to open a popup may be gone by now
await knock({ challenge: c }) // may reject with KnockError('blocked')
})
```
The challenge must be the one your verifier gave, as `0x` and 64 hex digits. A challenge you make up yourself fails check 2.
## Check what comes back
`knock()` resolves with an `Assertion`: the knock as the Wedgie signed it. It isn't a decision yet. Nothing in it can be trusted until your verifier has checked it, so send it on:
```ts
const assertion = await knocked
const r = await fetch('/api/knock', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ assertion }),
})
```
Here `/api/knock` is your server, which asks your verifier and acts on its answer. [Run a verifier](verifier.md#verify-from-your-server) shows that side.
## When there's no knock
When no knock happens, `knock()` rejects with a `KnockError`. Its `code` says what happened, and its `message` says it in words you can show people as they are.
```ts
try {
const assertion = await knock({ challenge: challenge() })
// …
} catch (e) {
if (!(e instanceof KnockError)) throw e
if (e.code === 'rejected' || e.code === 'closed') return // they said no, or changed their mind
show(e.message)
}
```
### door-sdk's own codes
| Code | What happened | What to show or do |
| --- | --- | --- |
| `blocked` | The browser didn't open the Door. | Ask them to allow popups for your site. Check that `knock()` runs in the click itself. |
| `closed` | The Door was closed before the Wedgie knocked. | Nothing. Let them knock again. |
| `timeout` | No knock within `timeoutMs`, 3 minutes by default. The Door is closed. | "No knock in time. Try again." |
| `challenge` | Your challenge failed: the promise rejected, or it isn't `0x` and 64 hex digits. | Your verifier is down or busy. Say Knock is unavailable for now, and log it. |
| `refused` | The Door said no without a code. | Show the message. |
### The Door's codes
The Door answers with these, and door-sdk passes them on as they are.
| Code | What happened | What to show or do |
| --- | --- | --- |
| `rejected` | Red on the Wedgie. | Nothing: they said no. |
| `timeout` | Nobody pressed on the Wedgie within 2 minutes. | "No press on your Wedgie. Try again." |
| `not_enrolled` | The Wedgie has no name yet, or isn't bound to a human. | The message: it says to set the Wedgie up in the Knock Console. |
| `not_ready` | The Door couldn't talk to the Wedgie: unplugged, or gone mid-knock. | Ask them to check the Wedgie is plugged in, then knock again. |
| `bad_origin` | Your page isn't on `https://`, or on `localhost` or `127.0.0.1` for development. | Serve your page over https. |
| `bad_request` | The challenge or mode was wrong, or the Wedgie couldn't read the request. | Pass your verifier's challenge as it is, and leave `mode` out. |
| `bad_signature` | The Wedgie's knock didn't hold for your site and challenge, so the Door sent nothing. | Rare. Let them knock again. |
The Wedgie's own codes also come through the Door as they are, such as `no_key` when the Wedgie has no Knock key yet. Show the message.
### When the Door waits instead
Some trouble stays in the Door's window, and your page hears nothing until the person closes it (`closed`) or the time runs out (`timeout`):
- **Another tab has the Wedgie**, such as the Knock Console. Only one tab can talk to it at a time, so the Door asks them to disconnect it there.
- **The Wedgie hasn't been connected to the Door before.** The Door shows a button, and the browser asks which serial device to use.
- **The Door is framed.** It never works inside another page, since a frame could cover what it says.
## Keep it to one knock at a time
Disable the button while a knock is going, and say what's happening. Each `knock()` opens the popup window named `knock-door`, so a second knock while the first is open takes over its window.
| While | Say |
| --- | --- |
| The Door is open | "Waiting for your Wedgie…" |
| Your verifier is checking | "Checking…" |
| It passed | Who's in, by name |
| It bounced | The verifier's `why`, such as "wedgie.itzraj.eth is revoked: its owner turned it off" |
## Show it only where knocking works
The Door reaches the Wedgie over Web Serial, which only desktop Chromium browsers have. The Door runs in the same browser as your page, so your page can tell:
```ts
const canKnock = typeof navigator !== 'undefined' && 'serial' in navigator
```
Where `canKnock` is false, show your usual way in, or a line saying Knock needs Chrome, Edge or another Chromium browser on a computer.
## A React component
This one asks your server for the challenge and the check, so the browser never decides who's in.
```tsx title="KnockButton.tsx"
'use client'
import { KnockError, knock } from '@knock-dev/door-sdk'
import { useState } from 'react'
type Said = { busy: boolean; text: string | null }
async function challenge(): Promise<`0x${string}`> {
const r = await fetch('/api/knock/challenge', { method: 'POST' })
const j = await r.json()
if (!r.ok) throw new Error(j.why ?? 'Knock is unavailable right now')
return j.challenge
}
export function KnockButton({ onIn }: { onIn: (name: string) => void }) {
const [said, setSaid] = useState({ busy: false, text: null })
function enter() {
setSaid({ busy: true, text: 'Waiting for your Wedgie…' })
knock({ challenge: challenge() })
.then(async (assertion) => {
setSaid({ busy: true, text: 'Checking…' })
const r = await fetch('/api/knock', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ assertion }),
})
const v = await r.json()
if (v.ok) {
setSaid({ busy: false, text: null })
onIn(v.name)
} else setSaid({ busy: false, text: v.why })
})
.catch((e) => {
const quiet = e instanceof KnockError && (e.code === 'rejected' || e.code === 'closed')
setSaid({ busy: false, text: quiet ? null : e instanceof Error ? e.message : String(e) })
})
}
return (
{said.text}
)
}
```
The Human Club does the same in [`apps/bouncer/app/Knock.tsx`](https://github.com/getknock/knock/blob/main/apps/bouncer/app/Knock.tsx), and ticks off the checks as they pass.
## Options
| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `challenge` | `` `0x${string}` `` or a promise of one | Required | Your verifier's challenge. A promise lets the Door open at once. |
| `mode` | `1` | `1` | Bound: the human comes from the Wedgie's binding. Mode 2 isn't offered. |
| `door` | `string` | `DOOR`, `https://door.getknock.dev` | Where the Door is. |
| `timeoutMs` | `number` | `180000` | How long the whole knock may take, presses included. |
The [door-sdk reference](../reference/door-sdk.md) has every type, and the messages door-sdk and the Door send each other.
# Check knocks in your server
> @knock-dev/verify runs the eight checks inside your own server: Node, Bun, Deno, or serverless and edge functions. No service beside your site.
`@knock-dev/verify` is the verifier as a package. It runs the same eight checks as [`knock-verifier`](verifier.md), in the same order, with the same codes and words, but inside your own server, so there's no second service to run. It works wherever modern JavaScript runs on a server: Node 20 or later, Bun, Deno, and serverless or edge functions.
> [!IMPORTANT]
> Run it on your server, never in the visitor's browser. A check that runs in the browser is one the person being checked can skip, and what makes a knock count once (used challenges, counters, entries) would be theirs to clear.
## Add it
It isn't on npm yet. Use it from a checkout of `getknock/knock`, in one of two ways.
In a workspace with Knock's packages, as the Human Club does:
```json title="package.json"
{
"dependencies": {
"@knock-dev/verify": "workspace:*",
"@libsql/client": "^0.18.0"
}
}
```
Anywhere else, copy its file and the protocol's into your project, point one at the other, and add their two dependencies:
```bash
mkdir -p src/knock
cp ../knock/packages/protocol/src/index.ts src/knock/protocol.ts
cp ../knock/packages/verify/src/index.ts src/knock/verify.ts
sed -i.bak "s#'@knock-dev/protocol'#'./protocol'#" src/knock/verify.ts && rm src/knock/verify.ts.bak
bun add viem @noble/curves @libsql/client
```
Then import from `./knock/verify` instead of `@knock-dev/verify`.
## Make your verifier
One verifier checks knocks for one origin, your site's, and keeps what it has seen in a store.
```ts title="knock.ts"
import { createClient } from '@libsql/client'
import { Verifier, libsqlStore, rpcChain } from '@knock-dev/verify'
export const verifier = new Verifier({
// Your site's origin, exactly as browsers send it
origin: 'https://club.example',
// What it remembers between knocks: a SQLite file here, or Turso
store: libsqlStore(
createClient({ url: process.env.KNOCK_DB_URL ?? 'file:knock.db', authToken: process.env.KNOCK_DB_TOKEN }),
),
// Your own RPCs. Public ones are used if you leave these out.
chain: rpcChain({ eth: process.env.ETH_RPC, world: process.env.WORLD_RPC }),
})
```
## Give out challenges
Your page asks your server for a challenge just before it knocks. Each one is good for one knock, for two minutes.
```ts title="challenge.ts"
import { Refusal } from '@knock-dev/verify'
import { verifier } from './knock'
export async function POST() {
try {
const { challenge } = await verifier.challenge()
return Response.json({ challenge })
} catch (e) {
if (e instanceof Refusal) return Response.json(e, { status: e.status })
throw e
}
}
```
A `Refusal` turns into `{ ok: false, check, error, why }` as JSON, the same shape `knock-verifier` answers with, and its `status` is the HTTP status the service would give.
## Check a knock
The page hands you what door-sdk's `knock()` resolved with. `verify()` runs V1 to V8, then writes what the knock used up, all of it or none:
```ts title="verify.ts"
import { Refusal } from '@knock-dev/verify'
import { verifier } from './knock'
export async function POST(req: Request) {
const { assertion } = await req.json()
try {
// `action` lets each human do it once (V8). Leave it out to let them in every time.
const k = await verifier.verify(assertion, { action: 'enter' })
// In: one human, bound on World Chain. Their nullifier is the same on every Knock site.
await letIn({ human: k.nullifier, name: k.name })
return Response.json({ ok: true, name: k.name })
} catch (e) {
if (e instanceof Refusal) return Response.json(e, { status: e.status })
throw e
}
}
```
These are web-standard handlers, so they fit Next.js route handlers, Bun, Deno, Hono and the like as they are. Express and Fastify take the same calls with their own request and response.
The page side doesn't change: [Add a Knock button](knock-button.md) shows `knock()` with a challenge from your server, then sending the assertion back.
## Settings
Everything is an option to `new Verifier()`. Only `origin` and `store` are needed.
| Option | Default | What it does |
| --- | --- | --- |
| `origin` | none | Your site's origin, exactly as browsers send it: `https://host` or `https://host:port`, or `http://localhost` or `http://127.0.0.1`, with or without a port, for development. Knocks for any other origin fail check 2. |
| `store` | none | Where it keeps challenges, counters, revoked keys and entries. See below. |
| `chain` | public RPCs | Where it reads ENS on mainnet and bindings on World Chain: `rpcChain({ eth, world })`, each a URL or a viem client. |
| `maxBindingDays` | `365` | How old a binding's World ID proof may be, in days. Older bindings fail check 6 as `binding_expired`. |
| `strict` | `'off'` | Whether a binding must be final on World Chain first: `'off'`, `'safe'` or `'finalized'`. [Strict mode](strict-mode.md) explains the choice. `strictFrom()` reads `on`, `true` and the like from an environment variable. |
| `contract` | Knock's KnockBinding | The KnockBinding whose bindings count. Leave it alone unless you take knocks from [a network of your own](../self-host/own-network.md). |
The defaults are the service's. The public RPCs are fine for trying Knock out, but rate-limited, and they're what the checks believe: [Its RPCs](verifier.md#its-rpcs) says why your own matter on a real site.
## Where it remembers
A verifier remembers four things between knocks: the challenges it gave out, the highest counter it has seen from each Wedgie, the keys it has seen revoked, and who has done each action. Every instance of your server has to reach the same store, or a challenge given by one instance won't be found by the next, and a knock could be replayed against another.
- **`libsqlStore(client)`** keeps them in a libSQL database: a SQLite file (`file:knock.db`), [Turso](https://turso.tech) (`libsql://…` with its token), or memory. It makes its four tables on first use, each named `knock_…`, so they can sit beside your own. On edge runtimes, make the client with `@libsql/client/web`.
- **`memoryStore()`** keeps them in the process. It's for tests, and for a site that's one long-lived process. Serverless functions don't share memory, so don't use it there.
- **Your own**: anything that implements [`Store`](../reference/verify.md#store). Its `commit()` must check and write the challenge, the counter and the entry in one transaction.
The tables mean what the service's do, and losing them costs the same: [Its database](verifier.md#its-database).
## Serverless and edge functions
- Use `libsqlStore` with Turso, or your own store over a database the functions share.
- Set `origin` from your configuration, never from the request's `Host` header: the origin is what the Wedgie signed for, and a proxy can change the header.
- A check makes three to five RPC calls. Allow your function at least ten seconds.
- Each instance keeps a name's records for a minute, as the service does. A new instance reads them again, which costs a call and nothing else.
## Writing what a knock uses up yourself
`verify()` keeps its own tables. If your site needs to write what a knock uses up in its own transaction, together with what the knock was for, use the checks on their own: `parse()` for V1, then `check()` for V3 to V7. The Human Club does this: a signature, the human and the Wedgie's counter go in one transaction, so a knock that's turned away writes nothing.
```ts title="own-records.ts"
import { type Memory, check, parse } from '@knock-dev/verify'
declare const memory: Memory
declare function takeChallenge(challenge: string): Promise
export async function knocked(body: { assertion: unknown }) {
const a = parse(body.assertion) // V1
if (!(await takeChallenge(a.challenge))) throw new Error('not our challenge') // V2: yours, used once
const k = await check({ origin: 'https://club.example', memory }, a) // V3–V7, writing nothing but a revoked key
// Then, in one transaction: the counter, only if it rose (V5 again, for two knocks racing), your V8, your act
return k
}
```
`memory` answers the counter for a key and remembers revoked keys. [`apps/bouncer/lib/club.ts`](https://github.com/getknock/knock/blob/main/apps/bouncer/lib/club.ts) is a whole example, with libSQL.
## The package, the service or the crate
All three run the same checks, held to the same test vectors.
- **`@knock-dev/verify`**, when your server runs JavaScript or TypeScript: nothing extra to run.
- **[`knock-verifier`](verifier.md)**, the Rust service, when your server is in another language: it answers over HTTP.
- **[`knock-verify`](../reference/knock-verify.md)**, the Rust crate, when your server is in Rust.
The package measures time in milliseconds, as JavaScript does; the service and the crate count seconds.
# Run a verifier
> knock-verifier is the service that decides whether a knock gets in. Its settings, where to run it, and how your server asks it.
`knock-verifier` is a small Rust service you run beside your site. It gives out challenges and checks knocks for one origin, your site's, and keeps what it has seen in a SQLite file. It reads ENS on Ethereum mainnet and bindings on World Chain through RPCs, needs no keys, and never calls Knock.
If your server runs JavaScript or TypeScript, you don't need the service: [`@knock-dev/verify`](verify-in-your-server.md) runs the same checks inside your server.
## Build it
From the root of a checkout of `getknock/knock`:
```bash
cargo build --release -p knock-verifier
./target/release/knock-verifier
```
It's one binary, with SQLite built in.
## Settings
Everything is an environment variable, and only `VERIFIER_ORIGIN` usually needs setting.
| Variable | Default | What it is |
| --- | --- | --- |
| `VERIFIER_ORIGIN` | `http://localhost:3001` | Your site's origin, exactly as browsers send it: `https://host` or `https://host:port`, or `http://localhost` or `http://127.0.0.1`, with or without a port, for development. Knocks for any other origin fail check 2, and CORS allows only this one. |
| `VERIFIER_ADDR` | `127.0.0.1:8788` | Where it listens. |
| `VERIFIER_DB` | `knock-verifier.db` | Its SQLite file. |
| `VERIFIER_ETH_RPC` | `https://ethereum-rpc.publicnode.com` | An Ethereum mainnet RPC, for ENS. |
| `VERIFIER_WORLD_RPC` | `https://worldchain-mainnet.g.alchemy.com/public` | A World Chain RPC, for bindings. |
| `VERIFIER_MAX_BINDING_DAYS` | `365` | How old a binding's World ID proof may be, in days. Older bindings fail check 6 as `binding_expired`. |
| `VERIFIER_STRICT` | `off` | Whether a binding must be final on World Chain first: `off`, `on` (or `safe`), or `finalized`. [Strict mode](strict-mode.md) explains the choice. |
| `VERIFIER_KNOCK_BINDING` | `knock` | The KnockBinding whose bindings count: `knock` for Knock's, or the address of [a network of your own](../self-host/own-network.md)'s. |
The default origin is `http://localhost:3001`, for development, so set yours:
```bash
VERIFIER_ORIGIN=https://club.example \
VERIFIER_DB=/var/lib/knock/verifier.db \
VERIFIER_ETH_RPC=https://your-mainnet-rpc.example \
VERIFIER_WORLD_RPC=https://your-worldchain-rpc.example \
./target/release/knock-verifier
```
## What it checks before it starts
Before it takes a single knock, the verifier makes sure of where it reads from, and stops if anything is off:
- `VERIFIER_ORIGIN` is an origin it can check knocks for.
- `VERIFIER_ETH_RPC` is chain 1, Ethereum mainnet, and ENS's Universal Resolver is there.
- `VERIFIER_WORLD_RPC` is chain 480, World Chain, and the KnockBinding it accepts reports version 3.
The KnockBinding it accepts is Knock's production v3 on World Chain, `0x7748D2787DcB6E6024968edC19fcA13ccAbA1699`, built in from [`contracts/deployments/480.json`](https://github.com/getknock/knock/blob/main/contracts/deployments/480.json), unless `VERIFIER_KNOCK_BINDING` names [a network of your own](../self-host/own-network.md)'s. Bindings on any other KnockBinding, including staging's and older versions', never count.
## Verify from your server
Your page gets the knock, but your server should decide what it means. If the browser asks the verifier and then tells your server "they're in", your server is taking the browser's word for it. Instead, have the browser hand the knock to your server, and your server ask the verifier:
```text
browser -- the knock --> your server -- POST /verify --> verifier
browser <-- in, or why not -- your server <-- the answer --
```
Keep the verifier on `127.0.0.1`, where only your server can reach it, and give the browser two routes on your own origin. In Next.js:
```ts title="app/api/knock/challenge/route.ts"
const VERIFIER = process.env.KNOCK_VERIFIER ?? 'http://127.0.0.1:8788'
// A challenge for the page, passed through as it is
export async function POST() {
const r = await fetch(`${VERIFIER}/challenge`, { method: 'POST' })
return new Response(r.body, { status: r.status, headers: { 'content-type': 'application/json' } })
}
```
```ts title="app/api/knock/route.ts"
const VERIFIER = process.env.KNOCK_VERIFIER ?? 'http://127.0.0.1:8788'
export async function POST(req: Request) {
const { assertion } = await req.json()
const r = await fetch(`${VERIFIER}/verify`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ assertion, action: 'enter' }),
})
const v = await r.json()
if (!v.ok) return Response.json({ ok: false, check: v.check, error: v.error, why: v.why }, { status: r.status })
// In: one human, bound on World Chain. Their nullifier is the same on every Knock site.
const { name, nullifier } = v.knock
await letIn({ human: nullifier, name }) // your session, vote, claim…
return Response.json({ ok: true, name })
}
```
Check 2 compares the origin inside the knock, the one the Door saw, with `VERIFIER_ORIGIN`. Where the request to `/verify` comes from doesn't matter, so your server can make it.
### From the browser, for trying things out
A page can ask the verifier itself, because the verifier allows CORS from `VERIFIER_ORIGIN`. It's fine for a demo, or when nothing on your server depends on the answer. Then the verifier must be reachable from the browser, behind your TLS proxy, at an address such as `https://knock.club.example`.
## One verifier per origin
A verifier checks knocks for one origin, because the origin is part of what the Wedgie signs. Run one for each:
- `https://club.example` and `https://www.club.example` are two origins, so two verifiers, or one canonical host.
- Staging at `https://staging.club.example` gets its own verifier and its own database.
- For development, run one with `VERIFIER_ORIGIN=http://localhost:5173` or wherever your dev server is.
## Its database
The SQLite file at `VERIFIER_DB` holds four tables:
| Table | Holds | If you lose it |
| --- | --- | --- |
| `challenges` | Each challenge, its origin, when it was given out and whether it's been used. Expired ones are cleared as new ones are given out. | Knocks under way fail check 2. People knock again. |
| `counters` | The highest counter seen for each Wedgie's key. | No harm: old knocks still fail check 2, since their challenges are gone. |
| `revoked` | Keys it has seen revoked in ENS, which stay refused even if their records come back. | A revoked key could get in again if its owner turned it back on. |
| `entries` | For each action, the humans who've done it, with the name they knocked as and when. | Every human can do every action once more. |
Back it up if `entries` matter to you. It's one SQLite file, so one verifier process uses it at a time.
## Its RPCs
The default RPCs are free public endpoints. They're good for trying Knock out, but they limit how often you can call them, and the verifier makes several calls for each knock: the mainnet head, the name's five Knock records at the latest block and again 3 blocks deep (unless it read the same ones in the last minute), and the binding's receipt on World Chain. For a real site, use your own endpoints or a paid provider.
Your RPCs are also what the verifier believes. An RPC that lies could show records or a binding that aren't on chain. Use providers you trust, or your own nodes. The verifier never logs an RPC's URL, since URLs often carry keys.
When an RPC can't be reached, the verifier refuses the knock as `unavailable`, with status 503. It never lets a knock in on a guess.
## Limits
| Limit | Value | When it's hit |
| --- | --- | --- |
| Request body | 16 KiB | Refused at check 1, status 400. A knock is under 1 KiB. |
| Challenges | 600 an hour, per process | `busy`, status 429 |
| Checks | 600 an hour, per process | `busy`, status 429 |
| Challenges outstanding | 10,000 at once | `busy` at check 2, status 429 |
## Health and logs
`GET /health` says what the verifier checks for:
```json
{
"binding": "0x7748d2787dcb6e6024968edc19fca13ccaba1699",
"maxBindingDays": 365,
"ok": true,
"origin": "https://club.example",
"strict": "off"
}
```
It logs each answer, who got in or which check said no, and nothing about who asked:
```text
INFO let in wedgie.itzraj.eth (counter 12)
INFO refused at V8: already_entered
```
## In your own Rust server
`knock-verifier` is a thin HTTP layer over the [`knock-verify`](../reference/knock-verify.md) crate. If your server is in Rust, you can call the checks directly and skip the extra service.
# One human, once
> Actions let each human do something on your site once, such as enter, vote or claim, however many Wedgies they own.
A knock that passes tells you a real human is there. An **action** tells you whether that human has done something on your site before. Name an action when you send a knock to be checked, and check 8 lets each human do it once.
```json
{ "assertion": { "…": "the knock" }, "action": "enter" }
```
## How check 8 counts
The verifier counts humans by the human's **nullifier**, not by Wedgie or by name. One human gets one nullifier from Knock's World ID action, and every Wedgie they bind carries it. So a human who owns three Wedgies, under three names, still does each action once.
When every other check has passed, the verifier records the action and nullifier in the same transaction that uses up the challenge. A knock that's refused at any check uses up nothing, and doesn't count as the human's turn.
The second time the same human knocks for the same action, check 8 refuses it:
```json
{ "ok": false, "check": 8, "error": "already_entered", "why": "this human has done enter here already" }
```
## Naming actions
An action is 1 to 32 characters of `a`–`z`, `0`–`9` and `-`. Anything else is refused at check 1. Pick names for what people do, and keep them stable:
| Action | Once means |
| --- | --- |
| `enter` | One entry per human, ever |
| `vote-2026-budget` | One vote per human in this poll |
| `claim-launch-drop` | One claim per human for this drop |
| `signup` | One account per human |
Actions are counted per verifier, so per origin. `enter` on your site and `enter` on someone else's are separate.
To start an action over, say for a new poll, use a new name. There's no way to clear an action through the API.
## Without an action
Leave `action` out, and check 8 doesn't run. The same human can come in as often as they knock. Each knock still needs its own challenge and a higher counter, so none can be replayed. Use this when you want to know that a human is here now, and not whether they've been here before. For example:
- before posting a comment, or sending a message;
- to slow down sign-ups, alongside your own limits;
- to step up an existing session before something sensitive.
## Keeping track yourself
The verifier answers with the knock that got in, including `nullifier`:
```json
{
"ok": true,
"knock": {
"name": "wedgie.itzraj.eth",
"key": "0x02…",
"nullifier": "0x1f…",
"counter": 12,
"origin": "https://club.example",
"binding": { "tx": "0x…", "block": 37301500, "provedAt": 1793860000 }
}
}
```
To tie your own records to a human, such as an account or an allowance, key them by `nullifier`. Don't key them by `name` or `key`: one human can have several Wedgies, and can kill a name and set up the Wedgie again under another.
> [!IMPORTANT]
> The nullifier is public. It's in the name's ENS records, and it's the same on every site that uses Knock. Treat it as an identifier for a human that others can see, and don't show it next to anything you'd keep private. [Security](../security.md) says more.
## What once doesn't stop
- **A human who lends their Wedgie.** Knock knows a Wedgie is bound to a human, and that someone pressed it. It can't know who pressed.
- **Your other verifiers.** Each one keeps its own entries, so staging and production, or two origins, count separately.
- **A lost database.** Entries live in the verifier's SQLite file. [Run a verifier](verifier.md#its-database) says what to back up.
# Strict mode
> Whether a binding must be final on World Chain before its knocks count. Off by default; what it costs to turn on, and when it's worth it.
A knock stands on its Wedgie's binding: a transaction on World Chain. By default, a verifier counts the binding as soon as that transaction is in a block. A **strict** verifier waits until World Chain has posted the block to Ethereum, or until that post is final there. Strict mode only affects people who knock within minutes of binding their Wedgie. After that, every binding is final anyway.
## How World Chain settles a block
World Chain is an OP Stack chain:
1. **In a block.** Its sequencer puts the binding in a block within a few seconds: World Chain makes one every 2 seconds. This is its latest head.
2. **Safe.** Its batcher posts the block's data to Ethereum. Once that post is in an Ethereum block, the block is at or below World Chain's safe head. The batcher posts every few minutes.
3. **Finalized.** Once the Ethereum block holding that post is finalized, about two epochs or 13 minutes later, the World Chain block is at or below the finalized head.
Measured on 2026-11-06: World Chain's batcher posted to Ethereum 40 times in four hours, every 4.6 to 7.4 minutes, 6.2 minutes apart in the middle. Between posts the safe head stands still, so it falls up to 8.5 minutes behind the latest block before it jumps. One binding mined at 05:14:39 UTC became safe at 05:21:12, 6.5 minutes later. The finalized head runs about 20 to 25 minutes behind.
## The three settings
| `VERIFIER_STRICT` | A binding counts once | Someone who just bound waits |
| --- | --- | --- |
| `off` (default) | Its transaction is in a block | Nothing |
| `on`, or `safe` | World Chain has posted its block to Ethereum | Up to ~9 minutes, ~6 typically |
| `finalized` | That post is final on Ethereum | About 20 to 25 minutes |
`on`, `true`, `1`, `yes` and `safe` all mean safe. `off`, `false`, `0`, `no` and an empty value all mean off. Anything else stops the verifier at start. `GET /health` reports the setting as `off`, `safe` or `finalized`.
## What a strict verifier says
A knock whose binding isn't final yet fails check 6, as `binding_not_final`, with the wait in words:
| Setting | `why` |
| --- | --- |
| Safe | this site is strict: it takes a binding once World Chain has posted it to Ethereum, which it does every ~6 minutes. Try again in a few minutes |
| Finalized | this site is strict: it takes a binding once World Chain's post of it to Ethereum is final, ~20–25 minutes after binding. Try again later |
Nothing is used up, so the same person can knock again a few minutes later and get in. Show them the `why`.
## What a verifier that isn't strict risks
The risk is a binding that World Chain drops, or replaces, before its batch reaches Ethereum. A human could then knock on a binding that, in the end, never happened. For that to happen, World Chain's sequencer would have to misbehave, or its batches would have to stop reaching Ethereum for hours. Even then, the knock is still signed by the Wedgie after a press, and its name is still checked in ENS. Only the World ID proof behind the binding would be in doubt.
## Which to choose
- **Off**, for most sites. People can knock as soon as they've set up their Wedgie, and the risk above is remote.
- **Safe**, when a knock is worth something that can't be taken back, such as a payout or a one-time claim, and a few minutes' wait for new Wedgies is fine.
- **Finalized**, rarely: when you want only what Ethereum itself has settled, and a new Wedgie can wait half an hour.
## Setting it
For `knock-verifier`:
```bash
VERIFIER_STRICT=on ./target/release/knock-verifier
```
With the `knock-verify` crate:
```rust
use knock_verify::{Strict, Verifier};
let verifier = Verifier::new(chain, store, origin, contract, 365 * 86_400).strict(Strict::Safe);
```
A verifier that isn't strict never asks World Chain for its safe or finalized head, so it makes one call fewer for each knock.
# Test your integration
> What to test without a Wedgie, the knocks to try with one, and the Human Club and its bot as a working reference.
Most of a Knock integration can be tested without a Wedgie: what your page does with each answer, and what your server does with each verdict. You need a Wedgie, and a person to press it, only for the knocks that get in.
## Without a Wedgie
### Your page's answers
Keep the code that calls `knock()` apart from the code that shows the result, so a test can hand it any answer. Here `enter` takes `knock` and `fetch` as arguments:
```ts title="enter.ts"
import { type Assertion, KnockError, type KnockOptions, knock as realKnock } from '@knock-dev/door-sdk'
export type Said = { in: string } | { out: string } | { quiet: true }
type Post = (url: string, init: RequestInit) => Promise
export async function enter(
challenge: () => Promise<`0x${string}`>,
knock: (o: KnockOptions) => Promise = realKnock,
post: Post = fetch,
): Promise {
try {
const assertion = await knock({ challenge: challenge() })
const v = await (await post('/api/knock', { method: 'POST', body: JSON.stringify({ assertion }) })).json()
return v.ok ? { in: v.name } : { out: v.why }
} catch (e) {
if (e instanceof KnockError && (e.code === 'rejected' || e.code === 'closed')) return { quiet: true }
return { out: e instanceof Error ? e.message : String(e) }
}
}
```
```ts title="enter.test.ts"
import { expect, test } from 'bun:test'
import { type Assertion, KnockError } from '@knock-dev/door-sdk'
import { enter } from './enter'
const challenge = async () => `0x${'ab'.repeat(32)}` as const
test('red on the Wedgie says nothing', async () => {
const red = () => Promise.reject(new KnockError('rejected', 'Red on the Wedgie: no knock.'))
expect(await enter(challenge, red)).toEqual({ quiet: true })
})
test('a bounce shows the verifier’s why', async () => {
const knocked = async () => ({}) as Assertion
const bounced = async () =>
Response.json({ ok: false, check: 3, error: 'revoked', why: 'wedgie.itzraj.eth is revoked: its owner turned it off' })
expect(await enter(challenge, knocked, bounced)).toEqual({
out: 'wedgie.itzraj.eth is revoked: its owner turned it off',
})
})
```
Cover each code in [Add a Knock button](knock-button.md#when-theres-no-knock), and a verifier that can't be reached.
### Your server's verdicts
Your server route acts on the verifier's answer, so test it against each kind: `{ok: true}`, a refusal from each check, `busy` with status 429 and `unavailable` with status 503. Stub the verifier's HTTP answers, or run a real verifier and send it knocks that bounce.
The Human Club's bot sends a real verifier the knocks a bot would try, for the club's origin, `http://localhost:3001`. It signs them with a key of its own, so none should get in, and it says where each one bounced:
```bash
cd apps/bouncer
bun run bot wedgie.itzraj.eth http://localhost:8788
```
```text
The Human Club's bot tries to get in as wedgie.itzraj.eth, without its Wedgie (verifier http://localhost:8788):
its own key, a fresh challenge bounced at check 4: not signed by wedgie.itzraj.eth's key, for this site and challenge
a knock signed for another site bounced at check 2: the knock is for https://evil.example, not http://localhost:3001
…
Nothing got in.
```
It exits with 1 if anything got in. Each try bounces at the first check that catches it, so what it prints depends on the name you give it, and on the verifier: a name with no Knock records bounces at check 3, and a verifier for another origin turns most tries away at check 2. [`apps/bouncer/scripts/bot.ts`](https://github.com/getknock/knock/blob/main/apps/bouncer/scripts/bot.ts) is a starting point for a bot of your own.
### The protocol
[`spec/vectors/v1.json`](https://github.com/getknock/knock/blob/main/spec/vectors/v1.json) holds test vectors: a published test key, and the digests and signatures of each message Knock signs. Don't use that key for anything real. `@knock-dev/protocol`'s tests check the TypeScript side against them, and `knock-verify`'s tests take the vectors' signed knock through all eight checks with a stand-in chain:
```bash
bun run --filter @knock-dev/protocol test
cargo test -p knock-verify
```
## With a Wedgie
Run your page and a verifier for your page's origin, then try each of these. Each row is one knock.
| Do this | Expect |
| --- | --- |
| Press green | In |
| Knock again, with the same action | Check 8: `already_entered` |
| Knock again, with no action | In |
| Press red | `KnockError` `rejected` |
| Close the Door before pressing | `KnockError` `closed` |
| Leave the KNOCK screen for 2 minutes | `KnockError` `timeout`, from the Wedgie |
| Stop your verifier, then knock | `KnockError` `challenge` |
| Keep the Wedgie connected in the Console's tab, then knock | The Door asks you to disconnect it there |
| Set the name's `knock.status` to `revoked` in the ENS app, then knock | Check 3: `revoked` |
| Kill the name in the Console, then knock | Check 3: `no_name` |
| Name a Wedgie, then knock within a minute | Check 3: `records_changing`, then in a minute later |
| With `VERIFIER_STRICT=on`, knock within minutes of binding | Check 6: `binding_not_final` |
| Run a verifier for another origin, then knock from your page | Check 2: `wrong_origin` |
Revoking and killing change the name for every site, so use a Wedgie set up for testing.
## The Human Club
The Human Club is a whole Knock site: petitions and fundraisers where each human signs once, with the checks ticked off as they pass. It checks knocks in its own functions with `@knock-dev/verify`, so it needs no verifier beside it. It's in [`apps/bouncer`](https://github.com/getknock/knock/blob/main/apps/bouncer/lib/club.ts), and runs on port 3001:
```bash
# two terminals, from the root of the checkout
cd apps/door && bun run dev # the Door, on 3000
cd apps/bouncer && bun run dev # the club, on 3001
```
Its records go in `apps/bouncer/human-club.db`. Delete the file to start again. [Host the Human Club](../self-host/human-club.md) has its settings.
# door-sdk
> Every export of @knock-dev/door-sdk, the knock it returns field by field, and the messages it and the Door send each other.
`@knock-dev/door-sdk` is the browser side of Knock: one TypeScript file with no dependencies, in [`packages/door-sdk/src/index.ts`](https://github.com/getknock/knock/blob/main/packages/door-sdk/src/index.ts). It needs a browser, since it opens a window.
```ts
import { DOOR, type Assertion, type Hex, KnockError, type KnockOptions, knock } from '@knock-dev/door-sdk'
```
## Get it
It isn't on npm yet. Use it from a checkout of `getknock/knock`, in one of three ways:
- **Import it from the checkout**, as the [quickstart](../quickstart.md) does: `from '../knock/packages/door-sdk/src/index.ts'`.
- **Copy the file** into your project, such as `src/door-sdk.ts`, and import from it.
- **In a workspace with Knock's packages**, add `"@knock-dev/door-sdk": "workspace:*"` to your dependencies. With Next.js, also add `transpilePackages: ['@knock-dev/door-sdk']` to `next.config.ts`, since the package ships TypeScript.
## knock()
```ts
function knock(options: KnockOptions): Promise
```
Opens the Door and resolves with the knock the Wedgie signed, or rejects with a `KnockError`. Call it from a click: it opens the Door straight away, and browsers allow that only while they're handling a click.
In order, `knock()`:
1. opens `/knock` in a popup window named `knock-door`, 420 by 680 pixels, or rejects with `blocked`;
2. waits for the Door's `knock/ready`, and for your challenge, whichever comes last;
3. checks the challenge is `0x` and 64 hex digits, or rejects with `challenge`;
4. posts `knock/request` to the Door, addressed to the Door's origin only;
5. resolves with the `knock/result`'s assertion, or rejects with its error.
It rejects with `closed` if the popup is closed first, and with `timeout`, closing the popup, if `timeoutMs` passes. It takes messages only from the popup it opened, at the Door's origin.
## KnockOptions
```ts
type KnockOptions = {
challenge: Hex | Promise
mode?: 1
door?: string
timeoutMs?: number
}
```
| Field | Default | What it is |
| --- | --- | --- |
| `challenge` | Required | Your verifier's challenge from `POST /challenge`, or a promise of it. With a promise, the Door opens at once and gets the challenge when it arrives. |
| `mode` | `1` | Bound: the human comes from the Wedgie's binding. Mode 2, Fresh, isn't offered. |
| `door` | `DOOR` | The Door's URL. Only its origin is used: the popup opens at `/knock` there. |
| `timeoutMs` | `180000` | How long the whole knock may take, presses included. The Wedgie itself gives up after 2 minutes without a press. |
## Assertion
The knock, as the Wedgie signed it and the Door handed it over. Send it to your verifier as it is, and trust none of it until the verifier says `ok`.
```ts
type Assertion = {
v: 1
name: string
challenge: Hex
origin: string
deviceNode: Hex
counter: number
mode: 1
nullifier: Hex
humanRef: Hex
r: Hex
s: Hex
}
```
| Field | What it is |
| --- | --- |
| `v` | The format's version: `1`. |
| `name` | The ENS name the Wedgie knocks as, such as `wedgie.itzraj.eth`. |
| `challenge` | Your verifier's challenge, 32 bytes. |
| `origin` | Your page's origin, as the Door read it from the browser. |
| `deviceNode` | The name's ENS namehash. |
| `counter` | The Wedgie's knock counter. It rises with each knock, on any site. |
| `mode` | `1`, Bound. |
| `nullifier` | The human's nullifier, 32 bytes, as the Wedgie holds it. Check 6 compares it with the name's ENS records. |
| `humanRef` | 32 zero bytes in mode 1. |
| `r`, `s` | The Wedgie's P-256 signature over the knock's `KnockPass` digest, `s` low. |
The [protocol](protocol.md#knockpass) page says how the digest is made.
## KnockError
```ts
class KnockError extends Error {
readonly code: string
}
```
Why there was no knock. `code` is for your code, and `message` is a sentence you can show people.
| `code` | From | Meaning |
| --- | --- | --- |
| `blocked` | door-sdk | The browser didn't open the popup. |
| `closed` | door-sdk | The popup was closed before a knock. |
| `timeout` | door-sdk | `timeoutMs` passed. |
| `challenge` | door-sdk | The challenge's promise rejected, or it isn't `0x` and 64 hex digits. |
| `refused` | door-sdk | The Door said no without a code. |
| `rejected` | The Door | Red on the Wedgie. |
| `timeout` | The Door | Nobody pressed on the Wedgie within 2 minutes. |
| `not_enrolled` | The Door | The Wedgie has no name, or isn't bound to a human. |
| `not_ready` | The Door | The Door couldn't talk to the Wedgie. |
| `bad_origin` | The Door | The page isn't on `https://`, or `localhost` or `127.0.0.1` for development. |
| `bad_request` | The Door | The challenge or mode was wrong, or the Wedgie couldn't read the request. |
| `bad_signature` | The Door | The Wedgie's knock didn't hold for this site and challenge, so nothing was sent. |
| `no_key` | The Wedgie, through the Door | The Wedgie has no Knock key yet. |
The Door passes the Wedgie's other codes on as they are. [Add a Knock button](../guides/knock-button.md#when-theres-no-knock) says what to show for each.
## DOOR
```ts
const DOOR = 'https://door.getknock.dev'
```
The Door that door-sdk opens unless you pass `door`: Knock's own. To use [one you serve yourself](../self-host/door.md), pass its address as `door`.
## Hex
```ts
type Hex = `0x${string}`
```
## The messages
door-sdk and the Door talk with `postMessage`. You only need these if you write your own client.
| Message | From, to | Sent to | Body |
| --- | --- | --- | --- |
| `knock/ready` | Door to your page | Any origin | `{ type: 'knock/ready' }`. It says only that the Door is up. |
| `knock/request` | Your page to the Door | The Door's origin | `{ type: 'knock/request', challenge, mode: 1 }` |
| `knock/result` | Door to your page | Your page's origin | `{ type: 'knock/result', ok: true, assertion }`, or `{ type: 'knock/result', ok: false, error, why }` |
The Door:
- takes a request only from the window that opened it, and reads that window's origin from the browser, as the message's `origin`;
- answers once, and only to that origin;
- takes one request, and closes itself a moment after a knock;
- refuses to work inside a frame.
# @knock-dev/verify
> Every export of the verifier package, the eight checks for a site's own server, and what each takes and gives back.
`@knock-dev/verify` is one TypeScript file, [`packages/verify/src/index.ts`](https://github.com/getknock/knock/blob/main/packages/verify/src/index.ts), with two dependencies: `@knock-dev/protocol` and `viem`. [Check knocks in your server](../guides/verify-in-your-server.md) shows it in use. Times are in milliseconds unless a name says otherwise.
## Verifier
```ts
new Verifier(site: Site & { store: Store })
```
A verifier for one origin. It throws if `origin` isn't one browsers write.
- **`challenge(now?)`** gives out a challenge and keeps it in the store: `Promise`. It refuses with `busy` at check 2 when 10,000 are outstanding.
- **`verify(assertion, { action?, now? })`** runs V1 to V8 on what door-sdk's `knock()` resolved with, then commits what the knock used up: its challenge, its Wedgie's counter, and, with an `action`, the human's entry for it. All of it is written, or none. It resolves with `Knocked`, or rejects with a `Refusal`.
- **`site`** is the options it was made with.
### Site
What the checks need to know about your site. `Verifier` takes it with a `store`, and `check()` with a `memory`.
| Field | Default | |
| --- | --- | --- |
| `origin` | none | Your site's origin: `https://host[:port]`, or `http://localhost[:port]` or `http://127.0.0.1[:port]` for development. |
| `chain` | `rpcChain()`, public RPCs | A `Chain`. |
| `contract` | `KNOCK_BINDING` | The KnockBinding whose `Bound` logs count. |
| `maxBindingDays` | `MAX_BINDING_DAYS`, 365 | How old a binding's proof may be. |
| `strict` | `'off'` | A `Strict`: how final a binding must be. |
### Challenge
`{ challenge, origin, expiresAt }`: the challenge for door-sdk's `knock()`, as `0x` and 64 hex digits, your origin, and when it expires.
### Knocked
A knock that got in:
| Field | |
| --- | --- |
| `name` | The Wedgie's ENS name. |
| `key` | Its compressed P-256 key, as `knock.key` holds it, in lowercase. |
| `keyHash` | `keccak256(x ‖ y)` of that key, as `Bound` names it. |
| `nullifier` | The human: a 32-byte word in lowercase hex, the same on every site that takes Knock. |
| `counter` | The Wedgie's counter in this knock. |
| `origin` | Your origin. |
| `binding` | A `Binding`: `{ tx, block, provedAt }`, the World Chain transaction and block, and about when its World ID proof was made, in unix seconds. |
## Refusal
```ts
class Refusal extends Error { check: number; error: string; why: string; status: number }
```
A no: the check that said it, 1 to 8 (0 for the verifier's own trouble), its code, and why, in words a person can read. Its codes and words are the service's: [Checks and refusals](checks.md) lists them. `toJSON()` gives `{ ok: false, check, error, why }`, and `status` the HTTP status `knock-verifier` answers with: 400 for check 1, 403 for checks 2 to 8, 429 for `busy`, 503 for `unavailable`, 500 for check 0.
## The checks
- **`parse(x)`**, V1: `x` as an `Assertion`, whole and well formed, its `s` low. Anything else is refused at check 1 with `bad_assertion`.
- **`check(site, assertion, now?)`**, V3 to V7, for a knock that passed V1 and whose challenge you've checked yourself. It needs `site.memory`, a `Memory`, and writes nothing but a key seen revoked. Store the counter and anything V8 asks in your own transaction. It resolves with `Knocked`.
- **`checkRecords(name, deep, latest)`**, V3: the name's `Records` at the latest block and 3 blocks deep. It gives back a `Valid`: `{ key, keyHash, nullifier, poh }`.
- **`checkBinding(receipt, wanted)`**, V6: a `Receipt` and what's `Wanted` of it, `{ tx, contract, nullifier, keyHash, node, final?, now, maxAge }`, with `now` in unix seconds and `maxAge` in seconds. It gives back a `Binding`.
- **`okName(name)`** and **`okOrigin(origin)`**: whether Knock takes this name and this origin.
- **`parseKey(text)`**: a `knock.key` record as `{ key, keyHash }`, or `null` if it isn't a compressed P-256 key.
### Assertion
What door-sdk's `knock()` resolves with: `{ v, name, challenge, origin, deviceNode, counter, mode, nullifier, humanRef, r, s }`. [The protocol](protocol.md#knockpass) says what each field is.
### Strict
`'off' | 'safe' | 'finalized'`. **`strictFrom(text)`** reads it as people write it: `off` (or nothing, `false`, `0`, `no`), `on` (`true`, `1`, `yes`, `safe`) or `finalized`. Anything else throws.
## Chain
Where the checks read the chains:
```ts
interface Chain {
head(): Promise // mainnet's latest block
records(name: string, block: bigint): Promise // the name's Knock records at a block
receipt(tx: Hex): Promise // a World Chain receipt
finalBlock(strict: 'safe' | 'finalized'): Promise // World Chain's safe or finalized head
}
```
**`rpcChain({ eth?, world? })`** makes one over JSON-RPC with viem. Give each an RPC URL or a viem client, a mainnet one made with `chain: mainnet`. Left out, they're `ETH_RPC` and `WORLD_RPC`, public endpoints. ENS is read through its Universal Resolver, at the block asked for.
- **`Records`**: `{ v, key, status, nullifier, poh }`, the five `knock.*` text records, each `null` when empty. **`recordsOf(texts)`** makes one from the texts in `RECORDS`' order.
- **`Receipt`**: `{ ok, block, logs }`, where each log is `{ address, topics, data }`.
A chain that throws is a chain that couldn't be read: the knock is refused as `unavailable`, and the error's URLs are left out of `why`.
## Store
What a verifier remembers between knocks. Every instance of your server must reach the same one.
```ts
interface Store extends Memory {
issue(challenge: Hex, origin: string, now: number): Promise // false: too many out
challenge(challenge: Hex): Promise // { origin, issuedAt, used }
entered(action: string, nullifier: Hex): Promise
commit(c: Commit): Promise<'challenge' | 'counter' | 'entry' | null>
}
```
**`commit()`** takes a `Commit`, `{ challenge, origin, key, counter, entry, now }`, where `entry` is `{ action, nullifier, name }` or `null`. In one transaction it marks the challenge used (if it's this origin's, unused and unexpired), raises the key's counter (only upward), and makes the entry (if it's new). Each is checked again as it's written, so of two knocks racing for one challenge, counter or entry, one gets it. It answers with what the other lost, or `null`, and writes nothing when it loses.
### Memory
What `check()` needs from a store:
```ts
interface Memory {
counter(key: Hex): Promise // the key's highest counter taken
revoke(key: Hex, now: number): Promise
isRevoked(key: Hex): Promise // a key seen revoked stays revoked
}
```
### The stores here
- **`memoryStore()`**: in the process, for tests and a site that's one long-lived process.
- **`libsqlStore(client)`**: in a libSQL database, from `@libsql/client` (or `@libsql/client/web` on edge runtimes): a SQLite file, Turso, or memory. Any client with `execute`, `executeMultiple` and `transaction('write')` will do: that's `SqlClient`. It runs `SCHEMA` on first use: four tables, `knock_challenges`, `knock_counters`, `knock_revoked` and `knock_entries`.
## Constants
| Constant | Value | |
| --- | --- | --- |
| `WORLD_CHAIN` | `480` | World Chain's chain id. |
| `CHALLENGE_MS` | `120_000` | How long a challenge lasts. |
| `DEPTH` | `3n` | How many blocks deep `knock.status=active` must be. |
| `CACHE_MS` | `60_000` | How long a name's deep records are kept. |
| `MAX_BINDING_DAYS` | `365` | The oldest binding that counts, by default. |
| `RECORDS` | | `knock.v`, `knock.key`, `knock.status`, `knock.nullifier`, `knock.poh`. |
| `KNOCK_BINDING` | `0x7748D2787DcB6E6024968edC19fcA13ccAbA1699` | KnockBinding v3, production, on World Chain. |
| `ETH_RPC` | `https://ethereum-rpc.publicnode.com` | The public mainnet RPC used by default. |
| `WORLD_RPC` | `https://worldchain-mainnet.g.alchemy.com/public` | The public World Chain RPC used by default. |
# Verifier HTTP API
> knock-verifier's three routes, their requests and answers, status codes, CORS and limits.
`knock-verifier` listens on `VERIFIER_ADDR`, `127.0.0.1:8788` by default, and takes JSON. Every refusal has the same shape, whichever route gives it.
## POST /challenge
A challenge for one knock: 32 random bytes, for the verifier's origin, good once, for 120 seconds. It takes no body.
```bash
curl -s -X POST http://127.0.0.1:8788/challenge
```
```json
{
"challenge": "0x6c1f…e2a9",
"origin": "https://club.example",
"expiresAt": 1793860320
}
```
| Field | What it is |
| --- | --- |
| `challenge` | 32 bytes in hex. Pass it to `knock()` as it is. |
| `origin` | The origin knocks must be for: `VERIFIER_ORIGIN`. |
| `expiresAt` | When it expires, in unix seconds. |
It can refuse with `busy` and status 429, at check 0 when the hour's limit is reached, or at check 2 when 10,000 challenges are outstanding.
## POST /verify
Checks a knock, V1 to V8 in order, and if all pass, uses up what it used in one transaction.
```json title="Request"
{
"assertion": {
"v": 1,
"name": "wedgie.itzraj.eth",
"challenge": "0x6c1f…e2a9",
"origin": "https://club.example",
"deviceNode": "0x…",
"counter": 12,
"mode": 1,
"nullifier": "0x…",
"humanRef": "0x0000000000000000000000000000000000000000000000000000000000000000",
"r": "0x…",
"s": "0x…"
},
"action": "enter"
}
```
| Field | What it is |
| --- | --- |
| `assertion` | The knock from `knock()`, as it is. Unknown fields are refused. |
| `action` | Optional. Lets each human do this once: 1 to 32 characters of `a`–`z`, `0`–`9` and `-`. [One human, once](../guides/one-human-once.md). |
The body may have no other fields.
### When it passes
Status 200:
```json
{
"ok": true,
"knock": {
"name": "wedgie.itzraj.eth",
"key": "0x02…",
"nullifier": "0x…",
"counter": 12,
"origin": "https://club.example",
"binding": { "tx": "0x…", "block": 37301500, "provedAt": 1793860000 }
}
}
```
| Field | What it is |
| --- | --- |
| `name` | The ENS name it knocked as. |
| `key` | The Wedgie's public key, from ENS: compressed P-256, 33 bytes in hex. |
| `nullifier` | The human's nullifier, the same on every Knock site. Key your records by it. |
| `counter` | The Wedgie's counter for this knock. |
| `origin` | Your origin. |
| `binding.tx` | The binding's World Chain transaction, from `knock.poh`. |
| `binding.block` | The World Chain block it's in. |
| `binding.provedAt` | About when its World ID proof was made, in unix seconds. Bindings older than `VERIFIER_MAX_BINDING_DAYS` are refused. |
### When it's refused
```json
{
"ok": false,
"check": 3,
"error": "revoked",
"why": "wedgie.itzraj.eth is revoked: its owner turned it off"
}
```
| Field | What it is |
| --- | --- |
| `check` | The check that said no, 1 to 8, or 0 for the verifier's own trouble. Every check before it passed, and none after it ran. |
| `error` | A code your code can act on. [Checks and refusals](checks.md) lists them all. |
| `why` | The reason in words, for people. |
A refused knock uses up nothing: its challenge, the counter and the entry are all as they were.
## GET /health
What the verifier checks knocks for.
```json
{
"binding": "0x7748d2787dcb6e6024968edc19fca13ccaba1699",
"maxBindingDays": 365,
"ok": true,
"origin": "https://club.example",
"strict": "off"
}
```
| Field | What it is |
| --- | --- |
| `binding` | The KnockBinding whose bindings count: Knock's v3, production, on World Chain, or the one `VERIFIER_KNOCK_BINDING` names. |
| `maxBindingDays` | `VERIFIER_MAX_BINDING_DAYS`. |
| `origin` | `VERIFIER_ORIGIN`. |
| `strict` | `off`, `safe` or `finalized`. [Strict mode](../guides/strict-mode.md). |
## Status codes
| Status | When |
| --- | --- |
| 200 | A challenge, a knock that passed, or health. |
| 400 | Check 1: the body or the knock isn't well formed. |
| 403 | Checks 2 to 8 said no. |
| 429 | `busy`: the verifier's limits. Try again later. |
| 500 | `internal`: the verifier's own store failed. |
| 503 | `unavailable`: a chain couldn't be read. Try again. |
## CORS
The verifier allows exactly one origin, `VERIFIER_ORIGIN`, with `GET`, `POST` and the `content-type` header. Browsers on any other origin can't read its answers. Requests from your server aren't affected, since CORS is between browsers and sites.
## Limits
| Limit | Value |
| --- | --- |
| Body | 16 KiB |
| `POST /challenge` | 600 an hour, per process |
| `POST /verify` | 600 an hour, per process |
| Outstanding challenges | 10,000 |
| A challenge's life | 120 seconds, single use |
## TypeScript types
```ts title="verifier.ts"
export type Challenge = { challenge: `0x${string}`; origin: string; expiresAt: number }
export type Knocked = {
name: string
key: `0x${string}`
nullifier: `0x${string}`
counter: number
origin: string
binding: { tx: `0x${string}`; block: number; provedAt: number }
}
export type Verdict = { ok: true; knock: Knocked } | { ok: false; check: number; error: string; why: string }
```
# knock-verify (Rust)
> The verifier's checks as a Rust library, for a server that checks knocks itself. Verifier, the Chain trait, Store, and every public item.
`knock-verify` is what `knock-verifier` runs: V1 to V8 in order, failing closed, and a SQLite store for what's been used. Use it when your server is in Rust and you'd rather not run a second service. Its source is [`crates/knock-verify/src/lib.rs`](https://github.com/getknock/knock/blob/main/crates/knock-verify/src/lib.rs).
## Add it
It isn't on crates.io, so depend on the repo:
```toml title="Cargo.toml"
[dependencies]
knock-verify = { git = "https://github.com/getknock/knock" }
alloy = { version = "2.5", default-features = false, features = ["std"] }
```
Its types are alloy's (`Address`, `B256`, `U256`), so use the same alloy, 2.5. Its Rust is edition 2024: Rust 1.88 or later.
## Use it
```rust title="src/knock.rs"
use knock_verify::{Store, Strict, Verifier, accepted_binding};
fn now() -> u64 {
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs()
}
pub async fn example(chain: MyChain, assertion: serde_json::Value) -> anyhow::Result<()> {
let verifier = Verifier::new(
chain, // your Chain: mainnet and World Chain
Store::open("knock.db")?, // or ":memory:" for tests
"https://club.example".to_string(), // the origin knocks must be for
accepted_binding().map_err(anyhow::Error::msg)?, // KnockBinding v3, production
365 * 86_400, // the oldest binding it takes, in seconds
)
.strict(Strict::Off);
// For the page: pass challenge.challenge to knock()
let challenge = verifier.challenge(now()).map_err(|r| anyhow::anyhow!(r.why))?;
// The knock the page sent back
match verifier.verify(&assertion, Some("enter"), now()).await {
Ok(knocked) => println!("in: {} (human {})", knocked.name, knocked.nullifier),
Err(no) => println!("refused at check {}: {} ({})", no.check, no.why, no.error),
}
Ok(())
}
```
Share one `Verifier` across requests, in an `Arc`. It's `Send` and `Sync` when your `Chain` is.
## Verifier
```rust
pub struct Verifier {
pub chain: C,
pub store: Store,
pub origin: String,
pub contract: Address,
pub max_binding_age: u64,
pub strict: Strict,
/* a cache of deep ENS reads */
}
```
| Method | What it does |
| --- | --- |
| `Verifier::new(chain, store, origin, contract, max_binding_age)` | A verifier for one origin. Not strict. |
| `.strict(strict)` | The same verifier, strict: it takes a binding only once World Chain's safe or finalized head has reached it. |
| `.challenge(now)` | A new `Challenge` for the page, good once, for `CHALLENGE_TTL` seconds. Refuses with `busy` at check 2 when 10,000 are outstanding. |
| `.verify(&assertion, action, now).await` | V1 to V8, then one commit. `assertion` is the knock's JSON as a `serde_json::Value`. `action` is the site's action for V8, or `None`. Returns `Knocked`, or the first `Refusal`. |
`now` is unix seconds. The store's commit happens only after every check has passed, so a refused knock uses up nothing.
## The Chain trait
How the verifier reads the chains. You implement it over your RPCs:
```rust
pub trait Chain: Sync {
fn head(&self) -> impl Future