The Verb Spec
How a verb is declared, what shape it returns, how it reaches four transports, what proof it hands back, and what a human has to approve before it runs. This is the contract behind the verb surface — written for the agents that consume it.
Manifest Format
Every verb is declared as a VerbRecord — a single JSON object that carries the verb's identity, schema, shape, and surface coverage.
{
"name": "payments.preview_refund_impact",
"description": "Project the impact of refunding a transaction WITHOUT executing it.",
"shape": "preview",
"side_effects": "read",
"input_schema": {
"type": "object",
"properties": {
"transaction_id": { "type": "integer", "minimum": 1 }
},
"required": ["transaction_id"]
},
"output_schema": { "$ref": "/schema/shapes/preview.json" },
"surfaces": ["http", "mcp_stdio", "webmcp", "ucp", "cli"],
"requires_consent": false,
"tier_floor": "starter"
}Field reference
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Dot-notation (payments.preview_refund_impact) or snake_case (charge_invoice). Must match ^[a-zA-Z][a-zA-Z0-9_.]*$. |
description | string | yes | Written for an LLM, not a human. States what the verb does and what the agent should expect. |
shape | enum | yes | One of the shapes below. |
side_effects | enum | no | read (default, safe to call freely), write (mutates state), mixed (writes but reverses cleanly). |
input_schema | JSON Schema | yes | OpenAI-compatible tool parameter schema. |
output_schema | JSON Schema | no | Describes the response. If omitted, the response is { type: object, additionalProperties: true }. |
surfaces | string[] | no | Transports: http, mcp_stdio, webmcp, ucp, cli. Defaults to all five. |
requires_consent | boolean | no | Whether the runtime gates this verb behind human approval. |
tier_floor | string | no | Minimum subscription tier. Defaults to starter. |
Naming conventions
Dot-notation for agent-attraction verbs (the multi-transport surface): {domain}.{verb} — the domain groups verbs logically (crm, payments, inventory, infrastructure, deal, customer).
Snake_case for ADA dispatcher verbs (the typed-phrase / consent-gated surface) — charge_invoice, subscription_upgrade_tier. These are the verbs that require human confirmation; the snake_case name is what the LLM emits in its function call.
Both styles coexist in one registry. The ADA-to-manifest bridge auto-mirrors snake_case names into the unified registry so discovery surfaces see the full set.
Registration semantics
| Rule | Meaning |
|---|---|
| One declaration per verb | The unified registry is the single source. MCP tool list, WebMCP manifest, UCP capabilities and CLI verbs all derive from it. |
| Last write wins | Re-registering the same name overwrites the previous record. |
| Surface filtering | A verb with surfaces: ["http", "cli"] will not appear in MCP or WebMCP tool lists. |
| Parity invariant | A verb visible on one transport MUST be visible on every transport in its surfaces set. Enforced by integration tests. |
company_id is NEVER in the schema
company_id is injected by the runtime from the authenticated session — never accepted from the LLM. Including it in input_schema.properties is a registration-time error. This is what stops cross-tenant data access via prompt injection. The same rule binds user_id: both come from the runtime, not the model.
Manifest endpoint
The live manifest is public and needs no key to read:
GET /api/v1/agent/verbs?surface=mcp_stdioReturns VerbRecords filtered by surface, cached for 5 minutes. New verbs appear within 5 minutes of a backend deploy — no client-side package republish.
Shapes
A shape is the response contract. An agent that knows the shape knows how to read the answer before it calls the verb.
Every verb declares one. The registry is the authority on which shapes exist and how many verbs carry each — the tiles above read it live. The table below describes what each shape gives an agent, which is the part that does not drift.
| Shape | What it gives the agent |
|---|---|
receipt | Attestation on a side-effecting write — audit id, rollback handle, idempotency key. |
discovery | Introspection: what exists, what this agent may call, what it costs. |
transaction | ACID grouping over multi-step writes — start, append, commit, abort. |
aggregate | Collapse 5–8 sub-queries into one tenant-scoped read. |
suggest | Ranked next-actions in a confidence envelope (confidence, band, reason, model_version, sample_size). |
preview | Dry-run a write before commit. Returns reversible: true|false. |
explain | The causal chain that produced a state. |
revert | Single-action undo against whitelisted audit rows. |
macro | Saved verb chains, promoted from observed sequences. |
reputation | Per-verb reliability history — what happened last time. |
subscribe | Cursor-polling observe streams over the audit log. |
trail | The chain of actions that led here. |
telemetry | Runtime health of the agent surface itself. |
Transport Bindings
One registry, four projections. Adding a verb to the unified registry lights up all four transports automatically.
UNIFIED_VERB_REGISTRY
│
┌──────────────┬───────┴──────────┬──────────────┐
CLI MCP stdio WebMCP UCP
shell agent Claude Desktop in-browser buyer-agent
Cursor/Windsurf Chrome agent Gemini/ChatGPTCLI
Shell-running agents (Claude Code, Codex, scripts) invoke verbs via the CLI.
solid verbs list --json # full manifest
solid verbs list --shape preview # filter by shape
solid agent dispatch <verb> --args '{"key": "value"}' --json [--confirm] [--phrase "..."]Write verbs require --confirm. Typed-phrase verbs require --phrase "EXACT PHRASE". Shortcut commands promote high-traffic verbs to first-class: solid infra diagnose, solid deal create, solid customer-context 42.
MCP stdio
MCP-speaking runtimes (Claude Desktop, Cursor, Windsurf, Cline) load the server via npx. Discovery is standard tools/list; invocation is standard tools/call. Verb names map 1:1 to tool names.
{
"mcpServers": {
"solidnumber": {
"command": "npx",
"args": ["-y", "@solidnumber/mcp"],
"env": { "SOLID_API_KEY": "sk_solid_..." }
}
}
}The MCP server is a thin bridge. It fetches the manifest from /api/v1/agent/verbs?surface=mcp_stdio and caches it for 5 minutes, so new verbs appear in tools/list without an npm republish. Auth is the SOLID_API_KEY env var — optional for read verbs, required for writes.
WebMCP
In-browser agents discover verbs via the W3C draft API — navigator.modelContext.registerTool() on page load, registered per surface (dashboard, portal, developer, tenant-site, public). Auth is the session cookie; the browser's authenticated session binds the company_id. Tenants are born WebMCP-aware with no manual setup.
GET /api/v1/webmcp/manifest?surface={dashboard|portal|developer|tenant-site|public}
POST /api/v1/webmcp/execute/{tool_name}
{ "input": { ... }, "surface": "dashboard" }UCP (Universal Commerce Protocol)
Public buyer-agents (Gemini AI Mode, ChatGPT shopping) discover capabilities at GET /.well-known/ucp. Invocation is HTTP with RFC 9421 ES256 message signatures; verbs map to capabilities under the com.solidnumber.* namespace. Auth is a per-company JWK rotation lifecycle (pending → active → rotating → retired), private keys encrypted at rest and RLS-protected.
The differentiator: Solid# exposes a hierarchical agent graph (platform → company → sales / AR / AP / commissions / service / compliance / marketing / ops sub-agents) instead of the stock UCP single-checkout-endpoint model.
Error contract
All four transports return errors in the same envelope:
{
"ok": false,
"verb": "infrastructure_resize",
"error": {
"reason": "no_managed_droplet",
"message": "This company has no managed droplet.",
"hint": "Use subscription_upgrade_tier for capacity changes."
}
}reason is machine-readable — the agent pattern-matches on it. message is for humans and LLMs. hint is the actionable next step.
Receipt Contract
Every write verb returns a receipt. The receipt is the proof that an action happened, the handle to undo it, and the key to replay it safely.
| Field | Type | Description |
|---|---|---|
audit_id | integer | Unique ID in the audit log. Pass to audit.revert to undo. |
rollback_handle | string | Opaque token encoding the inverse action (e.g. infra:resize:1:s-2vcpu-4gb). |
idempotency_key | string | Replay-safe — calling the same verb with the same key is a no-op. |
timestamp | ISO 8601 | When the action was executed. |
status | string | completed, pending, failed. |
Rollback is best-effort, not guaranteed
Each action type has a whitelisted revert handler. If none exists, audit.revert returns a structured refusal with reason: "no_revert_handler" rather than pretending. An agent can check reversible: true in preview output to know ahead of time whether an action is undoable.
Idempotency
| State | Behaviour |
|---|---|
| Key exists + succeeded | Returns the original receipt. No re-execution. |
| Key exists + failed | Re-executes the action. |
| Key absent | Executes normally, stores the key. |
If no key is supplied the runtime generates one. Keys expire after 24 hours.
Receipt chains
A transaction produces a parent receipt with child receipts per step. Reverting the parent reverts all children in reverse order.
{
"transaction_id": "tx_abc123",
"status": "committed",
"audit_id": 4243,
"steps": [
{ "verb": "crm_contact_update", "audit_id": 4244, "status": "completed" },
{ "verb": "email_send", "audit_id": 4245, "status": "completed" }
],
"rollback_handle": "tx:tx_abc123:rollback_all"
}Treat rollback_handle as opaque — never parse or construct one.
Audit log
Every dispatch writes an AIAuditLog row — including denials. The row captures company and user, agent type, action type, category, status, request data, reversibility and approval trail.
Denied attempts are the most important rows. They prove the safety layer is working.
Consent Gates
Write verbs are classified by risk. Each class defines the minimum approval gate the runtime enforces before the action executes.
| Class | Gate | Min role | Daily cap | Example |
|---|---|---|---|---|
READ | none | read_only | none | crm_contacts_search |
WRITE_REVERSIBLE | confirmation click | employee | ada_writes_per_day | crm_contact_update |
WRITE_FINANCIAL | typed phrase | admin | ada_financial_writes_per_day | charge_invoice |
IRREVERSIBLE | typed phrase | owner | ada_irreversible_writes_per_day | gdpr_delete_contact |
BLOCKED | never | — | — | subscription_cancel_immediate |
Roles are hierarchical — read_only < employee < admin < owner — and each class enforces a minimum. A WRITE_FINANCIAL verb denies employee and read_only at dispatch time.
Typed-phrase protocol
- Agent calls the verb with
_confirmed=false. The runtime returns a preflight response with metadata and no side effects. - Runtime generates the required phrase — e.g.
CHARGE $50.00 to invoice #4242. - Frontend displays the phrase verbatim and asks the human to type it. No paraphrasing.
- Human types it. The frontend sends
{ verb, args, confirm: true, typed_phrase }. - Runtime validates. The phrase is bound into an HMAC nonce at step 2, so a mismatch is denied with
reason: "phrase_mismatch"and tampering between dispatch and confirm is cryptographically blocked. - Action executes. Receipt is returned.
The nonce embeds a timestamp plus an HMAC-SHA256 of the dispatch payload, and expires after 5 minutes. Replay or tampering fails validation.
Daily caps
| Cap key | Default | Purpose |
|---|---|---|
ada_writes_per_day | Bounds reversible write volume | |
ada_financial_writes_per_day | Bounds money-movement volume | |
ada_irreversible_writes_per_day | Bounds destructive action volume |
Caps reset at midnight UTC. Exceeding one returns reason: "daily_cap_exceeded".
Cross-transport consent
| Transport | Confirmation UX |
|---|---|
| CLI | --confirm flag + --phrase "..." option |
| MCP stdio | No interactive prompt — write verbs return a dispatch_pending envelope and the agent must re-call with confirm: true. |
| WebMCP | Browser dialog with a phrase input field |
| UCP | RFC 9421 signed request implies consent — the buyer-agent is pre-authorized by its principal |
Blocked verbs
Some names are permanently blocked — never callable by an agent, regardless of role or confirmation:
These exist as defense-in-depth against an LLM hallucinating a dangerous verb name. The safe alternatives (gdpr_delete_contact behind an IRREVERSIBLE gate) are named differently on purpose.
The spec is the contract. Solid# is the implementation.
Every verb in the registry, across four transports — receipt-backed, reversible, and serving real businesses in production.