§1 · Protocol spec

The protocol, in full.

People API is a request/response protocol for machine-readable intents and structured offers. This page is the protocol spec — everything downstream (scoring, privacy, MCP) is policy on top of these shapes.

For: anyone evaluating adoption or writing a compatible server.

Design principles

Four rules govern every shape on this page. A server that keeps all four is a People API. A server that breaks one isn't.

  1. Hand-writable, agent-generable. Every field can be produced from a short natural-language sentence. No required field needs a catalog lookup or a vendor-specific code.
  2. Progressive disclosure. The data the registry returns is a function of the caller's relationship to the intent. A stranger sees the least, a transacting vendor sees the most, enforced at the type level — not by documentation.
  3. Self-hostable. A single-file SQLite node is a valid deployment. No required external services.
  4. Transport-agnostic. HTTP is the default; MCP is the agent integration. Any request/response transport that carries the shapes below is fair game.

Intent schema

An intent is a buyer's published demand. It is the only resource the protocol considers primary — offers, session tokens, and vendors exist to serve intents.

IntentFull example with the pink-sink running case.
{
  "id": "int_7f3a1b2c",
  "userId": "user-1",
  "category": "plumbing_fixtures",
  "description": "Pink undermount kitchen sink, porcelain, 30 inches",
  "attributes": {
    "material": "porcelain",
    "color": "pink",
    "size_inches": 30,
    "mount": "undermount"
  },
  "constraints": [
    { "type": "material_exclude", "value": "stainless_steel" }
  ],
  "budget": { "min": 200, "max": 400, "currency": "USD" },
  "urgency": "ready_to_buy",
  "location": { "metro": "NYC", "zip": "10013" },
  "tags": ["kitchen", "renovation"],
  "status": "active",
  "autoApproveBelow": 300,
  "createdAt": "2025-11-22T18:04:11Z",
  "expiresAt": null
}
Intent · fields
FieldTypeReqNotes
idstringServer-assigned.
userIdstringBuyer identity. Not exposed in Tier 1.
categorystringNamespaced category key. Part of Tier 1.
descriptionstringFree-form summary. Tier 2 only.
attributesRecord<string, string | number | boolean>Structured facets. Matched against offer.attributes. Tier 2.
constraintsConstraint[]Hard filters. material_exclude, brand_exclude, etc. Used for auto-reject.
budget{ min, max, currency }Required for auto-reject. Exposed in Tier 1 as min/max only.
urgency'browsing' | 'planning' | 'ready_to_buy'Drives delivery-speed scoring. Tier 1.
location{ metro, zip }metro is Tier 1. zip is Tier 2 only.
tagsstring[]Free-form tags for filtering. Tier 1.
status'active' | 'fulfilled' | 'expired' | 'cancelled'Server-maintained.
autoApproveBelownumber | nullIf set, the server auto-accepts qualifying offers at or below this price.
createdAtISO-8601UTC.
expiresAtISO-8601 | nullPlanned; null today.

urgency is an enum of browsing, planning, or ready_to_buy — it drives delivery-speed scoring and is considered part of Tier 1.

constraints is an array of objects with type and value fields. The reference server recognizes material_exclude, brand_exclude, and shipping_region_exclude. Matching an _exclude value auto-rejects the offer.

Offer schema

An offer is a vendor's proposed fulfillment of a specific intent. Every field except score is vendor-supplied. Every field including score is returned in API responses.

OfferAccepted offer for the pink-sink intent.
{
  "id": "off_9a2b4c8d",
  "intentId": "int_7f3a1b2c",
  "vendorId": "ven_abc",
  "price": 280,
  "currency": "USD",
  "deliveryDays": 2,
  "inStock": true,
  "description": "Pink undermount porcelain sink, 30\"",
  "attributes": {
    "material": "porcelain",
    "color": "pink",
    "size_inches": 30,
    "mount": "undermount"
  },
  "matchPercentage": 95,
  "isAlternative": false,
  "alternativeReason": null,
  "score": 82,
  "status": "accepted",
  "createdAt": "2025-11-22T19:11:03Z"
}
Offer · fields
FieldTypeReqNotes
idstringServer-assigned.
intentIdstringTarget intent.
vendorIdstringResolved from x-api-key server-side.
pricenumberIn currency units.
currencystringISO 4217.
deliveryDaysnumberDays from acceptance. Drives delivery-speed scoring.
inStockbooleanBinary scoring input.
descriptionstringShown to the buyer.
attributesRecord<string, string | number | boolean>Matched against intent.attributes unless matchPercentage is set.
matchPercentagenumber (0-100)Vendor self-reported. Trusted as a shortcut for attribute scoring.
isAlternativebooleanCounter-offer. Triggers the -5 alternative penalty.
alternativeReasonstring | nullFree-form justification shown to the buyer.
scorenumber (0-100)Server-computed. Not accepted from the vendor.
status'pending' | 'accepted' | 'rejected' | 'countered' | 'expired'Server-maintained.
createdAtISO-8601UTC.

Privacy tiers, summarized

Privacy is not a policy decision by the server. It's a type decision — each tier has its own shape. The privacy deep-dive walks through the filter functions; this is the at-a-glance summary.

TierWho sees itIn the shape
Tier 1 · PublicAny caller. No auth.category, budget (min/max), urgency, metro, tags, status
Tier 2 · EngagedVendor whose offer was accepted.everything in Public, description, attributes, constraints, ZIP
Tier 3 · TransactionalVendor on confirmed purchase only.everything in Engaged, identity, full address

HTTP surface

Thirteen endpoints grouped by actor. Every endpoint enforces tier visibility on its response shape — no endpoint leaks fields from a higher tier than the caller has unlocked. See the API reference for request bodies and error responses.

User

POST
/api/intents

Publish a new intent.

GET
/api/intents

List the caller's intents.

GET
/api/intents/:id

Get an intent with its offer list.

PATCH
/api/intents/:id

Update an intent (cancel, extend, revise).

Vendor

POST
/api/vendors

Register a vendor. Returns the API key ONCE.

GET
/api/vendors/me

Get the authenticated vendor's profile.

Offer

POST
/api/offers

Submit an offer. Server scores it; may auto-accept (returns session token) or auto-reject (422).

POST
/api/offers/:id/accept

Buyer accepts an offer. Issues a session token to the vendor.

POST
/api/offers/:id/reject

Buyer rejects an offer.

POST
/api/offers/:id/counter

Buyer counters an offer with a message.

Registry

GET
/api/registry/intents

Browse active intents. Returns Tier 1 data only. Filter by category, metro, urgency, budget.

GET
/api/registry/intents/:id/details

Tier 2 data for a specific intent. Requires a matching session token.

Events

GET
/api/events

Server-Sent Events stream. Emits intent_created, intent_updated, offer_created, offer_status_changed.

Info

GET
/api/health

Liveness probe.

Conformance

A People API node is any URL that serves the thirteen endpoints with correctly filtered tier data, validated request bodies, and a scoring engine that respects the auto-accept / auto-reject rules. It need not run on the reference stack — Postgres, Python, Go, or an edge function all qualify if the shapes round-trip.

This is the federation story. A private enterprise registry and a public marketplace can both call themselves People API nodes, and an agent can query both with the same MCP tools. Cross-registry discovery is on the platform roadmap, but a minimum viable federation — agent-side fan-out — already works today.

PROTOCOL.md on GitHub