Specification
About the specDraft 0.5v0.3 bindingAmbiguitiesSpec changelogRCTP v0.3 binding (Starter profile of Draft 0.5)
Capability version: 2026-11-draft5 · Release: v0.3.0-draft.5 · UCP release: v2026-08-25 (UCP Ask at commit
f2626c2)
Draft 0.5 says what an enforcement point must decide. This document fixes the wire details v0.3
needs so that a marketplace gateway, the reference Go gateway, the TypeScript and Python SDKs, @rctp/mcp, the sandbox
and the conformance runner interoperate, over REST and over MCP (section 13). It replaces
v0.2-binding.md, which stays as the record of v0.2. Where the draft is silent, the choice and its
reason are logged in AMBIGUITIES.md (rows B-n), always the more restrictive reading.
Key words follow BCP 14 as in the draft. Section numbers in brackets refer to Draft 0.5. Schemas are in
schemas/; every body below names its schema.
What changed from v0.2: authentication as UCP does it, recorded on every entry (2); grants key-bound only when
signed (3.3); tiers from the marketplace's own policy (3.1, 6); proposed terms pre-filled on the seller's confirmation
(4.2); no counter term and no counter on below_floor (4.3, 6); offer.counter optional (5.1); the offer state
awaiting_approval; dev.ucp.common.identity_linking at the UCP release version (10); the MCP binding (13).
1. Keys and UCP profiles [4.1]
- Agents, operators and gateways sign with ECDSA P-256 (
ES256;ecdsa-p256-sha256in RFC 9421), the UCP baseline. ECDSA signatures use rawr||sencoding (RFC 9421 §3.3.1), as UCP requires. - Every party publishes a UCP profile (schema
ucp-profile) athttps://<host>/.well-known/ucp, with its public keys inkeys[]. - RCTP requires each key's
kidto be its RFC 7638 JWK SHA-256 thumbprint, base64url without padding (B-1). The RFC 9421keyidis thatkid. A key whosekidis not its thumbprint is unusable. - An agent without a UCP profile of its own (a plugin on an agent host) uses its developer's profile as operator, or a
profile the marketplace hosts for its registered OAuth client [4.1(5)]. A profile used only with OAuth needs no
keys[]entry for request signing. - An operator whose agents call an enforcement point MUST list
dev.rctp.operatorinucp.capabilitieswithconfig=legal_entity,jurisdiction,incident_contact,incident_sla_hours, and MAY addcertifications,data_residency,key_custody[4.1(2)]. An operator that wants webhooks listsdev.rctp.dealwithconfig.webhook_url(section 9). - Reference gateway policy: operators are onboarded by profile URL in gateway configuration. The gateway caches a profile for at most 5 minutes.
- Private keys never appear in any profile, record, log or example.
2. Authentication [4.1]
Every agent request authenticates by one mechanism UCP allows, chosen from those the marketplace accepts. Whatever the
mechanism, the request carries UCP-Agent: profile="<operator profile URL>" (an OAuth request MAY omit it, 2.2), every
POST carries Idempotency-Key (below), and the gateway confirms that the authenticated principal may act for that
profile (UCP identity binding).
authenticated_by |
The request carries | Identity binding |
|---|---|---|
http_signature (RECOMMENDED) |
RFC 9421 signature (2.1) | keyid is in the profile's keys[] |
oauth |
Authorization: Bearer <access token>, unsigned (2.2) |
The token's client_id is registered to the profile |
api_key |
The marketplace's API key header, unsigned | The key is registered to the profile |
mtls |
A client certificate (RFC 8705) | The certificate is registered to the profile |
A request with no mechanism the gateway accepts is refused 401: signature_invalid where the gateway requires
signatures; otherwise grant_invalid on seller routes and operator_unknown on buyer routes (S1). A request that
carries a signature is verified as a signed request, whatever else it carries.
Reference gateway policy: v0.3.0-draft.5 accepts http_signature and oauth; each is a deployment setting.
API keys and mTLS are left to marketplaces.
2.1 Signed requests
A signed request follows UCP Message Signatures (REST binding, RFC 9421), with the RCTP additions marked RCTP.
| Item | Value |
|---|---|
| Identity header | UCP-Agent: profile="<operator profile URL>" (RFC 8941 Dictionary) |
| Signature label | sig1 |
keyid |
kid of a key in the operator profile's keys[] (its thumbprint) |
created, expires |
RCTP: REQUIRED; expires - created ≤ 300 s; created within ±60 s of the verifier's clock (B-2) |
Idempotency-Key |
REQUIRED on every POST; at least 128 bits of entropy |
| Covered components | "@method" "@authority" "@path" "ucp-agent"; "@query" when the target has a query; "idempotency-key" on POST; "content-digest" and "content-type" when there is a body; RCTP: "authorization" when that header is present |
| Body digest | Content-Digest per RFC 9530 with sha-256, over the raw body bytes |
The verifier checks, in this order, and refuses with 401 signature_invalid unless a row says otherwise:
Signature,Signature-Input(labelsig1) andUCP-Agentare present.UCP-Agentnames an onboarded profile that resolves and validates → elseoperator_unknown.keyidis akidin that profile'skeys[]→ elseoperator_unknown.- The signature verifies; every required component is covered;
createdandexpireshold;Content-Digestmatches the body.
2.2 OAuth without a signature
- The seller agent sends the grant's access token (section 3) as
Authorization: Bearer. The token MUST NOT be key-bound: a token withcnf.jktis usable only on a signed request, and is refused 401grant_invalidwithout one. - The gateway introspects the token (3.3). The marketplace registers one operator profile for each OAuth client. A
request without
UCP-Agentacts for the profile registered for the token'sclient_id(B-13). A request withUCP-AgentMUST name that same profile; else 401operator_unknown(V35). - An operator without a profile of its own accepts the duties of Draft 0.5 4.1(2) and 4.1(4) in its client
registration. The registration carries what
dev.rctp.operatorwould: legal entity, jurisdiction, incident contact and SLA. - Bearer-token controls: access tokens live at most 900 s, refresh tokens rotate on every use, and the grant is introspected on every call (3.2, 3.3).
- A buyer agent with no grant authenticates with an OAuth client-credentials token from the marketplace's authorization server, checked the same way.
- The record proves "OAuth client X, acting for profile Y", not each request's content [4.1(7)].
2.3 Recording the mechanism
Every deal record entry the gateway writes for an agent call, decisions and refusals included, carries
authenticated_by and agent_profile and, for OAuth and API-key calls, client_id (schema
attestation-entry) [4.1(6)]. A signed call needs no client_id: its
keyid is the key in the profile, and its token is key-bound (B-11).
2.4 Idempotency
Idempotency (UCP replay protection). The gateway stores each POST's Idempotency-Key with the request digest and
the response for 48 hours, per operator profile. A repeat with the same key and digest gets the stored response without
re-executing. A repeat with the same key and a different digest gets 409 {"error":"idempotency_conflict"}. If the
store is unavailable, the gateway answers 503 and executes nothing.
3. Agent Grant [5]
3.1 Discovery
The marketplace's gateway serves OAuth protected-resource metadata (RFC 9728) at
/.well-known/oauth-protected-resource naming its authorization server. The authorization server serves RFC 8414
metadata at /.well-known/oauth-authorization-server, as UCP Identity Linking requires (issuer matched byte for byte).
RCTP scopes are declared, as every UCP scope is, in the config.scopes of the marketplace's
dev.ucp.common.identity_linking capability entry. Each scope's policy object carries the marketplace's own
default_tier and allowed_tiers [5.2(2), 8.4]; the registry (actions) holds the
reference defaults. A scope the marketplace does not offer, such as dev.rctp.deal:offer_counter under floor defence
only, is left out:
"dev.rctp.deal:offer_accept": { "default_tier": "ask", "allowed_tiers": ["ask", "auto"] }
3.2 Issuance [5.2]
- Authorization code grant with PKCE (
S256). The authorization request carries RFC 9396authorization_detailswith one object of typedev.rctp.agent_grant:{"type", "agent_id", "jkt", "tiers": {"<scope>": "<tier>"}}, wherejktis the thumbprint of the agent key that will sign calls. An agent that will not sign omitsjktand receives an unbound token (2.2). The requested scopes are the keys oftiers. - The consent page shows the agent, its operator, each verb with its tier, and the grant expiry. It asks for no prices.
The seller may lower a tier within the scope's
allowed_tiers; the grant records what the seller confirmed. - The token response carries the access token (opaque),
token_typeBearer,expires_in≤ 900 s, a refresh token, and the grantedauthorization_details. Refresh tokens rotate on every use; reuse of a rotated refresh token revokes the grant. - A grant expires at most 90 days after issue [5.2(4)]; refresh never extends it past that.
3.3 Use on every call [5.3]
- The seller agent sends
Authorization: Bearer <access token>. On a signed request the signature MUST coverauthorizationand the token MUST be key-bound. - The gateway introspects the token on every seller-side call (RFC 7662; the reference gateway authenticates with
client_secret_basic), never caching the answer (INV-7). The response (schemagrant-introspection) carriesactive,scope,sub(the seller's pairwise pseudonym),exp,cnf.jkt,grant_statusand thedev.rctp.agent_grantauthorization details. - Checks and refusals, in order:
| Check | Refusal |
|---|---|
Authorization present (and covered by the signature, when signed) |
401 grant_invalid |
| Token known to the authorization server | 401 grant_invalid |
Signed: cnf.jkt present and equal to the request keyid. Unsigned: no cnf.jkt |
401 grant_invalid |
grant_status is not revoked |
403 grant_revoked |
grant_status is not expired (grant past exp) |
403 grant_expired |
active (the access token itself is current; else refresh and retry) |
401 grant_invalid |
| The verb's scope is granted | 403 action_forbidden |
- RFC 7662 lets an inactive token answer only
active: false. RCTP's authorization server addsgrant_statusfor tokens it issued, to the authenticated gateway only, so revoked and expired grants get their own reasons (B-3).
3.4 Revocation [5.4]
The seller revokes on a marketplace screen; operators revoke their own tokens (RFC 7009). Because every call is introspected, a revoked or expired grant fails the next call, under every mechanism (INV-7, V6).
4. Listing Terms [6]
4.1 Term sets [6.2]
A term set (schema term-set, registry term-sets)
lists its terms with type, visibility, whether required, and constraints from the closed set min, max, gt_term,
lt_term, lte_list_price, min_ratio_of_list_price. The gateway advertises the term sets it enforces in
config.term_sets of its dev.rctp.listing entry.
A term set lists its terms with required meaning "has a value at confirmation" [6.2(2)]: the hosted page fills each
public term from the marketplace's category defaults, and the seller changes them only if they want to.
4.2 Setting terms on the hosted page [6.1]
- The seller agent calls
POST /listings(schemalisting-create-request, which names theterm_set). It MAY carryproposed_terms: what the seller told it, such as the floor. Under the reference default tier the answer is 202ask. Where the marketplace allowsauto, the answer is 201 with a listing that has no terms; the agent then callslisting.set_terms[6.1(5)]. - The seller opens
hosted_url.GET /approve/{id}/request(the seller's browser only) returns the approval request, the term-set definition andprefill: the proposed terms merged over the category defaults. The page builds the form from them, every value editable [6.1(2)]. - The page posts the seller's values to
POST /approve/{id}/terms(schematerms-submission). This route is for the approval page only: it is not agent-signed and is refused for any request carryingUCP-Agent. The gateway checks the constraints (422terms_invalidbefore the seller can approve, [6.2]), draws a 128-bit salt and returnsterms_commitmentand the WebAuthn options whose challenge binds it. - The seller approves with their passkey (
POST /approvals/{id}/response, carryingterms_commitment). The gateway stores exactly the terms behind that commitment [6.1(3)]. - The agent learns the outcome from
GET /approvals/{id}or the webhook: the listing, itsterm_setandterms_commitment, never the values, not even the ones it proposed [6.1(4)]. - Proposed terms are held with the pending approval only. They are never enforced, and are discarded when the seller declines or the approval expires [6.1(3)].
terms_commitment = "sha-256:" + hex(SHA-256(JCS({"salt": <base64url salt>, "terms": <terms>}))), where terms is
the listing-terms terms object and JCS is RFC 8785 [6.4(2)].
POST /listings/{id}/terms (listing.set_terms; schema
listing-set-terms-request, optional proposed_terms) starts the
same flow for an existing listing. It is always 202 ask (V31).
4.3 Privacy [6.4]
The private term (floor), proposed or confirmed, never appears in listing data, offers, decisions, approval request
bodies, webhooks, partner-readable logs, deal records or any response to an agent (INV-1, V11). Request bodies that may
carry proposed_terms are the only place an agent sends it. Public terms (available_until, fulfillment_options, payment_terms,
release_condition, deliver_within_days, …) appear in listing data under public_terms.
5. Offers and Deal Terms [7]
5.1 Objects
An offer and a counter are both offer objects, with by = buyer or seller.
Each carries deal_terms (schema deal-terms: price, fulfillment,
payment), negotiation_id, optional supersedes, and expires_at.
| Operation | Caller | Verb | Effect |
|---|---|---|---|
POST /offers |
buyer | offer.create |
New buyer offer. With supersedes naming a counter or the buyer's own open offer, that one becomes superseded |
POST /offers/{id}/counter |
seller | offer.counter |
New seller offer (by = seller, supersedes = {id}); {id} becomes countered. Where the marketplace does not offer dev.rctp.deal:offer_counter, 403 action_forbidden [7.3(5)] |
POST /offers/{id}/counter |
buyer | — | Refused 400; a buyer answers a counter with POST /offers and supersedes [7.3(1)] |
POST /offers/{id}/accept |
seller | offer.accept |
Only on a buyer offer; reserves the listing (5.3) |
POST /offers/{id}/accept |
buyer | — | 403 action_forbidden [8.3] |
POST /offers/{id}/decline |
either party | — | Always allowed for the parties |
States follow offer-state: an offer is awaiting_approval while an ask on it
waits for the seller [7.3].
5.2 Identity, revision and expiry [7.2, 7.3]
negotiation_id(neg_…) is assigned by the gateway on a buyer agent's first offer on a listing and is the same for every later offer and counter between that buyer agent (its key thumbprint, or its OAuthclient_idwhen it does not sign) and that listing. It is random, never derived from the buyer.supersedesmust name an offer in the same negotiation that isopenorcountered; otherwise 422terms_invalid.expires_at: the creator MAY send it; otherwise the gateway setsnow + offer_expiry.default_seconds. The gateway caps it atnow + offer_expiry.max_secondsand at the listing'savailable_until. Both numbers are inconfigof the gateway'sdev.rctp.dealentry (reference defaults: 48 hours and 7 days).- At
expires_atan offer not accepted becomesexpired; any later action on it is 409offer_expired. Anaskon an offer expires no later than the offer.
5.3 Acceptance, reservation and checkout [7.4]
- A completed accept (directly under
auto, or after an approvedask) makes the offeracceptedand gives itcheckout: a compact JWS signed by a key in the gateway profile (typrctp-checkout+jwt), claims percheckout:iss,offer_id,negotiation_id,listing_id,listing_hash,deal_terms,buyer_jkt,nonce,iat,exp(reference default 30 minutes, never pastavailable_untilplus 30 minutes). - The listing becomes
reserved. While it is, every otheroffer.accepton it gets 409listing_reserved; buyer offers are still accepted asopen. Accepting the same offer again gets 409offer_replayed. - The buyer redeems with
POST /deals(schemadeal-create-request), signed by the key whose thumbprint isbuyer_jkt. A checkout is bound to a buyer key [7.4(1)], so redeeming it is always a signed request; a buyer agent that does not sign pays through the marketplace's UCP checkout instead (B-10). First use → 201deal, and the listing becomessold. A later use of the same nonce → 409offer_replayed; afterexp→ 409offer_expired; a different key → 401signature_invalid. - At checkout
expwithout redemption the reservation ends: the listing returns toactiveand the offer toexpired. - Accepted Deal Terms are fixed:
listing.set_termsandlisting.repriceon a reserved listing change future offers only. If the seller withdraws a reserved listing on the marketplace, redemption fails with 403not_in_scopeand the withdrawal is written to the deal record.
5.4 Buyer authority [7.5]
AP2 Checkout Mandate verification (7.5(1), vector V25) is not part of v0.3.0-draft.5: AP2 v0.2 mandates are SD-JWT delegation chains, and no pinned Go library verifies them, so implementing it now would mean custom cryptography (INV-8). Tracked in #59. Until then a buyer agent's authority is its operator's onboarding.
6. Decisions [8]
RCTP-Decision is an RFC 9651 Item: a Token with parameters (schema decision).
| Decision | HTTP | Header | Body |
|---|---|---|---|
| auto | 200 (201 for creates) | auto |
the resource |
| ask | 202 | ask;approval_id="apr_…";expires_in=900;on_timeout=deny |
{"approval_request": {…}} |
| forbid | 401, 403, 409, 422 | forbid;action="offer.accept";reason=below_floor |
error |
| budget exhausted | 429 | forbid;reason=offer_budget_exhausted;retry_after=3600 |
error |
- A
below_floorrefusal carries no private value in the header or the body [8.2(1)]. - Protocol errors that are not decisions (malformed JSON, schema violations, unknown routes, idempotency conflicts) use
400/404/409/503 with
{"error": "invalid_request" | "not_found" | "unsupported_version" | "idempotency_conflict" | "unavailable", "message": "…"}and noRCTP-Decisionheader. - Check order is exactly Draft 0.5 section 8.1: authentication and operator → grant (seller side) → listing →
buyer requirements (buyer side) → terms, Deal Terms and budget → tier. Every decision, refusals included, appends a
deal record entry with
authenticated_byandagent_profile. - Tiers come from the grant's
tiersfor the verb's scope, within theallowed_tiersthe marketplace publishes; otherwise the marketplace'sdefault_tier. A marketplace MAY publish different tiers per mechanism; the reference gateway uses one policy for all. Fixedforbidverbs (deal.sign_contract,account.update) have no scope and are always 403action_forbidden(INV-3). - Offer budget [8.5]: below-floor offers are counted per buyer agent (key thumbprint, or OAuth
client_id) per listing, and per buyer operator per listing, overwindow_hours. Once either count reaches its limit, every offer from that agent or operator on that listing gets 429 until the window frees a slot. The limits are inconfig.offer_budgetof the gateway'sdev.rctp.dealentry (reference: 3 per agent, 10 per operator, 24 hours). - Request bodies are closed schemas: a body field such as
floorortieris a 400, never an override (INV-5). The only exception isproposed_termson the two listing routes, which the seller confirms before anything is stored.
6.1 Which verb each operation evaluates
| Operation | Caller | Verb | Floor | Tier |
|---|---|---|---|---|
POST /listings |
seller | listing.create |
— | grant (reference default ask) |
POST /listings/{id}/terms |
seller | listing.set_terms |
— | always ask |
POST /listings/{id}/price |
seller | listing.reprice |
new price ≥ floor | grant |
POST /listings/{id}/answers |
seller | listing.answer_buyer |
— | grant |
POST /offers |
buyer | offer.create |
yes, plus budget | none: delivered at 201 |
POST /offers/{id}/counter |
seller | offer.counter |
counter price ≥ floor | grant |
POST /offers/{id}/accept |
seller | offer.accept |
offer price ≥ floor, whatever the tier (V1) | grant |
POST /deals/{id}/commit-to-ship |
seller | deal.commit_to_ship |
— | grant |
POST /deals/{id}/contract |
seller | deal.sign_contract |
— | always 403 |
POST /account |
seller | account.update |
— | always 403 |
An offer on a listing without approved terms has no seller agent: it is delivered to the marketplace as an ordinary
offer (decision auto), and a seller agent acting on that listing is refused not_in_scope [6.5].
7. Approvals at level hosted_embedded [9]
As in v0.1 binding section 6, with these changes:
- The 202 body's
approval_request.authorization_details[0](typedev.rctp.approval) addsterm_setand, once the seller has filled the form,terms_commitmentforlisting.createandlisting.set_terms. For those two verbs the challenge is computed afterPOST /approve/{id}/terms, over the details that includeterms_commitment; the approval request in the 202 body carries no challenge yet (B-4). displaynever contains private terms; the terms form, pre-filled, appears only on the hosted page [9.2(2)].approval_responseaddsterms_commitment(required for listing approvals).- On
approve, the gateway re-checks the grant (introspection), the floor and the reservation, then executes the pending action exactly as it was asked. - No valid response by
expires_atis a decline (INV-2); a pending offer becomesexpired. - Seller passkey enrolment stays a reference-gateway feature (v0.1 binding 6.5), with the enrolment ticket issued by the marketplace authorization server.
8. Deal record and events [10]
8.1 Writing events
POST /deals/{id}/events (schema deal-event-post-request) carries
{"event": "<compact JWS>"}, typ rctp-deal-event+jwt, signed by the same key as the request, payload
deal-event.
| Event | Kind | Who may write it | Precondition (else 409 release_condition_unmet) |
|---|---|---|---|
funds_held |
RCTP | the holder of funds (an operator profile configured as such) | none |
shipped, delivered, returned_to_sender |
UCP fulfillment event | carrier (first_hand) or marketplace (relayed) |
funds_held |
accepted |
RCTP | the buyer agent (buyer_jkt) |
funds_held; neither accepted nor rejected yet |
rejected (reason: not_as_described, not_received) |
RCTP | the buyer agent | as accepted; not_received only after deliver_within_days since funds_held |
return |
UCP adjustment | the seller agent, carrier or Custodian | rejected with not_as_described |
released |
RCTP | the holder of funds | funds_held and the release condition (default accepted) |
refund |
UCP adjustment | the holder of funds | rejected + not_as_described + return; or rejected + not_received |
Where the listing's term set sets required_observation for an event, the event counts only with that observation
[10.3(4)]. A relayed event names reported_by and source_event_id [10.4(2)].
8.2 The record
- The enforcement point writes an entry (
attestation-entry) for every decision, including refusals, and for every accepted event, into a hash chain per subject (negotiation, deal or listing).prev_hash=sha-256:<hex>of SHA-256 over the JCS of the previous entry; the first is 64 zeros. Gateway entries carry its JWS over the entry body. - Entries for agent calls carry
authenticated_byandagent_profile(2.3). - Entries carry
terms_commitmentwhere terms apply, never a floor [10.4(1)], and no names, addresses or phone numbers; locations are city-level; evidence is digests [10.4(5)]. GET /deals/{id}/eventsreturns the deal's entries to its parties.
9. Webhooks
UCP signed webhooks (Standard Webhooks headers, RFC 9421 signature), to the config.webhook_url of the dev.rctp.deal
entry in the receiving operator's profile.
| Header | Value |
|---|---|
Webhook-Id |
the event's event_id |
Webhook-Timestamp |
Unix seconds of the event |
UCP-Agent |
profile="<gateway profile URL>" |
Content-Type, Content-Digest |
application/json; RFC 9530 sha-256 |
Signature-Input, Signature |
label sig1, components "@method" "@authority" "@path" "content-digest" "content-type", with keyid, created |
Body: webhook-event {"event_id", "type", "created_at", "data"}, types
approval.decided, offer.updated, deal.event. Receivers verify the signature against the gateway profile's
keys[], check Content-Digest, reject a Webhook-Timestamp more than 300 s old, and drop repeated Webhook-Ids. The
SDKs do this (verifyWebhook / verify_webhook).
10. Discovery and versions
GET /.well-known/ucp returns the gateway profile: UCP version 2026-08-25; capabilities
dev.ucp.common.identity_linking at the UCP release version 2026-08-25, as UCP requires of every dev.ucp.* entry
(RCTP scopes with the marketplace's tiers); dev.rctp.listing (config.term_sets) and dev.rctp.deal
(config.offer_expiry, config.offer_budget), each at version 2026-11-draft5; the services it serves (13.1); and
keys[].
Clients MAY send RCTP-Version: 2026-11-draft5 (an sf-string); an unsupported version is 400 unsupported_version. The
reference gateway v0.3 serves 2026-11-draft5 only; a deployment that must keep 2026-11-draft4 runs v0.2 beside it
for at most 90 days [4.5(3)].
The namespace dev.rctp.* is backed by rctp.dev. Schemas and registries are published at
https://rctp.dev/schemas/ and https://rctp.dev/registries/.
10a. Open values in v0.3
Draft 0.5 4.5(2) makes term names and event types open values. v0.3 narrows both, so that closed schemas can carry them:
- Term names: only the ten names of Draft 0.5 6.2 are accepted (
counteris no longer one). A term set that needs another term waits for a registry addition. - Deal events: an
eventis an RCTP checkpoint, one of the UCP values in section 8.1, or a reverse-domain extension such asexample.carrier:picked_up_sealed. Extension events are recorded as informative and never change a money rule. - Fulfillment and payment options in Listing Terms are lists of
{method_type, option_ids}and payment term IDs, so the gateway can check an offer'sselected_option_idandselected_term_idagainst them.
11. Personal data [15, INV-9]
No RCTP object carries names, addresses, phone numbers or identity numbers. The seller is a pairwise pseudonym (sub
of the grant); a buyer is its agent key thumbprint. Schemas for every object are closed and have no field where
personal data could go.
12. Endpoints (reference gateway)
Binding routes are part of this binding (the conformance suite uses them); reference routes belong to the reference gateway only.
| Method and path | Caller | Decides | Status |
|---|---|---|---|
POST /listings |
seller | listing.create |
spec 12 |
GET /listings/{id} |
any operator | — | reference |
POST /listings/{id}/terms |
seller | listing.set_terms |
spec 12 |
POST /listings/{id}/price |
seller | listing.reprice |
binding |
GET /listings/{id}/questions, POST /listings/{id}/questions |
seller, buyer | — | reference |
POST /listings/{id}/answers |
seller | listing.answer_buyer |
binding |
POST /offers |
buyer | offer.create |
spec 12 |
GET /offers?listing_id= |
seller | — | reference |
GET /offers/{id} |
either party | — | spec 12 |
POST /offers/{id}/counter, /accept, /decline |
parties | offer.counter, offer.accept |
spec 12 |
GET /approvals/{id} |
operator that asked | — | spec 12 |
POST /approvals/{id}/response |
approval page only | — | spec 12 |
GET /approve/{id}, /approve/{id}/request; POST /approve/{id}/terms, /approve/{id}/options |
the seller's browser | terms_invalid |
binding |
POST /deals |
buyer (checkout key) | single use | spec 12 |
GET /deals/{id} |
parties | — | reference |
POST /deals/{id}/events, GET /deals/{id}/events |
parties, holder of funds, carrier | money rules | spec 12 |
POST /deals/{id}/commit-to-ship |
seller | deal.commit_to_ship |
binding |
POST /deals/{id}/contract |
seller | deal.sign_contract → always 403 |
binding (INV-3) |
POST /account |
seller | account.update → always 403 |
binding (INV-3) |
GET /enroll, POST /enroll/options, POST /enroll/complete |
the seller's browser | — | reference |
GET /.well-known/ucp, GET /.well-known/oauth-protected-resource |
anyone | — | binding |
POST /mcp |
agents | every verb, as tools (section 13) | binding |
13. MCP binding
A marketplace MAY expose RCTP over UCP's MCP transport, unchanged, beside or instead of REST. Each tool is one REST
operation of section 12 with the same body schema, the same checks in the same order, the same decisions and the same
deal record entries. An agent needs nothing RCTP-specific to call it beyond an MCP client; @rctp/mcp is one such
client (#85).
13.1 Discovery
The gateway profile lists the MCP endpoint in ucp.services, as UCP lists every transport:
"services": {
"dev.rctp": [
{ "transport": "rest", "endpoint": "https://gateway.marketplace.example/" },
{ "transport": "mcp", "endpoint": "https://gateway.marketplace.example/mcp" }
]
}
13.2 Requests
- Streamable HTTP, JSON-RPC 2.0, method
tools/call. Authentication is on the HTTP POST that carries the JSON-RPC body, exactly as section 2: a signed POST covers"@method""@authority""@path""ucp-agent""content-digest""content-type", and"authorization"when present; an OAuth POST carries the bearer token. arguments.meta["ucp-agent"]is{"profile": "<operator profile URL>"}. When present it MUST equal theUCP-Agentheader and, on an OAuth call, the client's registered profile; else 401operator_unknown. An OAuth call MAY omit both: a plugin on an agent host cannot add them, and the gateway uses the client's registered profile (2.2, B-13). Tools that write requirearguments.meta["idempotency-key"], handled asIdempotency-Key(2.4).- The other arguments are the REST body, plus the path identifiers (
listing_id,offer_id,deal_id,approval_id) as named members.
13.3 Tools
| Tool | REST operation | Verb |
|---|---|---|
create_listing |
POST /listings |
listing.create |
set_listing_terms |
POST /listings/{id}/terms |
listing.set_terms |
reprice_listing |
POST /listings/{id}/price |
listing.reprice |
answer_buyer |
POST /listings/{id}/answers |
listing.answer_buyer |
create_offer |
POST /offers |
offer.create |
counter_offer |
POST /offers/{id}/counter |
offer.counter (only where offered) |
accept_offer |
POST /offers/{id}/accept |
offer.accept |
decline_offer |
POST /offers/{id}/decline |
— |
get_offer |
GET /offers/{id} |
— |
get_approval |
GET /approvals/{id} |
— |
create_deal |
POST /deals |
redeem a checkout |
commit_to_ship |
POST /deals/{id}/commit-to-ship |
deal.commit_to_ship |
post_deal_event |
POST /deals/{id}/events |
money rules |
get_deal_events |
GET /deals/{id}/events |
— |
tools/list returns only the tools the caller may use: a marketplace without dev.rctp.deal:offer_counter omits
counter_offer. Fixed forbid verbs have no tool (INV-3).
13.4 Results and errors
autoandask: a successful result.structuredContentis{"decision": <decision>, "result": <REST body>}, withdecisionper schemadecision, so the agent's model reads the decision without seeing headers.contentcarries the same JSON as text. Forask,resultis{"approval_request": {…}}and the agent pollsget_approvalwithdecision.approval_id(or receives the webhook) [9].forbid: a JSON-RPC error-32000, witherror.data={"code": <reason>, "decision": <decision>}plus the members of the RESTerrorbody (retry_after,offer_id,message). Abelow_floorerror carries no private value (INV-1).- Protocol errors (section 6): JSON-RPC error
-32000witherror.data.code= the protocol error (invalid_request,not_found,unsupported_version,idempotency_conflict,unavailable) and nodecision.
The OpenRPC document and per-tool input and output schemas are published at https://rctp.dev/schemas/mcp.openrpc.json
(#82); they are generated from the REST schemas, so the two transports
cannot drift.
Source: spec/v0.3-binding.md