# 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
