Documentation
OverviewQuickstartConceptsDecisions and errorsApprovalsVersioningSecurityExtensionsFAQPartner integration profileConcepts
Status: Draft 0.4, pre-release. Section numbers in brackets, such as [9.3], point to
spec/rctp-draft-0.4.md. "Binding" points tospec/v0.2-binding.md.
RCTP is built from a handful of ideas. Each one below starts in plain language, then gives the technical detail.
flowchart LR
Seller((Seller)) -->|"consents on"| AS["Marketplace<br/>authorization server"]
AS -->|"Agent Grant (held by the platform)"| Agent["Seller agent"]
Seller -->|"sets terms and approves<br/>on the hosted page"| EP["Enforcement point"]
Profile["Operator UCP profile"] -.->|"names who runs, publishes keys"| Agent
Agent -->|"signed call + access token"| EP
Buyer["Buyer agent"] -->|"signed offer with Deal Terms"| EP
EP -->|"introspects every call"| AS
EP -->|"decision: auto, ask or forbid"| Agent
EP -->|"writes"| Record[("Hash-chained deal record")]
Agent Grant
In plain words. An Agent Grant is the seller's permission for one agent. On their marketplace's consent page the seller sees the agent, who runs it, each verb it may use and whether that verb needs their approval, and when the permission ends. The page asks for no prices. The marketplace keeps the grant; the agent only gets an access token that works with its own key. The seller can revoke it at any time, and the agent's next call fails.
Technical detail.
| Issuance | OAuth 2.0 authorization code with PKCE S256; RFC 9396 authorization_details of type dev.rctp.agent_grant with agent_id, jkt (thumbprint of the agent key) and tiers per scope [5.2] (binding 3.2) |
| Tokens | Opaque access token, at most 900 s; rotating refresh token (reusing a rotated one revokes the grant); the grant itself ends at most 90 days after issue [5.2(4)] |
| On every call | Authorization: Bearer …, covered by the request signature. The gateway introspects the token on every seller-side call (RFC 7662), never caching the answer, and checks cnf.jkt against the request keyid [5.3] (binding 3.3) |
| Introspection view | active, scope, sub (the seller's pairwise pseudonym, B-9), exp, cnf.jkt, grant_status (B-3) and the authorization details. Schema grant-introspection |
| Refusals | 401 grant_invalid (unknown, not bound to this key, not covered by the signature), 403 grant_revoked, 403 grant_expired, 403 action_forbidden (scope not granted) |
| Revocation | The seller revokes on a marketplace screen; operators revoke their own tokens (RFC 7009). The next signed call fails (INV-7) [5.4] |
| SDK | SellerConnect.discover, authorizationUrl, exchange, refresh, revoke (TypeScript and Python) |
Listing Terms and term sets
In plain words. Listing Terms are the seller's rules for one item: the lowest price they take (the floor), how it can be delivered and paid for, when the listing closes, and how many days the seller has to deliver. The seller fills them in on a page the marketplace hosts, when they approve the listing. The floor is private: the platform keeps it, and never discloses it to any agent. The seller's agent may propose terms, the floor included, which only pre-fill the seller's form; what the seller confirms is stored. A term set says which terms a kind of item has and what values are allowed.
Technical detail.
| Term sets | goods.small/1 (phones, electronics, fashion, household) and vehicles/1 (adds deposit_pct, inspection_before_deposit, required_observation; floor at least half the list price). Registry term-sets; schema term-set [6.2]. The reference gateway enforces goods.small/1 by default; term_sets in its configuration adds vehicles/1 |
| Private terms | floor (required, at most the list price) [6.4]. Draft 0.5 removed the counter term |
| Public terms | available_until, fulfillment_options, payment_terms, release_condition, deliver_within_days, optional counterparty_requirements; shown in listing data under public_terms |
| How they are set | listing.create (ask by default; at auto the listing is created without terms) and listing.set_terms (always ask). The agent may send proposed_terms, which pre-fill the hosted page and are never stored as such. The seller fills the form on the hosted page; POST /approve/{id}/terms validates it (422 terms_invalid), then the seller approves with their passkey over a challenge that binds terms_commitment [6.1] (binding 4.2, B-4). See Approvals |
| Commitment | terms_commitment = "sha-256:" + hex SHA-256 of the JCS form of {"salt", "terms"}, with a 128-bit salt. Agents see the commitment, never the values [6.4(2)] |
| Changes | listing.set_terms (always ask) and listing.reprice (never below the floor) change future offers only [6.3] |
| Schemas | listing-terms, terms-submission (the only two schemas that can carry a floor), listing |
| Without terms | A listing without approved terms has no seller agent: offers on it go to the marketplace as ordinary offers, and a seller agent acting on it is refused not_in_scope [6.5] |
Offer and Deal Terms
In plain words. An offer is a price plus how the item will be delivered and paid for. A buyer agent makes one; the seller's agent may counter, and the buyer answers a counter with a new offer that replaces it. Every offer has an expiry. When the seller accepts, the listing is reserved for that buyer, who gets a single-use checkout to open the deal.
Technical detail.
| Object | offer with by (buyer or seller), deal_terms (deal-terms: price, fulfillment, payment), negotiation_id, optional supersedes, expires_at [7.1, 7.2] |
| Negotiation | negotiation_id (neg_…) is random, assigned on a buyer agent's first offer on a listing, and shared by every later offer and counter between that agent key and that listing (binding 5.2) |
| Revision | supersedes names an open or countered offer in the same negotiation; it becomes superseded. Otherwise 422 terms_invalid [7.3] |
| Expiry | Default and maximum from config.offer_expiry of the gateway's dev.rctp.deal entry (reference: 48 hours, 7 days), capped at available_until. After it, 409 offer_expired |
| Who accepts | Only the seller side, only a buyer offer (offer.accept). A buyer cannot counter or accept [7.3(1)] |
| States | open, countered, awaiting_approval, accepted, declined, expired, superseded, refused (offer-state) |
| Reservation | An accepted offer reserves the listing: other accepts get 409 listing_reserved until the checkout is redeemed or expires. Its Deal Terms are fixed [7.4] |
| Checkout | Compact JWS (typ rctp-checkout+jwt) signed by the gateway profile key, bound to listing_hash, offer_id, negotiation_id, deal_terms, buyer_jkt, a single-use nonce and exp (INV-6). Schema checkout. Redeemed with POST /deals |
Decision
In plain words. Every governed call gets one of three answers from the enforcement point: done (auto), waiting
for the seller (ask), or refused (forbid). A refusal always names one registered reason, so the agent can act on
it without a human reading an error message.
Technical detail.
| Object | RCTP-Decision response header, an RFC 9651 Item (a Token with parameters), plus the HTTP status [8.2] (binding 6) |
| Examples | auto · ask;approval_id="apr_…";expires_in=900;on_timeout=deny · forbid;action="offer.accept";reason=below_floor |
| Schemas | decision.schema.json (parsed header), error.schema.json (refusal body) |
| Registry | registries/reason-codes.json (22 reasons) [13] |
| More | Decisions and errors |
Verb, scope and tier
In plain words. A verb is something an agent can do, such as accept an offer. A tier is the rule for it: auto
means the agent may do it, ask means only after the seller approves. The seller chooses tiers when they grant the
agent, within what the marketplace allows. Signing a contract and changing the seller's account are never allowed,
whatever anyone chooses.
Technical detail.
| Verbs | registries/actions.json: listing.create, listing.set_terms, listing.reprice, listing.answer_buyer, offer.create (buyer, no tier), offer.counter, offer.accept, deal.commit_to_ship, deal.disclose_passport, deal.accept_deposit, and the fixed forbids deal.sign_contract and account.update [8.4] |
| Scopes | Each seller verb has a scope under dev.rctp.listing or dev.rctp.deal, such as dev.rctp.deal:offer_accept. The fixed forbids have no scope (INV-3) |
| Tiers | From the grant's tiers for the verb's scope, within the scope's allowed_tiers; else the scope's default_tier. listing.create and listing.set_terms are always ask (binding 6) |
| Discovery | Scopes with default_tier and allowed_tiers are in config.scopes of the gateway's dev.ucp.common.identity_linking entry (binding 3.1) |
| In v0.2 | Routes exist for every scoped verb except deal.disclose_passport and deal.accept_deposit, and for the two fixed forbids (binding 12) |
Approval
In plain words. When a verb is ask, the enforcement point holds it and asks the seller. The seller sees the
exact action, amount and text, and says yes or no with their own passkey. For a listing, the same page is where the
seller types in the terms. The partner's app can show the page, but only the seller's key can sign the answer. If the
seller does not answer in time, the answer is no.
Technical detail.
| Objects | Approval request (202 body), approval response (from the hosted page), approval status (polling and webhooks) [9.2] |
| Binding | One-time challenge = SHA-256 over the JCS form of RFC 9396 authorization_details of type dev.rctp.approval; for listings it binds term_set and terms_commitment (B-4). WebAuthn assertion with user verification (binding 7) |
| Schemas | approval-request, approval-response, approval-status |
| Rules | Only the seller's passkey decides (INV-4). No answer by expires_at is a decline (INV-2). display never contains private terms [9.2(2)] |
| In v0.2 | Level hosted_embedded only. See Approvals |
Deal record and events
In plain words. Every decision, every approval and every step that moves money or responsibility is written into a signed, hash-chained record: the buyer's money is held, the item shipped, the buyer accepted it, the money was released. Each event is signed by the party that saw it, and the enforcement point refuses events that come too early, such as a release before the buyer accepts.
Technical detail.
| Writing events | POST /deals/{id}/events with {"event": "<compact JWS>"}, typ rctp-deal-event+jwt, signed by the same key as the request [10.2] (binding 8.1) |
| Events | RCTP checkpoints funds_held, accepted, rejected (not_as_described, not_received), released; UCP fulfillment events shipped, delivered, returned_to_sender; UCP adjustments return, refund; reverse-domain extensions, recorded as informative |
| Money rules | released needs funds_held and the release condition (default buyer_accepted); refund needs rejected + not_as_described + return, or rejected + not_received after deliver_within_days. Out of order → 409 release_condition_unmet [10.3] |
| Who writes what | funds_held, released, refund: the holder of funds. accepted, rejected: the buyer agent (buyer_jkt). Fulfillment events: the carrier (first_hand) or the marketplace (relayed, naming reported_by and source_event_id) |
| The record | One hash chain per subject (negotiation, deal or listing); prev_hash over the JCS of the previous entry; gateway entries carry its JWS. Entries carry terms_commitment, never a floor [10.4]. GET /deals/{id}/events returns a deal's entries to its parties |
| Schemas | deal-event, deal-event-post-request, deal-event-list, attestation-entry, deal |
| Registries | checkpoints, release-conditions |
UCP profile (who runs an agent)
In plain words. Every operator, and every gateway, publishes a small public document saying who it is and which keys it signs with. An operator's profile names the company, its country, an incident contact and how fast it responds. The marketplace only lets in agents whose operator it has onboarded.
Technical detail.
| Object | UCP profile at https://<host>/.well-known/ucp with keys[] (ES256; each kid is the key's RFC 7638 thumbprint, B-1). Schema ucp-profile (binding 1) |
| Operator entry | dev.rctp.operator in ucp.capabilities, config: legal_entity, jurisdiction, incident_contact, incident_sla_hours; optional certifications, data_residency, key_custody [4.1(2)]. dev.rctp.deal with config.webhook_url to receive webhooks |
| On the wire | Every agent request carries UCP-Agent: profile="<profile URL>" and a UCP message signature (RFC 9421, label sig1, keyid = a kid in the profile) (binding 2) |
| Rules | A profile that is not onboarded or does not resolve, or a keyid not in its keys[], gets 401 operator_unknown. The reference gateway onboards operators by profile URL and caches a profile for at most 5 minutes |
| SDK | operatorProfile / operator_profile, generateAgentKey, fetchProfile |
Enforcement point
In plain words. The enforcement point sits in front of the marketplace API and checks every agent call against the seller's grant and terms. It decides; the agent does not. Prompts, listing text and request bodies cannot change its answer. It also writes a signed record of every decision, including every refusal.
Technical detail.
| Role | Evaluates every seller-agent verb and buyer offer, holds Listing Terms, verifies approvals, applies the money rules, writes the deal record [3, 8] |
| Check order | Signature and operator → grant (seller side) → listing → buyer requirements (buyer side) → terms, Deal Terms and budget → tier [8.1] |
| Reference code | packages/gateway-go: enforce (decision engine and HTTP middleware), msgsig, profile, grant, terms, approval, attest, idempotency, gateway; authserver for the sandbox and tests |
| Discovery | GET /.well-known/ucp (its UCP profile) and GET /.well-known/oauth-protected-resource (RFC 9728, naming the authorization server) |
| In v0.2 | Go middleware you can mount in front of any marketplace API, and a reference gateway. Production gateways are separate (see FAQ) and must pass the conformance suite |
Marketplace authorization server
In plain words. The marketplace's login service runs the consent page, issues and revokes grants, and tells the enforcement point on every call whether a grant is still good. It never sees or stores prices.
Technical detail.
| Discovery | RFC 8414 metadata at /.well-known/oauth-authorization-server, as UCP Identity Linking requires |
| Endpoints | Authorization (consent), token (code and refresh), introspection (RFC 7662, client_secret_basic, plus grant_status for the gateway, B-3), revocation (RFC 7009) |
| Passkeys | The seller's approval key is their marketplace account passkey [9.3(5)]. The reference gateway's enrolment page takes a ticket signed by the authorization server (reference only) |
| In v0.2 | Each platform runs its own. The sandbox's rctp-authserver (port 4000) and the conformance runner's test server are TEST ONLY |
Other terms
| Term | Meaning |
|---|---|
| Deal | Opened when the buyer redeems a checkout (POST /deals); the listing becomes sold. Events are written against it. Schema deal |
| Listing hash | A digest that binds approvals and checkouts to the exact listing |
| Trusted Surface | A screen the human controls, where consent, terms and approvals are given [2] |
| Holder of funds | The party that holds the buyer's payment and writes funds_held, released and refund [10.2]. An operator profile the gateway is configured to treat as one |
| Offer budget | Below-floor offers counted per buyer agent and per buyer operator, per listing, over a window; then 429 for every offer [8.5] |
| Party Credential, band | Facts about a trader expressed as coarse ranges [11.1]. Not in v0.2 |
Source: docs/concepts.md