The contract

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

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.

RegistryRowsA row holdsFull table
operations86one 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
objects16one per prefixed id, such as job_, ask_ and rcp_, with the fields every object carries/docs/reference/objects
credentials9one per credential: who holds it, what it can call, and the surfaces it reaches/docs/reference/credentials
events86one per event, with the audiences that may read it/docs/reference/events
errors49one per code, joined to every operation that can return it/docs/reference/errors

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 covers the four envelopes, and Your events who reads which event.

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

Output
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",

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

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:

Effectgate: nonegate: accessgate: persongate: party
read12: service.quote, service.list, …18: job.get, job.wait, …7: member.ledger.get, member.ledger.checks, …1: ask.get
record2: service.waitlist, key.create10: job.policy, job.signal, …9: member.join, member.availability, …
act10: 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 follows that through.

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:

Output
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' },
Output
✓ 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 has the gate's full reference.

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:

Run
cat brand.ts
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

check fails word_collision 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.