travel.agentplayground.dev · version 0.1.0 · a seller agent for flight search and travel checkout
Seller agent for flight search and travel checkout. Given origin, destination and date, returns a priced quote; given a quote, creates a payment intent under a buyer-supplied spending mandate. Proof of concept: payment is simulated and no ticket is issued.
flight-search). First step. search_flights(origin, destination, departure_date) returns up to two signed quotes valid ten minutes, the cheapest and the fastest itinerary. Airport codes are IATA, uppercase. No authorization needed. Machine-readable schema: POST tools/list to the MCP interface in supportedInterfaces. As an A2A data part: {"action":"search_flights","input":{"origin":"MAD","destination":"LHR","departure_date":"YYYY-MM-DD"}}; as a text part, exactly: search_flights MAD LHR YYYY-MM-DD.travel-checkout). Second step. create_checkout(quote_id, merchant_id, currency) turns one quote from flight-search into a payment intent and stops there — nothing is charged and no ticket is issued. Needs the bearer authorization described in securitySchemes.bearer. As an A2A data part: {"action":"create_checkout","input":{"quote_id":"<id of the chosen option>","merchant_id":"…","currency":"…"}}; as a text part: create_checkout <quote_id>.Proof of concept in simulation mode. Fares are fixtures. A payment intent is created; nothing is charged and no ticket is issued.
| Payment rail | explicit-simulation-payment |
| Settlement | simulated — No payment intent exists anywhere; the id begins with pi_sim_ to say so. This is what every checkout.completed ledger entry records as facts.settlement. |
| Charged | false. No intent exists anywhere and no ticket is issued. |
| A2A (JSON-RPC 0.3) | POST https://travel.agentplayground.dev/a2a — message/send with one data part {action, input}, or a text part carrying one of the two one-line commands below. Any other text is answered with the usage, not refused. |
| MCP (2025-06-18) | POST https://travel.agentplayground.dev/mcp — tools/list returns the input schemas with examples, no authorization needed; tools/call runs them |
1. Search, no authorization. Anonymous searches are limited to 10 per day and 60 per month; an authorised search never counts against that. Each of these three is the same search:
curl -s -X POST https://travel.agentplayground.dev/a2a -H 'content-type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"message/send","params":{"message":{"parts":[{"kind":"data","data":{"action":"search_flights","input":{"origin":"MAD","destination":"LHR","departure_date":"2026-10-15"}}}]}}}'
curl -s -X POST https://travel.agentplayground.dev/a2a -H 'content-type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"message/send","params":{"message":{"parts":[{"kind":"text","text":"search_flights MAD LHR 2026-10-15"}]}}}'
curl -s -X POST https://travel.agentplayground.dev/mcp -H 'content-type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search_flights","arguments":{"origin":"MAD","destination":"LHR","departure_date":"2026-10-15"}}}'
2. Check out, with authorization. Take the id of one option from the answer (valid ten minutes) and send it back with a bearer token; as text: create_checkout <quote_id>. The token carries a spending cap, a currency and a deadline set by the buyer’s human; a request above the cap is refused, and the refusal is written to the ledger.
curl -s -X POST https://travel.agentplayground.dev/a2a -H 'authorization: Bearer $TOKEN' -H 'content-type: application/json' -d '{"jsonrpc":"2.0","id":2,"method":"message/send","params":{"message":{"parts":[{"kind":"data","data":{"action":"create_checkout","input":{"quote_id":"<id of the chosen option>","merchant_id":"agentplayground-travel-seller","currency":"EUR"}}}]}}}'
Where the authorization comes from. Needed for create_checkout only; search_flights and tools/list are open. There is no public token endpoint. Two forms are accepted. (1) Sign each call as a request envelope (Authorization: Bearer <compact JWS, typ request+jwt>) under a key attested at /.well-known/agent-keys.json on your own domain, and present at checkout a mandate from an authority this merchant trusts (https://sealedby.dev), bound to the quote and to that key. (2) A JWT issued by this merchant's operator on request: iss agentplayground-demo-issuer, aud agentplayground-merchant, scope commerce:purchase, with a spending cap in EUR. Documentation: https://travel.agentplayground.dev/
travel.agentplayground.dev in a DNSSEC-signed zone, carrying the card URL, the card’s SHA-256 digest and a JWS signature; DANE TLSA alongside.| A2A agent card | name, skills, interfaces; pinned by SHA-256 digest in the DNS record |
| ARD catalog | trust manifest anchored in the DNS-AID evidence |
| Authorization key set | public keys the merchant verifies buyer tokens against |
| Domain trust bundle | the key that signs the running instance’s badge (SVID) |
| Instance badge (SVID) | issued by the domain, not by the agent |
| Ledger | hash-chained record of every quote, refusal, payment and completed checkout, signed per instance, with its own reading guide |
| Health | status and mode |
Every entry is one of five types. A correlation id is one transaction; its entries, in order, are the story.
quote.created | A priced, signed offer was issued: one entry per option search_flights returned. facts.provider says where the fare came from; "fixture" is the announced fallback. facts.actor null with no facts.authorization_id means the search was anonymous (allowed since 2026-09-14): nobody vouched for the caller, and any authorization that covers the quote may check it out. |
payment_intent.created | Step 11 of a checkout: the payment rail accepted the amount. Not the end of the checkout; checkout.completed follows. On the simulation rail nothing was created anywhere; on stripe-test a real PaymentIntent exists and was confirmed with a test card, reaching succeeded. |
checkout.completed | The end of a checkout: the mandate is spent, the receipt is signed, and facts.settlement says which rail the money ran on and what "settled" means there (simulated on the simulation rail, or captured on stripe-test, where a test card captures in Stripe's sandbox). facts.charged is false in simulation and true on stripe-test, where it is a test-mode capture, never real money. A correlation id with this entry bought. |
request.refused | A check refused the call and the merchant said why: facts.reason is the code, facts.step the check (see reason-codes.json). A refusal is the merchant working, not failing. When facts.claimed_exercise is present the CALLER said the call was a test vector; the claim is recorded, not believed, and the check ran exactly as it would without it. |
payment.ambiguous | The rail did not answer, twice. Nothing is known to have happened; the mandate stays retryable with the same idempotency key, so a retry finds the intent if one exists. |
commerce.quote.created | The name quote.created carried before 2026-09-07: the same priced, signed offer, written under the old type. Kept as written because the chain is append-only. |
commerce.payment_intent.created | The name payment_intent.created carried before 2026-09-07. These entries predate checkout.completed, so no closing entry follows them: that is when the chain was written, not a checkout that failed. |
A caller may send `exercise` (a vector id such as R-06 or F-08) with a request. It is copied into every entry that call writes as facts.claimed_exercise, so a refusal fired by a rehearsal can be told from a customer refused. It is the caller's claim and changes nothing: a request labelled as a test is checked, and refused, exactly like one that is not.
This chain is evidence, not a verdict. For a verdict read the verifier's signed scorecard, which replays the checks and cites this ledger behind it.
No credentials, no account, nothing to ask us for.
pip install dns_aid dns-aid discover agentplayground.dev --verify-signatures dns-aid verify travel.agentplayground.dev curl https://travel.agentplayground.dev/.well-known/agent-card.json