Add a signer

The running jobjob_7Hq2 · accepted · document.fr-CA.pdf · $49.00 final

A sworn translator from your Pool signs every job. A rules row reserves the act to their credential, and the signature is stamped into the file it binds.

Some work is finished only when a credentialed person signs it. A Service meets that by design: the signer is a role filled from your Pool, a rules row reserves signing to their credential, and the gate commits the signature, bound to the exact file. AI still does the work. This is Grow 3.

Three things join Translation: a role, the Pool that fills it, and one row of your rules table.

grow/role.ts
import { Pool, Rules, Service } from 'services.dev'

// Grow 3, add a signer: a sworn translator from lingua's Pool signs every job before delivery, and the
// stamp writes the member, their credential and the date into the file the signature binds. The rules
// row reserves signing to that credential, so only a verified member can take the step. AI does the work.
export default Service({
  name: 'Translation',
  description: 'A document, translated and signed.',
  input: { document: 'file', to: 'language: a BCP 47 tag, such as fr-CA' },
  output: { document: 'file' },
  rules: rules(),
  roles: { translator: { pool: translators(), pay: 30 } },
  deliver: {
    glossary: { ask: 'buyer', to: 'answer', items: () => [{ id: 'terms', label: 'Which terms stay in English?' }] },
    translate: 'ai',
    harden: { review: 'adversarial', lenses: ['meaning', 'terms'], rounds: 2 },
    sign: { by: 'translator', stamp: true, est: '20min' },
  },
  price: 89,
})
KeyWhat it does
roles.translatorWho takes the sign step: filled from pool, and paid pay dollars an act, posted on the Task. A role with no pool is your own staff
Pool({ roles, join, verify })Your marketplace: the credential each role needs, what a person sends to join, and the verifier, the only writer of that credential
Rules({ version, services })Your table. This row reserves sign to a sworn translator, and position records why
sign.stampWrites the member, their credential and the date into the file before the signature hashes it
sign.estHow long signing takes, so an offer shows its rate

Here the Pool and the table sit below the Service. In a brand they're files of their own, pools/ and rules.ts, and either can ship as a package several brands import. The row is illustrative; your counsel writes the real one (law.org.ai).

Run
npx services.dev check
magic values: none
services/translation.ts · 13 lines · $89 per job, within 48 hours, charged on delivery.
  who     A translator signs every document before delivery. (rules 2026-10) The buyer answers at glossary.
  roles   translator · sworn translator · from sworn-translators · $30 an act
  steps   glossary · buyer · answer → translate · ai → harden · ai · adversarial · 2 lenses → sign · translator · * · stamped
  bounds  reserved sign to sworn translator
  agent   order_translation · 25 of 30 words · job_7Hq2 waits on party
  ledger  +$89.00 · AI $1.26 (example) · translator $30.00 · you $57.74
  margin  at least $41.20 when the first translator signs ($89.00 − $30.00 translator − $17.80 AI cap) · $26.20 at the ladder's cap, the lower of 50% and what the floor leaves · $11.20 after a decline and a second translator
          a job that stops costs you at most $47.80
  apps    customer portal · delivery portal · owner console

mcp     13 buyer tools · 4,490 of 8,000 tokens · 9 more with ?toolset=sandbox
✓ ready to sell · 1 Service · it keeps at least 46% of every job
  next    npx services.dev dev

AI and money are simulated until you wire them. $0.42 a call is an example.

bounds is the rules row. margin prices each way the signature goes: the first translator, the fill ladder's cap, or a second translator after a decline. Give the role another credential and check refuses with reserved_act_unstaffed:

Run
npx services.dev check
magic values: none
services/translation.ts · 13 lines · $89 per job, within 48 hours, charged on delivery.
  who     A translator signs every document before delivery. (rules 2026-10) The buyer answers at glossary.
  roles   translator · certified translator · from sworn-translators · $30 an act
  steps   glossary · buyer · answer → translate · ai → harden · ai · adversarial · 2 lenses → sign · translator · * · stamped
  bounds  reserved sign to sworn translator
  agent   order_translation · 25 of 30 words · job_7Hq2 waits on party
  ledger  +$89.00 · AI $1.26 (example) · translator $30.00 · you $57.74
  margin  at least $41.20 when the first translator signs ($89.00 − $30.00 translator − $17.80 AI cap) · $26.20 at the ladder's cap, the lower of 50% and what the floor leaves · $11.20 after a decline and a second translator
          a job that stops costs you at most $47.80
  apps    customer portal · delivery portal · owner console
  ✗ A step performs a reserved act, and no role that does it holds the credential its row reserves it to. Give the step to a role that holds the credential, or set the role's credential to one that meets it. sign performs sign, reserved to sworn translator (rules 2026-10), and the translator role holds only certified translator.
    at services/translation.ts:26 · roles.translator.credential
    https://services.dev/docs/reference/check/reserved_act_unstaffed

mcp     13 buyer tools · 4,490 of 8,000 tokens · 9 more with ?toolset=sandbox
✗ 1 to fix
  next    Give the step to a role that holds the credential, or set the role's credential to one that meets it. sign performs sign, reserved to sworn translator (rules 2026-10), and the translator role holds only certified translator.

AI and money are simulated until you wire them. $0.42 a call is an example.

A sworn translator joins first. On localhost a fixture verifier writes their credential, marked simulated:

Call
→ POST /sworn-translators/join {"email":"ines@example.com","answers":{"name":"Inès Martin","credential":"sworn translator","languages":["fr","en"]}}
magic values: none
← 200 mbr_7Hq2 · sworn-translators · ines@example.com · verified (simulated): sworn translator in * · on

The job waits on the Task until the translator signs, then the buyer's next wait answers delivered:

npx services.dev translation document=https://example.com/lease.pdf to=fr-CA --max 89 --key grow
job_7Hq2 · working · $89.00 held
job_7Hq2 · waiting · on party · $89.00 held
  record  
  next    hand http://localhost:8787/a/ask_7Hq2?t=8FaWT5EoCj4h-uMyjlXp3_ to the person it asks

# 2 hours later, the buyer answers through the link
→ POST /asks/ask_7Hq2?t=8FaWT5EoCj4h-uMyjlXp3_ {"items":{"terms":{"answer":"Lessee, Lessor, Premises"}}}

$ npx services.dev jobs wait job_7Hq2
job_7Hq2 · waiting · on claim · $89.00 held
  record  glossary.terms · answered | glossary · buyer · answer · clock paused 2h | translate · ai | harden · ai · adversarial · 2 lenses · pass
  next    a member claims tsk_7Hq2: npx services.dev tasks sheet tsk_7Hq2, in their session

# mbr_7Hq2, the sworn translator, claims tsk_7Hq2 in the delivery portal and signs, in their own session
→ POST /tasks/tsk_7Hq2/claim · POST /tasks/tsk_7Hq2/clear · POST /tasks/tsk_7Hq2/attest

$ npx services.dev jobs wait job_7Hq2
job_7Hq2 · delivered · lease.fr-CA.pdf · $89.00 charged
curl -X POST http://localhost:8787/jobs -H 'content-type: application/json' -d '{"service":"translation","document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr-CA","key":"grow","max_usd":89}'
  next    get_job {"id":"job_7Hq2"}

$ curl http://localhost:8787/jobs/job_7Hq2?wait=50
← 200 · job_7Hq2 · waiting · on party · $89.00 held
  next    hand_to_person {"ask":"ask_7Hq2"}

# 2 hours later, the buyer answers through the link
→ POST /asks/ask_7Hq2?t=8FaWT5EoCj4h-uMyjlXp3_ {"items":{"terms":{"answer":"Lessee, Lessor, Premises"}}}

$ curl http://localhost:8787/jobs/job_7Hq2?wait=50
← 200 · job_7Hq2 · waiting · on claim · $89.00 held
  next    get_job {"id":"job_7Hq2"}

# mbr_7Hq2, the sworn translator, claims tsk_7Hq2 in the delivery portal and signs, in their own session
→ POST /tasks/tsk_7Hq2/claim · POST /tasks/tsk_7Hq2/clear · POST /tasks/tsk_7Hq2/attest

$ curl http://localhost:8787/jobs/job_7Hq2?wait=50
← 200 · job_7Hq2 · delivered · lease.fr-CA.pdf · $89.00 charged
→ tools/call order_translation {"document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr-CA","key":"grow","max_usd":89}
← job_7Hq2 · working · $89.00 held
  next    get_job {"id":"job_7Hq2"}

→ resources/read job://job_7Hq2?wait=50
← job_7Hq2 · waiting · on party · $89.00 held
  next    hand_to_person {"ask":"ask_7Hq2"}

# 2 hours later, the buyer answers through the link
→ POST /asks/ask_7Hq2?t=8FaWT5EoCj4h-uMyjlXp3_ {"items":{"terms":{"answer":"Lessee, Lessor, Premises"}}}

→ resources/read job://job_7Hq2?wait=50
← job_7Hq2 · waiting · on claim · $89.00 held
  next    get_job {"id":"job_7Hq2"}

# mbr_7Hq2, the sworn translator, claims tsk_7Hq2 in the delivery portal and signs, in their own session
→ POST /tasks/tsk_7Hq2/claim · POST /tasks/tsk_7Hq2/clear · POST /tasks/tsk_7Hq2/attest

→ resources/read job://job_7Hq2?wait=50
← job_7Hq2 · delivered · lease.fr-CA.pdf · $89.00 charged
await lingua.translation.order({"document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr-CA","key":"grow","max_usd":89})
← job_7Hq2 · working · $89.00 held
  next    get_job {"id":"job_7Hq2"}

await lingua.jobs.wait("job_7Hq2")
← job_7Hq2 · waiting · on party · $89.00 held
  next    hand_to_person {"ask":"ask_7Hq2"}

# 2 hours later, the buyer answers through the link
→ POST /asks/ask_7Hq2?t=8FaWT5EoCj4h-uMyjlXp3_ {"items":{"terms":{"answer":"Lessee, Lessor, Premises"}}}

await lingua.jobs.wait("job_7Hq2")
← job_7Hq2 · waiting · on claim · $89.00 held
  next    get_job {"id":"job_7Hq2"}

# mbr_7Hq2, the sworn translator, claims tsk_7Hq2 in the delivery portal and signs, in their own session
→ POST /tasks/tsk_7Hq2/claim · POST /tasks/tsk_7Hq2/clear · POST /tasks/tsk_7Hq2/attest

await lingua.jobs.wait("job_7Hq2")
← job_7Hq2 · delivered · lease.fr-CA.pdf · $89.00 charged

You own the brand, and you still can't sign:

Call
→ POST /sworn-translators/jobs/job_7Hq2/approve
magic values: none
← 403 needs_a_person: sign is tsk_7Hq2, a Task for a member of sworn-translators: the member who claims and clears it attests, in their own session.

Only the member who claims the Task attests, in their own session:

Call
→ POST /tasks/tsk_7Hq2/claim
magic values: none
← tsk_7Hq2 · claimed · translator · sign · * · $30.00 · 20 min · $90.00 an hour · due 2026-10-27 16:02
→ POST /tasks/tsk_7Hq2/clear · GET /tasks/tsk_7Hq2/packet
→ POST /tasks/tsk_7Hq2/attest
← tsk_7Hq2 · done · translator · sign · * · $30.00 · attested · 20 min · $90.00 an hour · due 2026-10-27 16:02

← job_7Hq2 · delivered · lease.fr-CA.pdf · $89.00 charged
  record  glossary.terms · answered | glossary · buyer · answer · clock paused 2h | translate · ai | harden · ai · adversarial · 2 lenses · pass | sign · person · sworn translator · * · mbr_7Hq2
  ledger  +$89.00 · AI $1.26 · translator $30.00 · you $57.74
  signed  {"member":"mbr_7Hq2","credential":"sworn translator","region":"*","rules":"2026-10","fields":["document"],"hash":"sha256:7fc0ab6fb17cb034de1bdb37a90b2e59a9dd4191277212ea85890e001962353f"}
  stamp   Signed by mbr_7Hq2, sworn translator, 2026-10-25.

signed is what the gate committed: who, under which credential and rules version, and a hash of the file as signed. A later change voids it. The gate commits AI and people the same way.

Run
npx services.dev diff grow/ask.ts grow/role.ts
magic values: none
grow/ask.ts (9 lines) → grow/role.ts (13 lines)
Translation · changed description, deliver, human, price, portal, roles, rules · adds a person: the human line, or a step a role does; a role from a Pool: its members, and the Tasks they claim

API   ~ operation service.order.translation  POST /translation  item.description
      ~ operation service.quote.translation  GET /translation.json  answers.description, answers.deliver, answers.human.sentence
      + operation job.approve                POST /{portal}/jobs/{id}/approve
      + operation member.join                POST /{portal}/join
      + operation member.verify              POST /{portal}/members/{id}/verify
      + operation member.availability        POST /me/availability
      + operation member.ledger.get          GET /me/ledger
      + operation member.ledger.add          POST /me/ledger
      + operation member.ledger.checks       GET /me/ledger/checks
      + operation member.missed              GET /me/missed
      + operation task.list                  GET /tasks
      + operation task.get                   GET /tasks/{id}
      + operation task.sheet                 GET /tasks/{id}/sheet
      + operation task.pass                  POST /tasks/{id}/pass
      + operation task.claim                 POST /tasks/{id}/claim
      + operation task.clear                 POST /tasks/{id}/clear
      + operation task.packet                GET /tasks/{id}/packet

A Pool brings its own surface: joining and every Task act on the API and CLI, and the delivery portal in the App. A person acts in their own session, so none of it is an MCP tool.

The tutorial goes on to File it somewhere. On the licensed path, read human: the line, then rules and roles.