# Verifiable Settlement Receipt (VSR) v0.4

**Status:** Draft
**Profile:** `vsr/v0.4/sha3-256`
**Date:** 2026-07-31
**Editor:** Echelon Intelligence Group LLC (Rubric Protocol)

---

## 1. Purpose

A machine buying a service over HTTP 402 gets a payment confirmation from the
settlement rail and a response body from the seller. Neither, alone, evidences
the transaction: the rail knows an amount moved between addresses but not what
was bought, and the response body is unattributable once it leaves the wire.

A Verifiable Settlement Receipt binds the two. It commits to the request, the
response, the payment, and the time of service in a single signed object that
the buyer holds. Verification requires the receipt and nothing else: the signing
public key travels inside it, and the ledger anchor is resolvable from a public
mirror node.

This document specifies the receipt structure, the hash constructions, the
signature block, and the anchor lifecycle, so that a buyer can implement a
verifier without access to the seller's systems.

## 2. Terminology

MUST, MUST NOT, SHOULD and MAY are to be interpreted per RFC 2119.

- **Receipt core** — the receipt object without its `anchor` member. The
  payload hash and the signature both cover the core.
- **Payload hash** — `sha3-256` over the canonicalized receipt core, domain
  tagged. Identifies the receipt in the anchor index.
- **Anchor** — a Hedera Consensus Service message committing to the payload
  hash, reachable through a public mirror node.

## 3. Receipt structure

    {
      "vsrVersion": "0.4-draft",
      "receiptId":  "<uuid>",
      "hashProfile": "vsr/v0.4/sha3-256",
      "payment": {
        "rail": "x402",
        "railVersion": "2",
        "network": "eip155:8453",
        "asset": "<token contract>",
        "amount": "<atomic units, string>",
        "amountExponent": 6,
        "payer": "<address>",
        "payee": "<address>",
        "reference": { "type": "eip3009-authorization", "value": "<tx>" },
        "settledAt": "<RFC 3339>"
      },
      "delivery": {
        "resource": "<absolute URL>",
        "method": "POST",
        "requestHash":  "sha3-256:<hex>",
        "responseHash": "sha3-256:<hex>",
        "responseBytes": <integer>,
        "status": 200,
        "servedAt": "<RFC 3339>",
        "commitmentScheme": "direct",
        "excludedMembers": ["vsr"]
      },
      "anchor": { ... },
      "signature": { ... }
    }

`amount` MUST be a string in atomic units. `amountExponent` gives the decimal
places, so a verifier never parses a float.

`commitmentScheme` is `direct` when `responseHash` covers a single response
body. Batch endpoints use `merkle`, with the per-item construction of section 6.

## 4. Canonicalization

Objects are serialized as an RFC 8785 (JCS) subset:

1. Object keys sorted lexicographically, recursively.
2. No whitespace between tokens.
3. Members whose value is `undefined` are omitted.
4. `null`, `true` and `false` lowercase.
5. Array order preserved.
6. Output encoded UTF-8.

Implementers should note that this profile does not exercise RFC 8785's
number-formatting rules. Receipt fields are strings, small integers and
booleans; monetary values are strings by construction. Producers MUST NOT place
floating-point values in a receipt.

## 5. Hash constructions

All hashes are `sha3-256` over a single domain-tag byte followed by the payload,
rendered as `sha3-256:` plus lowercase hex.

    hash(tag, payload) = "sha3-256:" || hex( SHA3-256( tag || payload ) )

Domain tags:

| Tag | Value | Covers |
|---|---|---|
| LEAF | 0x00 | Merkle leaf (batch receipts) |
| NODE | 0x01 | Merkle internal node |
| REQ | 0x02 | Request body |
| RESP | 0x03 | Response body |
| RCPT | 0x04 | Receipt core |

**requestHash** = `hash(0x02, JCS(request body))`. Where the request has no
body, the payload is empty and the hash covers the tag alone.

**responseHash** = `hash(0x03, JCS(response object with the members named in
delivery.excludedMembers removed))`. In practice `vsr` is excluded, since the
receipt cannot commit to a body containing itself.

**payloadHash** = `hash(0x04, JCS(receipt core))`, where the core is the receipt
without its `anchor` member. Excluding the anchor avoids the circularity of
hashing an object that would contain its own hash, and allows the sequence
number and consensus timestamp to be assigned after issuance.

## 6. Batch receipts

Where one payment covers several delivered items, `commitmentScheme` is
`merkle` and `responseHash` carries an RFC 6962 root over the canonicalized
items:

    leaf(i) = SHA3-256( 0x00 || JCS(item_i) )
    node    = SHA3-256( 0x01 || left || right )

The tree is built bottom-up. An odd node at any level is **promoted**
unchanged to the next level; it MUST NOT be duplicated and paired with itself.
This is equivalent to RFC 6962's recursive split.

## 7. Signature

    "signature": {
      "algorithm": "ML-DSA-65",
      "standard": "FIPS-204",
      "scheme": "single",
      "signedPayloadHash": "sha3-256:<hex>",
      "canonicalization": "attestor JCS over the receipt object",
      "signatures": [{
        "signer": "<oracle id>",
        "publicKeyId": "<key id>",
        "publicKey": "<base64>",
        "value": "<hex>"
      }]
    }

The public key travels inside the receipt. A verifier checks the signature over
the canonical message without retrieving anything from the issuer. Establishing
that the key belongs to the claimed issuer is a separate step: Rubric commits
its federation signing keys to the same HCS topic, so a verifier can bind the
key to the ledger rather than to an assurance from the seller.

## 8. Anchor lifecycle

A receipt is issued the moment the service is delivered; the ledger anchor
follows on the next aggregation flush, typically under a minute. The receipt is
explicit about which state it is in:

    "anchor": {
      "type": "hcs",
      "network": "hedera-mainnet",
      "topicId": "0.0.10416909",
      "payloadHash": "sha3-256:<hex>",
      "mirrorNode": "https://mainnet-public.mirrornode.hedera.com",
      "status": "pending"
    }

`status` MUST be `pending` until the anchor is confirmed and MUST NOT assert an
anchor that does not yet exist. A receipt whose enqueue failed carries
`unanchored`.

The anchor references the payload hash, not the reverse. To resolve:

    GET https://rubric-protocol.com/verify/v1/by-payload-hash/<hex>

returning the attestation id, from which the HCS sequence number and consensus
timestamp follow. Both are then readable directly from the mirror node.

A signed but unanchored receipt is still evidence of what was delivered and
paid; it is not yet evidence of when, beyond the issuer's own `servedAt`. The
consensus timestamp is the trustworthy time bound.

## 9. Verification procedure

Given a receipt and the response body it accompanied:

1. Remove the members named in `delivery.excludedMembers` from the response
   object. Canonicalize per section 4 and compute `hash(0x03, ...)`. It MUST
   equal `delivery.responseHash`.
2. Canonicalize the request body and compute `hash(0x02, ...)`. It MUST equal
   `delivery.requestHash`.
3. Remove `anchor` from the receipt, canonicalize, compute `hash(0x04, ...)`.
   It MUST equal `anchor.payloadHash`.
4. Verify the ML-DSA-65 signature over the canonical message using the public
   key in the receipt.
5. Resolve `anchor.payloadHash` to an attestation, then read the HCS message
   from the mirror node and confirm the consensus timestamp.
6. Compare `payment.reference.value` against the settlement rail directly. For
   Base, the transaction is public.

Steps 1 through 4 require only the receipt. Steps 5 and 6 require public
infrastructure. No step requires the issuer's cooperation.

## 10. Security considerations

**Key binding.** The receipt carries the public key that signed it. That proves
internal consistency, not authorship. A verifier that cares who signed MUST
bind the key to the issuer's ledger-published key set rather than trusting the
receipt's own claim.

**Unanchored window.** Between issuance and the next flush, a receipt is signed
but not anchored. The issuer could in principle decline to anchor it. A buyer
who requires ledger-backed timing MUST confirm the anchor resolved.

**Response exclusion.** `excludedMembers` names what was removed before hashing
the response. A verifier MUST use the receipt's own list rather than assuming
`vsr`, and SHOULD reject a receipt excluding members beyond those needed to
break the self-reference.

**Request privacy.** `requestHash` commits without disclosing. A buyer proves
what was requested by producing the request; the receipt alone reveals nothing.

**Amount representation.** Atomic units as strings with an explicit exponent.
Verifiers MUST NOT parse amounts as floats.

## 11. References

- RFC 8785 — JSON Canonicalization Scheme
- RFC 6962 §2.1 — Certificate Transparency, Merkle tree
- RFC 2119 — Key words
- FIPS 204 — ML-DSA
- Hedera Consensus Service

---

Comments and implementation reports: contact@rubric-protocol.com


## 12. Channel-settled payments (v0.4.1 addendum)

Some x402 facilitators settle through payment channels: the payer signs a
voucher per request, the facilitator accepts it off-chain immediately, and
many vouchers are claimed on-chain later in a single batch transaction.
Between acceptance and claim, no per-payment record exists on any chain.
This section defines how a receipt describes such a payment truthfully at
the moment of delivery.

### 12.1 Design constraint

A receipt is signed and anchored at delivery time and is immutable
thereafter (§7, §8). A channel payment's on-chain claim occurs later, so
claim state MUST NOT be represented as a mutable field of the receipt.
The receipt commits to the voucher; the eventual claim transaction
includes that voucher; a verifier links the two without either document
changing. This mirrors the anchor lifecycle (§8), in which a receipt may
truthfully carry a pending anchor that resolves externally.

### 12.2 Payment block fields

A channel-settled payment extends the payment block as follows:

    "payment": {
      "rail": "x402",
      "railVersion": "2",
      "network": "<CAIP-2>",
      "asset": "<token contract or asset id>",
      "amount": "<atomic units, string>",
      "amountExponent": 6,
      "payer": "<address>",
      "payee": "<address>",
      "settlementMode": "channel",
      "reference": { "type": "batch-voucher", "value": "<voucher hash or id>" },
      "claimStatus": "pending",
      "settledAt": "<RFC 3339 of facilitator acceptance>"
    }

- `settlementMode` — `"direct"` or `"channel"`. Absent means `"direct"`;
  every receipt issued before this addendum is a direct settlement.
- `reference.type` `"batch-voucher"` — `value` carries the voucher hash or
  facilitator voucher id, exactly as accepted. This joins the existing
  reference types (`eip3009-authorization`, `solana-transaction`,
  `algorand-transaction`).
- `claimStatus` — always `"pending"` at issuance, permanently, by design.
  Claim resolution is a verifier lookup (§12.4), not a receipt field.
- `settledAt` — the facilitator acceptance time. Acceptance is the moment
  value became owed to the payee under the channel's rules; it is the
  honest settlement instant for a channel payment even though the chain
  has not yet recorded it.

### 12.3 Ledger semantics (informative)

An issuer's settlement ledger SHOULD record a channel payment as settled
at acceptance, carrying `settlementMode` and the voucher reference. When
a batch claim is observed on-chain, the issuer SHOULD append a separate
`claim-event` record listing the covered voucher references and the claim
transaction. Ledgers remain append-only; no payment row is rewritten.
Revenue accounting MAY distinguish accepted from claimed by filtering on
claim events.

### 12.4 Verification

Delivery and signature verification (§9) are unchanged; neither depends
on settlement mode. For the payment reference, a verifier of a channel
receipt SHOULD attempt to resolve the voucher to an on-chain claim via
the facilitator's claim index or by inspecting claim transactions. Three
outcomes are meaningful and MUST be distinguished:

- `claimed` — a claim transaction including the voucher exists on-chain.
- `accepted-unclaimed` — no claim yet observed. This is a state, not a
  failure: channel schemes claim on their own schedule, and a receipt's
  evidentiary value (what was delivered, who signed) does not depend on
  claim timing.
- `voucher-mismatch` — a claim exists whose terms conflict with the
  receipt's payment block. This is an integrity failure and MUST be
  reported.

### 12.5 Security considerations

Between acceptance and claim, the facilitator's ledger is the only
record of the payment. The receipt is therefore the payer's and payee's
sole facilitator-independent evidence during that window — which is the
reason this addendum exists. A verifier MUST NOT treat
`accepted-unclaimed` as proof that funds will move; it proves the
facilitator accepted the stated voucher for the stated delivery. Refund
or dispute processes SHOULD rest on the delivery commitment (§5, §9):
a response-hash match is a defense against a refund claim, and a
mismatch supports one.

*Changelog: v0.4.1 — added §12, reserving `settlementMode`,
`claimStatus`, and reference type `batch-voucher`. No changes to
existing receipt semantics; all prior receipts remain valid direct
settlements.*
