---
title: "Every surface, from one contract"
url: https://services.dev/docs/surfaces
---
# Every surface, from one contract

URL: https://services.dev/docs/surfaces

> 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.

## The smallest form

Four lines are a whole Service:

```ts
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:

```txt
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.

## Two layers

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:

```txt
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
```

## What each surface renders

Each surface is the [operations registry](https://services.dev/docs/reference/operations.md) read in one column, plus what that surface adds.

| Surface | Renders | Rows | Adds |
|---|---|---|---|
| **API** | the HTTP column | 88 rows | `/openapi.json` with webhooks · one error shape · idempotency keys · `/verify/suite.json` |
| **CLI** | the CLI column | 67 rows | `listen` · test clocks · `npx <brand>` · `login` · a member's commands |
| **MCP** | the MCP column, minus every row only a person's session calls | 23 rows | annotations from each effect · a prompt per Service · stdio · the `job://` resource |
| **SDK** | the SDK column, typed from the manifest | 35 rows | literal unions and typed errors · `webhooks.verify()` · published under your name |
| **Site** | each Service's name, description, price sentence and human sentence; its steps as a timeline; the boundary | the manifest | a page per Service · `/pricing` · `/terms` · `.md` twins |
| **Docs** | every registry | all five registries | `/llms.txt` · a page per Service and per operation · `/llms-full.txt` · a changelog per deploy |
| **App**: customer portal | every operation a buyer, a client or a party calls | 55 rows | its screens, each control an operation |
| **App**: delivery portal | every operation a member's session calls | 30 rows | its screens, each control an operation |
| **App**: owner console | every operation | 88 rows | its 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](https://services.dev/docs/surfaces/contract.md) shows it row by row, and [What an agent can never do](https://services.dev/docs/mcp/never.md) explains why.

## Change one line, and every surface changes

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

```txt
$ 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
      + operation task.packet                GET /tasks/{id}/packet
      + operation task.release               POST /tasks/{id}/release
      + operation task.offer                 POST /owner/tasks/{id}/offer
      + operation task.attest                POST /tasks/{id}/attest
      + operation task.send_back             POST /tasks/{id}/send_back
      + operation task.decline               POST /tasks/{id}/decline
      + operation task.recuse                POST /tasks/{id}/recuse
      + operation task.extend                POST /tasks/{id}/extend
      + operation task.contest               POST /tasks/{id}/contest
      + operation task.message               POST /tasks/{id}/messages
      + operation member.login               POST /owner/members/{id}/login
      ~ operation brand.manifest             GET /manifest.json  answers.services, answers.portals
      + object tsk_                          task
      + object mbr_                          member
      + 11 errors                            needs_a_person, not_waiting, ineligible, not_cleared, task_taken, defects_required, memo_required, needs_direction, not_contestable, reserved, question_open
CLI   ~ operation service.order.translation  services.dev translation document=… to=…  price
      + operation job.approve                services.dev jobs approve <job>
      + operation member.join                services.dev members add <pool> <email> k=v …
      + operation member.availability        services.dev me availability <availability>
      + operation member.ledger.get          services.dev me ledger
      + operation member.ledger.add          services.dev me ledger add <name> k=v …
      + operation member.ledger.checks       services.dev me ledger checks
      + operation member.missed              services.dev me missed
      + operation task.list                  services.dev tasks list
      + operation task.get                   services.dev tasks get <task>
      + operation task.sheet                 services.dev tasks sheet <task>
      + operation task.pass                  services.dev tasks pass <task> <reason>
      + operation task.claim                 services.dev tasks claim <task>
      + operation task.clear                 services.dev tasks clear <task>
      + operation task.packet                services.dev tasks packet <task>
      + operation task.release               services.dev tasks release <task>
      + operation task.offer                 services.dev tasks offer <task> <member>
      + operation task.attest                services.dev tasks attest <task> k=v …
      + operation task.send_back             services.dev tasks send-back <task> <defects>
      + operation task.decline               services.dev tasks decline <task> <memo>
      + operation task.recuse                services.dev tasks recuse <task>
      + operation task.extend                services.dev tasks extend <task>
      + operation task.contest               services.dev tasks contest <task> <reason>
      + operation task.message               services.dev tasks message <task> <kind> <text>
      + operation member.login               services.dev members login <member>
      + 11 errors                            needs_a_person, not_waiting, ineligible, not_cleared, task_taken, defects_required, memo_required, needs_direction, not_contestable, reserved, question_open
MCP   ~ operation order_translation          order_translation  description
      ~ prompt buy_translation               prompts/get buy_translation  description
SDK   ~ object job_ (translation)            Job<"translation">  + stage: "glossary" | "translate" | "harden" | "sign"
      + object tsk_                          task
      + object mbr_                          member
      + 11 errors                            needs_a_person, not_waiting, ineligible, not_cleared, task_taken, defects_required, memo_required, needs_direction, not_contestable, reserved, question_open
Site  ~ operation service.order.translation  /translation  + A document, translated and signed., + $89 per job, within 48 hours, charged on delivery. A translator signs at sign, stamped. Th
      ~ operation service.quote.translation  /translation.json  description, deliver, human.sentence
Docs  ~ operation service.order.translation  /llms.txt · Translation  + A document, translated and signed. $89 per job, within 48 hours, charged on delivery. A tr, + curl http://localhost:8787/jobs -H 'content-type: application/json' -d '{"service":"transl
      + 11 errors                            needs_a_person, not_waiting, ineligible, not_cleared, task_taken, defects_required, memo_required, needs_direction, not_contestable, reserved, question_open
App   ~ operation job.list                   /portal · customer portal  + translation: A translator signs at sign, stamped. The buyer answers at glossary.
      + operation job.approve                /sworn-translators
      ~ operation job.ledger                 /console · owner console  + translation: $89 per job, within 48 hours, charged on delivery. A translator signs at sign
```

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.

## The seven surfaces

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

1. **API**: your routes, answers and events. [Your API](https://services.dev/docs/api.md).
2. **CLI**: `services.dev` for building, and the same calls for buyers. [The CLIs](https://services.dev/docs/cli.md).
3. **MCP**: a tool per call an agent may make. [Your MCP server](https://services.dev/docs/mcp.md).
4. **SDK**: each call as a typed SDK method. [The SDKs](https://services.dev/docs/sdk.md).
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.

## Your brand, never ours

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.

## One contract, never renamed

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](https://services.dev/docs/surfaces/contract.md) shows both.

---

- Up: [Your first paid job](https://services.dev/docs.md)
- Previous: [Plans](https://services.dev/docs/money/plans.md)
- Next: [The contract](https://services.dev/docs/surfaces/contract.md)
