Pool(): your marketplace
One kind of people, verified once, offered only work they can do, paid for every act and never charged to work. A role names a Pool, and the Pool fills it.
A Pool answers one question: who may take a role's work, and how does it reach them?
AI does every step it can, and a Pool member is the rung above your staff in who does the step. Where you want a person, or the work needs a credentialed one, a role names a Pool. Each step that role does posts a Task, and the Pool's members claim it. A Pool is your marketplace for one kind of people: verified once, offered only work they can do, and paid for every act.
Four keys: its name, the roles it fills, the application and the verifier. A role on Translation names it, and a rules row reserves the signature to the credential:
cat pools/translators.tsimport { Pool } from 'services.dev'
export const translators = Pool({
name: 'translators',
roles: { translator: { credential: 'sworn translator' } },
join: { name: 'text', languages: 'language[]' },
verify: 'https://verify.example.test/translators',
})cat services/translation.tsimport { translators } from '../pools/translators.ts'
roles: { translator: { pool: translators } },
deliver: {
translate: 'ai',
sign: { by: 'translator', stamp: true },check reads the Pool back, and dev serves its portal, its /join and its verifier's write path:
npx services.dev checkpools/translators.ts · 4 lines · translators · 1 role: translator · joins in 1 step · feed · hold 24h
roles translator · sworn translator · from translators
bounds reserved sign to sworn translator
apps customer portal · delivery portal · owner console
✓ ready to sell · 1 Service · it keeps at least 80% of every jobnpx services.dev dev --no-try translators http://localhost:8787/translators · join at /translators/join, or npx services.dev members add translators <email>
verifier vt_test_7Hq2 · the only writer of verified facts, at POST /<pool>/members/:id/verify- Work they can do. Eligibility is derived from the role and the rules table, never declared. The first predicate a member fails comes back with its cure.
- An offer, never an assignment. A Task reaches members by its dispatch, at a fee fixed when it posts. Only the fill ladder moves it, and only up.
- Their floor, their hours. Their minimum fee, areas, hours and the brands they work for filter what they are offered, in one feed order for every brand.
- Only what they need, when they need it. A teaser, then a sheet, then the packet, as What a member sees shows.
- Their conflicts kept theirs. A hashed ledger no one else reads.
- Paid for every act, on a clock they can read. A send-back or a decline is paid like an attestation, and Pay says when each dollar is theirs.
- Their record, their own. A track record signed by the door, which they carry and share with another door's Pool when they choose.
No line charges a member per Task, per lead or by a percentage, and check fails member_charged on any line a member would pay. The one member charge a Pool can carry is a flat software fee, declared in the Pool as fee and shown on the door before anyone joins. It is the first line of the member charter, which no Pool, importer or door can weaken.
Credentialed steps are how a Service meets licensing by design. The rules table reserves an act to a credential, and only a member who holds it, verified and where the act needs it, can claim the Task.
The buyer pays the job's price. The member's fee is one line of it, paid for the act, and the brand keeps what's left after AI. A raise the ladder makes comes out of the brand's share, and the cap keeps a brand-funded raise from losing money. Here the ladder lifted a $50 fee to $55, and the buyer still paid the $99 they were quoted:
→ POST /matters/job_7Hq2/accept
← job_7Hq2 · accepted · flags.pdf · memo.pdf · $99.00 final
ledger +$99.00 · attorney's fee $55.00, sold by the attorney (rail member) · brand $44.00 · AI $1.26 · you $42.74Two Services, or two brands, that name one Pool share its members and one queue. The legal example's five Services all name its one Pool, each at its own fee:
$ npx services.dev check
services/contract-review.ts · 16 lines · $99 per job, within 48 hours, charged on acceptance.
roles attorney · bar admission · from attorneys · $50 an act
services/demand-letter.ts · 26 lines · $149 per job, within 48 hours, charged on send's receipt.
roles attorney · bar admission · from attorneys · $75 an act
services/docket-watch.ts · 10 lines · $12 per month, charged at the start of each month.
roles watcher · patent attorney or agent · * · from attorneys · $4 an act
services/formation.ts · 30 lines · $299 per job, within 48 hours, charged on form_entity's receipt.
roles attorney · bar admission · from attorneys · $60 an act
Its 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
Each key links to the section that explains it.
| Key | Written | Left out | Does |
|---|---|---|---|
name | string | required | The portal's name: its path, /<name>, and its people. |
door | string | /<name> on the Service's brand | Where members join and work: a domain your account operates, or an existing door, pinned at its terms' version. |
register | 'expert' | 'work' | 'expert' | What members deliver: an approval on a packet, or a completed Task at a site, in a window. |
roles | { [K in R]: PoolRole } | required | The roles this Pool fills, each with its credential expression and defaults. A Service names one by its key. |
scopes | { [K in C]?: '*' | 'region' } | 'region' | Each credential's scope: '*' (one admission counts everywhere) or 'region'. A credential the roles name and this leaves out fails credential_unscoped. |
join | Shape | required | The application, field by field, in the type words. 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. |
fresh | Within | '30d' | Re-verify every: freshness is read at use, so time alone makes a fact stale. |
sees | { teaser: string[]; sheet?: string[] } | { teaser: ['act', 'region', 'fee_usd'] } | What a member sees: the teaser before claiming (always with est_minutes, rate and due, never a party name), and the sheet. |
clear | string[] | { attest: string[]; check?: string[] } | none | What a member attests before the packet opens, and the party inputs checked, hashed, against their ledger. |
hold | Within | { claim; then: 'touch' } | max(24h, 3 × est) | How long a claim holds before it reopens, never past its due: a span, or renewed by each touch. A member may extend it once. |
window | ShortSpan | '1h' | How long one member has a cascade's offer, or a first refusal, to themselves. |
sheet | ShortSpan | '10min' | How long one member has a sheet to themselves. |
jump | { min?: number } | { min: 3 } | When passes move the ladder: at least min distinct sheet viewers since the last rung, more than half passing for one reason. |
kill | Pct | '50%' | The share of its fee a member keeps when a Task they cleared is withdrawn. |
affinity | Within | none | First refusal on follow-on work for the same subject. |
rank | ('fit' | 'track' | 'speed' | 'price')[] | ['fit', 'track', 'speed'] | How cascade ranks members. A member on probation ranks by fit and speed alone. |
dispatch | Dispatch | 'feed', or 'cascade' when clear.check names parties | How this Pool's Tasks reach members. |
fill | FillRung[] | false | raise 10% at 2h, widen at 4h, firms at 8h, staff at 12h | The fill ladder, for a Task no one claims. A raise is of the posted fee, never lowered; a rung never widens past eligibility. false waits, with an alarm. |
fill_cap | Pct | the lower of 50% and what the job's margin leaves | Raises stop here, over the posted fee, so a brand-funded raise never loses money. |
tiers | Tier[] | none | Tiers above eligibility, low to high, computed from the track record: a Task offers to the top eligible tier first. |
probation | { tasks: number; sample: Pct } | { tasks: 5, sample: '100%' } | A new member's first Tasks, all sampled. |
golden | Pct | false | false | A share of Tasks with a known answer, scored only for quality. |
contest | { by: 'peer' | 'staff'; within?: Within } | 'peer' where a row reserves the act, else 'staff' | Who rules a contest of a member's work. |
limits | { claims?; open?; declines?; late_releases? } | none | Throttles and breakers, kept outside the track record. |
firms | boolean | none | A firm may enroll a bench. |
coverage | { per_act: number; by: 'pool' | 'member' | 'either'; usd?: number } | none | Insurance in force per act, who carries it, and what it costs the brand per act, a ledger line. |
contact | 'thread' | 'thread' | Member contact with a party relays through the job's thread: email and phone never cross either way. |
fee | { usd: number; per: 'month' } | none | The one member charge allowed: a flat software fee, shown on the door. |
invite_by_buyer | boolean | none | A buyer may invite their own professional. |
standby | { window: string; hours: number; retainer_usd: number } | none | Committed capacity: a dated window, hours a week and a weekly retainer. |
grow | { waitlist?: true; referral_usd?: number; invites?: number } | none | Supply growth: a waitlist, a referral bonus, invites per member. |
public | boolean | none | Publish the demand board on the door and in llms.txt. |
export | { placement; to; min_pay?; notice?; first? } | none | Let other brands import this Pool, each paying placement dollars per filled Task. |
descriptor | string, with {member} | the member's name, then VIA and your domain | How a member-rail charge reads on the buyer's statement. |
payouts | { at: 'act'; after?: Within } | 'same-day' | 'weekly' | { schedule; hold_first?; instant_fee? } | { at: 'act', after: '2d' } | When a member's pay is payable, and when payouts move it. |
| Op | API | CLI | MCP | SDK | Site | Docs | App |
|---|---|---|---|---|---|---|---|
member.join | POST /:portal/join | members add <pool> <email> k=v … | never: a person acts | never: a person acts | 2 error pages | delivery portal, owner console | |
member.verify | POST /:portal/members/:id/verify | 2 error pages | the verifier's write path, with its vt_ token | ||||
member.availability | POST /me/availability | me availability <availability> | never: a person acts | never: a person acts | 1 error page | delivery portal | |
member.ledger.get | GET /me/ledger | me ledger | never: a person acts | never: a person acts | delivery portal | ||
member.ledger.add | POST /me/ledger | me ledger add <name> k=v … | never: a person acts | never: a person acts | 1 error page | delivery portal | |
member.ledger.checks | GET /me/ledger/checks | me ledger checks | never: a person acts | never: a person acts | delivery portal | ||
member.missed | GET /me/missed | me missed | never: a person acts | never: a person acts | delivery portal | ||
member.earnings | GET /me/earnings | me earnings | never: a person acts | never: a person acts | delivery portal | ||
member.track | GET /me/track | me track | never: a person acts | never: a person acts | delivery portal | ||
member.login | POST /owner/members/:id/login | members login <member> | never: a person acts | never: a person acts | 1 error page | owner console | |
task.list | GET /tasks, GET /owner/tasks | tasks list | never: a person acts | never: a person acts | delivery portal, owner console | ||
task.get | GET /tasks/:id, GET /owner/tasks/:id | tasks get <task> | never: a person acts | never: a person acts | 2 error pages | delivery portal, owner console |
Objects: tsk_, mbr_.
Events: member.applied, task.posted, task.unreached, task.offered, task.claimed, task.hold_expiring, task.extended, task.raised, task.widened, task.due_moved, task.declined, task.recused, task.contested, task.ruled, pay.accrued, pay.payable, pay.paid.
Operations has every column of every row.
Every member act is a person's own, in their session. No MCP tool or SDK method exists for one, and an agent that reaches one gets the link to hand on.
| Code | Category | HTTP | Why |
|---|---|---|---|
ineligible | blocked | 403 | The member can't take this Task: the answer names the first eligibility predicate they fail, and its cure. |
not_cleared | blocked | 403 | The packet opens only to the member who claimed the Task and cleared it. |
task_taken | conflict | 409 | Another member holds this Task, or has it offered to them first. |
defects_required | invalid_request | 422 | A send-back names its defects: what is wrong, for the step it returns to. |
memo_required | invalid_request | 422 | A decline carries a reasoned memo, which only the client reads. |
check gate | Why |
|---|---|
pool_role_unknown | A Service's role names a Pool that has no role by that name, so no member could ever be offered its Task. |
reserved_act_unstaffed | A step performs a reserved act, and no role that does it holds the credential its row reserves it to. |
fill_bypasses_reserved | A fill rung could offer a reserved act to someone who doesn't hold its credential. |
rush_uncapped | A role is paid from the buyer, its Pool's fill ladder raises the fee, and nothing caps what the buyer pays for it. |
fill_underwater | The Pool's fill_cap lets the ladder raise a brand-funded fee past what the job leaves: a job filled at the cap loses money. |
sheet_without_consent | A Pool's sheet names the job's parties to the members who read it, and the client hasn't consented before the first Task posts. |
| When the file won't load | Thrown by | Why |
|---|---|---|
rung_unparsed | Pool() | Pool translators: fill[0] is 'raise 10% in 2h'. A rung is 'raise 10% 2h', 'widen 4h', 'firms 8h', 'staff 12h' or 'cancel 24h'. |
rungs_out_of_order | Pool() | Pool translators: fill[1] 'raise 10% 2h' comes before 'widen 4h': rungs run in time order, so write them that way. |
The legal example's Pool, as its file writes it:
$ cat pools/attorneys.ts
import { Pool } from 'services.dev'
// One Pool, shared by every legal Service: attorneys join, the verifier checks each admission, and
// a cascade offers each Task one member at a time, because `check` names parties. Tiers are computed from outcomes,
// so the default fill ladder widens to the next one down. An attorney's own malpractice policy covers their act; one
// who carries none is covered by the Pool's, at $4 an act. A claim holds three times the step's estimate, at least a day.
export const attorneys = Pool({
name: 'attorneys',
roles: {
attorney: { credential: 'bar admission' },
practitioner: { credential: ['patent attorney', 'patent agent'], where: '*' },
watcher: { credential: ['patent attorney', 'patent agent'], where: '*' },
The attorneys' Pool runs it end to end, and the preparers' Pool adds a season.
Whether a live Pool fills and pays, with an alarm and a control for each measure: Pool health.