RPP402-003: Commerce Session
| RFC | RPP402-003 |
| Title | Commerce Session |
| Status | Draft |
| Version | 1.0.0 |
| Requires | RPP402-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
| Term | Definition |
|---|---|
| Session | The sess_ object defined below. |
| Leg | One Quote attached to a Session, representing one Service/Capability purchase within it. |
| State machine | The fixed sequence of statuses a Session moves through; see below. |
Protocol
State machine
created → quoting → intent_authorized → settling → settled → receipt_issued
↘ failed
↘ expired ↘ cancelled| Status | Meaning |
|---|---|
created | Session exists, no legs yet. |
quoting | ≥1 leg attached; agent may still add more legs or remove unsettled ones. |
intent_authorized | A Payment Intent (RPP402-004) covering all current legs has been authorized. Legs are now frozen -no further additions. |
settling | Settlement (RPP402-005) submitted onchain, not yet confirmed. |
settled | Settlement confirmed on Robinhood Chain. |
receipt_issued | Receipt (RPP402-006) issued. Terminal, successful state. |
expired | Session's expires_at passed before reaching intent_authorized. Terminal. |
cancelled | Agent explicitly cancelled before settling. Terminal. |
failed | Settlement 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
| Method | Path | Purpose |
|---|---|---|
POST | {endpoints.session} | Create a Session. |
POST | {endpoints.session}/{id}/legs | Attach 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}/cancel | Cancel. Only valid before settling. |
POST | {endpoints.session}/{id}/intent | Authorize a Payment Intent covering the session's current legs -the {intent-endpoint} referenced by RPP402-004. |
GET | {endpoints.session}/{id}/settlement | Read the Settlement once one exists -see RPP402-005; Settlement itself is initiated automatically, this is a read path, not a trigger. |
Session object:
| Field | Type | Description |
|---|---|---|
rpp402_version | string | "1.0". |
id | string | sess_ prefixed. |
agent_wallet | string | The agent's wallet address; must match every leg's agent_wallet at Quote time. |
status | string | One of the state-machine values above. |
legs | array<Leg> | |
intent_id | string | null | Set once intent_authorized. |
settlement_id | string | null | Set once settling. |
receipt_id | string | null | Set once receipt_issued. |
created_at | string (RFC 3339) | |
expires_at | string (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:
{ "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
{
"$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:
POST {endpoints.session}
{ "agent_wallet": "0xaaaa...1234" }{ "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" }POST {endpoints.session}/sess_3fd8c2/legs
{ "quote_id": "quote_8f2e9a1c" }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'sagent_wallet-a Session cannot absorb a Quote issued to a different agent. - Once
statusisintent_authorized, the leg set is immutable; a Service MUST reject furtherPOST .../legscalls rather than silently accepting them, since the Payment Intent's signed total (RPP402-004) would no longer match. - A Session's
expires_atis enforced the same way a Quote's is -server-side, not just advisory.
Errors
type | status | Meaning |
|---|---|---|
https://rpp402.com/errors/session-not-found | 404 | |
https://rpp402.com/errors/session-locked | 409 | Attempted to modify legs after intent_authorized. |
https://rpp402.com/errors/session-expired | 410 | |
https://rpp402.com/errors/leg-wallet-mismatch | 422 | Quote'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_authorizedrequires a new Session, there is no amend-in-place. - When a Session's
expires_atis close, prefer authorizing the Intent over adding one more leg that would tighten it further. - Poll
GET {endpoints.session}/{id}for state changes duringsettling, or better, use the Playground/SDK's event stream once available (Phase 9) rather than tight-polling a Service's endpoint.
Reference Implementation
Schema: -packages/protocolcommerce-session.schema.json (Phase 4, this phase). Client: rpp.session.create() / session.quote() in @rpp402/sdk (Phase 5, matching the shape in the original brief).
Previous: RPP402-002 -Quote · Next: RPP402-004 -Payment Intent
