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.
- 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.
- 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.
- Self-hostable. A single-file SQLite node is a valid deployment. No required external services.
- 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
}| Field | Type | Req | Notes |
|---|---|---|---|
id | string | Server-assigned. | |
userId | string | Buyer identity. Not exposed in Tier 1. | |
category | string | Namespaced category key. Part of Tier 1. | |
description | string | Free-form summary. Tier 2 only. | |
attributes | Record<string, string | number | boolean> | — | Structured facets. Matched against offer.attributes. Tier 2. |
constraints | Constraint[] | — | 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. |
tags | string[] | — | Free-form tags for filtering. Tier 1. |
status | 'active' | 'fulfilled' | 'expired' | 'cancelled' | Server-maintained. | |
autoApproveBelow | number | null | — | If set, the server auto-accepts qualifying offers at or below this price. |
createdAt | ISO-8601 | UTC. | |
expiresAt | ISO-8601 | null | — | Planned; 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"
}| Field | Type | Req | Notes |
|---|---|---|---|
id | string | Server-assigned. | |
intentId | string | Target intent. | |
vendorId | string | Resolved from x-api-key server-side. | |
price | number | In currency units. | |
currency | string | ISO 4217. | |
deliveryDays | number | — | Days from acceptance. Drives delivery-speed scoring. |
inStock | boolean | — | Binary scoring input. |
description | string | — | Shown to the buyer. |
attributes | Record<string, string | number | boolean> | — | Matched against intent.attributes unless matchPercentage is set. |
matchPercentage | number (0-100) | — | Vendor self-reported. Trusted as a shortcut for attribute scoring. |
isAlternative | boolean | — | Counter-offer. Triggers the -5 alternative penalty. |
alternativeReason | string | null | — | Free-form justification shown to the buyer. |
score | number (0-100) | Server-computed. Not accepted from the vendor. | |
status | 'pending' | 'accepted' | 'rejected' | 'countered' | 'expired' | Server-maintained. | |
createdAt | ISO-8601 | UTC. |
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.
| Tier | Who sees it | In the shape |
|---|---|---|
| Tier 1 · Public | Any caller. No auth. | category, budget (min/max), urgency, metro, tags, status |
| Tier 2 · Engaged | Vendor whose offer was accepted. | everything in Public, description, attributes, constraints, ZIP |
| Tier 3 · Transactional | Vendor 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
/api/intentsPublish a new intent.
/api/intentsList the caller's intents.
/api/intents/:idGet an intent with its offer list.
/api/intents/:idUpdate an intent (cancel, extend, revise).
Vendor
/api/vendorsRegister a vendor. Returns the API key ONCE.
/api/vendors/meGet the authenticated vendor's profile.
Offer
/api/offersSubmit an offer. Server scores it; may auto-accept (returns session token) or auto-reject (422).
/api/offers/:id/acceptBuyer accepts an offer. Issues a session token to the vendor.
/api/offers/:id/rejectBuyer rejects an offer.
/api/offers/:id/counterBuyer counters an offer with a message.
Registry
/api/registry/intentsBrowse active intents. Returns Tier 1 data only. Filter by category, metro, urgency, budget.
/api/registry/intents/:id/detailsTier 2 data for a specific intent. Requires a matching session token.
Events
/api/eventsServer-Sent Events stream. Emits intent_created, intent_updated, offer_created, offer_status_changed.
Info
/api/healthLiveness 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