carousell/rctp

RCTP 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-sha256 in RFC 9421), the UCP baseline. ECDSA signatures use raw r||s encoding (RFC 9421 §3.3.1), as UCP requires.
  • Every party publishes a UCP profile (schema ucp-profile) at https://<host>/.well-known/ucp, with its public keys in keys[].
  • RCTP requires each key's kid to be its RFC 7638 JWK SHA-256 thumbprint, base64url without padding (B-1). The RFC 9421 keyid is that kid. A key whose kid is 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.operator in ucp.capabilities with config = legal_entity, jurisdiction, incident_contact, incident_sla_hours, and MAY add certifications, data_residency, key_custody [4.1(2)]. An operator that wants webhooks lists dev.rctp.deal with config.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:

  1. Signature, Signature-Input (label sig1) and UCP-Agent are present.
  2. UCP-Agent names an onboarded profile that resolves and validates → else operator_unknown.
  3. keyid is a kid in that profile's keys[] → else operator_unknown.
  4. The signature verifies; every required component is covered; created and expires hold; Content-Digest matches 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 with cnf.jkt is usable only on a signed request, and is refused 401 grant_invalid without one.
  • The gateway introspects the token (3.3). The marketplace registers one operator profile for each OAuth client. A request without UCP-Agent acts for the profile registered for the token's client_id (B-13). A request with UCP-Agent MUST name that same profile; else 401 operator_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.operator would: 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 9396 authorization_details with one object of type dev.rctp.agent_grant: {"type", "agent_id", "jkt", "tiers": {"<scope>": "<tier>"}}, where jkt is the thumbprint of the agent key that will sign calls. An agent that will not sign omits jkt and receives an unbound token (2.2). The requested scopes are the keys of tiers.
  • 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_type Bearer, expires_in ≤ 900 s, a refresh token, and the granted authorization_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 cover authorization and 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 (schema grant-introspection) carries active, scope, sub (the seller's pairwise pseudonym), exp, cnf.jkt, grant_status and the dev.rctp.agent_grant authorization 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 adds grant_status for 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]

  1. The seller agent calls POST /listings (schema listing-create-request, which names the term_set). It MAY carry proposed_terms: what the seller told it, such as the floor. Under the reference default tier the answer is 202 ask. Where the marketplace allows auto, the answer is 201 with a listing that has no terms; the agent then calls listing.set_terms [6.1(5)].
  2. The seller opens hosted_url. GET /approve/{id}/request (the seller's browser only) returns the approval request, the term-set definition and prefill: the proposed terms merged over the category defaults. The page builds the form from them, every value editable [6.1(2)].
  3. The page posts the seller's values to POST /approve/{id}/terms (schema terms-submission). This route is for the approval page only: it is not agent-signed and is refused for any request carrying UCP-Agent. The gateway checks the constraints (422 terms_invalid before the seller can approve, [6.2]), draws a 128-bit salt and returns terms_commitment and the WebAuthn options whose challenge binds it.
  4. The seller approves with their passkey (POST /approvals/{id}/response, carrying terms_commitment). The gateway stores exactly the terms behind that commitment [6.1(3)].
  5. The agent learns the outcome from GET /approvals/{id} or the webhook: the listing, its term_set and terms_commitment, never the values, not even the ones it proposed [6.1(4)].
  6. 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 OAuth client_id when it does not sign) and that listing. It is random, never derived from the buyer.
  • supersedes must name an offer in the same negotiation that is open or countered; otherwise 422 terms_invalid.
  • expires_at: the creator MAY send it; otherwise the gateway sets now + offer_expiry.default_seconds. The gateway caps it at now + offer_expiry.max_seconds and at the listing's available_until. Both numbers are in config of the gateway's dev.rctp.deal entry (reference defaults: 48 hours and 7 days).
  • At expires_at an offer not accepted becomes expired; any later action on it is 409 offer_expired. An ask on 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 approved ask) makes the offer accepted and gives it checkout: a compact JWS signed by a key in the gateway profile (typ rctp-checkout+jwt), claims per checkout: iss, offer_id, negotiation_id, listing_id, listing_hash, deal_terms, buyer_jkt, nonce, iat, exp (reference default 30 minutes, never past available_until plus 30 minutes).
  • The listing becomes reserved. While it is, every other offer.accept on it gets 409 listing_reserved; buyer offers are still accepted as open. Accepting the same offer again gets 409 offer_replayed.
  • The buyer redeems with POST /deals (schema deal-create-request), signed by the key whose thumbprint is buyer_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 → 201 deal, and the listing becomes sold. A later use of the same nonce → 409 offer_replayed; after exp → 409 offer_expired; a different key → 401 signature_invalid.
  • At checkout exp without redemption the reservation ends: the listing returns to active and the offer to expired.
  • Accepted Deal Terms are fixed: listing.set_terms and listing.reprice on a reserved listing change future offers only. If the seller withdraws a reserved listing on the marketplace, redemption fails with 403 not_in_scope and 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_floor refusal 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 no RCTP-Decision header.
  • 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_by and agent_profile.
  • Tiers come from the grant's tiers for the verb's scope, within the allowed_tiers the marketplace publishes; otherwise the marketplace's default_tier. A marketplace MAY publish different tiers per mechanism; the reference gateway uses one policy for all. Fixed forbid verbs (deal.sign_contract, account.update) have no scope and are always 403 action_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, over window_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 in config.offer_budget of the gateway's dev.rctp.deal entry (reference: 3 per agent, 10 per operator, 24 hours).
  • Request bodies are closed schemas: a body field such as floor or tier is a 400, never an override (INV-5). The only exception is proposed_terms on 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] (type dev.rctp.approval) adds term_set and, once the seller has filled the form, terms_commitment for listing.create and listing.set_terms. For those two verbs the challenge is computed after POST /approve/{id}/terms, over the details that include terms_commitment; the approval request in the 202 body carries no challenge yet (B-4).
  • display never contains private terms; the terms form, pre-filled, appears only on the hosted page [9.2(2)].
  • approval_response adds terms_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_at is a decline (INV-2); a pending offer becomes expired.
  • 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_by and agent_profile (2.3).
  • Entries carry terms_commitment where 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}/events returns 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 (counter is no longer one). A term set that needs another term waits for a registry addition.
  • Deal events: an event is an RCTP checkpoint, one of the UCP values in section 8.1, or a reverse-domain extension such as example.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's selected_option_id and selected_term_id against 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 the UCP-Agent header and, on an OAuth call, the client's registered profile; else 401 operator_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 require arguments.meta["idempotency-key"], handled as Idempotency-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

  • auto and ask: a successful result. structuredContent is {"decision": <decision>, "result": <REST body>}, with decision per schema decision, so the agent's model reads the decision without seeing headers. content carries the same JSON as text. For ask, result is {"approval_request": {…}} and the agent polls get_approval with decision.approval_id (or receives the webhook) [9].
  • forbid: a JSON-RPC error -32000, with error.data = {"code": <reason>, "decision": <decision>} plus the members of the REST error body (retry_after, offer_id, message). A below_floor error carries no private value (INV-1).
  • Protocol errors (section 6): JSON-RPC error -32000 with error.data.code = the protocol error (invalid_request, not_found, unsupported_version, idempotency_conflict, unavailable) and no decision.

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