Personal Agent Protocol · draft 0.1 · codename “Poppy”

How a personal agent
books your trip

PAP is an open protocol for AI agents that act for a person (such as Claude, or your own assistant) to talk to a business on that person’s behalf: discover what the company offers, start a session, sign the user in with the right scope, chat with the company’s own support agent, call its APIs, and get explicit user approval before anything with real consequences happens.

Draft 0.1 · Apache License 2.0 Built on OAuth 2 · DPoP · MCP · OpenAPI Draft. Breaking changes expected
Gate A1 The cast

Three parties, one rule

The protocol never lets the company mistake an agent for a person. Every request carries a session token that says which agent is calling, and the agent is forbidden from dropping it to look like a human browser.

The principal

User

Owns the intent (“get me to Lisbon next Friday under $600”). Signs in to the company themselves, and is the only one who can approve real actions, either one at a time or as a standing permission.

Client · client_id is a URL

Personal agent

Identified by an HTTPS URL serving its metadata and public keys. Gives each user a stable, pairwise, non-PII user id per company. Holds tokens as secrets, never in model context.

Resource + auth server

Company

Publishes /.well-known/poppy.json. Runs an OAuth issuer, optionally a support agent (with human handoff), APIs (OpenAPI or MCP), and a website the agent can browse inside the same session.

Gate A2 poppy.json

One file declares everything

An agent knows nothing about a company until it fetches https://{domain}/.well-known/poppy.json. That document points to five surfaces. Only organization plus at least one of agent, apis or web is required, so a company can start small.

/.well-known/poppy.json

protocol_version “0.1” · organization {name, domain} · must match host (ignoring one leading www.) · cached per HTTP headers

auth
OAuth issuer

RFC 8414 metadata with poppy_domains. Sign-in modes: direct, device, mediated. Optional custom_scopes.

agent.protocols
Conversation

Endpoint where the personal agent chats with the company’s support agent, with optional handoff to a human.

apis[]
OpenAPI or MCP

Typed tools. OpenAPI takes DPoP tokens, MCP (2025-06-18, streamable HTTP) takes Bearer tokens bound to its URL.

web
Browser session

Agent joins its own headless browser to the session, so the website sees the same signed-in state.

extensions
operations · vendor.com/x

operations is the propose → approve → confirm ledger. Others must be domain-prefixed.

A poppy.json for a travel company

{
  "protocol_version": "0.1",
  "organization": { "name": "Poppy Travel", "domain": "poppy.travel" },
  "auth": {
    "issuer": "https://poppy.travel",
    "direct":  { "scopes": ["poppy:read", "poppy:write", "travel:loyalty"] },
    "device":  { "scopes": ["poppy:read", "poppy:write"] },
    "custom_scopes": {
      "travel:loyalty": "See and redeem your Poppy Miles balance"
    }
  },
  "agent": { "protocols": [
    { "type": "poppy", "endpoint": "https://poppy.travel/poppy/conversations" }
  ]},
  "apis": [
    { "type": "mcp",     "url": "https://poppy.travel/mcp",
      "description": "Search flights, hotels, fares and seat maps" },
    { "type": "openapi", "url": "https://poppy.travel/openapi.json",
      "description": "Trips, bookings, changes and refunds" }
  ],
  "web": { "browser_session_endpoint": "https://poppy.travel/poppy/browser-session" },
  "extensions": {
    "operations": { "version": "1", "endpoint": "https://poppy.travel/poppy/operations" }
  }
}

Paths under /poppy/*, /oauth/* and /mcp are this example's choice. The spec fixes only /.well-known/poppy.json; the Company publishes every other URL in poppy.json and its OAuth metadata.

Gate B1 Journey

End to end: “Book me Lisbon”

  1. §3 discoveryFetch poppy.json, then the issuer’s OAuth metadataAgent checks issuer == auth.issuer and poppy_domains contains the domain. Metadata fetches carry no cookies or tokens.
  2. §4.2 sessionStart a signed-out sessionJWT-bearer grant authenticated with private_key_jwt; sub is the pairwise user id. Response: session_id, DPoP-bound access token, signed_in:false, lifetime of hours.
  3. §6 apisSearch anonymouslyFlights and hotels via MCP with a Bearer token minted for the MCP URL. The company already knows an agent is calling, though not who the user is.
  4. §4.4 step-upSign in when the task needs an accountCompany answers with an authorization event or a WWW-Authenticate challenge naming the scope. User signs in via direct (PKCE S256) or device code. Agent receives a new session token plus an account token (refresh token).
  5. §7 conversationAsk the support agent the soft questions“Can I bring a surfboard?” Message in, events out (long-poll wait or SSE). A human can take over; responder flips to human.
  6. operations §3Company proposes the bookingReturns an operation: revision, plain-language summary, exact terms (fare, fare class, refundability), expires_at. Nothing is charged yet.
  7. operations §5User approves in the agent’s own UIAgent shows company name plus summary. Company text can never count as approval. Or a standing permission covers every term.
  8. operations §4–6Confirm the exact revision, executed at most onceIf the fare moved, the company issues revision 2 and confirm returns 409 terms_changed. On success, result.summary says what changed.
Sequence · signed-out search, step-up sign-in, approved booking
sequenceDiagram
  autonumber
  actor U as User
  participant A as Personal agent
  participant I as Company issuer (OAuth)
  participant M as Company MCP / OpenAPI
  participant O as Operations ledger
  A->>I: GET /.well-known/poppy.json + oauth-authorization-server
  A->>I: POST /oauth/token (jwt-bearer, private_key_jwt, DPoP)
  I-->>A: session_id, token (signed_in=false)
  A->>M: search_flights LIS, Fri (Bearer, resource=/mcp)
  M-->>A: 14 options
  U->>A: "Book the 08:40 TAP, aisle seat"
  A->>M: POST /trips/book (DPoP)
  M-->>A: 401 WWW-Authenticate scope="poppy:write"
  A->>U: Sign in to Poppy Travel?
  U->>I: Direct sign-in (PKCE S256, consent page)
  I-->>A: new session token + account token
  A->>M: POST /trips/book (DPoP)
  M->>O: create operation op_7Q rev 1
  M-->>A: operation proposed (summary, terms, expires_at)
  A->>U: "Poppy Travel wants to: Book TP1351 LIS, EUR 412, non-refundable"
  U-->>A: Approve
  A->>O: POST /operations/op_7Q/confirm {revision:1}
  O-->>A: succeeded, result.summary "Booked. PNR K9XT2B"
Gate B2 Tokens

Five credentials, each with one job

CredentialWho mintsWhere it’s acceptedKey rules
Client assertionAgent (its JWKS key)Token endpointprivate_key_jwt (RFC 7523), short-lived; guides: ES256 preferred, none and HS256 rejected
Session token (DPoP)Company issuerOpenAPI, conversation, operations, browser-sessionHours not days; bound to agent key; proof checks htm, htu, iat, jti, ath, optional nonce
Session token (Bearer)Company issuerMCP server onlyRequested without DPoP, with resource = the MCP url (RFC 8707); works at that MCP server only
Account tokenCompany issuer (refresh token)Token + revocation endpoints onlyStored as a secret, never in prompts/logs/URLs; refresh can narrow scope, never widen
Browser assertionAgentBrowser session endpoint (POST body)≤60s; sets a Secure, HttpOnly cookie that tracks sign-in state and dies with the session
Why DPoP everywhere? A leaked session token is useless without the agent’s private key. That is what lets a company trust an agent with write scope at all.
Gate B3 Sign-in

Sessions start anonymous, then step up

ModeHowUse whenSpec notes
directAuthorization code + PKCE S256, user approves on company consent pageAgent has a UI that can open a browserExact redirect_uri match; agent checks iss (RFC 9207); consent page has CSRF token and frame-ancestors none
deviceRFC 8628 device code; user enters a code on their phoneVoice assistants, chat apps, headless agentsOpening the link does not approve; polling too fast gives slow_down (+5s)
mediatedAgent relays fields (email, password, OTP) to auth.mediated.endpointLegacy logins onlySecret fields never echoed or logged; rate-limited; one-time code on unusual sign-ins. Agents prefer it last.

Scopes: poppy:read, poppy:write, plus your own (e.g. travel:loyalty). The company never grants more than requested. A session can sign out and keep going anonymously, but can never switch to a different account (account_mismatch).

Gate C1 Conversations

Agent-to-agent chat, with a human in the loop

Your company’s support agent becomes an endpoint. Messages carry text, data (structured JSON) and/or context (locale, time_zone, user_available). Each message says whether a person or an agent wrote it: sender: human only for the user’s own words.

Conversation endpoints and event types
flowchart LR
  A[Personal agent] -- "POST /conversations" --> C((cnv_…))
  A -- "POST …/messages {id, sender, text|data|context}" --> C
  A -- "GET …/events?cursor&wait  or  SSE" --> C
  A -- "POST …/handoff" --> H[Human agent]
  A -- "POST …/close" --> C
  C --> E1[message]
  C --> E2["state: working | idle | queued | closed"]
  C --> E3["authorization: needs scope X"]
  C --> E4["user_requested: need the user present"]
  C --> E5["direct_opened / direct_closed"]
  H -. "responder = human" .-> C
Gate C2 Operations

The part that makes travel work

Anything with consequences (book, change, cancel, refund, redeem miles) becomes an operation. The company spells out exactly what will happen, the user approves it, and the company does it at most once.

Operation lifecycle
stateDiagram-v2
  [*] --> proposed: company proposes rev 1
  proposed --> proposed: price moved → new revision (old one frozen)
  proposed --> in_progress: confirm latest revision
  proposed --> cancelled: agent cancels
  proposed --> expired: expires_at passes
  in_progress --> succeeded: result.summary
  in_progress --> failed: only if known not to have happened
  succeeded --> [*]
  failed --> [*]
  cancelled --> [*]
  expired --> [*]
{
  "operation_id": "op_7Q2c",
  "revision": 2,
  "state": "proposed",
  "summary": "Book TAP TP1351 SFO→LIS, Fri 17 Oct 08:40, seat 14C, Basic fare, non-refundable. Charge EUR 437 to Visa ••4242.",
  "terms": { "total": {"amount": "437.00", "currency": "EUR"}, "fare_class": "basic",
             "refundable": false, "change_fee": {"amount": "75.00", "currency": "EUR"} },
  "expires_at": "2026-10-10T18:15:00Z",
  "user_approval_required": true,
  "confirmation": null,
  "result": null
}
Standing permissions. The agent may confirm without asking only if a permission the user granted covers the company, the action and every term. Example: “Poppy Travel may rebook me on a delayed flight if the new departure is within 3 hours and costs ≤ EUR 0 extra.” This is the strongest feature for travel disruptions.
Gate D1 Travel map

Every protocol feature, used by a travel business

Signed-out session

Anonymous flight, hotel and package search over MCP. Rate-limit per client_id, not per IP. Agents get real inventory instead of scraping.

poppy:read

“What’s on my trip?” Itineraries, PNRs, boarding passes, loyalty balance, visa reminders.

poppy:write + operations

Book, hold, change date, upgrade seat, add bags, cancel, refund. Each one is a proposal with terms the user approves.

Revisions + terms_changed

Fares move every few minutes. A price change issues a new revision; the agent re-asks the user only when it matters.

Standing permissions

Auto-rebook on disruption, auto-check-in, auto-accept a free upgrade, price-drop rebook under a ceiling.

custom_scopes

travel:loyalty (redeem miles), travel:documents (passport details for APIS), travel:payments.

Conversation + handoff

Your own AI concierge answers policy questions; complex disruptions hand off to a human, with responder: human visible to the agent.

user_requested event

“Need the traveller to confirm passport expiry” or a 3-D Secure challenge only the person can complete.

Direct conversations

Side thread with a specific hotel’s front desk or a group-booking desk, while the main trip thread stays open.

Browser session

Hand the agent the seat-map or ancillaries page that has no API yet, already signed in.

context

time_zone and locale for local departure times and currency; user_available:false tells the concierge to batch questions.

Gate D2 Framework

poppy-next: drop-in PAP for any Next.js app

The protocol is mostly plumbing: OAuth, JWT/DPoP, session store, event log, operation ledger. Every company needs the same plumbing; what differs is the domain logic. So the framework owns the plumbing and the developer writes only tools, operations and a support agent.

Package layout
flowchart TB
  subgraph App["Your Next.js app (App Router)"]
    CFG["poppy.config.ts
definePoppy({...})"] RT["app/[...poppy]/route.ts
one catch-all handler"] UI["app/poppy/consent · /device · /admin"] end subgraph Core["@poppy/core (framework-agnostic)"] DISC[discovery + poppy.json builder] OAUTH[issuer: jwt-bearer · PKCE · device · revoke] DPOP[DPoP + client metadata verifier] CONV[conversation engine: events, cursors, SSE, handoff] OPS[operations ledger: revisions, at-most-once] MCP[MCP server + OpenAPI generator] end subgraph Adapters ST[(store: Postgres / Drizzle · Redis · memory)] AU[user auth: Auth.js · Clerk · Supabase] LLM[support agent: Claude via AI SDK] end CFG --> RT --> Core UI --> Core Core --> ST OAUTH --> AU CONV --> LLM

What a developer writes

// poppy.config.ts
import { definePoppy, tool, operation, z } from "@poppy/next";
import { drizzleStore } from "@poppy/store-drizzle";
import { authJs } from "@poppy/auth-authjs";
import { claudeConcierge } from "@poppy/agent-claude";

export default definePoppy({
  organization: { name: "Poppy Travel", domain: "poppy.travel" },
  store: drizzleStore(db),
  auth: authJs({ signIn: ["direct", "device"], customScopes: {
    "travel:loyalty": "See and redeem your Poppy Miles balance" } }),

  tools: {                                   // exposed over MCP + OpenAPI
    searchFlights: tool({ scope: null, input: z.object({ from: z.string(), to: z.string(), date: z.string() }),
      run: ({ input }) => inventory.flights(input) }),
    myTrips: tool({ scope: "poppy:read", run: ({ account }) => trips.list(account.id) }),
  },

  operations: {                              // propose → approve → confirm
    bookFlight: operation({
      scope: "poppy:write",
      input: z.object({ offerId: z.string(), seat: z.string().optional() }),
      propose: async ({ input }) => {
        const offer = await inventory.price(input.offerId);
        return { summary: describe(offer), terms: termsOf(offer), expiresIn: "15m" };
      },
      reprice: async ({ input }) => termsOf(await inventory.price(input.offerId)), // new revision if changed
      execute: async ({ input, account, idempotencyKey }) => {
        const pnr = await gds.book(input, account, { idempotencyKey });
        return { summary: `Booked. PNR ${pnr.locator}`, data: pnr };
      },
    }),
  },

  concierge: claudeConcierge({ model: "claude-sonnet-5-5", knowledge: "./policies",
    handoff: { when: "disruption|complaint", inbox: "/poppy/admin/inbox" } }),
});
// app/[...poppy]/route.ts  — serves .well-known, /oauth/*, /mcp, /poppy/*
export { GET, POST } from "@poppy/next/handler";

What ships in the box

PieceWhy it matters
create-poppy-app starterTemplates: travel, retail, SaaS. Seeded data, dev TLS, a fake personal agent to click through.
Agent simulator (dev UI)Plays the personal agent: discovery, DPoP, sign-in, approvals. Like Stripe’s test mode for PAP.
Admin consoleLive sessions per client_id, operation ledger, human handoff inbox, client allow/blocklist, revoke.
Conformance runnerRuns requirement-tagged checks, each citing the official spec section it proves, against your deployment.
<PoppyBadge/> + <AgentBanner/>Shows humans when an agent session joined the website; links the browser session to the agent.
Client SDK @poppy/agentThe other side: lets anyone build a personal agent (or a Claude tool) that talks to any PAP company.
Gate E1 Demo plan

Poppy Travel demo, built in four legs

  1. Leg 1 · discovery + anonymouspoppy.json, issuer metadata, jwt-bearer sessions, MCP flight/hotel searchSeed ~200 routes with fake fares. Demo: agent finds Lisbon options with zero sign-in.
  2. Leg 2 · identityDirect + device sign-in, consent page, account tokens, read scopeDemo: “What’s on my trip?” triggers step-up on the user’s phone via device code.
  3. Leg 3 · operationsBook, change, cancel as operations with revisions and fare driftFare simulator nudges prices so you can show terms_changed live. Standing-permission rebook on a simulated delay.
  4. Leg 4 · conciergeClaude-powered support agent with policy RAG and human handoffAdmin inbox where a human takes over a disruption; agent sees responder: human.
Demo hero moment. Split screen: left, the user chatting with their own agent; right, the Poppy Travel admin console. A flight gets cancelled, the standing permission fires, the operation ledger shows a rebooking with approved_by: standing_permission, and the user only gets a notification.
Gate E2 Ideas

Where to take it

Product

“Stripe for PAP”

Hosted poppy issuer + operations ledger as a service. A company adds a DNS record and a webhook per operation; you run the OAuth and DPoP.

Product

PAP adapters for existing stacks

Shopify, Booking engines (Amadeus, Sabre, Duffel), Cal.com. Wrap their APIs as tools and operations so they become agent-ready overnight.

Product

Agent-readiness checker

Paste a domain, get a score: poppy.json valid, issuer checks, DPoP, scopes, conformance. Lead-gen for the framework.

Travel

Disruption autopilot

Standing permissions plus a flight-status feed: rebooking, hotel vouchers and lounge passes as operations the agent approves within user-set rules.

Travel

Trip as a multi-company graph

One user agent talks PAP to airline, hotel and car rental; a trip planner composes operations across companies and shows one approval sheet.

Travel

Group trips

Each traveller’s own agent approves their own leg and payment. The company sees N sessions bound to one group booking.

Platform

Agent analytics

Which client_ids convert, abandonment at sign-in step-up, terms_changed rate. A new funnel nobody measures yet.

Platform

Policy engine for standing permissions

A shared vocabulary for terms (money, dates, refundability) so agents can evaluate permissions consistently. Could become a PAP extension (poppy.travel/terms).

Platform

Payments extension prototype

Payments are out of scope for 0.1. Prototype a domain-prefixed extension using tokenised cards or Stripe’s agent payments so you are ready when it lands.

Gate F1 Risks

Before you build