The Personal Agent Protocol (PAP), codenamed Poppy, is an open standard that lets a person’s own AI agent deal directly with a business: find out whether the company supports agents, prove who the agent is, get the user signed in with limited permissions, and then finish a task through an API, a web page or the company’s own agent. Sierra announced it on October 6, 2026, built with Meta and a group of partners that Sierra and Meta named as Genesys, Instinct, Rocket, Shopify, Stripe and Walmart (plus NiCE and Decagon on Meta’s side). It is a draft: version 0.1 is frozen, 0.2 is the current revision, and the authors say any part can change incompatibly before 1.0. It matters because it targets a problem the other agent protocols skip: not how an agent calls a tool, but how a company knows whose agent is knocking and what it is allowed to do.
The Problem: Agents Without a Front Door
Today a consumer agent that wants to check an order or rebook a flight usually does what a person would do: it drives the website. That is slow, breaks when the page changes, and gets blocked by bot defenses that cannot tell a helpful agent from a scraper. When it fails, the agent falls back to a support line or a web chat widget built for humans. Sierra’s pitch is that a direct, authenticated connection could finish the same tasks faster and more safely.
Three needs sit underneath that, and none of the existing protocols covers all of them:
- Discovery. How does an agent learn that a company supports agents at all, and where?
- Identity and consent. How does the company recognize which agent this is, and how does the user grant it narrow access without handing over a password?
- Visibility and control. How can a company set limits on what agents do and see what they did? The stated principle is that consumers decide who acts for them and companies decide what those agents may do.
MCP standardizes tool access, A2A standardizes agent-to-agent messaging, and agentic commerce protocols standardize shopping and payment. PAP sits underneath them as the session and sign-in layer that says who is calling.
The Flow at a Glance
%%{init: {'theme': 'base'}}%%
graph TD
classDef default fill:#ffffff,stroke:#4338CA,stroke-width:2px,color:#0F172A,rx:8px,ry:8px;
classDef data fill:#EEF0F7,stroke:#0D9488,stroke-width:2px,color:#0F172A,rx:8px,ry:8px;
classDef process fill:#F7F8FC,stroke:#6366F1,stroke-width:2px,color:#0F172A,rx:8px,ry:8px;
classDef output fill:#4338CA,stroke:#4338CA,stroke-width:2px,color:#ffffff,rx:8px,ry:8px;
A[Personal agent]:::data -->|"1. fetch /.well-known/poppy.json"| D[Company discovery document]:::process
D -->|"2. verify issuer metadata lists the domain"| I[Company OAuth issuer]:::process
A -->|"3. signed JWT assertion plus DPoP proof"| I
I -->|"4. session token, starts signed out"| S[Signed-out session]:::data
S -->|"5. direct, device or mediated sign-in"| U[User approves scopes]:::data
U --> T[Signed-in session]:::data
T --> P1[APIs: OpenAPI or MCP]:::output
T --> P2[Conversations with company agent]:::output
T --> P3[Browser session via cookie]:::output
Discovery: /.well-known/poppy.json
A participating company serves a JSON document at /.well-known/poppy.json. It carries the protocol version, the organization’s name and domain, the OAuth issuer and the sign-in types it supports, the entry points for conversations, a browser-session endpoint, a feedback endpoint, a list of APIs (OpenAPI or MCP) and any extensions.
The most useful design detail is the two-way check. The document must name the same domain that served it, and the issuer’s standard OAuth metadata (RFC 8414) must in turn list that domain in a poppy_domains field. That stops an attacker from copying a legitimate company’s discovery document onto their own site, and stops a malicious issuer from claiming a company it does not control. One issuer represents exactly one company, though a company can span several domains.
# Scenario: before trusting anything a site says about itself, the agent
# verifies the discovery document against the issuer. A copied document fails.
def trusted_discovery(host: str) -> dict | None:
doc = httpx.get(f"https://{host}/.well-known/poppy.json", follow_redirects=False).json()
if doc["organization"]["domain"] != host:
return None # document claims another domain
meta = httpx.get(doc["auth"]["issuer"] + "/.well-known/oauth-authorization-server").json()
if meta["issuer"] != doc["auth"]["issuer"]:
return None # issuer mismatch
if host not in meta.get("poppy_domains", []):
return None # issuer does not vouch for this domain
return doc
Agent Identity: A URL, a Key and a Signed Assertion
A personal agent identifies itself with a client_id that is an HTTPS URL. Fetching that URL returns the agent’s metadata: where its public keys live (jwks_uri), its redirect URIs, and the authentication method, which is private_key_jwt. Registration with each company is optional; companies may allowlist, blocklist or revoke an agent by client_id and rate-limit session creation.
To start a session the agent sends a JWT bearer grant (RFC 7523). The assertion’s claims are tightly constrained:
| Claim | Rule | Why it exists |
|---|---|---|
iss | The agent’s client_id | Says which agent is calling |
sub | A stable, opaque per-company user ID, not derived from personal data | Lets a company recognize the same user without learning who they are across companies |
aud | The company’s issuer string, never an endpoint URL | Stops an assertion made for one company being replayed at another |
iat / exp | exp at most five minutes after iat | Keeps stolen assertions short-lived |
jti | At least 128 random bits, rejected if reused | Stops replay of a captured assertion |
Each user also gets a separate DPoP key pair. DPoP (RFC 9449) binds a token to a key held only by the agent: every request carries a proof JWT signed with that key, covering the HTTP method, URL and a hash of the token. A stolen session token is useless without the private key. One pragmatic exception: an MCP server that cannot do DPoP receives a plain Bearer token issued for that server’s resource URL only, and companies must reject it everywhere else.
Interactive: Would This Assertion Be Accepted?
The checks above are cheap to state and easy to get wrong. The widget below is a simplified model of a company’s validation, not the spec’s exact error mapping. Change the inputs and watch which rule trips.
Drag the lifetime past 300 seconds, or switch aud to a token endpoint URL, and the session disappears. Every rule is a small piece of anti-replay or anti-impersonation hygiene that costs the legitimate agent almost nothing.
Sessions, Tokens and Signing In
PAP separates two tokens that are easy to blur together. The session token is short-lived, opaque and key-bound; it carries the agent through one working session. The account token is a long-lived OAuth refresh token that proves the user once signed in; it is accepted only at the token and revocation endpoints, and only from its own client_id. Ending a session does not touch the account token, and signing out revokes it.
Every session starts signed out. The spec does not use the phrase “guest access”, but a signed-out session is the closest thing: the company can recognize the same opaque user across sessions and may allow limited, read-only tasks like checking stock, while limiting everything else. Signing in then upgrades the session, and a session can only ever be signed in to one account (a different account returns account_mismatch).
There are three ways to sign in, chosen to keep the user’s credentials out of the agent’s hands:
| Sign-in type | How it works | Where the user’s password goes |
|---|---|---|
| Direct | OAuth authorization code flow with PKCE; the user signs in on the company’s own page and approves scopes | Only to the company |
| Device | OAuth device grant; the agent shows a link and code, the user approves on another device | Only to the company |
| Mediated | The agent passes credentials to a company endpoint and relays one-time codes | Through the agent, so credentials must be discarded or kept only in a store the user chose |
Scopes are company-defined (an example is orders:read versus orders:write), plus two broad ones, poppy:read and poppy:write. The company must not grant more than was requested and allowed for the sign-in type used, and may limit mediated sign-in to read access.
Getting Work Done: Three Doors
Once connected, an agent can reach the company three ways, all under the same session:
- APIs. The discovery document lists
openapi(3.0 or 3.1) ormcp(Streamable HTTP) entries. Errors arrive inWWW-Authenticatewith codes likesign_in_requiredorinsufficient_scope, so the agent knows whether to prompt the user or ask for more scope. - Web browsing. If the company offers a browser-session endpoint, the agent signs a short-lived browser assertion (60 seconds at most) and posts it from its own browser; the company sets its normal cookie and redirects. The agent keeps using the real website, but as a recognized, signed-in session.
- Conversations. A company agent can talk to the personal agent through a small message API: start a conversation, send messages, read events, request a human with a handoff, and close. Every message declares the
senderasagentorhuman, so each side knows whether a person or an AI is speaking.
# Scenario: the personal agent could not finish a refund through the API, so it
# opens a conversation with the company's own agent and escalates to a person.
# Endpoint shapes follow the 0.2 draft; verify against the live spec.
conv = client.post(conv_endpoint, json={"context": {"locale": "en-US", "user_available": True}}).json()
client.post(f"{conv_endpoint}/{conv['id']}/messages", json={
"id": new_message_id(), # chosen by the agent; makes retries idempotent
"sender": "agent",
"text": "Order 8841 arrived damaged. The user wants a refund to the original card.",
"data": {"order_id": "8841", "reason": "damaged"},
})
events = client.get(f"{conv_endpoint}/{conv['id']}/events", params={"wait": 20}).json()
for e in events:
if e["type"] == "authorization": # sign_in_required or insufficient_scope
ask_user_to_approve_more_access(e)
elif e["type"] == "user_requested": # company needs the person directly
hand_to_user(e)
# if the company agent is stuck: request a human responder
client.post(f"{conv_endpoint}/{conv['id']}/handoff")
Security: Treat the Company as Untrusted Input
The spec’s security section is the most reusable idea in it for anyone building agents. Text coming from a company, whether a message, an API or MCP result, or a web page, is data, never instruction. Only the user can approve an action, and nothing a company says counts as consent. That is the same discipline needed to resist prompt injection, written down as a protocol rule. A few other rules worth copying:
- Keep secrets out of model context. Account tokens, session tokens, private keys, device codes and one-time codes should never be visible to the language model.
- Bind assertions to one audience. Because
audis the issuer and browser assertions use their owntyp, an assertion captured at one endpoint is useless at another. - Share the minimum. The agent gives each company only what the task needs and never leaks one company’s information to another.
- Don’t connect to internal addresses. Clients must not follow protocol URLs to loopback, private or link-local addresses unless configured to.
The feedback.endpoint is a nice small touch: an agent can report a task the protocol could not complete (a kind of missing, plus a short task and description) with no personal data, and the company must treat that text as data too.
What’s New (2025-2026)
| Date | Event |
|---|---|
| Oct 6, 2026 | Sierra announces the Personal Agent Protocol, built with Meta, with Shopify, Stripe, Walmart, Genesys and others as partners |
| Oct 2026 | Draft 0.1 published (now frozen); draft 0.2 becomes the current revision |
- Deliberately left out of the draft. The authors list payments, push notifications (how a company reaches an agent when no request is open), version negotiation, richer attachments, and self-hosted personal agents that need to sign in without a server learning which companies each user works with.
- Open and reviewable. The spec and docs are Apache 2.0 on GitHub, with a “Path to 1.0” page and a company implementation kit with JSON Schemas and a validator. The names “Personal Agent Protocol” and “Poppy” are not covered by the license.
- Pairs with the rest of the stack. The payments gap points directly at agentic commerce protocols, and the MCP and OpenAPI entry points mean existing company tools can be exposed behind a PAP session without rewrites.
Practical Guidance
| Situation | Recommendation |
|---|---|
| You run customer-facing APIs or support | Publish a discovery document, point it at your existing OpenAPI or MCP, and prefer direct sign-in so passwords never touch the agent |
| You build a consumer agent | Fetch and verify discovery before anything else, keep one opaque ID and one DPoP key per user per company, and keep tokens and keys out of model context |
| You need the agent to pay | Not covered yet; look at agentic commerce protocols in the meantime |
| You want to ship to production | Wait: the authors say 0.2 can still change incompatibly, so build against it for feedback, not for a launch |
| A task needs a decision | Keep a human in the loop; company text is never consent |
PAP does not make an agent smarter. It makes an agent recognizable: a company can tell whose agent this is, what the user allowed it to do, and revoke that permission in one place. That is the front door consumer agents have been missing.
How to Use: A personal agent discovers a company and starts a Poppy session
import time, uuid, httpx, jwt # PyJWT
# Scenario: a user's personal agent wants to check an order at shop.example.com.
# Step 1 is discovery; step 2 is proving who the agent is with a signed
# assertion. Field names follow the Poppy 0.2 draft; check the current spec
# before relying on the exact shape, since it may change before 1.0.
COMPANY = "shop.example.com"
AGENT_ID = "https://agent.example.org/client.json" # client_id is an HTTPS URL
doc = httpx.get(f"https://{COMPANY}/.well-known/poppy.json").json()
issuer = doc["auth"]["issuer"]
# Two-way check: the document must name the host that served it, and the
# issuer's own metadata must list that domain back.
meta = httpx.get(f"{issuer}/.well-known/oauth-authorization-server").json()
assert doc["organization"]["domain"] == COMPANY
assert COMPANY in meta["poppy_domains"] and meta["issuer"] == issuer
now = int(time.time())
assertion = jwt.encode(
{"iss": AGENT_ID, "sub": opaque_user_id_for(COMPANY), # stable, opaque, not derived from PII
"aud": issuer, # the issuer string, never an endpoint URL
"iat": now, "exp": now + 120, # at most 5 minutes
"jti": uuid.uuid4().hex + uuid.uuid4().hex}, # 128+ random bits, single use
agent_private_key, algorithm="ES256")
session = httpx.post(meta["token_endpoint"], data={
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": assertion,
}, headers={"DPoP": dpop_proof("POST", meta["token_endpoint"])}).json()
print(session["signed_in"], session["scope"]) # starts signed out; sign-in comes next
Ready to build?
Leverage AI technologies to build your product stack
Superteams can help you build, deploy and launch AI application stacks using open source technologies — from architecture through to production.
Talk to Superteams