---
title: "The contract"
url: https://services.dev/docs/surfaces/contract
captured: [brand-words.txt]
---
# The contract

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

> Five registries derived from your Services (operations, objects, credentials, events and errors) that every surface renders and none renames.

The contract is everything your brand can be asked, by anyone, on any surface. You never write it: services.dev derives it from your Services on every `dev` and `check`, and every surface is generated from it. A route, a command, an MCP tool and an SDK method are each one rendering of a row, and `check` holds all four to the registry on every run.

## Five registries

| Registry | Rows | A row holds | Full table |
|---|---|---|---|
| operations | 86 | one per call: its HTTP route, CLI command, MCP tool and SDK method, who may call it, its effect and gate, what it emits and every answer it can give | [/docs/reference/operations](https://services.dev/docs/reference/operations.md) |
| objects | 16 | one per prefixed id, such as `job_`, `ask_` and `rcp_`, with the fields every object carries | [/docs/reference/objects](https://services.dev/docs/reference/objects.md) |
| credentials | 9 | one per credential: who holds it, what it can call, and the surfaces it reaches | [/docs/reference/credentials](https://services.dev/docs/reference/credentials.md) |
| events | 86 | one per event, with the audiences that may read it | [/docs/reference/events](https://services.dev/docs/reference/events.md) |
| errors | 49 | one per code, joined to every operation that can return it | [/docs/reference/errors](https://services.dev/docs/reference/errors.md) |

The counts are lingua's; the reference pages print every row.

The registries join: an operation names the credentials that may call it, the events it emits and the codes it can answer with. That join is why each SDK method carries its own typed errors. [Answers](https://services.dev/docs/api/answers.md) covers the four envelopes, and [Your events](https://services.dev/docs/api/events.md) who reads which event.

## One row

`service.order`, every field, then where two rows land in what lingua's `dev` serves:

```txt
magic values: none

# service.order: one row of lingua's operations registry, every field
{
  "id": "service.order",
  "says": "Order a job, or get the offer: the price and the exact next call. A Service whose price has every answers a subscription, sbn_….",
  "http": "POST /jobs",
  "also": [
    "POST /:service"
  ],
  "cli": "<service> k=v …",
  "mcp": "order_<service>",
  "sdk": "<service>.order()",
  "actor": [
    "sk_test_",
    "sk_live_",
    "dlg_",
    "tk_"
  ],
  "effect": "act",
  "gate": "access",
  "idempotent": "key",
  "returns": "job",
  "takes": {
    "service": {
      "in": "body",
      "type": "string",
      "says": "The Service to order. POST /<service> names it in the path instead."
    },
    "max_usd": {
      "in": "body",
      "type": "number",
      "says": "The most you will pay, in dollars. Missing or below the price, you get an offer instead of a job."
    },
    "key": {
      "in": "body",
      "type": "string",
      "says": "Any string you choose. Retrying with the same key returns the same job."
    },
    "metadata": {
      "in": "body",
      "type": "object",
      "says": "Your own keys, echoed on the job and its events."
    },
    "test_clock": {
      "in": "body",
      "type": "string",
      "says": "A test clock the job runs on: tc_…."
    }
  },
  "emits": [
    "job.ordered",
    "charge.held"
  ],
  "can_answer": [
    "OFFER",
    "input_invalid",
    "policy_invalid",
    "key_reused",
    "not_found",
    "not_live",
    "act_refused",
    "region_not_ratified",
    "brand_unverified",
    "period_taken"
  ],
  "status": "live"
}

# service.order, rendered, each looked up in what lingua's dev serves
http POST /jobs                      ✓ a route
http POST /:service                  ✓ a route
cli  translation                     ✓ a command
mcp  order_translation               ✓ a tool in tools/list
sdk  translation.order()             ✓ a method

# job.approve, rendered, each looked up in what lingua's dev serves
http POST /:portal/jobs/:id/approve  ✓ a route
http POST /jobs/:id/approve          ✓ a route
http POST /jobs/:id/sign             ✓ a route
http POST /owner/jobs/:id/approve    ✓ a route
cli  jobs approve                    ✓ a command
mcp  never                           only a person's session calls it, and no key or MCP client holds one
sdk  never                           only a person's session calls it, and no key or MCP client holds one

✓ every rendering above is served, and check holds each of lingua's 88 rows to its surfaces the same way
```

`<service>` fans out: one row is an order command, MCP tool and SDK method for each of your Services. `takes` is every surface's parameters: the OpenAPI body and query, the CLI's `k=v` fields, the tool's input schema and the SDK method's argument type. `can_answer` types each SDK method's errors.

## Effect and gate

Every operation carries two words that decide how each surface treats it.

Its **effect** says what calling it does:
- **read** changes nothing;
- **record** writes to the record without moving the work or the money, such as setting a policy value or putting a file;
- **act** moves the job or the money: ordering, accepting, canceling, approving, signing.

Its **gate** says who gets through:
- **none**: anyone, with no key;
- **access**: a key or session allowed to make that call;
- **person**: only a person's own session;
- **party**: only the named party, through their own link.

Every operation, by effect and gate:

| Effect | gate: none | gate: access | gate: person | gate: party |
|---|---|---|---|---|
| **read** | 12: `service.quote`, `service.list`, … | 18: `job.get`, `job.wait`, … | 7: `member.ledger.get`, `member.ledger.checks`, … | 1: `ask.get` |
| **record** | 2: `service.waitlist`, `key.create` | 10: `job.policy`, `job.signal`, … | 9: `member.join`, `member.availability`, … |  |
| **act** |  | 10: `service.order`, `job.accept`, … | 14: `job.approve`, `task.claim`, … | 3: `ask.complete`, `ask.revoke`, … |

An MCP tool for a read is marked read-only and one for an act destructive, so an agent's client knows which calls change something. A row gated on a person or a party has no MCP tool and no SDK method, because no key or tool holds a person's session: `job.approve` above. [What an agent can never do](https://services.dev/docs/mcp/never.md) follows that through.

## surface_drift

`check` reads what each surface actually serves (its routes, its CLI commands, its MCP tools and resources, its SDK methods) and holds each one against the operations registry. It fails `surface_drift` when a surface serves a call with no row, or when a row has no rendering on a surface its callers reach and no `never` saying why.

In this run, lingua's API is given one route more than its contract:

```txt
magic values: none

# lingua serves GET /stats as brand.stats, and the operations registry has no row for it
{
  "code": "surface_drift",
  "reason": "A surface serves a call with no row, or lacks a live row.",
  "next": "Add the row, or render it. http GET /stats serves brand.stats, which has no row.",
  "doc_url": "https://services.dev/docs/reference/check/surface_drift"
}
```

The fix is the row. With it, the route has callers, an effect and a gate, and every other surface knows whether to render it:

```diff
--- operations
+++ operations
+  { id: 'brand.stats', says: 'Jobs delivered and accepted this month.', http: 'GET /stats',
+    cli: null, mcp: null, sdk: null, actor: ['anyone'],
+    effect: 'read', gate: 'none', idempotent: true, returns: 'stats',
+    emits: [], can_answer: [], status: 'live' },
```

```txt
✓ surface_drift · every route, command, tool and method lingua serves has a row, and each of the 87 rows is rendered where its callers reach
```

[surface_drift](https://services.dev/docs/reference/check/surface_drift.md) has the gate's full reference.

## Brand.words: the one declared renaming

The contract's words are generic: job, subject, Task, member, Pool, end. The core, these docs and every op id keep them. A brand whose buyers say "matter" declares its words once, in `Brand.words`, and every Layer B surface renders them: the path `/matters/:id`, the event `matter.ordered`, the command `matters get`, the tool `get_matter`, the SDK method `matters.get()`, and the copy on its docs, Site and apps. The manifest carries both words.

The legal example's brand file sets them. Its dev renders `job.get` and `service.order` in each surface's column, and each rendering is looked up in what that dev serves:

```bash
cat brand.ts
```

```txt title="magic values: none"
import { Brand } from 'services.dev'

// A legal brand shaped like api.lawyer. Its words render every surface its
// buyers, agents and signers read: /matters, matter.ordered, get_matter. `also` keeps api.lawyer's own paths
// for the same rows: a matter's journal and attestations are its record, its documents are its files, and a
// signer's conflicts ledger is /conflicts.
export default Brand({
  domain: 'api.lawyer',
  words: { job: 'matter', subject: 'asset', task: 'review', member: 'signer', pool: 'network', end: 'outcome' },
  also: {
    'job.record': ['/matters/:id/journal', '/matters/:id/attestations?type=attested'],
    'job.files.list': '/matters/:id/documents',
    'job.files.put': 'POST /matters/:id/documents',
    'job.end': '/matters/:id/terminal',
    'member.ledger.get': '/conflicts',
    'member.ledger.add': '/conflicts/entries',
  },
})

# job.get: the generic column, then the legal brand's, each looked up in what its dev serves
http   GET /jobs/:id             → GET /matters/:id            ✓ a route
cli    jobs get                  → matters get                 ✓ a command
mcp    get_job                   → get_matter                  ✓ a tool in tools/list
sdk    jobs.get()                → matters.get()               ✓ a method

# service.order: the generic column, then the legal brand's, each looked up in what its dev serves
http   POST /jobs                → POST /matters               ✓ a route
cli    contract-review           → contract-review             ✓ a command
mcp    order_contract_review     → order_matter                ✓ a tool in tools/list
sdk    contractReview.order()    → contractReview.order()      ✓ a method
emits  job.ordered, charge.held  → matter.ordered, charge.held ✓ on GET /events, after the order below
```

`check` fails [word_collision](https://services.dev/docs/reference/check/word_collision.md) when a brand's word is another generic word, a word of an op id or a path services.dev already serves, or when it would make two rows render the same.

---

- Up: [Every surface, from one contract](https://services.dev/docs/surfaces.md)
- Previous: [Every surface, from one contract](https://services.dev/docs/surfaces.md)
- Next: [Your API](https://services.dev/docs/api.md)
