RPP402-003Draft

RPP402-003: Commerce Session

RFCRPP402-003
TitleCommerce Session
StatusDraft
Version1.0.0
RequiresRPP402-002

Overview

A Commerce Session is the atomic container that holds a purchase from the moment an agent commits to one or more Quotes through Payment Intent, Settlement, and Receipt. It is the primitive that makes RPP402 structurally different from a bare 402 challenge/response cycle: a Session can hold multiple legs, each a Quote from a (possibly different) Service, and moves through one shared state machine until every leg is settled together.

Motivation

Restated from RPP402-000: an agent's real purchase is rarely one resource. A Commerce Session is where that multi-leg shape actually lives in the protocol -everything before it (Discovery, Quote) is about finding and pricing options; everything after it (Payment Intent, Settlement, Receipt) is about executing and proving a decision the Session already holds.

Problem

Without a session boundary, an agent buying from three Services in one logical purchase has three independent, uncorrelated transactions with no shared identity, no shared expiry, and no way for a downstream system (an accounting tool, an audit log, the agent's own memory) to know they were one decision. RPP402-003 gives that purchase one sess_ id that every later primitive (Intent, Settlement, Receipt) references.

Terminology

TermDefinition
SessionThe sess_ object defined below.
LegOne Quote attached to a Session, representing one Service/Capability purchase within it.
State machineThe fixed sequence of statuses a Session moves through; see below.

Protocol

State machine

created → quoting → intent_authorized → settling → settled → receipt_issued
                                    ↘ failed
             ↘ expired          ↘ cancelled
StatusMeaning
createdSession exists, no legs yet.
quoting≥1 leg attached; agent may still add more legs or remove unsettled ones.
intent_authorizedA Payment Intent (RPP402-004) covering all current legs has been authorized. Legs are now frozen -no further additions.
settlingSettlement (RPP402-005) submitted onchain, not yet confirmed.
settledSettlement confirmed on Robinhood Chain.
receipt_issuedReceipt (RPP402-006) issued. Terminal, successful state.
expiredSession's expires_at passed before reaching intent_authorized. Terminal.
cancelledAgent explicitly cancelled before settling. Terminal.
failedSettlement was attempted and failed. Terminal -see RPP402-005 §Errors for retry guidance; a new Session is required to retry, sessions are not reopened.

Session host

A Session is created against exactly one Service's endpoints.session -that Service is the session host for the purchase's lifecycle, and its endpoints.session and derived sub-resources (.../legs, .../intent, .../settlement) are authoritative regardless of which Service each individual leg is with. A host does not renegotiate a leg's terms; it is bound by the referenced Quote object (RPP402-002) as issued by that leg's own Service. Choosing a session host is an agent-side or SDK-side decision (typically: the Service supplying the first, or the largest, leg) -RPP402 does not mandate a specific choice in v1.

Endpoints

MethodPathPurpose
POST{endpoints.session}Create a Session.
POST{endpoints.session}/{id}/legsAttach a Quote as a leg. Only valid while status is created or quoting.
DELETE{endpoints.session}/{id}/legs/{quote_id}Remove an unsettled leg.
GET{endpoints.session}/{id}Read current state.
POST{endpoints.session}/{id}/cancelCancel. Only valid before settling.
POST{endpoints.session}/{id}/intentAuthorize a Payment Intent covering the session's current legs -the {intent-endpoint} referenced by RPP402-004.
GET{endpoints.session}/{id}/settlementRead the Settlement once one exists -see RPP402-005; Settlement itself is initiated automatically, this is a read path, not a trigger.

Session object:

FieldTypeDescription
rpp402_versionstring"1.0".
idstringsess_ prefixed.
agent_walletstringThe agent's wallet address; must match every leg's agent_wallet at Quote time.
statusstringOne of the state-machine values above.
legsarray<Leg>
intent_idstring | nullSet once intent_authorized.
settlement_idstring | nullSet once settling.
receipt_idstring | nullSet once receipt_issued.
created_atstring (RFC 3339)
expires_atstring (RFC 3339)Defaults to the soonest expires_at across attached Quotes; adding a leg with an earlier expiry pulls the session's own expiry in.

Leg object:

json
{ "quote_id": "quote_8f2e9a1c", "service_id": "svc_marketdata01", "capability": "market-data.quote", "price": { "asset": { "...": "AssetRef" }, "amount": "0.002" } }

A Session's total is the sum of each leg's price.amount grouped by asset -a Session with legs priced in two different assets produces one Payment Intent per asset (see RPP402-004).

JSON Schema

json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://rpp402.com/schemas/v1/commerce-session.json",
  "title": "RPP402 Commerce Session",
  "type": "object",
  "required": ["rpp402_version", "id", "agent_wallet", "status", "legs", "created_at", "expires_at"],
  "properties": {
    "rpp402_version": { "type": "string", "const": "1.0" },
    "id": { "type": "string", "pattern": "^sess_[a-zA-Z0-9]{8,}$" },
    "agent_wallet": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$" },
    "status": {
      "type": "string",
      "enum": ["created", "quoting", "intent_authorized", "settling", "settled", "receipt_issued", "expired", "cancelled", "failed"]
    },
    "legs": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["quote_id", "service_id", "capability", "price"],
        "properties": {
          "quote_id": { "type": "string", "pattern": "^quote_[a-zA-Z0-9]{8,}$" },
          "service_id": { "type": "string", "pattern": "^svc_[a-zA-Z0-9]{8,}$" },
          "capability": { "type": "string" },
          "price": {
            "type": "object",
            "required": ["asset", "amount"],
            "properties": {
              "asset": { "$ref": "https://rpp402.com/schemas/v1/asset-ref.json" },
              "amount": { "type": "string", "pattern": "^[0-9]+(\\.[0-9]+)?$" }
            }
          }
        }
      }
    },
    "intent_id": { "type": ["string", "null"] },
    "settlement_id": { "type": ["string", "null"] },
    "receipt_id": { "type": ["string", "null"] },
    "created_at": { "type": "string", "format": "date-time" },
    "expires_at": { "type": "string", "format": "date-time" }
  }
}

Examples

Creating a session and attaching two legs from two different Services:

json
POST {endpoints.session}
{ "agent_wallet": "0xaaaa...1234" }
json
{ "rpp402_version": "1.0", "id": "sess_3fd8c2", "agent_wallet": "0xaaaa...1234", "status": "created", "legs": [], "intent_id": null, "settlement_id": null, "receipt_id": null, "created_at": "2026-07-10T18:22:10Z", "expires_at": "2026-07-10T19:22:10Z" }
json
POST {endpoints.session}/sess_3fd8c2/legs
{ "quote_id": "quote_8f2e9a1c" }
json
POST {endpoints.session}/sess_3fd8c2/legs
{ "quote_id": "quote_1c77b0" }

GET {endpoints.session}/sess_3fd8c2 now returns status: "quoting" with both legs present, expires_at tightened to whichever attached Quote expires first.

Security

  • Every leg-attach request MUST verify the Quote's agent_wallet (RPP402-002) matches the Session's agent_wallet -a Session cannot absorb a Quote issued to a different agent.
  • Once status is intent_authorized, the leg set is immutable; a Service MUST reject further POST .../legs calls rather than silently accepting them, since the Payment Intent's signed total (RPP402-004) would no longer match.
  • A Session's expires_at is enforced the same way a Quote's is -server-side, not just advisory.

Errors

typestatusMeaning
https://rpp402.com/errors/session-not-found404
https://rpp402.com/errors/session-locked409Attempted to modify legs after intent_authorized.
https://rpp402.com/errors/session-expired410
https://rpp402.com/errors/leg-wallet-mismatch422Quote's agent_wallet doesn't match the Session's.

Best Practices

  • Attach all intended legs before requesting a Payment Intent -adding a leg after intent_authorized requires a new Session, there is no amend-in-place.
  • When a Session's expires_at is close, prefer authorizing the Intent over adding one more leg that would tighten it further.
  • Poll GET {endpoints.session}/{id} for state changes during settling rather than tight-polling a Service's endpoint. The SDK's session.settle() and session.receipt() handle this polling for you, with a configurable interval and timeout.

Reference Implementation

Schema: @rpp402/protocol - commerce-session.schema.json. Client: rpp.session.create() and session.quote() / session.intent() / session.settle() / session.receipt() in @rpp402/sdk.


Previous: RPP402-002 -Quote · Next: RPP402-004 -Payment Intent