| Internet-Draft | Hedera Session | August 2026 |
| Rowbotham & Walker | Expires 8 February 2027 | [Page] |
This document defines the "session" intent for the "hedera" payment method in the Payment HTTP Authentication Scheme. It specifies unidirectional streaming payment channels for incremental, voucher-based payments suitable for low-cost metered services on the Hedera network.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 8 February 2027.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document.¶
This document is published as Informational but contains normative requirements using BCP 14 keywords [RFC2119] [RFC8174] to ensure interoperability between implementations. Payment method specifications that reference this document inherit these requirements.¶
The session intent establishes a unidirectional streaming
payment channel using on-chain escrow and off-chain
[EIP-712] vouchers. This enables high-frequency, low-cost
payments by batching many off-chain voucher signatures into
periodic on-chain settlements.¶
Unlike the charge intent which requires the full payment
amount upfront, the session intent allows clients to pay
incrementally as they consume services, paying exactly for
resources received.¶
The escrow contract (HederaStreamChannel.sol) is deployed on Hedera's EVM layer and uses standard ERC-20 token transfers. Hedera Token Service (HTS) tokens are exposed as ERC-20 via [HIP-218], enabling payment channels with native HTS tokens such as Circle USDC [CIRCLE-USDC-HEDERA].¶
Consider an LLM inference API that charges per output token:¶
Client requests a streaming completion (SSE response)¶
Server returns 402 with a session challenge¶
Client opens a payment channel on-chain, depositing funds into the HederaStreamChannel escrow¶
Server begins streaming response¶
As response streams, or over incremental requests, client signs vouchers with increasing amounts¶
Server settles periodically or at stream completion¶
The client pays exactly for tokens received, with no worst-case reservation.¶
The following diagram illustrates the Hedera session flow:¶
Client Server Hedera EVM
| | |
| (1) GET /resource | |
|----------------------> | |
| | |
| (2) 402 Payment | |
| Required | |
| intent="session" | |
| (includes | |
| challengeId) | |
|<---------------------- | |
| | |
| (3) approve() + | |
| open() on-chain | |
|---------------------------------------> |
| | |
| (4) GET /resource | |
| Authorization: | |
| Payment | |
| action="open" | |
| (channelId, | |
| txHash, voucher) | |
|----------------------> | |
| | |
| | (5) verify |
| | on-chain state |
| |----------------> |
| | |
| (6) 200 OK + Receipt | |
| (streaming | |
| response) | |
|<---------------------- | |
| | |
| (7) HEAD /resource | |
| action="voucher" | |
| (top-up, same | |
| URI) | |
|----------------------> | |
| | |
| (8) 200 OK + Receipt | |
|<---------------------- | |
| | |
| (9) GET /resource | |
| action="voucher" | |
| (incremental | |
| request) | |
|----------------------> | |
| | |
| (10) 200 OK + Receipt | |
| (additional | |
| response) | |
|<---------------------- | |
| | |
| (11) GET /resource | |
| action="close" | |
|----------------------> | |
| | (12) close() |
| |----------------> |
| | |
| (13) 200 OK + Receipt | |
| (includes | |
| txHash) | |
|<---------------------- | |
| | |
¶
Unlike Tempo session where the client sends a signed
transaction for the server to broadcast, in the Hedera
session the client broadcasts the open (and topUp)
transactions directly via Hashio JSON-RPC and presents
the transaction hash to the server. The server verifies
the on-chain state.¶
Voucher updates and close requests are submitted to the
same resource URI that requires payment. This allows
sessions to work on any endpoint without dedicated
payment control plane routes. Servers SHOULD support
voucher updates via any HTTP method; clients MAY use
HEAD for pure voucher top-ups when no response body
is needed.¶
A channel supports one active session at a time. The cumulative voucher semantics ensure correctness -- each voucher advances a single monotonic counter. The channel is the unit of concurrency; no additional session locking is required.¶
When a client sends a new streaming request on a channel that already has an active session, servers SHOULD terminate the previous session and start a new one. Voucher updates MAY arrive on separate HTTP connections (including HTTP/2 streams) and MUST be processed atomically with respect to balance updates.¶
Servers MUST ensure that voucher acceptance and balance deduction are serialized per channel to prevent race conditions.¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
A unidirectional off-chain payment mechanism where the payer deposits funds into an escrow contract and signs cumulative vouchers authorizing increasing payment amounts.¶
An [EIP-712] signed message authorizing a cumulative payment amount for a specific channel. Vouchers are monotonically increasing in amount.¶
A payment relationship between a payer and payee,
identified by a unique channelId. The channel holds
deposited funds and tracks cumulative settlements.¶
The on-chain ERC-20 transfer that converts off-chain voucher authorizations into actual token movement. HTS tokens are transferred via standard ERC-20 interfaces exposed through [HIP-218].¶
An address delegated to sign vouchers on behalf of the payer. Defaults to the payer if not specified.¶
The smallest indivisible unit of an HTS token. For example, Circle USDC on Hedera uses 6 decimal places; one million base units equals 1.00 USDC.¶
Hedera's EVM-compatible JSON-RPC relay that enables standard Ethereum tooling (e.g., viem, ethers.js) to interact with smart contracts deployed on Hedera's EVM layer.¶
This section defines normative encoding rules for interoperability.¶
All byte arrays (addresses, hashes, signatures, channelId) use:¶
| Type | Length | Example | ||||
|---|---|---|---|---|---|---|
| address | 42 chars (0x + 40 hex) |
0x742d...f8fe00
|
||||
| bytes32 | 66 chars (0x + 64 hex) |
0x6d0f...8e9f
|
||||
| signature | 130-132 chars | 65-byte r | s | v |
Implementations MUST use lowercase hex. Implementations SHOULD accept mixed-case input but normalize to lowercase before comparison.¶
Note: Hedera "long-zero" EVM addresses (e.g.,
0x0000000000000000000000000000000000001549 for HTS token
0.0.5449) are valid 20-byte addresses and MUST be handled
correctly. Implementations MUST use case-insensitive
comparison for all address fields.¶
Integer values (amounts, timestamps) are encoded as decimal strings in JSON to avoid precision loss with large numbers:¶
| Field | Encoding | Example |
|---|---|---|
cumulativeAmount
|
Decimal string |
"250000"
|
requestedAt
|
Decimal string |
"1736165100"
|
chainId
|
JSON number |
296
|
The chainId uses JSON number encoding as values are
small enough to avoid precision issues.¶
HTTP headers and receipt fields use [RFC3339] formatted
timestamps: 2026-04-12T12:05:00Z. Timestamps in
EIP-712 signed data use Unix seconds as decimal strings.¶
Streaming payment channels require an on-chain escrow contract that holds user deposits and enforces voucher-based withdrawals. On Hedera, this contract is deployed on the EVM layer and interacts with HTS tokens via their ERC-20 interface [HIP-218].¶
Each channel is identified by a unique channelId and
stores:¶
| Field | Type | Description |
|---|---|---|
payer
|
address | User who deposited funds |
payee
|
address | Server authorized to withdraw |
token
|
address | ERC-20 token address (HTS via HIP-218) |
authorizedSigner
|
address | Authorized signer (0 = payer) |
deposit
|
uint128 | Total amount deposited |
settled
|
uint128 | Cumulative amount withdrawn by payee |
closeRequestedAt
|
uint64 | Timestamp when close was requested (0 if not) |
finalized
|
bool | Whether channel is closed |
The channelId MUST be computed deterministically using
the escrow contract's computeChannelId() function:¶
channelId = keccak256(abi.encode(
payer,
payee,
token,
salt,
authorizedSigner,
address(this),
block.chainid
))
¶
Note: The channelId includes address(this) (the
escrow contract address) and block.chainid, explicitly
binding the channel to a specific contract deployment and
chain. Clients MUST use the contract's
computeChannelId() function or equivalent logic to
ensure interoperability.¶
Channels have no expiry -- they remain open until explicitly closed.¶
+-------------------------------------------------+
| CHANNEL OPEN |
| Client approves ERC-20 + calls open() |
| on HederaStreamChannel via Hashio JSON-RPC |
+-------------------------------------------------+
|
v
+-------------------------------------------------+
| SESSION PAYMENTS |
| Client signs EIP-712 vouchers off-chain |
| Server may periodically settle() on-chain |
+-------------------------------------------------+
|
+-----------+-----------+
v v
+---------------------+ +-----------------------+
| COOPERATIVE CLOSE | | FORCED CLOSE |
| Server calls | | 1. Client calls |
| close() with | | requestClose() |
| final voucher | | 2. Wait 15 min grace |
| | | 3. Client calls |
| | | withdraw() |
+---------------------+ +-----------------------+
| |
+-----------+-----------+
v
+-------------------------------------------------+
| CHANNEL CLOSED |
| Funds distributed, channel finalized |
+-------------------------------------------------+
¶
Compliant escrow contracts MUST implement the following functions. The signatures shown are the reference HederaStreamChannel.sol implementation.¶
Opens a new channel with escrowed funds.¶
| Parameter | Type | Description |
|---|---|---|
payee
|
address | Server's withdrawal address |
token
|
address | ERC-20 token contract address |
deposit
|
uint128 | Amount to deposit in base units |
salt
|
bytes32 | Random value for channelId |
authorizedSigner
|
address | Delegated signer; 0x0 = payer |
Returns the computed channelId.¶
function open(
address payee,
address token,
uint128 deposit,
bytes32 salt,
address authorizedSigner
) external returns (bytes32 channelId);
¶
The client MUST approve the escrow contract to spend
deposit tokens before calling open(). On Hedera,
HTS token approvals via the ERC-20 interface require
higher gas limits (approximately 1,000,000 gas) due to
the HTS precompile overhead.¶
Server withdraws funds using a signed voucher without closing the channel.¶
| Parameter | Type | Description |
|---|---|---|
channelId
|
bytes32 | Channel identifier |
cumulativeAmount
|
uint128 | Cumulative total authorized |
signature
|
bytes | EIP-712 signature |
The contract computes
delta = cumulativeAmount - channel.settled and
transfers delta tokens to the payee.¶
function settle(
bytes32 channelId,
uint128 cumulativeAmount,
bytes calldata signature
) external;
¶
User adds more funds to an existing channel. If a close
request is pending (closeRequestedAt != 0), calling
topUp() MUST cancel it by resetting
closeRequestedAt to zero and emitting a
CloseRequestCancelled event.¶
| Parameter | Type | Description |
|---|---|---|
channelId
|
bytes32 | Existing channel identifier |
additionalDeposit
|
uint256 | Additional amount in base units |
function topUp(
bytes32 channelId,
uint256 additionalDeposit
) external;
¶
Note: The additionalDeposit parameter is uint256
(not uint128) in HederaStreamChannel.sol; the contract
checks for overflow internally.¶
Server closes the channel, settling any outstanding voucher and refunding the remainder to the payer. Only callable by the payee.¶
| Parameter | Type | Description |
|---|---|---|
channelId
|
bytes32 | Channel to close |
cumulativeAmount
|
uint128 | Final cumulative amount |
signature
|
bytes | EIP-712 signature |
Transfers cumulativeAmount - channel.settled to payee,
refunds channel.deposit - cumulativeAmount to payer,
and marks channel finalized.¶
function close(
bytes32 channelId,
uint128 cumulativeAmount,
bytes calldata signature
) external;
¶
User requests channel closure, starting a grace period of at least 15 minutes.¶
| Parameter | Type | Description |
|---|---|---|
channelId
|
bytes32 | Channel to request closure for |
Sets channel.closeRequestedAt to current block
timestamp. The grace period allows the payee time to
submit any outstanding vouchers before forced closure.¶
function requestClose(
bytes32 channelId
) external;
¶
User withdraws remaining funds after the grace period expires.¶
| Parameter | Type | Description |
|---|---|---|
channelId
|
bytes32 | Channel to withdraw from |
Requires block.timestamp >= channel.closeRequestedAt +
CLOSE_GRACE_PERIOD. Refunds all remaining deposit to
payer and marks channel finalized.¶
function withdraw(bytes32 channelId) external;¶
Associates the escrow contract with an HTS token so it can receive transfers. This is a Hedera-specific function with no Tempo equivalent.¶
| Parameter | Type | Description |
|---|---|---|
token
|
address | HTS token to associate |
function associateSelf(
address token
) external returns (int256 responseCode);
¶
This function calls the HTS precompile at address
0x167 to perform token association. Anyone can call
it. The escrow contract MUST be associated with the
payment token before channels using that token can be
opened.¶
The escrow contract MUST enforce the following access control:¶
| Function | Caller | Description |
|---|---|---|
open
|
Anyone | Creates channel; caller = payer |
settle
|
Payee only | Withdraws with voucher |
topUp
|
Payer only | Adds funds |
close
|
Payee only | Closes with final voucher |
requestClose
|
Payer only | Initiates forced close |
withdraw
|
Payer only | Withdraws after grace |
associateSelf
|
Anyone | HTS token association |
The escrow contract MUST perform the following signature
verification for all functions that accept voucher
signatures (settle, close):¶
Canonical signatures: The contract MUST reject
ECDSA signatures with non-canonical (high-s) values.
Signatures MUST have
s <= secp256k1_order / 2 where the half-order is
0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E73
57A4501DDFE92F46681B20A0.
See Section 13.8 for rationale.¶
Authorized signer verification: The contract MUST recover the signer address from the EIP-712 signature and verify it matches the expected signer:¶
Domain binding: The contract MUST use its own
address as the verifyingContract in the EIP-712
domain separator, ensuring vouchers cannot be
replayed across different escrow deployments.¶
Failure to enforce these requirements on-chain would allow attackers to bypass server-side validation by submitting transactions directly to the contract.¶
The request parameter in the WWW-Authenticate
challenge contains a base64url-encoded JSON object.¶
| Field | Type | Required | Description |
|---|---|---|---|
amount
|
string | REQUIRED | Price per unit in base units |
unitType
|
string | OPTIONAL | Unit being priced (e.g., "llm_token") |
suggestedDeposit
|
string | OPTIONAL | Suggested deposit in base units |
currency
|
string | REQUIRED | ERC-20 token address (HTS via HIP-218) |
recipient
|
string | REQUIRED | Payee address (server's withdrawal address) |
For the session intent, amount specifies the price
per unit of service in base units (e.g., 6 decimals for
USDC), not a total charge. When unitType is present,
clients can use it together with amount to estimate
costs before streaming begins. The total cost depends on
consumption: total = amount * units_consumed.¶
The optional suggestedDeposit indicates the server's
recommended channel deposit for typical usage. Clients
MAY deposit less (if they expect limited usage) or more
(for extended sessions). The minimum viable deposit is
implementation-defined but SHOULD be at least amount
to cover one unit of service.¶
Challenge expiry is specified via the expires
auth-param in the WWW-Authenticate header per
[I-D.httpauth-payment], using [RFC3339] timestamp
format. Unlike the charge intent, the session request
JSON does not include an expires field -- expiry is
conveyed solely via the HTTP header.¶
As of version 00, session-specific request fields are
placed in methodDetails. A future high-level "session"
intent definition may promote common fields to the core
schema.¶
| Field | Type | Required | Description |
|---|---|---|---|
methodDetails.escrowContract
|
string | REQUIRED | Escrow contract address |
methodDetails.channelId
|
string | OPTIONAL | Channel ID if resuming |
methodDetails.minVoucherDelta
|
string | OPTIONAL | Minimum voucher increment |
methodDetails.chainId
|
number | OPTIONAL | Hedera chain ID (default: 295) |
Note: Unlike the Tempo session spec, there is no
feePayer field in this version. Hedera supports native
fee delegation via feePayerAccountId but this is
deferred to a future revision (see Section 7.3).¶
Channel reuse is OPTIONAL. Servers MAY include
channelId to suggest resuming an existing channel:¶
New channel (no channelId): Client generates a
random salt locally, computes channelId using the
formula in Section 5.1, opens the channel
on-chain, and returns the channelId in the
credential.¶
Existing channel (channelId provided): Client
MUST verify
channel.deposit - channel.settled >= amount before
resuming. If insufficient, client SHOULD either call
topUp() with the difference or open a new channel.¶
Servers MAY cache
(payer address, payee address, token) -> channelId
mappings to suggest channel reuse, reducing on-chain
transactions.¶
Example (new channel):¶
{
"amount": "25",
"unitType": "llm_token",
"suggestedDeposit": "10000000",
"currency": "0x000000000000000000000000000000000006f89a",
"recipient": "0x742d35cc6634c0532925a3b844bc9e7595f8fe00",
"methodDetails": {
"escrowContract":
"0x8Aaf6690C2a6397d595F97E224fC19759De6fdaE",
"chainId": 295
}
}
¶
This requests a price of 0.000025 USDC per LLM token,
with a suggested deposit of 10.00 USDC (10000000 base
units). The currency is Circle USDC on Hedera mainnet
(HTS token 0.0.456858, exposed as ERC-20 via HIP-218).¶
Example (existing channel):¶
{
"amount": "25",
"unitType": "llm_token",
"currency": "0x000000000000000000000000000000000006f89a",
"recipient": "0x742d35cc6634c0532925a3b844bc9e7595f8fe00",
"methodDetails": {
"escrowContract":
"0x8Aaf6690C2a6397d595F97E224fC19759De6fdaE",
"channelId":
"0x6d0f4fdf1f2f6a1f6c1b0fbd6a7d5c2c0a8d3d7b"
"1f6a9c1b3e2d4a5b6c7d8e9f",
"chainId": 295
}
}
¶
For existing channels, suggestedDeposit is omitted
since the channel already has funds. The channelId
tells the client to resume this channel.¶
In this version, the client pays all transaction fees for
channel operations (open, topUp, ERC-20 approve).
The client broadcasts these transactions directly via
Hashio JSON-RPC.¶
Hedera's EVM layer has predictable, low transaction fees. However, HTS precompile interactions require higher gas limits than standard ERC-20 operations:¶
| Operation | Recommended Gas Limit |
|---|---|
ERC-20 approve (HTS) |
1,000,000 |
open
|
1,500,000 |
topUp
|
1,500,000 |
settle
|
1,500,000 |
close
|
1,500,000 |
Clients MUST set gas limits appropriate for HTS precompile operations. The default gas estimates from Hashio JSON-RPC may be insufficient.¶
The settle and close contract functions are
server-originated on-chain transactions. The server pays
transaction fees for these operations:¶
Hedera natively supports fee delegation via the
feePayerAccountId field on transactions. A future
revision of this specification MAY add feePayer support
to methodDetails, enabling the server to pay
transaction fees on behalf of the client. This would pair
naturally with a pull-mode open flow where the client
signs the transaction and the server broadcasts it.¶
The credential in the Authorization header contains a
base64url-encoded JSON object per
[I-D.httpauth-payment].¶
| Field | Type | Required | Description |
|---|---|---|---|
challenge
|
object | REQUIRED | Echo of the challenge parameters |
payload
|
object | REQUIRED | Session-specific payload |
Implementations MUST ignore unknown fields in credential payloads, request objects, and receipts to allow forward-compatible extensions.¶
A streaming payment session progresses through distinct phases, each corresponding to a payload action:¶
Open: Client deposits funds on-chain (broadcasting
the transaction directly) and presents the open
action with the transaction hash. The server verifies
the on-chain deposit and validates the initial
voucher.¶
Streaming: Client submits voucher actions with
increasing cumulative amounts as service is consumed.
The server may periodically settle vouchers on-chain.¶
Close: Client sends the close action with the
final voucher. The server settles on-chain and
returns a receipt.¶
Each action carries action-specific fields directly in
the payload object, with the action field
discriminating between phases.¶
The payload object uses an action discriminator with
action-specific fields at the same level:¶
| Field | Type | Required | Description |
|---|---|---|---|
action
|
string | REQUIRED | One of the actions below |
| Action | Description |
|---|---|
open
|
Confirms channel is open on-chain |
topUp
|
Adds funds to an existing channel |
voucher
|
Submits updated cumulative voucher |
close
|
Requests server to close channel |
The open action confirms an on-chain channel opening
and begins the streaming session. Unlike the Tempo
session where the client sends a signed transaction for
server broadcast, the Hedera client broadcasts the
open() transaction itself and presents the transaction
hash.¶
Payload fields (in addition to action):¶
| Field | Type | Required | Description |
|---|---|---|---|
channelId
|
string | REQUIRED | Channel identifier (hex bytes32) |
txHash
|
string | REQUIRED | Transaction hash from open() |
cumulativeAmount
|
string | REQUIRED | Initial authorized amount (see below) |
signature
|
string | REQUIRED | EIP-712 voucher signature |
The client broadcasts the open() transaction via
Hashio JSON-RPC, waits for the transaction receipt, and
presents the txHash for server verification.¶
The server uses the txHash to verify the on-chain
channel state: deposit amount, payee, token, and that
the channel is not finalized.¶
The initial voucher (cumulativeAmount and signature)
proves the client controls the signing key and
establishes the voucher chain. Implementations MAY set
cumulativeAmount to zero or to the first request's
cost; both are valid starting points for the
cumulative voucher sequence.¶
Example:¶
{
"challenge": {
"id": "kM9xPqWvT2nJrHsY4aDfEb",
"realm": "api.llm-service.com",
"method": "hedera",
"intent": "session",
"request": "eyJ...",
"expires": "2026-04-12T12:05:00Z"
},
"payload": {
"action": "open",
"channelId":
"0x6d0f4fdf1f2f6a1f6c1b0fbd6a7d5c2c"
"0a8d3d7b1f6a9c1b3e2d4a5b6c7d8e9f",
"txHash":
"0x1a2b3c4d5e6f7890abcdef12345678"
"90abcdef1234567890abcdef12345678",
"cumulativeAmount": "2500",
"signature": "0xabcdef1234567890..."
}
}
¶
Note: cumulativeAmount here is "2500" (the cost
of the first request at 25 base units per token for
100 tokens). Implementations MAY also send "0".¶
The challenge object MUST echo the challenge
parameters from the server's WWW-Authenticate header
per [I-D.httpauth-payment].¶
The topUp action adds funds to an existing channel
during a streaming session. The client broadcasts the
topUp() transaction itself and presents the
transaction hash.¶
Clients MUST include a challenge object in the Payment
credential for topUp actions. To obtain a challenge
for a top-up outside an active streaming response,
clients MAY send a HEAD request to the protected
resource; the server returns 402 with a
WWW-Authenticate challenge (no body). Servers MUST
reject topUp actions referencing an unknown or expired
challenge id with problem type challenge-not-found.¶
Payload fields (in addition to action):¶
| Field | Type | Required | Description |
|---|---|---|---|
channelId
|
string | REQUIRED | Channel ID |
txHash
|
string | REQUIRED | Transaction hash from topUp() |
additionalDeposit
|
string | REQUIRED | Additional amount in base units |
Example:¶
{
"challenge": {
"id": "kM9xPqWvT2nJrHsY4aDfEb",
"realm": "api.llm-service.com",
"method": "hedera",
"intent": "session",
"request": "eyJ...",
"expires": "2026-04-12T12:05:00Z"
},
"payload": {
"action": "topUp",
"channelId":
"0x6d0f4fdf1f2f6a1f6c1b0fbd6a7d5c2c"
"0a8d3d7b1f6a9c1b3e2d4a5b6c7d8e9f",
"txHash":
"0x2b3c4d5e6f7890abcdef1234567890ab"
"cdef1234567890abcdef1234567890ab",
"additionalDeposit": "5000000"
}
}
¶
Upon successful verification, the server updates the channel's available balance. The new deposit is immediately available for voucher authorization.¶
The voucher action submits an updated cumulative
voucher during streaming.¶
Payload fields (in addition to action):¶
| Field | Type | Required | Description |
|---|---|---|---|
channelId
|
string | REQUIRED | Channel identifier |
cumulativeAmount
|
string | REQUIRED | Cumulative amount authorized |
signature
|
string | REQUIRED | EIP-712 voucher signature |
Example:¶
{
"challenge": {
"id": "kM9xPqWvT2nJrHsY4aDfEb",
"realm": "api.llm-service.com",
"method": "hedera",
"intent": "session",
"request": "eyJ...",
"expires": "2026-04-12T12:05:00Z"
},
"payload": {
"action": "voucher",
"channelId":
"0x6d0f4fdf1f2f6a1f6c1b0fbd6a7d5c2c"
"0a8d3d7b1f6a9c1b3e2d4a5b6c7d8e9f",
"cumulativeAmount": "250000",
"signature": "0xabcdef1234567890..."
}
}
¶
The close action requests the server to close the
channel and settle on-chain.¶
Payload fields (in addition to action):¶
| Field | Type | Required | Description |
|---|---|---|---|
channelId
|
string | REQUIRED | Channel identifier |
cumulativeAmount
|
string | REQUIRED | Final cumulative amount |
signature
|
string | REQUIRED | EIP-712 voucher signature |
The server uses the voucher fields to call
close(channelId, cumulativeAmount, signature) on-chain
via Hashio JSON-RPC.¶
Example:¶
{
"challenge": {
"id": "kM9xPqWvT2nJrHsY4aDfEb",
"realm": "api.llm-service.com",
"method": "hedera",
"intent": "session",
"request": "eyJ...",
"expires": "2026-04-12T12:05:00Z"
},
"payload": {
"action": "close",
"channelId":
"0x6d0f4fdf1f2f6a1f6c1b0fbd6a7d5c2c"
"0a8d3d7b1f6a9c1b3e2d4a5b6c7d8e9f",
"cumulativeAmount": "500000",
"signature": "0xabcdef1234567890..."
}
}
¶
Vouchers use typed structured data signing compatible with [EIP-712]. This section normatively defines the signing procedure; [EIP-712] is referenced for background only.¶
Voucher fields are placed directly in the credential
payload object (alongside action) rather than in a
nested structure:¶
| Field | Type | Required | Description |
|---|---|---|---|
channelId
|
string | REQUIRED | Channel ID (hex bytes32) |
cumulativeAmount
|
string | REQUIRED | Cumulative amount (decimal) |
signature
|
string | REQUIRED | EIP-712 signature (hex) |
The EIP-712 domain and type definitions are fixed by
this specification. Implementations MUST reconstruct the
full typed data structure using the domain parameters
from the challenge (chainId, escrowContract) before
signature verification.¶
The types object MUST contain exactly:¶
{
"Voucher": [
{ "name": "channelId", "type": "bytes32" },
{
"name": "cumulativeAmount",
"type": "uint128"
}
]
}
¶
Note: The EIP712Domain type is implicit per EIP-712
and SHOULD NOT be included in the types object.¶
The domain object MUST contain:¶
| Field | Type | Value |
|---|---|---|
name
|
string |
"Hedera Stream Channel"
|
version
|
string |
"1"
|
chainId
|
number | Hedera chain ID (295 or 296) |
verifyingContract
|
string | Escrow contract address |
To sign a voucher, implementations MUST:¶
Construct the domain separator hash:¶
domainSeparator = keccak256(
abi.encode(
keccak256(
"EIP712Domain(string name,"
"string version,"
"uint256 chainId,"
"address verifyingContract)"
),
keccak256(bytes(name)),
keccak256(bytes(version)),
chainId,
verifyingContract
)
)
¶
Construct the struct hash:¶
structHash = keccak256(
abi.encode(
keccak256(
"Voucher(bytes32 channelId,"
"uint128 cumulativeAmount)"
),
channelId,
cumulativeAmount
)
)
¶
Compute the signing hash:¶
signingHash = keccak256( "\x19\x01" || domainSeparator || structHash )¶
Sign with ECDSA using secp256k1 curve¶
Encode signature as 65-byte r || s || v where
v is 27 or 28¶
Vouchers specify cumulative totals, not incremental deltas:¶
Voucher #1: cumulativeAmount = 100 (100 total)¶
Voucher #2: cumulativeAmount = 250 (250 total)¶
Voucher #3: cumulativeAmount = 400 (400 total)¶
When settling, the contract computes:
delta = cumulativeAmount - settled¶
On action="open", servers MUST:¶
Transaction verification: Wait for the
transaction receipt using txHash. Verify the
transaction succeeded (receipt status = success).¶
On-chain state verification: Query the escrow
contract's getChannel(channelId) to verify:¶
Voucher verification: If cumulativeAmount and
signature are provided, verify the initial voucher:¶
Initialize server-side channel state¶
On action="topUp", servers MUST:¶
On action="voucher", servers MUST:¶
Verify voucher signature using EIP-712 recovery¶
Verify canonical low-s values (see Section 13.8)¶
Recover signer and MUST verify it matches expected signer from on-chain state¶
Verify channel.closeRequestedAt == 0. Servers
MUST reject vouchers on channels with a pending
forced close.¶
Verify monotonicity:¶
Verify cumulativeAmount <= channel.deposit¶
Persist voucher to durable storage before providing service¶
Update highestVoucherAmount = cumulativeAmount¶
Servers MUST derive the expected signer from on-chain
channel state by querying the escrow contract. The
expected signer is channel.authorizedSigner if
non-zero, otherwise channel.payer. Servers MUST NOT
trust signer claims in HTTP payloads.¶
Servers MUST persist the highest voucher to durable storage before providing the corresponding service. Failure to do so may result in unrecoverable fund loss if the server crashes after service delivery.¶
Servers MUST treat voucher submissions idempotently:¶
Resubmitting a voucher with the same
cumulativeAmount as the highest accepted MUST
return 200 OK with the current highestAmount¶
Submitting a voucher with lower cumulativeAmount
than highest accepted MUST return 200 OK with the
current highestAmount (not an error)¶
Clients MAY safely retry voucher submissions after network failures¶
If verification fails, servers MUST return an appropriate HTTP status code with a Problem Details [RFC9457] response body:¶
| Status | When |
|---|---|
| 400 Bad Request | Malformed payload or missing fields |
| 402 Payment Required | Invalid signature or signer mismatch |
| 410 Gone | Channel finalized or not found |
Error responses use Problem Details format:¶
{
"type":
"https://paymentauth.org/problems/"
"session/invalid-signature",
"title": "Invalid Signature",
"status": 402,
"detail": "Voucher signature could not "
"be verified",
"channelId": "0x6d0f4fdf..."
}
¶
Problem type URIs:¶
| Type URI | Description |
|---|---|
.../session/invalid-signature
|
Voucher signature invalid |
.../session/signer-mismatch
|
Signer not authorized |
.../session/amount-exceeds-deposit
|
Exceeds deposit |
.../session/delta-too-small
|
Below minVoucherDelta |
.../session/channel-not-found
|
No such channel |
.../session/channel-finalized
|
Channel closed |
.../session/challenge-not-found
|
Challenge expired |
.../session/insufficient-balance
|
Insufficient balance |
All problem type URIs above are prefixed with
https://paymentauth.org/problems.¶
For errors on the Payment Auth protected resource,
servers MUST return 402 with a fresh
WWW-Authenticate: Payment challenge per
[I-D.httpauth-payment].¶
Servers MUST maintain per-session accounting state to track authorized funds versus consumed service.¶
For each active session identified by
(challengeId, channelId), servers MUST maintain:¶
| Field | Type | Description |
|---|---|---|
acceptedCumulative
|
uint128 | Highest valid voucher accepted |
spent
|
uint128 | Cumulative amount charged |
settledOnChain
|
uint128 | Last settled amount (informational) |
The available balance is computed as:¶
available = acceptedCumulative - spent¶
For each request carrying a Payment credential with
intent="session", servers MUST follow this procedure:¶
Voucher acceptance (if provided in credential):¶
Verify signature and monotonicity per Section 10.3¶
If valid, persist the new acceptedCumulative¶
If invalid, return 402 with a fresh challenge¶
Balance check:¶
Charge and deliver (if available >= cost):¶
Receipt generation:¶
Include balance state in receipt¶
To prevent fund loss from server crashes:¶
To prevent double-charging on retries:¶
Clients SHOULD include an Idempotency-Key header¶
Servers SHOULD track (challengeId, idempotencyKey)
pairs and return cached responses for duplicates¶
Servers MUST NOT increment spent for duplicate
idempotent requests¶
Example idempotent request:¶
GET /api/chat HTTP/1.1 Host: api.example.com Idempotency-Key: req_a1b2c3d4e5f6 Authorization: Payment eyJ...¶
The cost for a request depends on the pricing model
declared in the challenge. Servers MUST support at least
one of:¶
Fixed cost: A predetermined amount per request¶
Usage-based fees: Pricing proportional to resource consumption¶
For streaming responses (SSE, chunked), servers SHOULD:¶
When a streaming response exhausts available balance:¶
Server MUST stop delivering additional content¶
Server MAY hold the connection open awaiting a voucher top-up¶
Server MAY close the response; client retries with a higher voucher¶
If client submits a voucher update, server SHOULD resume delivery if the connection is still open¶
For SSE responses, servers MUST emit a
payment-need-voucher event when balance is exhausted:¶
event: payment-need-voucher
data: {"channelId":"0x6d0f4fdf...",
"requiredCumulative":"250025",
"acceptedCumulative":"250000",
"deposit":"500000"}
¶
The payment-need-voucher event data MUST be a JSON
object containing:¶
| Field | Type | Required | Description |
|---|---|---|---|
acceptedCumulative
|
string | REQUIRED | Current highest voucher |
channelId
|
string | REQUIRED | Channel identifier |
deposit
|
string | REQUIRED | Current on-chain deposit |
requiredCumulative
|
string | REQUIRED | Minimum next voucher |
The deposit field allows the client to determine the
correct recovery action. When requiredCumulative
exceeds deposit, the client MUST submit
action="topUp" before sending a new voucher. When
requiredCumulative is within deposit, the client
can submit action="voucher" directly.¶
After emitting payment-need-voucher, the server MUST
pause delivery until a valid voucher is accepted.
Servers SHOULD close the stream if no voucher is
received within a reasonable timeout (e.g., 60 seconds).¶
Servers SHOULD NOT deliver service beyond the authorized balance under any circumstances. See Section 13.3 for rate limiting requirements.¶
Servers MAY settle at any time using their own criteria:¶
Periodically (e.g., every N seconds or M base units)¶
When action="close" is received¶
When accumulated unsettled amount exceeds a threshold¶
Based on gas cost optimization¶
Settlement frequency is an implementation detail left to servers.¶
The close() function settles any delta between the
provided cumulativeAmount and channel.settled. If
the server has already settled the highest voucher via
settle(), calling close() with the same amount will
only refund the payer the remaining deposit.¶
When the client sends action="close":¶
Server receives the signed close request¶
Server calls
close(channelId, cumulativeAmount, signature)
on-chain via Hashio JSON-RPC¶
Contract settles any delta and refunds remainder¶
Server returns receipt with transaction hash¶
Servers SHOULD close promptly when clients request -- the economic incentive is to claim earned funds immediately.¶
The server MUST set a gas limit of at least 1,500,000
for the close() call due to HTS precompile overhead.¶
If the server does not respond to close requests:¶
Client calls requestClose(channelId) on-chain¶
15-minute grace period begins¶
Server can still settle() or close() during
the grace period¶
After grace period, client calls
withdraw(channelId)¶
Client receives all remaining (unsettled) funds¶
Clients SHOULD wait at least 16 minutes after
requestClose() before calling withdraw() to account
for block time variance.¶
A single channel supports sequential sessions. Each
session uses the same cumulative voucher counter. When a
new session begins on a channel, the previous session's
spending state is irrelevant -- the channel's
highestVoucherAmount is the source of truth for the
next voucher's minimum value.¶
Vouchers are submitted via HTTP requests to the same resource URI that requires payment. There is no separate session endpoint. Clients SHOULD use HTTP/2 multiplexing or maintain separate connections for voucher updates and content streaming when topping up during a long-lived response.¶
For voucher-only updates (no response body needed),
clients MAY use HEAD requests. Servers SHOULD support
voucher credentials on HEAD requests for resources
that require session payment.¶
Servers MUST return a Payment-Receipt header on
every successful paid request. For streaming
responses (SSE, chunked transfer), servers MUST include
the receipt in the initial response headers AND in the
final message of the stream.¶
For SSE responses, the final receipt SHOULD be delivered as an event:¶
event: payment-receipt
data: {"method":"hedera","intent":"session",
"status":"success",...}
¶
The session intent extends the receipt with balance tracking:¶
| Field | Type | Description |
|---|---|---|
method
|
string |
"hedera"
|
intent
|
string |
"session"
|
status
|
string |
"success"
|
timestamp
|
string | [RFC3339] response time |
challengeId
|
string | Challenge identifier |
channelId
|
string | Channel identifier |
acceptedCumulative
|
string | Highest voucher accepted |
spent
|
string | Total charged so far |
reference
|
string | Transaction or channel ref |
units
|
number | OPTIONAL: Units consumed |
txHash
|
string | OPTIONAL: Transaction hash |
The reference field satisfies the core MPP receipt
reference requirement. It is set to txHash when a
transaction was broadcast (open, close), otherwise
set to channelId (voucher).¶
The txHash field is OPTIONAL because not every
response involves an on-chain settlement -- voucher
updates are off-chain.¶
Example receipt (per-request with metering):¶
{
"method": "hedera",
"intent": "session",
"status": "success",
"timestamp": "2026-04-12T12:08:30Z",
"challengeId": "c_8d0e3b5a9f2c1d4e",
"channelId":
"0x6d0f4fdf1f2f6a1f6c1b0fbd6a7d5c2c"
"0a8d3d7b1f6a9c1b3e2d4a5b6c7d8e9f",
"reference":
"0x6d0f4fdf1f2f6a1f6c1b0fbd6a7d5c2c"
"0a8d3d7b1f6a9c1b3e2d4a5b6c7d8e9f",
"acceptedCumulative": "250000",
"spent": "237500",
"units": 500
}
¶
Example receipt (on close with settlement):¶
{
"method": "hedera",
"intent": "session",
"status": "success",
"timestamp": "2026-04-12T12:10:00Z",
"challengeId": "c_8d0e3b5a9f2c1d4e",
"channelId":
"0x6d0f4fdf1f2f6a1f6c1b0fbd6a7d5c2c"
"0a8d3d7b1f6a9c1b3e2d4a5b6c7d8e9f",
"reference":
"0x1a2b3c4d5e6f7890abcdef12345678"
"90abcdef1234567890abcdef12345678",
"acceptedCumulative": "250000",
"spent": "250000",
"txHash":
"0x1a2b3c4d5e6f7890abcdef12345678"
"90abcdef1234567890abcdef12345678"
}
¶
Vouchers are bound to a specific channel and contract via:¶
channelId in the voucher message¶
verifyingContract in EIP-712 domain¶
chainId in EIP-712 domain¶
Cumulative amount semantics (can only increase)¶
The escrow contract enforces:¶
Vouchers have no validUntil field. This simplifies
the protocol:¶
Channels have no expiry -- closed explicitly¶
Vouchers remain valid until the channel closes¶
The close grace period protects against clients disappearing¶
Operational guidance: Servers SHOULD settle and close channels inactive for extended periods (e.g., 30+ days).¶
To mitigate voucher flooding, servers MUST implement rate limiting:¶
Servers SHOULD limit voucher submissions to 10 per second per session¶
Servers MAY implement additional IP-based rate limiting¶
Servers MUST enforce minVoucherDelta when present¶
Servers SHOULD skip expensive signature verification
for vouchers that do not advance state (return 200 OK
with current highestAmount per Section 10.4)¶
Servers SHOULD perform format validation before expensive ECDSA signature recovery.¶
To mitigate channel griefing via dust deposits:¶
Cumulative voucher semantics prevent front-running
attacks. If a client submits a higher voucher while a
server's settle() transaction is pending, the
settlement will still succeed -- it merely leaves
additional unsettled funds.¶
The EIP-712 domain includes verifyingContract, binding
vouchers to a specific escrow contract address.¶
The escrow contract provides:¶
ECDSA signatures are malleable: for any valid signature
(r, s), the signature (r, -s mod n) is also valid.
To prevent signature substitution attacks,
implementations MUST enforce canonical signatures:¶
Signatures MUST use "low-s" values with
s <= secp256k1_order / 2¶
The secp256k1 half-order is:
0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E73
57A4501DDFE92F46681B20A0¶
Servers MUST reject signatures with s values
exceeding this threshold¶
Accepted signature formats:¶
The HederaStreamChannel.sol contract uses Solady's
SignatureCheckerLib which enforces these requirements.¶
The voucher message contains only channelId and
cumulativeAmount. Wallet implementations are
encouraged to:¶
Vouchers are bound to channels but not to specific HTTP
sessions or API requests. The challengeId provides
correlation across requests. Servers MUST implement
challenge-to-voucher mapping for:¶
Vouchers use cumulative amount semantics: each voucher
authorizes a total payment up to cumulativeAmount, and
the on-chain contract enforces strict monotonicity
(cumulativeAmount > channel.settled). A voucher can
only ever advance the channel state forward.¶
A separate sessionHash binding is unnecessary:¶
Cross-session replay is harmless: If a voucher from session A is presented in session B, it can only authorize funds up to the amount already committed.¶
Cross-resource replay: Vouchers authorize
cumulative payment on a channel, not access to
specific resources. Resource authorization is handled
at the application layer via challengeId.¶
Hedera achieves asynchronous Byzantine Fault Tolerant (aBFT) consensus with deterministic finality in approximately 3-5 seconds [HEDERA-DOCS]. Once a transaction reaches consensus, it cannot be reversed.¶
For high-value channels, servers SHOULD:¶
Before an escrow contract can receive HTS tokens, it
MUST be associated with the token via the
associateSelf() function (see Section 5.3.7).
This is a one-time operation per token. If the escrow
contract is not associated with the payment token,
open() will fail with a transfer error.¶
Servers deploying escrow contracts MUST ensure the contract is associated with all supported payment tokens before advertising session challenges.¶
Hedera's EVM layer routes HTS token operations through
the HTS precompile at address 0x167. This precompile
has higher gas requirements than standard ERC-20
operations. Implementations MUST set appropriate gas
limits (see Section 7) to avoid transaction
failures.¶
The default gas estimates from Hashio JSON-RPC
(eth_estimateGas) may underestimate gas for HTS
precompile calls. Implementations SHOULD use hardcoded
minimum gas limits for escrow operations.¶
This document registers the following payment intent in the "HTTP Payment Intents" registry established by [I-D.httpauth-payment]:¶
| Intent | Methods | Description | Reference |
|---|---|---|---|
session
|
hedera
|
Streaming payment channel | This document |
Contact: Tom Rowbotham (tom@xeno.money)¶
This document registers the following problem types in the "HTTP Problem Types" registry established by [RFC9457]:¶
| Type URI | Title | Status | Ref |
|---|---|---|---|
.../session/invalid-signature
|
Invalid Signature | 402 | This document |
.../session/signer-mismatch
|
Signer Mismatch | 402 | This document |
.../session/amount-exceeds-deposit
|
Amount Exceeds Deposit | 402 | This document |
.../session/delta-too-small
|
Delta Too Small | 402 | This document |
.../session/channel-not-found
|
Channel Not Found | 410 | This document |
.../session/channel-finalized
|
Channel Finalized | 410 | This document |
.../session/challenge-not-found
|
Challenge Not Found | 402 | This document |
.../session/insufficient-balance
|
Insufficient Balance | 402 | This document |
All type URIs above are prefixed with
https://paymentauth.org/problems.¶
Each problem type is defined in Section 10.5.¶
Note: In examples throughout this appendix, hex values
shown with ... (e.g., "0x6d0f4fdf...") are
abbreviated. Actual values MUST be full-length as
specified in Section 4.¶
HTTP/1.1 402 Payment Required WWW-Authenticate: Payment id="kM9xPqWvT2nJrHsY4aDfEb", realm="api.llm-service.com", method="hedera", intent="session", expires="2026-04-12T12:05:00Z", request="<base64url-encoded JSON below>"¶
The request decodes to:¶
{
"amount": "25",
"unitType": "llm_token",
"suggestedDeposit": "10000000",
"currency":
"0x000000000000000000000000000000000006f89a",
"recipient":
"0x742d35cc6634c0532925a3b844bc9e7595f8fe00",
"methodDetails": {
"escrowContract":
"0x8Aaf6690C2a6397d595F97E224fC19759De6fdaE",
"chainId": 295
}
}
¶
Note: Challenge expiry is in the header expires
auth-param, not in the request JSON. The client
generates a random salt locally for new channels.¶
This requests 0.000025 USDC per LLM token, with a suggested deposit of 10.00 USDC (10000000 base units).¶
The client first broadcasts approve() and open() to
Hedera EVM via Hashio JSON-RPC, then retries the same
resource URI with the open credential:¶
GET /api/chat HTTP/1.1 Host: api.llm-service.com Authorization: Payment <base64url credential>¶
The credential payload for an open action:¶
{
"challenge": {
"id": "kM9xPqWvT2nJrHsY4aDfEb",
"realm": "api.llm-service.com",
"method": "hedera",
"intent": "session",
"request": "eyJ...",
"expires": "2026-04-12T12:05:00Z"
},
"payload": {
"action": "open",
"channelId": "0x6d0f4fdf...",
"txHash": "0x1a2b3c4d...",
"cumulativeAmount": "2500",
"signature": "0xabcdef1234567890..."
}
}
¶
During streaming, clients submit updated vouchers to
the same resource URI. HEAD is recommended for
pure top-ups when no response body is needed:¶
HEAD /api/chat HTTP/1.1 Host: api.llm-service.com Authorization: Payment <base64url credential with action="voucher">¶
Or with a regular request:¶
GET /api/chat HTTP/1.1 Host: api.llm-service.com Authorization: Payment <base64url credential with action="voucher">¶
The credential payload for a voucher update:¶
{
"challenge": {
"id": "kM9xPqWvT2nJrHsY4aDfEb",
"realm": "api.llm-service.com",
"method": "hedera",
"intent": "session",
"request": "eyJ...",
"expires": "2026-04-12T12:05:00Z"
},
"payload": {
"action": "voucher",
"channelId": "0x6d0f4fdf...",
"cumulativeAmount": "250000",
"signature": "0x1234567890abcdef..."
}
}
¶
GET /api/chat HTTP/1.1 Host: api.llm-service.com Authorization: Payment <base64url credential with action="close">¶
The credential payload for a close request:¶
{
"challenge": {
"id": "kM9xPqWvT2nJrHsY4aDfEb",
"realm": "api.llm-service.com",
"method": "hedera",
"intent": "session",
"request": "eyJ...",
"expires": "2026-04-12T12:05:00Z"
},
"payload": {
"action": "close",
"channelId": "0x6d0f4fdf...",
"cumulativeAmount": "500000",
"signature": "0xabcdef1234567890..."
}
}
¶
The voucher fields contain the final cumulative amount for on-chain settlement.¶
This appendix provides reference implementation details. These are informative and not normative.¶
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IHederaStreamChannel {
struct Channel {
bool finalized;
uint64 closeRequestedAt;
address payer;
address payee;
address token;
address authorizedSigner;
uint128 deposit;
uint128 settled;
}
function CLOSE_GRACE_PERIOD()
external view returns (uint64);
function VOUCHER_TYPEHASH()
external view returns (bytes32);
function open(
address payee,
address token,
uint128 deposit,
bytes32 salt,
address authorizedSigner
) external returns (bytes32 channelId);
function settle(
bytes32 channelId,
uint128 cumulativeAmount,
bytes calldata signature
) external;
function topUp(
bytes32 channelId,
uint256 additionalDeposit
) external;
function close(
bytes32 channelId,
uint128 cumulativeAmount,
bytes calldata signature
) external;
function requestClose(
bytes32 channelId
) external;
function withdraw(
bytes32 channelId
) external;
function getChannel(
bytes32 channelId
) external view returns (Channel memory);
function getChannelsBatch(
bytes32[] calldata channelIds
) external view returns (Channel[] memory);
function computeChannelId(
address payer,
address payee,
address token,
bytes32 salt,
address authorizedSigner
) external view returns (bytes32);
function getVoucherDigest(
bytes32 channelId,
uint128 cumulativeAmount
) external view returns (bytes32);
function domainSeparator()
external view returns (bytes32);
function associateSelf(
address token
) external returns (int256 responseCode);
event ChannelOpened(
bytes32 indexed channelId,
address indexed payer,
address indexed payee,
address token,
address authorizedSigner,
bytes32 salt,
uint256 deposit
);
event Settled(
bytes32 indexed channelId,
address indexed payer,
address indexed payee,
uint256 cumulativeAmount,
uint256 deltaPaid,
uint256 newSettled
);
event CloseRequested(
bytes32 indexed channelId,
address indexed payer,
address indexed payee,
uint256 closeGraceEnd
);
event CloseRequestCancelled(
bytes32 indexed channelId,
address indexed payer,
address indexed payee
);
event TopUp(
bytes32 indexed channelId,
address indexed payer,
address indexed payee,
uint256 additionalDeposit,
uint256 newDeposit
);
event ChannelClosed(
bytes32 indexed channelId,
address indexed payer,
address indexed payee,
uint256 settledToPayee,
uint256 refundedToPayer
);
event ChannelExpired(
bytes32 indexed channelId,
address indexed payer,
address indexed payee
);
error ChannelAlreadyExists();
error ChannelNotFound();
error ChannelFinalized();
error InvalidSignature();
error InvalidToken();
error InvalidPayee();
error AmountExceedsDeposit();
error AmountNotIncreasing();
error DepositOverflow();
error ZeroDeposit();
error NotPayer();
error NotPayee();
error TransferFailed();
error CloseNotReady();
}
¶
| Network | Chain ID | Contract Address |
|---|---|---|
| Hedera Testnet | 296 |
0x8Aaf6690C2a6397d595F97E224fC19759De6fdaE
|
| Hedera Mainnet | 295 |
0x8Aaf6690C2a6397d595F97E224fC19759De6fdaE
|
Both deployments are fully verified on Sourcify.¶
| Token | Network | HTS Token ID | EVM Address |
|---|---|---|---|
| USDC | Testnet | 0.0.5449 |
0x00...1549
|
| USDC | Mainnet | 0.0.456858 |
0x00...06f89a
|
The reference implementation is available at:
contracts/src/HederaStreamChannel.sol in the
mppx-hedera repository.¶
{
"$schema":
"https://json-schema.org/draft/2020-12/schema",
"$id":
"https://paymentauth.org/schemas/"
"hedera-session-request.json",
"title": "Hedera Session Request",
"type": "object",
"required": [
"amount", "currency",
"recipient", "methodDetails"
],
"properties": {
"amount": {
"type": "string",
"pattern": "^[0-9]+$"
},
"unitType": {
"type": "string"
},
"suggestedDeposit": {
"type": "string",
"pattern": "^[0-9]+$"
},
"currency": {
"type": "string",
"pattern": "^0x[0-9a-fA-F]{40}$"
},
"recipient": {
"type": "string",
"pattern": "^0x[0-9a-fA-F]{40}$"
},
"methodDetails": {
"$ref": "#/$defs/methodDetails"
}
},
"$defs": {
"methodDetails": {
"type": "object",
"required": ["escrowContract"],
"properties": {
"escrowContract": {
"type": "string",
"pattern": "^0x[0-9a-fA-F]{40}$"
},
"channelId": {
"type": "string",
"pattern": "^0x[0-9a-fA-F]{64}$"
},
"minVoucherDelta": {
"type": "string",
"pattern": "^[0-9]+$"
},
"chainId": {
"type": "integer",
"enum": [295, 296]
}
}
}
}
}
¶
{
"$schema":
"https://json-schema.org/draft/2020-12/schema",
"$id":
"https://paymentauth.org/schemas/"
"hedera-session-payload.json",
"title": "Hedera Session Payload",
"type": "object",
"required": ["action"],
"properties": {
"action": {
"enum": [
"open", "topUp", "voucher", "close"
]
},
"txHash": {
"type": "string",
"pattern": "^0x[0-9a-fA-F]{64}$"
},
"channelId": {
"type": "string",
"pattern": "^0x[0-9a-fA-F]{64}$"
},
"cumulativeAmount": {
"type": "string",
"pattern": "^[0-9]+$"
},
"signature": {
"type": "string",
"pattern": "^0x[0-9a-fA-F]{128,130}$"
},
"additionalDeposit": {
"type": "string",
"pattern": "^[0-9]+$",
"description":
"Additional deposit amount in base units "
"(topUp action only)"
}
}
}
¶
Servers MUST include Payment-Receipt only on
successful processing of a session action (2xx).¶
{
"$schema":
"https://json-schema.org/draft/2020-12/schema",
"$id":
"https://paymentauth.org/schemas/"
"hedera-session-receipt.json",
"title": "Hedera Session Receipt",
"type": "object",
"required": [
"method", "intent", "status",
"timestamp", "challengeId",
"channelId", "reference",
"acceptedCumulative", "spent"
],
"properties": {
"method": { "const": "hedera" },
"intent": { "const": "session" },
"status": { "const": "success" },
"timestamp": {
"type": "string",
"format": "date-time"
},
"challengeId": { "type": "string" },
"channelId": {
"type": "string",
"pattern": "^0x[0-9a-fA-F]{64}$"
},
"reference": {
"type": "string",
"description":
"txHash when a tx was broadcast "
"(open, close); channelId otherwise"
},
"acceptedCumulative": {
"type": "string",
"pattern": "^[0-9]+$"
},
"spent": {
"type": "string",
"pattern": "^[0-9]+$"
},
"units": {
"type": "integer"
},
"txHash": {
"type": "string",
"pattern": "^0x[0-9a-fA-F]{64}$"
}
}
}
¶
The author thanks the Tempo team for the MPP session payment channel design and the mppx ecosystem architecture that this specification builds upon. HederaStreamChannel.sol is a port of Tempo's TempoStreamChannel.sol adapted for Hedera's EVM layer and HTS token ecosystem.¶