Join: the application

The running jobjob_7Hq2 · waiting · on approval · $49.00 held

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:

Run
grep -A5 "join:" pools/attorneys.ts
magic values: client.email = signs@example.test
  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:

Call
→ POST /attorneys/join {"email":"grace@example.com"}
magic values: client.email = signs@example.test
← 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:

Call
→ 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
magic values: client.email = signs@example.test
← 401 key_invalid: This door takes the network's verifier token, Authorization: Bearer vt_…, and nothing else.
Call
→ 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
magic values: client.email = signs@example.test
← 200 mbr_7Hq2 · attorneys · grace@example.com · verified: bar admission in US-CA, US-NY, US-NJ · on

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

Run
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=40
magic values: none
mbr_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:

Run
npx services.dev signers login mbr_7Hq2
magic values: none
mbr_7Hq2 · a signer session, on localhost
  link    http://localhost:8787/attorneys/login?t=ms_bU0qS0IkbnMxlMCPdfIc9j
  next    export SERVICES_DEV_SESSION=ms_bU0qS0IkbnMxlMCPdfIc9j
Run
npx services.dev reviews list
magic values: none
Only 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_person

These 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

KeyWrittenLeft outDoes
joinShaperequiredThe 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.
verifyUrl | FnrequiredThe verifier: the only writer of verified facts, on its write path with its vt_ token. Verify a member.