Guides
About the guides
The specification covers only what a company and a personal agent need to work together. These guides cover the rest: recommended practices for a secure, reliable implementation, and advice for each party.
Following the guides is not required to comply with the protocol. Each item is a recommendation, in the sense of SHOULD: do it unless you have a good reason not to. Where a guide repeats something the specification requires, it says MUST and links to the section that requires it.
Start with For both parties, then read the part for your side. The examples use the same placeholder domains as the specification: example.com for the company and agent.example for the personal agent.
For both parties
Handling secrets
These are secrets:
- Session tokens and account tokens.
- Private keys, client assertions, session assertions, and DPoP proofs.
- Device codes, one-time sign-in codes, and credentials sent in mediated sign-in.
To handle them:
- Keep them out of logs, telemetry, error reports, model context, conversation messages, and URLs. Tokens MUST NOT be sent in a URL (4.3), and account tokens MUST NOT go in model context, messages, logs, or URLs (4.8).
- Redact the
Authorization,DPoP, andCookieheaders and credential fields before a request reaches a logger or tracer. - Send
Cache-Control: no-storeon every response that carries a token or code.
Signing and verifying JWTs
Assertions and DPoP proofs are JWTs. To sign and verify them consistently:
- Support ES256 and RS256, and sign with ES256 when you can. Use RSA keys of at least 2048 bits.
- Reject
alg: noneand symmetric algorithms such as HS256. - Verify assertions only with keys from the personal agent's verified
jwks_uri(4.1). Ignorejku,x5u, and any key embedded in an assertion. DPoP proofs are the one exception: the public key in theirjwkheader is how they work (4.3). - Check
aud,exp, andiat. Allow a small clock skew, such as 30 seconds, and reject assertions that claim a long lifetime. - Make each
jtiat least 128 random bits.
Rotating keys
- Give every key a
kid. - Publish a new key in
jwks_uribefore signing with it, early enough for companies' caches to pick it up. - Keep an old public key published until everything signed with it has expired.
- Companies refetch
jwks_uriwhen they see an unknownkid, with a rate limit so a stream of bad assertions can't trigger a fetch each time. - Rotating signing keys doesn't affect account tokens. They are tied to the
client_id, not a key (4.8).
Fetching metadata safely
Personal agents fetch poppy.json files and API descriptions. Companies fetch client metadata documents and key sets. Each fetch goes to a URL the other party controls, so:
- Set limits: a few redirects at most, a response size such as 100 KB, and a timeout of a few seconds.
- Validate HTTPS certificates. Personal agents MUST follow discovery redirects only to HTTPS URLs (section 3).
- Never send cookies, tokens, or other credentials on these fetches.
- Reject loopback, private, and link-local addresses unless you've configured them on purpose. Check the address you actually connect to, not only the one DNS returned first.
- Cache according to HTTP caching headers, within bounds you set, such as at least a minute and at most a day. The upper bound means a removed key stops being trusted.
Retries
- Send an
Idempotency-Keyon API writes that accept one. Use a random UUID (version 4), and reuse the same key when you retry the same request. - Give each conversation message an
idwith at least 128 random bits, and reuse it when you retry the message (7.3). - Each retry needs a new DPoP proof, and a retried token request needs a new assertion. The company rejects a
jtiit has already seen. - Wait at least as long as
Retry-Aftersays on a 429 (MUST, 4.3) or a 503. - Otherwise, back off exponentially with jitter, cap the delay, and limit the number of attempts.
- Companies keep idempotency records for a set time, such as a day, scoped to the session. They return the original response for a repeated key, and reject a repeated key with a different body.
Rate limits
Personal agents change the shape of web traffic: one user's request can turn into many automated calls, at any hour. Existing HTTP patterns handle most of this.
- Companies: limit by
client_id, by user, and by session, so one busy personal agent or user doesn't crowd out others. - Companies: also send the
RateLimitandRateLimit-Policyheaders (IETF draft), so personal agents can slow down before they hit a limit. - Personal agents: keep track of limits for each company and share them across your tasks and users, rather than letting each task find the limit on its own.
- Personal agents: prefer long polling or streaming over repeated short polls, and check for changes no more often than the task needs. Checking once a day for a price drop is usually enough.
- Personal agents: run work that isn't time-sensitive, such as checking order status in the background, outside the company's busy hours, and spread it out instead of starting it all at the same moment.
Transport and JSON
- Use TLS 1.2 or later. Prefer TLS 1.3.
- Encode JSON as UTF-8 and reject objects with duplicate member names.
- Write timestamps in RFC 3339 with a time zone offset, such as
2026-10-02T17:15:00-07:00. - Ignore fields you don't recognize, so later versions can add them.
For personal agents
User IDs
Each user needs a stable, opaque ID for each company (4.2). Two ways to make one:
- Store a random ID for each user and company the first time they're needed. This is the simplest choice.
- Derive one with a keyed hash, such as HMAC-SHA256 of your internal user ID and the company's
auth.issuer, with a secret key. Changing the key changes every ID, so the company sees all your users as new.
The ID MUST NOT contain or be derived from personal information, such as an email address, phone number, or name (4.2). This includes hashes of it, keyed or not. If you derive the ID, start from an internal ID that is itself random.
DPoP keys and proofs
- Generate a DPoP key pair for each user at each company (4.3). Make it non-extractable where your platform allows, such as in a key management service or WebCrypto.
- Never expose private keys to a model or to tools a model can call. Sign on the model's behalf in code it can't reach.
- Sign proofs in an HTTP client hook that sees the final method and URL. Don't let the client follow redirects with the old proof: each hop needs its own proof, and the
Authorizationheader shouldn't follow a redirect to another host. - Personal agents MUST support nonces (4.3). On
use_dpop_nonce, take the nonce from theDPoP-Nonceheader, keep it for that server, and retry once with a new proof that includes it. - To switch to a new DPoP key, request a new session token with a proof signed by the new key. The company binds the token to that key.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: DPoP error="use_dpop_nonce"
DPoP-Nonce: eyJ7S_zG.eyJH0-Z.HX4w-7vPOST https://api.example.com/orders/ord_7Hk2/exchanges
Authorization: DPoP eyJhbGciOi…Qp4w
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIs…
Idempotency-Key: 4f1c2e8a-9b7d-4c3e-a5f6-0d8b1e2c3a4fStoring tokens
- Account tokens MUST be stored as secrets (4.8). Encrypt them at rest, apart from the model and its tools. Give tools a reference to the token, not the token.
- Keep session tokens in memory and drop them when they expire.
- Request Bearer session tokens only for MCP (4.3). They can be used by anyone who copies them. Each one is limited to one MCP server by
resource, and you can also narrow it withscope. - When the company returns a new account token, you MUST use it from then on (4.8). Replace the stored one right away.
- After the user signs in again for more scopes, revoke the old account token (4.4).
- On
invalid_grantfor an account token, delete it and continue signed out.
Choosing how to sign in
- Before asking the user to sign in, check whether an account token you hold already covers the task (4.4).
- Prefer direct sign-in, then device sign-in, then mediated sign-in. With direct and device sign-in, the user's password stays with the company.
- Ask for the fewest scopes the task needs. For a task that only looks things up, request a read-only session token from a broader account token (4.8).
- The user may approve fewer scopes than you asked for. Check
scopein the response before continuing.
Direct and device sign-in
- Use direct or device sign-in only when the user signs in themselves (MUST, 4.4). Never type the user's password, enter their one-time code, or click the company's approval controls for them.
- Open the authorization URL in the user's own browser, not in a browser you automate.
- Make
staterandom and single-use. Record which company and session it belongs to, and exchange the code only at that company'stoken_endpoint. Check that the redirect'sissmatches that company'sauth.issuer(MUST, 4.5). - Use PKCE with
code_challenge_method=S256(4.5). - For device sign-in (4.6), show
verification_uri_completeas a link or QR code, and showuser_codetoo so the user can confirm it matches. Keepdevice_codeprivate. - Poll no faster than
interval. Onslow_down, add 5 seconds to the interval, as RFC 8628 describes.
Mediated sign-in
- Collect the fields in
auth.mediated.fieldsin a form that sends them straight to your sign-in code, so secret values never pass through the model. - Secret fields MUST never be shown back to the user, logged, or sent anywhere other than the company's
auth.mediated.endpoint(4.7). - Once you have an account token, discard the credentials. If the user needs to sign in again later, ask again.
- When the company asks for a one-time code, ask the user for it and submit only the code.
Native and distributed apps
- Don't put your signing private key in an app you distribute. Anyone could extract it and act as your personal agent. Keep the key on your server, and have the app ask the server to sign assertions for the signed-in user.
- DPoP keys are per user, so the app can generate and keep them on the device, in its platform keystore.
- Redirect URIs MUST be HTTPS URLs on your
client_iddomain (4.1). To return the user to an app, use an HTTPS link the app claims, such as an iOS universal link or an Android app link, or use device sign-in.
Web browsing
- Use a separate browser storage context, with its own cookies and local storage, for each session. Never share one across users.
- Don't type the user's password or other credentials into a company's website. Sign in with direct, device, or mediated sign-in, then join your browser to the session (section 5).
- Join the browser to the session right before you start browsing, and post a new browser assertion when the website stops recognizing the session.
- Close the storage context when the session ends.
Conversations
- Read events with a long
wait, such as 30 seconds, or a stream, rather than many short polls (7.5). - Before asking for a person, send a short message that sums up the problem, so they don't have to read the whole conversation (7.9).
- When a person takes over, write for them: concise, plain sentences, and time to reply.
- Close a conversation when the task is done (7.12).
Choosing an interface
- Prefer the company's APIs and company agent over browsing its website. They are faster and change less often.
- Don't phone a company that publishes a
poppy.json. Use the interfaces it lists instead. - Skip API types and conversation protocols you don't recognize (section 6).
For companies
Verifying personal agents
- Fetch the
client_idURL without following redirects. Itsclient_idMUST equal the URL you fetched (4.1). - Cache client metadata and key sets, as described in Fetching metadata safely.
- Keep allowlists or blocklists of
client_idvalues, and rate-limit session creation and newclient_idvalues (4.1).
Verifying DPoP
Section 4.3 lists the checks you MUST make on every proof. To make them reliably:
- Check that
typisdpop+jwt, thatalgis allowed, and thatjwkholds only a public key. - Keep one replay cache shared by every server, keyed by
jtiand holding entries for the acceptance window. Otherwise a proof rejected by one server can be replayed at another. - Behind proxies and load balancers, compare
htuwith the public URL the personal agent used. Build it from your configured public origin, not from theHostheader your server receives. - Use an acceptance window of about one minute, plus a small clock skew.
- Require nonces if you want proofs that can't be made in advance. A nonce stays valid for a while, so it doesn't stop a proof being replayed. You still need the shared replay cache.
- Check whether your authorization server can issue DPoP-bound tokens. If it can't, put a gateway in front of both your token endpoint and your APIs and conversation endpoint. It checks the proof on each token request, records the key for the token it issues, and checks that key on every later request. Checking a proof's signature alone isn't enough.
- Accept Bearer session tokens only on your MCP APIs (MUST, 4.3).
- Serve personal agent conversations only from your conversation endpoint (7.2). If your website chat or other channels can open the same conversations, they may not check DPoP, and anyone with a copied token could read them there.
Token lifetimes and sign-out
- Session tokens should last hours, not days (4.2).
- Check on each request whether the session is still signed in, so sign-out takes effect right away. If you can't, keep session tokens to a few minutes (4.9).
- Give account tokens a lifetime that fits the account, such as 90 days, and expire ones that go unused sooner.
- Revoke a user's account tokens when they reset their password or you suspect the account is compromised.
Consent pages
The page where the user approves access, in direct and device sign-in (4.5, 4.6):
- Show the personal agent's name and logo, and the domain of its
client_id. The personal agent chooses its own name, so the domain is what the user can trust. - Show the account being connected, each requested scope in plain language, and how long access lasts.
- Protect the page against cross-site request forgery, and against framing by other sites with
Content-Security-Policy: frame-ancestors. redirect_uriMUST exactly match an entry in the personal agent'sredirect_uris(4.5).- Accept PKCE with
S256only, and rejectplain. - Return
issin every redirect, success or error (MUST, 4.5). - For extra protection, support Pushed Authorization Requests (RFC 9126).
Device sign-in
- Opening the link MUST NOT approve the request by itself (4.6). The same goes for scanning the QR code. The user signs in and approves on the page.
- Ask the user to confirm that the code on the page matches the one the personal agent shows.
- Return
slow_downto personal agents that poll faster thaninterval, and limit wronguser_codeentries. - Keep
expires_inshort, such as 10 minutes.
Mediated sign-in
If you offer mediated sign-in (4.7), you MUST rate-limit attempts and MUST NOT return credentials in any response. Also:
- Rate-limit by account, by
client_id, and by user ID, and cap wrong one-time codes. - Require a one-time code when a sign-in looks unusual.
- Return the same
failedresponse for an unknown account and a wrong password, so responses don't reveal whether an account exists. - Never store raw credentials or codes, including in logs, traces, and idempotency records.
- Tell the user, for example by email, when a personal agent signs in.
Account settings
- In account settings, list each connected personal agent with its name, domain, scopes, and when it was last used.
- Offer a control to disconnect each one. Disconnecting revokes its account tokens and signs out its sessions (4.9).
Browser sessions
- Offer
web.browser_session_endpointif you want signed-in personal agents to use your website. Without it, they can only browse signed out, or ask users to sign in to your website themselves. - Don't log request bodies at the endpoint. They contain browser assertions.
- Limit the cookie to the hosts personal agents need, such as your main site, rather than your whole domain.
- Look up the session on each request, so sign-out and scope changes take effect on the website right away.
Enforcing scopes everywhere
- Apply the same scope rules on your website, APIs, company agent, and live agents. One shared check that every channel calls is easiest to keep consistent.
- Have the company agent's tools check the token's scopes on every call. Don't rely on the model to decide.
- Give live agents' tools the same checks, so a person can't make a change the token doesn't allow (7.11).
poppy:writedoesn't includepoppy:read(4.4). Check each one separately.
Internal services
- Don't forward a session token from one of your services to another.
- Verify the token at the edge, then pass the verified
client_id, user ID, account, and scopes to internal services through a gateway or service credentials. - Internal credentials used for a request shouldn't allow more than the session token does.
Guest access
Let a signed-out personal agent session do what a signed-out visitor can do on your website, such as search products, read store policies, or ask the company agent general questions. Limit abuse with rate limits by client_id, not by turning personal agents away.
Conversations
- Let your company agent do everything customers can do by phone, as fully as you can, so personal agents have no reason to call.
- Keep a conversation's events for at least 30 days, so a personal agent that reconnects rarely hits
cursor_expired(7.5). - On a stream with nothing to send, send an SSE comment every 15 to 30 seconds, so proxies don't close it (7.6).
- When a session signs out, close its conversations that used the account, so their account details can't be read signed out (7.11).
- Before closing an inactive conversation, say so in a message (7.12).
Error messages
- Keep
error_descriptionand other error text free of account details and other users' data. - Return the same error for "doesn't exist" and "belongs to someone else", as
conversation_not_founddoes (7.13).
For the Operations extension
These apply only to companies and personal agents that support the Operations extension.
Personal agents
- Show the user the operation's
summaryas the company wrote it, with the company's name and the account it applies to. Point out any cost, date, or condition, and whether the action can be undone. Don't replace the summary with your own paraphrase. - When a new revision arrives, show the user what changed before asking them to approve it again.
- Store each standing permission with the company, account, kind of action, limits such as a maximum amount or number of uses, and expiry. Check every limit against the revision's terms before confirming with
standing_permission. If a term you need to check is missing fromterms, ask the user instead. - For a permission with a limited number of uses, reserve a use before you confirm, so two tasks running at once can't both use the last one. Release the reservation if the company rejects the confirmation.
- After a confirm times out, read the operation or confirm again with the same revision. Wait for
Retry-Afterbetween reads.
Companies
- When you call another system to perform the action, such as a payment processor or a warehouse, derive its idempotency key from the operation ID. A retry then can't perform the action twice downstream either.
- If a downstream call times out, ask that system whether the action happened, using your business reference, before trying again. Keep the operation
in_progressuntil you know. - To spot the same action coming up in another channel, look for an open operation in the same session or account with the same terms before proposing a new one.
- Keep final operations readable for as long as the user can see the related order or booking on your website, or at least 30 days.
- Write the
summaryso it stands alone: what will happen, to what, and any cost, date, or condition. Personal agents show it to users as is.