carousell/rctp

Concepts

Status: Draft 0.4, pre-release. Section numbers in brackets, such as [9.3], point to spec/rctp-draft-0.4.md. "Binding" points to spec/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