Verify a member
An application, whole, is sent to the Pool's verifier as member.applied. The verifier checks it against the source that issued the credential, and writes back only what it checked, on its own write path with its vt_ token. Each write replaces the last, and freshness runs from it.
How does what a member claims become what the Pool relies on? A verifier you run checks each application against the source that issued the credential, and writes back only what it checked. Nobody else can write it, and nothing a member says counts until it does.
A member applies at the Pool's /join, one step at a time:
Once every step is answered, the Pool emits member.applied with every answer, for the verifier:
→ GET /owner/events?type=signer.applied # the owner's feed: what the verifier is sent{
"type": "OK",
"object": "list",
"data": [
{
"id": "evt_5gX1X2B5swzY61",
"object": "event",
"seq": 1,
"type": "signer.applied",
"created": "2026-10-25T14:02:11.000Z",
"job": null,
"service": null,
"data": {
"member": "mbr_7Hq2",
"pool": "attorneys",
"email": "grace@hopper.example",
"answers": {
"name": "Grace Hopper",
"credential": "patent agent",
"number": "71,234",
"regions": [
"US-CA"
],
"areas": [
"patents"
],
"hours": 10,
"turnaround": "24h",
"min_fee": 120
},
The verifier writes through POST /<pool>/members/:id/verify, with its own vt_ token as a bearer. facts.credentials names each credential held and the regions it's held in; facts.coverage is optional, the policy in force per act and when it ends. A secret key is refused there. Here is a 20-line verifier that looks the number up on a roster, and writes only what the roster says:
cat verify.ts// The attorneys Pool's verifier, illustrative: services.dev sends it member.applied when an application is whole. It
// looks the number up on the roster that issued it, and writes back only what the roster says, through the Pool's
// verifier door with its vt_ token. Each write replaces the last; nothing merges, and freshness runs from it.
type Applied = { member: string; pool: string; answers: { name?: string; credential?: string; number?: string; regions?: string[] } }
type Entry = { name: string; credential: string; regions: string[]; standing: 'good' | 'suspended'; coverage?: { per_act: number; until: string } }
export async function verify(event: Applied, roster: Record<string, Entry>, door: { url: string; token: string; fetch?: typeof fetch }) {
const a = event.answers
const entry = a.number ? roster[a.number] : undefined
const holds = entry?.standing === 'good' && entry.name === a.name && entry.credential === a.credential
// Only the regions the roster and the application both name, or everywhere for a credential that counts everywhere.
const regions = !holds ? [] : entry.regions.includes('*') ? ['*'] : entry.regions.filter(r => (a.regions ?? entry.regions).includes(r))
const facts = { credentials: holds ? { [entry.credential]: regions } : {}, ...(holds && entry.coverage ? { coverage: entry.coverage } : {}) }
const res = await (door.fetch ?? fetch)(`${door.url}/${event.pool}/members/${event.member}/verify`, {
method: 'POST',
headers: { authorization: `Bearer ${door.token}`, 'content-type': 'application/json' },
body: JSON.stringify({ facts }),
})
return { status: res.status, facts, why: holds ? 'on the roster, in good standing' : entry ? `the roster's entry for ${a.number} doesn't match, or isn't in good standing` : `no ${a.number ?? 'number'} on the roster` }
}→ await verify(event.data, roster, { url, token: 'vt_test_7Hq2' }) # the member.applied event above← {"status":200,"why":"on the roster, in good standing"}
facts {"credentials":{"patent agent":["*"]},"coverage":{"per_act":1000000,"until":"2027-06-30T00:00:00Z"}}A number the roster holds in another name, or not in good standing, writes no credential, and the member is offered nothing:
→ await verify(event.data, roster, { url, token }) # mbr_7Hq3, NY-777← {"status":200,"why":"the roster's entry for NY-777 doesn't match, or isn't in good standing"}
facts {"credentials":{}}→ POST /attorneys/signers/mbr_7Hq2/verify Authorization: Bearer sk_test_7Hq2← 401 key_invalid: This door takes the network's verifier token, Authorization: Bearer vt_…, and nothing else.Every write joins the member's journal, and the latest is what is true: nothing merges, so a write that leaves out coverage means none is in force. The member's record shows it, and who wrote it:
→ POST /attorneys/join {"email":"grace@hopper.example"} # the member reads their own record← 200 mbr_7Hq2 · attorneys · grace@hopper.example · verified: patent agent in * · on
verified {"credentials":{"patent agent":["*"]},"coverage":{"per_act":1000000,"until":"2027-06-30T00:00:00Z"},"at":"2026-10-25T14:02:11.000Z","by":"vt_test_7Hq2","fresh_until":"2026-11-24T14:02:11.000Z","fresh":true}The member's own portal shows the same card, their name as admitted, the credential and number they gave, how long it stays fresh, and the coverage written:
fresh_until is the write's time plus the Pool's fresh, 30 days unless you set it. It is read at each use, so time alone makes a fact stale: a stale member is offered nothing until the verifier writes again, and nothing waits on them. An unverified member is never offered a Task. The member verified at join, on a clock 32 days on:
→ POST /test_clocks {"frozen_at":"2026-11-26T14:02:11Z"}← tc_7Hq3
→ POST /matters {"service":"contract-review","contract":"lease.pdf","client":{"name":"Ada Lovelace","email":"signs@example.test"},"client_region":"US-CA","max_usd":99,"test_clock":"tc_7Hq3","key":"stale"}
← job_7Hq2 · waiting · on claim · $99.00 held · advise · tsk_7Hq2
→ POST /reviews/tsk_7Hq2/claim # as mbr_7Hq2
← 403 ineligible · availability: mbr_7Hq2's verified facts went stale on 2026-11-24.
cure The verifier rechecks your facts; nothing waits on you.dev stands in a fixture verifier: it writes what the application claims, labeled simulated, the moment an application is whole. Your own verifier's writes replace it, as above. Once deployed, each Pool's vt_ token is minted at deploy, and member.applied reaches your verify URL, signed and retried.
| Key | Written | Left out | Does |
|---|---|---|---|
verify | Url | Fn | required | The verifier: the only writer of verified facts, on its write path with its vt_ token. On localhost a fixture verifier writes what the application claims, labeled simulated. |
fresh | Within | '30d' | Re-verify every: freshness is read at use, so time alone makes a fact stale. |