---
title: "Verify a member"
url: https://services.dev/docs/people/pool/verify
captured: [legal-verify.txt, member-wire.txt, pool-join.txt]
---
# Verify a member

URL: https://services.dev/docs/people/pool/verify

> 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.

## The application, whole

A member applies at the Pool's `/join`, one step at a time:

*Screen: the delivery portal at `/attorneys/join`, nothing waiting (captured/screens/legal-delivery-join-1.html).*

*Screen: the delivery portal at `/attorneys/join`, nothing waiting (captured/screens/legal-delivery-join-2.html).*

Once every step is answered, the Pool emits `member.applied` with every answer, for the verifier:

```txt
→ GET /owner/events?type=signer.applied   # the owner's feed: what the verifier is sent
```

```json title="200 · list · 1 signer.applied · magic values: none"
{
  "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
        },
        "verify": "https://verify.example.test/credentials"
      },
      "request": {
        "id": "req_S0lIQP",
        "idempotency_key": null
      },
      "livemode": false,
      "simulated": true
    }
  ],
  "has_more": false,
  "url": "/events",
  "next_seq": 1
}
```

## The verifier's write path

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:

```bash
cat verify.ts
```

```txt title="magic values: none"
// 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` }
}
```

```txt
→ await verify(event.data, roster, { url, token: 'vt_test_7Hq2' })   # the member.applied event above
```

```txt title="magic values: none"
← {"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:

```txt
→ await verify(event.data, roster, { url, token })   # mbr_7Hq3, NY-777
```

```txt title="magic values: none"
← {"status":200,"why":"the roster's entry for NY-777 doesn't match, or isn't in good standing"}
  facts {"credentials":{}}
```

```txt
→ POST /attorneys/signers/mbr_7Hq2/verify   Authorization: Bearer sk_test_7Hq2
```

```txt title="magic values: none"
← 401 key_invalid: This door takes the network's verifier token, Authorization: Bearer vt_…, and nothing else.
```

## Each write replaces the last

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:

```txt
→ POST /attorneys/join {"email":"grace@hopper.example"}   # the member reads their own record
```

```txt title="magic values: none"
← 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:

*Screen: the delivery portal at `/attorneys`, nothing waiting (captured/screens/legal-delivery-verified.html).*

## Fresh, at the moment of use

`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:

```txt
→ POST /test_clocks {"frozen_at":"2026-11-26T14:02:11Z"}
```

```txt title="magic values: client.email = signs@example.test"
← 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.
```

## On localhost

`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.

## Keys

| 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. |

---

- Up: [Pool(): your marketplace](https://services.dev/docs/people/pool.md)
- Previous: [The door: where members join and work](https://services.dev/docs/people/pool/door.md)
- Next: [Eligibility](https://services.dev/docs/people/pool/eligibility.md)
