Every surface, from one contract

The running jobjob_7Hq2 · delivered · document.fr-CA.pdf · $49.00 charged

One Service() becomes an API, a CLI, an MCP server, an SDK, a Site, Docs and an App. Each one renders the same contract, so no surface can say something the others don't.

You write the Service. Everything else is generated from it: your API, CLI, MCP server, SDK, Site and Docs, and an App with a portal for buyers, a portal for the people you place in the loop, and a console for you. Each renders the same contract, so an order from a terminal, from an agent's MCP client or from your own code is one job, with one id, one price and one set of answers.

Four lines are a whole Service:

TypeScript
import { Service } from 'services.dev'

export default Service({
  name: 'Translation',
  input: { document: 'file', to: 'language: a BCP 47 tag, such as fr-CA' },
  output: { document: 'file' },
  price: 49,
})

npx services.dev serves it on localhost, and the same order works on four surfaces. Captured from one run:

Output
magic values: none

# CLI
$ npx services.dev translation document=https://example.com/lease.pdf to=fr-CA --max 49 --key lease
job_7Hq2 · working · $49.00 held

# API
$ 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","max_usd":49,"key":"lease"}'
← 202 · job_7Hq2 · working · $49.00 held

# MCP
→ tools/call order_translation {"document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr-CA","max_usd":49,"key":"lease"}
← job_7Hq2 · working · $49.00 held

# SDK
await lingua.translation.order({"document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr-CA","max_usd":49,"key":"lease"})
← job_7Hq2 · working · $49.00 held

same job, job_7Hq2, and the same body on all four surfaces, request ids aside

The capture fails unless all four surfaces answer with the same body.

The docs come in two layers, written for two different readers.

  • Layer A is what you're reading: services.dev's own docs, for you as the builder. What each surface derives from, how to shape it, and how to run the business.
  • Layer B is what your brand serves. Your buyers, their agents, your integrators and your members read it on your domain, in your words, with no services.dev name. Every page whose reader is a buyer, an integrator, an agent or a member is Layer B.

Layer A never re-explains a Layer B page. Layer B serves on localhost as you build, and on your own domain once you deploy. Everything lingua serves with no key, from one run:

Output
magic values: none

$ curl http://localhost:8787<path>   # no key
GET /.well-known/agents.json  200 · application/json  brand.card
GET /openapi.json             200 · application/json  brand.openapi
GET /llms.txt                 200 · text/markdown     brand.llms
GET /manifest.json            200 · application/json  brand.manifest
GET /pricing                  200 · application/json  brand.pricing
GET /verify/suite.json        200 · application/json  brand.verify
GET /translation              200 · text/html         the Site's page for Translation
GET /console                  200 · text/html         the owner console

Each surface is the operations registry read in one column, plus what that surface adds.

SurfaceRendersRowsAdds
APIthe HTTP column88 rows/openapi.json with webhooks · one error shape · idempotency keys · /verify/suite.json
CLIthe CLI column67 rowslisten · test clocks · npx <brand> · login · a member's commands
MCPthe MCP column, minus every row only a person's session calls23 rowsannotations from each effect · a prompt per Service · stdio · the job:// resource
SDKthe SDK column, typed from the manifest35 rowsliteral unions and typed errors · webhooks.verify() · published under your name
Siteeach Service's name, description, price sentence and human sentence; its steps as a timeline; the boundarythe manifesta page per Service · /pricing · /terms · .md twins
Docsevery registryall five registries/llms.txt · a page per Service and per operation · /llms-full.txt · a changelog per deploy
App: customer portalevery operation a buyer, a client or a party calls55 rowsits screens, each control an operation
App: delivery portalevery operation a member's session calls30 rowsits screens, each control an operation
App: owner consoleevery operation88 rowsits screens, each control an operation

A call only a person's session or a party's own link can make, such as approving a step or signing through an ask, has no MCP tool and no SDK method, so an agent gets the link to hand to the person instead. The contract shows it row by row, and What an agent can never do explains why.

Adding a signer to Translation is two lines in one file. diff prints what that did to every surface at once:

Terminal
$ npx services.dev diff grow/ask.ts grow/role.ts
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

The approval became a route, a CLI command and a delivery portal at /sworn-translators, never an MCP tool. The new price reached every surface in the same save.

In this order, everywhere they're listed. The table above gives each its status.

  1. API: your routes, answers and events. Your API.
  2. CLI: services.dev for building, and the same calls for buyers. The CLIs.
  3. MCP: a tool per call an agent may make. Your MCP server.
  4. SDK: each call as a typed SDK method. The SDKs.
  5. Site: a page per Service, with its price and who delivers it.
  6. Docs: a page per Service and per operation, and /llms.txt for agents.
  7. App: one shell with a view for everyone who takes part in a job.
    • The customer portal, for buyers, clients and parties, on every Service.
    • The delivery portal, for the people you place in the loop.
    • The owner console, for you: the ledger and the line.

The API, the CLI, the MCP server, the SDK, the site, the docs and the apps name only your brand. Your buyers run npx <brand>, never npx services.dev, and every surface serves on your own domain.

Every surface renders the same contract, and no surface renames it. A brand may use its own words, such as "matter" for job, but only by declaring them in Brand.words. check fails surface_drift the moment a surface serves a call the contract doesn't have. The contract shows both.