negotiate.v1 — Protocol Spec

A small HTTP protocol that lets any AI shopper agent negotiate with any store's seller-side AI agent, with no prior coordination. Stores advertise capability via a single discovery file. Agents drive the negotiation through plain GETs.

This document is the canonical spec. If you're building a store that wants to be negotiable, follow Section 2. If you're building a shopper agent that wants to negotiate against any negotiate.v1 store, follow Section 3.


1. Overview

The protocol has three surfaces:

Surface Purpose Format
Discovery "Is this store negotiable?" One JSON file at a known URL
Catalog "What can I negotiate over here?" JSON product list
Chat "Negotiate one product, turn by turn." Two GET endpoints + one for history

Everything is GET-based JSON. No POST, no WebSocket, no SSE, no auth. AI shoppers using GET-only fetch tools (Claude.ai web browsing, simple curl-based agents) can drive the full negotiation. Browser widgets MAY use POST equivalents — see §5.


2. Discovery — /negotiate.json

Every negotiate.v1-compliant store MUST serve a JSON descriptor at:

GET /negotiate.json

Stores SHOULD also mirror it at the IETF well-known URI:

GET /.well-known/negotiate.json

Both URLs MUST return identical content with Content-Type: application/json; charset=utf-8 and CORS header Access-Control-Allow-Origin: *.

Schema

{
  "negotiate_protocol": "negotiate.v1",        // required, exact string
  "store": {                                    // required
    "name":     "Atlas Premium Appliance",
    "city":     "Atlanta, GA",                  // optional
    "rep_name": "Chonkers",                     // the chat agent's display name
    "tagline":  "Premium home tech, outlet pricing.",
    "policy":   "Free expedited shipping. 30-day return. ..."
  },
  "merchant_skill": {                           // optional but recommended
    "name":        "pier39-merchant",
    "repo":        "https://github.com/...",
    "description": "..."
  },
  "endpoints": {                                // required
    "start_chat":   { "method": "GET", "url_template": "<base>/api/store/chat/start?product_id={product_id}", ... },
    "send_message": { "method": "GET", "url_template": "<base>/api/store/chat/{session_id}/say?message={url_encoded_message}", ... },
    "read_history": { "method": "GET", "url_template": "<base>/api/store/chat/{session_id}", ... },
    "catalog":      { "method": "GET", "url":          "<base>/api/store/catalog", ... }
  },
  "products": [                                 // required, may be empty
    {
      "id":             "hp07-hot-cool",
      "name":           "Dyson Pure Hot+Cool HP07",
      "subtitle":       "...",
      "list_price":     579,
      "currency":       "USD",
      "kind":           "purifier",             // optional, free-form category
      "page_url":       "https://.../store/p/hp07-hot-cool",
      "start_chat_url": "https://.../api/store/chat/start?product_id=hp07-hot-cool"
    }
  ],
  "limits": {                                   // required
    "max_chat_starts_per_hour_per_ip": 8,
    "max_messages_per_chat":           30,
    "session_idle_ttl_seconds":        3600,
    "max_message_length_chars":        2000,
    "currency":                        "USD"
  },
  "human_docs": {                               // optional
    "agent_guide_html": "...", "agent_guide_json": "...", "llms_txt": "..."
  },
  "negotiation_guidance": [...]                 // optional, free-form tips
}

Required URL templates

The endpoints block uses RFC 6570-style placeholders. A shopper agent substitutes them at request time. The four required endpoints:

Key Template variables
start_chat {product_id}
send_message {session_id}, {url_encoded_message}
read_history {session_id}
catalog none

3. Chat flow

3.1 Start a session

GET <start_chat URL with product_id substituted>

Response (HTTP 201):

{
  "session_id": "abc123...",
  "greeting":   "Hi! I'm Chonkers from Atlas...",
  "next":       "<send_message URL with session_id substituted, {url_encoded_message} unfilled>"
}

The next field is a hypermedia hint: take it, replace the {url_encoded_message} placeholder with your URL-encoded shopper turn, fetch the result. Following next is the recommended way to drive the conversation.

3.2 Send a turn

GET <send_message URL with session_id and url_encoded_message substituted>

Response (HTTP 200):

{
  "message": "I can do $585 with the extra battery and free shipping...",
  "closed":  false,
  "next":    "<URL for the next turn>"  // null when closed=true
}

Repeat until closed: true. Both sides converge on closed=true when: - The shopper accepts a deal explicitly, OR - The shopper walks away explicitly, OR - The merchant agent decides further negotiation is unproductive

After closed=true, further calls to send_message MUST return HTTP 400 with {"error": "this chat is closed"}.

3.3 Read history (optional)

GET <read_history URL with session_id substituted>

Returns:

{
  "session_id": "abc123...",
  "history": [
    { "speaker": "merchant", "message": "Hi! I'm Chonkers..." },
    { "speaker": "shopper",  "message": "Could you do $499..." },
    { "speaker": "merchant", "message": "..." }
  ]
}

Useful for resumption or for shoppers who lost their connection mid-negotiation.


4. For shopper agents

The complete flow against any negotiate.v1 store, given just the store's domain <DOMAIN>:

1. GET https://<DOMAIN>/negotiate.json
   → If "negotiate_protocol":"negotiate.v1", proceed. Otherwise, the store doesn't speak the protocol.

2. Find the product you want in `products[]`. You can also GET the live `endpoints.catalog.url` for the freshest list.

3. GET the product's `start_chat_url` (or substitute `endpoints.start_chat.url_template` with `{product_id}`).
   → Save the returned session_id.
   → Read the merchant's greeting.

4. Decide your shopper turn. URL-encode it. Substitute into the `next` URL from the previous response and GET it.
   → Read the merchant's reply.
   → If `closed:true`, you're done. Otherwise repeat with the new `next` URL.

5. Respect the limits in `limits` — back off on 429 responses.

A shopper that follows this loop works against any negotiate.v1-compliant store, regardless of who built it.


5. Optional POST endpoints

Stores MAY expose POST equivalents for browser widget use. If they do:

POST <base>/api/store/chat/start         body: {"product_id": "..."}
POST <base>/api/store/chat/{sid}/message body: {"message": "..."}

Both MUST return Access-Control-Allow-Origin: * and accept Content-Type: application/json. POST endpoints are advisory; shopper agents SHOULD NOT depend on them — GET is the required protocol surface.


6. Errors

All errors are JSON: {"error": "<message>"}. Standard status codes:

Status Meaning
400 Malformed request (missing param, bad product_id, message too long, chat closed)
404 Unknown product_id, unknown session_id
429 Rate limit hit (per-IP or per-session)
500 Server error (LLM upstream failed, etc.)

7. Hidden merchant state

Stores MUST NOT include the following in any GET response (only in the merchant agent's private system prompt):

Public catalog endpoints MUST strip these before returning. The reference implementation does this in _public_catalog_view().


8. Versioning

The protocol version is a string in negotiate_protocol. Today: "negotiate.v1".

Breaking changes increment the major version (negotiate.v2). Stores MAY serve multiple versions side by side at different paths (/negotiate.v1.json, /negotiate.v2.json) and MAY add a versions array to /negotiate.json when supporting more than one.

Shopper agents MUST refuse to negotiate against a store whose negotiate_protocol they don't understand.


9. Reference implementation

The Atlas Premium Appliance demo at https://negotiate.pier39.ai is a complete reference implementation:

The merchant agent is a Claude model with the pier39-merchant skill loaded as its system prompt. Stores can replace the skill, the model, the catalog, or the entire backend — as long as the protocol surface in §2–§7 stays compliant, any shopper agent will work against them.


License

This protocol spec is MIT-licensed — copy, fork, embed in your own product. Skills are Apache 2.0.