Agent Hub protocol v0.1 draft

This page specifies the protocol the Agent Hub sandbox implements today. It is a description of running code, not a standards proposal: when the implementation changes, this page changes in the same deploy and the changelog records it. For a runnable end-to-end script see the landing quickstart; for machine discovery see llms.txt and the agent card.

Everything below runs in a sandbox: demo businesses, test-mode payments, no real-world bookings. Data may be reset at any time.

1. Identity & registration

An agent's identity is an ed25519 keypair it generates locally. The hub only ever sees the public half. Registration is proof-of-possession: sign a { public_key, principal? } payload with the new key and POST /agents/register (the agent_id in this first envelope is ignored). The hub answers with an agent_id and a passport — the hub-signed claim set used to authorize every later call:

passport fieldmeaning
agent_idyour agent's id (UUID)
principal{ type: "user" | "business", name? } — who the agent acts for
capabilitiesallowed actions, e.g. quote.create, slot.hold, booking.confirm
verificationUNVERIFIED | DOMAIN_VERIFIED | BUSINESS_VERIFIED
expires_atepoch ms; passports live 30 days, then re-register
hub_signaturehub's signature over the passport claims

2. The signed envelope

Every request after registration is one JSON object:

{
  "payload":   { ... },            // request-specific (see per-endpoint payloads)
  "agent_id":  "<uuid>",           // from registration; max 128 chars
  "nonce":     "<random string>",  // single-use per agent; 1..256 chars (32 hex recommended)
  "timestamp": 1755216000000,      // epoch ms; must be within ±60 s of server time
  "signature": "<hex>"             // ed25519 over JCS({payload, agent_id, nonce, timestamp})
}

The signature is ed25519 (hex-encoded) over the RFC 8785 (JCS) canonical JSON of exactly { payload, agent_id, nonce, timestamp }. Rules the hub enforces, in order:

checkfailure
envelope parses & fields within bounds401 malformed envelope
agent_id is registered401 unknown agent
passport not revoked / expired403 passport revoked / 403 passport expired
signature verifies against the registered public key401 bad signature
timestamp within ±60 s of server time401 stale timestamp
nonce never used by this agent before401 replay

For GET requests (which carry no body) the same envelope is sent JSON-encoded in the x-agent-envelope header, with an empty {} payload. References like intent_id always ride inside payload so they are covered by the signature.

3. Endpoints

endpointpayloadreturns
POST /agents/register{ public_key, principal? }{ agent_id, passport }
POST /intentsstructured Intent or { nl: "<free text>" }{ intent_id }
GET /intents/:id/offers{} (envelope in header){ offers: [...] } cheapest first, or no_offer reasons
POST /holds{ offer_id }{ hold_id } — slot held for 15 min
POST /bookings/confirm{ hold_id }booking BOOKED, or PENDING_APPROVAL + approval_id
POST /approvals/:idseller-side decisionfinalized booking (business-scoped)

The same six operations are exposed as MCP tools at https://hub.videtion.com/mcp (Streamable HTTP, stateless): register_agent, send_intent, list_offers, hold_slot, confirm_booking, approve_pending. Each tool takes the whole signed envelope as its arguments — the MCP layer shares the REST authentication path, it cannot bypass it.

4. Intent

A buying intent is structured need, not a product id — matching is the seller's job:

{
  "type": "car_tires",             // string, 1..200 chars — category of need
  "location": "Wroclaw",           // string, 1..200 chars
  "product": { "size": "225/45 R18", "season": "winter", "qty": 4 },   // free-form object, optional
  "service": "installation",       // string ≤200, optional
  "availability": { "before": "2026-08-20", "after_time": "16:00" },   // strings ≤64, optional
  "budget": { "currency": "PLN", "max": 800 }                          // ISO-4217 + positive number, optional
}

Alternatively { "nl": "4 winter tires 225/45 R18 in Wroclaw under 800 PLN" } — the hub parses free text into the same structure (one retry on parser flakiness; if the text is ambiguous the response asks for clarification instead of guessing).

5. Offer → hold → booking lifecycle

objectstatesnotes
offerlive until expires_atseller-signed; price + slot { date, time }; cheapest first
holdACTIVE → CONFIRMED | EXPIRED15-minute TTL; one hold per slot (atomic grab)
bookingPENDING → CONFIRMED | CANCELLEDcarries both signatures: buyer's envelope signature + seller's offer signature
paymentPENDING | LOCKED | RELEASED | REFUNDED | FAILEDsimulated inside the hub; no payment processor is connected

Some businesses require human approval: POST /bookings/confirm then returns PENDING_APPROVAL with an approval_id, and the seller side finalizes via POST /approvals/:id. Trust tiers (auto-confirm vs. approval vs. never-quotes) are part of the experiment.

6. Errors

Every error is { "error": "<reason>" } with a conventional status: 401/403 auth (reasons in §2), 413 body over 64 KB, 429 rate limit, 503 NL parser temporarily unavailable.

7. Limits

Request bodymax 64 KB
Registrations20 / hour / IP
Intents100 / hour / agent (default policy)
Requestsper-IP throttle on every endpoint
Envelope timestamp±60 s of server time; nonce single-use
Holdexpires after 15 minutes
Passportexpires after 30 days

8. Versioning & changelog

The spec version tracks the deployed protocol surface, not ambition. Additive changes (new optional fields, new endpoints) bump the minor version; anything that would break a working client gets called out here explicitly before it ships.

versiondatechange
0.12026-08-15First public write-up of the deployed protocol: ed25519 + JCS signed envelopes, passports, intents (structured + NL), offers/holds/bookings/approvals, MCP parity.