Join: the application
The application a Pool writes becomes its /join funnel, email first and resumable, ending on the member's payout account. What a member says is an application; only the verifier writes what is true.
How does a person get into a Pool, and what makes them verified?
join is the application, field by field, in the same type words a Service's input uses. The legal example's Pool asks for a name, a credential, where it's held and what work the person wants:
grep -A5 "join:" pools/attorneys.ts join: {
name: 'text: your name as admitted', credential: ['bar admission', 'patent attorney', 'patent agent', 'law school enrollment'],
number: 'text: your bar or registration number', regions: 'jurisdiction[]: every place you are admitted',
areas: ['patents', 'trademarks', 'contracts', 'entities', 'collections'], hours: 'number: spare hours a week',
turnaround: ['same day', '24h', '48h'], min_fee: 'money: the least you would take per review',
},It becomes the Pool's /join funnel: email first, resumable, three fields a step. The feed reads what a member declares here, such as their areas, minimum fee and hours, and what they are offered is theirs to set. Each call brings the answers it has, and the member learns what the next step asks:
→ POST /attorneys/join {"email":"grace@example.com"}← 200 mbr_7Hq2 · attorneys · grace@example.com · applying, step 1 of 3: name, credential, number · on
→ POST /attorneys/join {"email":"grace@example.com","answers":{"name":"Grace Hopper","credential":"bar admission","number":"CA-301"}}
← 200 mbr_7Hq2 · attorneys · grace@example.com · applying, step 2 of 3: regions, areas, hours · on
→ POST /attorneys/join {"email":"grace@example.com","answers":{"regions":["US-CA","US-NY"],"areas":["contracts","collections"],"hours":10}}
← 200 mbr_7Hq2 · attorneys · grace@example.com · applying, step 3 of 3: turnaround, min_fee · on
→ POST /attorneys/join {"email":"grace@example.com","answers":{"turnaround":"24h","min_fee":40}}
← 200 mbr_7Hq2 · attorneys · grace@example.com · verified (simulated): bar admission in US-CA, US-NY · on
verified {"credentials":{"bar admission":["US-CA","US-NY"]},"coverage":{"per_act":1000000,"until":"2027-10-25T14:02:11.000Z"},"at":"2026-10-25T14:02:11.000Z","by":"fixture","fresh_until":"2026-11-24T14:02:11.000Z","fresh":true}A member never self-reports. What they apply with is what they say. What the Pool relies on is what verify writes, on its own write path with its own vt_ token. A secret key is refused there:
→ POST /attorneys/signers/mbr_7Hq2/verify {"facts":{"credentials":{"bar admission":["US-CA","US-NY","US-NJ"]},"coverage":{"per_act":2000000,"until":"2027-10-25T00:00:00Z"}}} Authorization: Bearer sk_test_7Hq2← 401 key_invalid: This door takes the network's verifier token, Authorization: Bearer vt_…, and nothing else.→ POST /attorneys/signers/mbr_7Hq2/verify {"facts":{"credentials":{"bar admission":["US-CA","US-NY","US-NJ"]},"coverage":{"per_act":2000000,"until":"2027-10-25T00:00:00Z"}}} Authorization: Bearer vt_test_7Hq2← 200 mbr_7Hq2 · attorneys · grace@example.com · verified: bar admission in US-CA, US-NY, US-NJ · onEvery write joins the member's recheck journal, and the latest is what is true, read fresh at each use: Verify a member has the verifier and its clock. On localhost a fixture verifier writes what the application claims and labels it simulated, so you can run the whole market before you wire your own.
The last step of /join connects the member's payout account, once for every Pool and brand.
The owner adds a person from the CLI, through the same funnel. Answers that leave a step short keep it open, and next names what it still needs:
npx services.dev signers add attorneys grace@example.com "credential=bar admission" number=CA-301 regions=US-CA,US-NY areas=contracts hours=10 turnaround=24h min_fee=40mbr_7Hq2 · attorneys · grace@example.com · applying, step 1 of 3: name, credential, number · on
login http://localhost:8787/attorneys/login?t=ms_bU0qS0IkbnMxlMCPdfIc9j
next npx services.dev signers add attorneys grace@example.com name=… credential=… number=…A member acts only in their own session. On localhost, members login mints one and prints the link. A key that tries a member's act is handed the link to pass on:
npx services.dev signers login mbr_7Hq2mbr_7Hq2 · a signer session, on localhost
link http://localhost:8787/attorneys/login?t=ms_bU0qS0IkbnMxlMCPdfIc9j
next export SERVICES_DEV_SESSION=ms_bU0qS0IkbnMxlMCPdfIc9jnpx services.dev reviews listOnly a signer, in their own session at http://localhost:8787/attorneys, does this. A key never can.
next hand http://localhost:8787/attorneys to the person it asks
docs https://api.lawyer/docs/errors/needs_a_personThese captures speak the brand's own words, as your brand's words shows:
legal's wordsmatter is a jobasset is a subjectreview is a tasksigner is a membernetwork is a pooloutcome is a end
| Key | Written | Left out | Does |
|---|---|---|---|
join | Shape | required | The application, field by field, in the type words: the /join funnel, email first, resumable, three fields a step. The feed reads areas, min_fee (dollars), hours and turnaround when it asks them. |
verify | Url | Fn | required | The verifier: the only writer of verified facts, on its write path with its vt_ token. Verify a member. |