Developers
Switchback Supply is a fictional company that implements the Personal Agent Protocol (Poppy), draft 0.1. Point your personal agent here to try the protocol against a real server. The inspector shows every request your agent makes, filtered by its client_id.
Start here
Fetch the discovery document, then the OAuth metadata for its issuer:
curl https://www.alpine-retail.com/.well-known/poppy.json
curl https://www.alpine-retail.com/.well-known/oauth-authorization-serverSwitchback is its own OAuth server, so auth.issuer is https://www.alpine-retail.com. The metadata gives the token_endpoint and revocation_endpoint. Before sending anything, check that its issuer matches and that poppy_domains lists the company's domain. The mediated sign-in endpoint, https://www.alpine-retail.com/agent-auth/sign-in, is Switchback's choice of path: another company can put it anywhere, and discovery says where.
Try it without a personal agent
Atlas is a small demo personal agent. It creates a key pair, registers the public key here, and then talks Poppy to Switchback, showing every request and response.
The quickest way is Atlas in your browser. It keeps its keys in the browser and calls Switchback's endpoints directly, with the chat on one side and the requests on the other.
Atlas also runs from a terminal, from this demo's repository. It prints every request as a curl command you can run again: each DPoP proof, assertion, and token in it comes from a pnpm atlas command that makes a new one. It needs Node 22.18 or later.
pnpm install
pnpm atlas init # create a key pair and register Atlas as a test agent
pnpm atlas session # discover, then start a signed-out session
pnpm atlas whoami # ask Switchback who it thinks you are
pnpm atlas renew # get a new Session Token for the same session
pnpm atlas sign-in --phone 4155550123
pnpm atlas use-account --new # a new session, signed in with the Account Token
pnpm atlas sign-out # revoke the Account Token
pnpm atlas say "Hello"
pnpm atlas say slow --stream # a reply that streams in as it's written
pnpm atlas poll # read new events
pnpm atlas close # close the conversationAtlas's inspector link opens the inspector with only Atlas's requests, so you can watch each one arrive and see what Switchback made of it.
Test agents
Your agent normally hosts its own client metadata document and JWKS. If you aren't ready to host them yet, register a public key here and Switchback hosts both for you. Send only the public key: Switchback rejects private key material. This is a demo-only endpoint, not part of the protocol, like everything under /demo/. The inspectors hide demo-only requests unless you ask for them.
curl -X POST https://www.alpine-retail.com/demo/test-agents \
-H 'Content-Type: application/json' \
-d '{"name": "My agent", "jwk": {"kty": "EC", "crv": "P-256", "x": "…", "y": "…", "alg": "ES256", "kid": "key-1"}}'The response is your client metadata document. Its client_id is the URL you use as iss in assertions. Test agents expire after 30 days.
Starting a session
Every request after discovery carries a DPoP proof (RFC 9449): a short JWT that names the request's method and URL, signed with a key your agent keeps for this. Switchback binds each Session Token to that key, so a stolen token is useless without it. Sign a new proof for every request: each one works once, for up to 60 seconds. pnpm atlas proof POST https://www.alpine-retail.com/oauth/token prints one.
To start a session, sign an assertion with the key in your JWKS: iss is your client_id, sub is your user's ID, aud is https://www.alpine-retail.com/oauth/token as a single string, plus iat, exp (at most 2 minutes later), and a unique jti. Switchback accepts ES256, ES384, RS256, PS256, and EdDSA, for assertions and proofs. pnpm atlas assertion prints one.
Like every token request, it also carries a client assertion that authenticates your agent (private_key_jwt): the same kind of JWT, with sub set to your client_id. pnpm atlas client-assertion prints one.
curl -X POST https://www.alpine-retail.com/oauth/token \
-H "DPoP: $(pnpm -s atlas proof POST https://www.alpine-retail.com/oauth/token)" \
--data-urlencode grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer \
--data-urlencode "assertion=$(pnpm -s atlas assertion)" \
--data-urlencode "client_id=$(pnpm -s atlas client-id)" \
--data-urlencode client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
--data-urlencode "client_assertion=$(pnpm -s atlas client-assertion)"The response has a signed-out access_token with token_type: "DPoP", and a session_id. To renew a signed-out session, send the same request with session_id added. Sessions last 7 days, and Session Tokens last 1 hour.
Send the token as Authorization: DPoP, with a new proof whose ath is the token's SHA-256 hash. https://www.alpine-retail.com/api/whoami is an example company API that echoes back what Switchback knows about the session:
curl https://www.alpine-retail.com/api/whoami \
-H "Authorization: DPoP $(pnpm -s atlas token)" \
-H "DPoP: $(pnpm -s atlas proof GET https://www.alpine-retail.com/api/whoami --with-token)"Signing in
Switchback supports mediated sign-in today: your agent sends the user's phone number, and Switchback asks for a one-time code. Mediated sign-in grants only poppy:read. Direct sign-in is coming soon.
Nothing is actually texted. The code is always 123456. Any phone number with 10 to 15 digits works and gets its own demo account, which lasts 3 days. Use a made-up number, like 415 555 0123. Anyone who knows the number can sign in to it.
curl -X POST https://www.alpine-retail.com/agent-auth/sign-in \
-H "Authorization: DPoP $(pnpm -s atlas token)" \
-H "DPoP: $(pnpm -s atlas proof POST https://www.alpine-retail.com/agent-auth/sign-in --with-token)" \
-H 'Content-Type: application/json' \
-d '{"scope": "poppy:read", "credentials": {"phone": "4155550123"}}'The response has status: "code_required" and a sign_in_id. Send the code to https://www.alpine-retail.com/agent-auth/sign-in/<sign_in_id> the same way, with {"code": "123456"}. A wrong code returns code_required again, and the fifth wrong code returns failed. A sign-in expires after 10 minutes, and only the session that started it can finish it.
A correct code returns status: "complete" with a signed-in Session Token for the same session, and an Account Token in refresh_token. Use the new Session Token from then on: older ones are still treated as signed out. A session can only ever be signed in to one account. Signing it in to another gets account_mismatch.
The Account Token remembers the sign-in for 30 days. To use it, send it to the token endpoint with grant_type=refresh_token and a client assertion. You get a signed-in token for session_id, or for a new session if you leave session_id out. No phone number or code needed. Keep the Account Token secret: send it only to the token and revocation endpoints.
To sign out, revoke the Account Token at the revocation_endpoint with token and a client assertion. Every session that used it is signed out at once and carries on signed out, so an open conversation continues. Renew to get a token that says so.
Talking to the company agent
Start a conversation by sending its first message to https://www.alpine-retail.com/poppy/conversations, with your Session Token and a DPoP proof. Your agent chooses each message's id, unique for the user.
curl -X POST 'https://www.alpine-retail.com/poppy/conversations?wait=10' \
-H "Authorization: DPoP $(pnpm -s atlas token)" \
-H "DPoP: $(pnpm -s atlas proof POST https://www.alpine-retail.com/poppy/conversations --with-token)" \
-H 'Content-Type: application/json' \
-d '{"message": {"id": "msg_first_8a6c7f20", "sender": "agent", "text": "Hello", "context": {"locale": "en-US"}}}'The response has a conversation_id. Send later messages to https://www.alpine-retail.com/poppy/conversations/<conversation_id>/messages, and read the conversation's events from …/<conversation_id>/events. With wait, a POST also returns the events since its cursor, including the reply. Close the conversation with an empty JSON object to …/<conversation_id>/close.
A conversation belongs to your client_id and the user, not to the session, so a new session can continue it. Once it has used the user's account, continuing it needs a token signed in to that account.
The company agent is scripted and deliberately minimal: this demo is about the protocol, not customer service. It understands four things:
| Say | What it shows |
|---|---|
Hello | Who Switchback thinks you are: client_id, user ID, sign-in state, and context |
order | An authorization event while signed out (7.11), the order once signed in |
slow | A reply after 6 seconds: status "working", waiting, and text-delta streaming |
bye | Closes the conversation (7.12) |
Choices this demo makes
Draft 0.1 leaves these open. Switchback answers them this way for now:
- Switchback never asks for a DPoP nonce. Other companies may, so a personal agent should still handle
use_dpop_nonce. - Switchback has no MCP server yet, so it never issues Bearer tokens. A token request without a DPoP proof gets
invalid_request, and one withresourcegetsinvalid_target. - Account Tokens don't rotate: the same one works until it expires or is revoked.
- A message ID is remembered for 7 days, as long as the conversation. Resending a message with the same ID and content returns the conversation without adding it again.
- A
POSTwithwaitholds the request until the company agent stops working, for up towaitseconds. AGETwithwaitholds it until there's an event.waitis capped at 30 seconds. - A conversation keeps its last 200 events. An older cursor gets
cursor_expired. Reads return up to 50 events at a time, withhas_morewhen there are more. - Streams close after about 50 seconds, or after the conversation closes. Reconnect with
Last-Event-ID. The "slow" reply streams its text astext-deltaevents for 3 seconds before it arrives. - Switchback has no people on staff, so a handoff request (7.9) shows the spec's "no one is available" path:
queuedfor 3 seconds, then back toidlewith a message saying why. - Direct Conversations (7.10) aren't supported yet, so
parent_conversation_idgetsinvalid_request. - The protocol endpoints accept calls from any origin (CORS), so a personal agent running in a browser on its own domain can call them. They never use cookies.
What works today
| Area | Spec section | Status |
|---|---|---|
| Discovery and OAuth metadata | 3 | Available |
| Sessions, renewal, and DPoP | 4.1 to 4.3 | Available |
| Conversations as an event log, with streaming | 7.1 to 7.8, 7.11, 7.12 | Available |
| Mediated sign-in, Account Tokens, and sign-out | 4.4, 4.6, 4.8, 4.9 | Available |
| Handoff and Direct Conversations | 7.9, 7.10 | Coming soon |
| Direct sign-in | 4.5 | Coming soon |
| Website and APIs | 5 and 6 | Coming soon |
Features marked "Coming soon" aren't in the discovery documents yet. They appear there when they ship.