# Buying as a guest: bring your own domain (v0.1, 2026-09-18) How a buyer nobody introduced buys from this seller, asking nobody for anything. It needs a domain with a DNSSEC-signed zone and one P-256 key. It does not need a web server, a SPIFFE bundle, an attestation, an account, a token from the operator, or an authority on the seller's list. `guest-buyer.mjs`, beside this file, is the whole buyer in one file with no dependencies (Node 20.18 or later): it makes the key, prints the record to publish, checks that the record validates, searches and buys. `vectors/positive/guest-purchase.json` is one purchase as the seller accepted it, with the key, the record, the envelope and the mandate. A seller says whether it takes guests on `GET /health` (`guests.accepted`, with the ceiling and the day's count), on its landing page and in its A2A usage answer. It is a policy of the seller, off by default (`GUEST_BUYERS=on`). ## 1. What a guest is, and what it is not The seller's full form has three parties behind a purchase: a buyer instance attested by its domain (`/.well-known/agent-keys.json` and the bundle beside it), a person, and an authority the seller lists (`MANDATE_ISSUERS`) that vouches the person approved this amount for this quote. That form stays what it was. A guest replaces the two things a stranger cannot get without asking somebody: | | Full form | Guest | |---|---|---| | Who answers for the key that signs the request (check 3) | The buyer's domain, through a key attestation its SVID signs, served over HTTPS | The buyer's domain, through a TXT record in its signed zone | | Who signs the mandate (check 4) | An authority on the seller's list, keys at `/.well-known/jwks.json` | The buyer's own domain: a key in the same TXT record set | | What the mandate proves | A person approved it at that authority | **The domain answers for the purchase. Nothing about a person.** | | Status at check 10 | `GET /mandates/`, live, seconds | The record read again; withdrawal takes effect within its TTL | | Amount | The mandate's | The mandate's, up to the guest ceiling | It is what DKIM does for mail: whoever can write the zone answers for the key, and the reader needs nothing from the sender but the DNS. ## 2. The record One TXT record per key at `_agent-keys.`, where `` is the trust domain of the SPIFFE ID the envelope names (`spiffe:///...`): ``` _agent-keys.example.org. 300 IN TXT "v=agentkey1; kty=EC; crv=P-256; x=<43 chars>; y=<43 chars>" ``` - Tags are separated by `;`, in any order; unknown tags are ignored; `v=agentkey1` is required; `kty=EC` and `crv=P-256` are the only key type; `x` and `y` are the JWK coordinates, base64url, 43 characters each. A record that is not this is skipped, so the name may carry other TXT records. - A key is named by its RFC 7638 thumbprint, computed by the reader from `x` and `y`. The record carries no name for it. - Several records are several keys: publish the new one beside the old one to rotate, take the old one out to withdraw it. One key may sign both the envelope and the mandate, or the record set may carry one for each. - **The zone must be signed**, with its DS at the registrar. The seller asks a validating resolver over DoH and accepts the record set only with the AD flag. A record from an unsigned zone is refused with `signer_unknown` and a message that says so. - Keep the TTL short (300 s or less): it is how long a withdrawn key keeps buying. The name is this contract's convention, not a standard. The reference has no record for a buyer's key; the closest precedent is DKIM's `._domainkey`. ## 3. The request envelope Unchanged: `schemas/request-envelope.schema.json`, `jws-policy.md`. `typ` `request+jwt`, `alg` ES256, `kid` the thumbprint of the signing key, and in the payload `iss` a SPIFFE ID under your domain (any path: the record vouches for the key, not for a workload), `aud` the seller's base URL, `iat`, a `nonce` of 16 characters or more, `method`, `path` and `body_sha256` (hex SHA-256 of the exact body bytes). Sent as `Authorization: Bearer `. The seller looks for the `kid` in `_agent-keys.` first and in `/.well-known/agent-keys.json` second. Found in the record, the buyer is a guest for that call: the ledger entries carry `buyer_key_source: "dns-txt"`, and its searches spend the anonymous search budget (`search_budget_exhausted` when it is spent). ## 4. The guest mandate `schemas/mandate.schema.json`, with these values: | Claim | Value | |---|---| | header | `alg` ES256, `typ` `mandate+jwt`, `kid` the thumbprint of a key in your record set | | `iss` | `https://`, the trust domain of the envelope's `iss`. Nothing is fetched from it | | `sub` | Whoever the domain says the purchase is for; the seller hashes it into its ledger | | `act.sub` | The envelope's `iss` | | `aud` | The seller's base URL | | `merchant_id`, `action`, `stage` | The seller's merchant id, `purchase`, `cart` | | `amount` | `{ value, currency }` **equal** to the quote total, in minor units | | `quote_hash` | The quote's, as returned by the search | | `cnf.jkt` | The thumbprint of the key that signs the envelope | | `jti` | 16 characters or more, never reused: the mandate buys once. It is yours: the seller keeps it under your issuer, so the same `jti` from another issuer is another mandate | | `nbf`, `exp` | `exp` at most 600 s after `nbf`. (The seller falls back to `iat` when `nbf` is absent; the schema requires `nbf`) | | `policy_version` | Any label of yours; the schema requires one and the seller writes it into its ledger | Sent as `mandate_jws` in the checkout body (`checkout.schema.json`). The AP2 v0.2 form (`mandate_ap2`) has no guest form: its roots come from a listed authority. Every check a listed authority's mandate passes is run on this one (4 to 10, same reason codes). Three more apply to a guest: | Check | Refusal | |---|---| | 6 | `guest_ceiling_exceeded`: the amount is above the seller's guest ceiling (`guests.ceiling_minor` on `/health`) | | 8 | `mandate_invalid`: the mandate lives longer than `guests.mandate_max_seconds`, or carries neither `nbf` nor `iat` | | 10 | `mandate_revoked`: read again, the record set no longer carries the key that signed the mandate; `authority_unavailable` when the zone cannot be read or does not validate (fail closed, the mandate is not spent) | And one limit that is not a check: `guest_budget_exhausted` (429) when the day's guest purchases, all guests together, are spent. What the seller writes, in a ledger anyone can read: your SPIFFE ID (so your domain), the key id, the quote, the amount and the hash of `sub`; never `sub` itself, the itinerary's traveller or an address. The purchase's entries carry `mandate_issuer: "https://"` and `mandate_tier: "guest"`, and so does a refusal of your mandate once the seller read it as a guest's (since 2026-09-25). A verifier reading the seller's ledger tells a guest purchase from one a listed authority vouched for by that fact, and does not look for the mandate in any authority's ledger; nor does it take your entries for an authority's that carry the same `jti`. ## 5. Revoking A guest has no status endpoint. It revokes by taking the key out of its zone, and the seller's next fresh read (check 10 of every checkout) refuses. How fast is the record's TTL at the resolver: minutes. A person who has to be able to change their mind in seconds is what the listed authorities are for; the two tiers are the same purchase with a different answer to "who said yes". ## 6. What is not solved - **The resolver is trusted.** The seller does not validate DNSSEC itself; it requires the AD flag of the resolver it asks (Cloudflare's, over DoH). Whoever runs or intercepts that resolver can forge a guest. The same holds for this seller's DANE and ANS reads. - **A domain is cheap.** The ceiling, the life of minutes, the daily count and the search budget bound what a throwaway domain can do; they do not identify anybody. - **No person.** `sub` is the domain's claim. Nothing here says a human saw the amount. - **The record name and tags are ours.** No draft defines them. ## 7. Over UCP UCP already names every platform by its profile, so a platform buying over UCP (`ucp-profile.md`) is a guest without the record: its keys come from its profile and nowhere else, and the DNS is not asked. The mandate is the AP2 Mandates extension's and nothing else: the two closed AP2 v0.2 chains of `mandate-ap2-profile.md`, the Checkout Mandate in `ap2.checkout_mandate` and the Payment Mandate as the payment instrument's `credential.token`, closed and presented as a listed authority's are (`ucp-profile.md` §4). On the envelope path the AP2 form has no guest form (§4); over UCP it is the only one, and a `mandate+jwt` there is refused (`mandate_invalid`, check 4), a guest's or an authority's. The guest issues the two roots itself: their open content's `iss` is the platform's origin (`https://` and the host of its profile URL), each root header's `kid` a P-256 key the profile's `keys[]` publishes under its RFC 7638 thumbprint, `cnf.jwk` the key that signs the completion's HTTP Message Signature and closes the hops, and `act.sub` a SPIFFE ID under the profile's host. The rest is what an authority's roots carry. The seller verifies such a root under the profile's key and reads no authority's key set; a root under a key the profile does not publish is `mandate_invalid`, one whose content names another origin `issuer_not_allowed` (check 4). Then every check of the AP2 form and every limit of §4 holds: the ceiling on the open payment mandate's amount (check 6), at most `guests.mandate_max_seconds` from the open content's `iat` to its `exp` (check 8), the day's count, and check 10, which reads the profile again, as often as UCP lets a business fetch one (a 60-second floor), for each key that signed a root, so taking one out of `keys[]` revokes within a minute. The entries say `mandate_tier: "guest"`, `mandate_issuer` the origin, `mandate_format: "ap2-v0.2"`, `buyer` the profile URL and `buyer_key_source: "ucp-profile"`, and the completion carries the two AP2 receipts, as any AP2 purchase does. The profile host's TLS stands where the resolver stood. The pair's `jti` is the platform's own. The seller keeps it under the origin, as it keeps every guest's (spent once, the evidence, the rail's idempotency key), so a `jti` read in an authority's public ledger buys nothing of the authority's mandate and blocks none of its purchases: under the origin it is a mandate of the guest's, held to everything above. The evidence of a guest's purchase is asked for with `?issuer=` (`evidence-api.md`).