| Internet-Draft | Stellar Charge | March 2026 |
| Salloum | Expires 1 October 2026 | [Page] |
This document defines the "charge" intent for the "stellar" payment method in the Payment HTTP Authentication Scheme. It specifies how clients and servers exchange one-time [SEP-41] token transfers on the Stellar blockchain, with optional server-sponsored transaction fees.¶
Two credential types are supported: type="transaction" (default),
where the client sends the signed transaction to the server for
submission, and type="hash" (fallback), where the client broadcasts
the transaction directly to the network and presents the on-chain transaction
hash for server verification.¶
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 1 October 2026.¶
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 may not be modified, and derivative works of it may not be created, except to format it for publication as an RFC or to translate it into languages other than English.¶
The charge intent represents a one-time payment of a specified amount, as
defined in [I-D.payment-intent-charge]. The server may settle the payment
any time before the challenge expires auth-param timestamp.¶
This document specifies how to implement the charge intent using SEP-41
[SEP-41] tokens on the Stellar smart contract platform. [SEP-41]
defines a standard token interface for Stellar smart contracts, including Stellar
Asset Contracts (SAC) [SAC] and custom token implementations.¶
The default flow, called "pull mode", uses type="transaction"
credentials. The client signs the transaction (or authorization entries)
and the server "pulls" it for submission to the Stellar network:¶
Client Server Stellar Network
| | |
| (1) GET /resource | |
|--------------------------> | |
| | |
| (2) 402 Payment Required | |
| intent="charge" | |
|<-------------------------- | |
| | |
| (3) Sign tx or auth entries| |
| | |
| (4) Authorization: Payment | |
|--------------------------> | |
| | (5) Verify + submit |
| |----------------------> |
| | (6) Confirmed |
| |<---------------------- |
| (7) 200 OK + Receipt | |
|<-------------------------- | |
| | |
¶
In this model the server controls transaction submission, enabling
fee sponsorship (Section 7.1.1) and server-side retry logic.
When feePayer is true, step (3) signs only authorization entries and
step (5) includes the server rebuilding the transaction as source. When
feePayer is false, step (3) builds and signs a complete transaction
and the server submits it without modification.¶
The fallback flow, called "push mode", uses type="hash" credentials. The client
"pushes" the transaction to the network itself and presents the confirmed transaction hash:¶
Client Server Stellar Network
| | |
| (1) GET /resource | |
|--------------------------> | |
| | |
| (2) 402 Payment Required | |
| intent="charge" | |
|<-------------------------- | |
| | |
| (3) Build & sign tx | |
| | |
| (4) Send transaction | |
|------------------------------------------------------>|
| (5) Confirmation | |
|<------------------------------------------------------|
| | |
| (6) Authorization: Payment | |
| (with txHash) | |
|--------------------------> | |
| | (7) getTransaction |
| |----------------------> |
| | (8) Verified |
| |<---------------------- |
| (9) 200 OK + Receipt | |
|<-------------------------- | |
| | |
¶
This flow is useful when the client cannot or does not wish to delegate submission to the server. The server verifies the payment by fetching and inspecting the on-chain transaction via RPC.¶
This document inherits the shared request semantics of the "charge"
intent from [I-D.payment-intent-charge]. It defines only the
Stellar-specific methodDetails, payload, and verification procedures
for the "stellar" payment method.¶
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 Stellar smart contract implementing the [SEP-41] token
interface, exposing transfer, balance, and related functions.
Identified by a C-prefixed Stellar smart contract address. Stellar Asset
Contracts (SAC) [SAC] are a common [SEP-41] implementation
that wrap classic Stellar assets.¶
A signed data structure scoping a Stellar smart contract invocation to a specific invoker and ledger sequence range. See [STELLAR-AUTH].¶
An arrangement where the server pays Stellar network fees on behalf of the client. The client signs only authorization entries; the server acts as the transaction source account. Servers MAY additionally wrap the rebuilt transaction in a fee bump transaction to adjust fees without invalidating the client's authorization entries.¶
The normative fallback value for the average Stellar ledger close time: 5 seconds. Used to convert wall-clock expiry to a ledger sequence number when network-provided estimates are unavailable or impractical.¶
The normative fallback challenge expiry duration: 5 minutes. Used when
the expires auth-param is absent from the challenge.¶
A chain identifier per the CAIP-2 Stellar namespace
[CAIP-2-STELLAR] (e.g., stellar:pubnet, stellar:testnet).¶
The default settlement flow where the client signs the transaction
(or authorization entries) and the server submits it
(type="transaction"). The server "pulls" the signed transaction
from the credential. Enables fee sponsorship and server-side retry
logic.¶
The fallback settlement flow where the client broadcasts the
transaction itself and presents the confirmed transaction hash
(type="hash"). The client "pushes" the transaction to the network
directly. Cannot be used with fee sponsorship.¶
The request parameter in the WWW-Authenticate challenge contains a
base64url-encoded JSON object. The JSON MUST be serialized using the JSON
Canonicalization Scheme (JCS) [RFC8785] before base64url encoding, per
[I-D.httpauth-payment].¶
This specification implements the shared request fields defined in [I-D.payment-intent-charge].¶
| Field | Type | Presence | Description |
|---|---|---|---|
methodDetails.network
|
string | REQUIRED | CAIP-2 Stellar chain identifier (stellar:pubnet or stellar:testnet) |
methodDetails.feePayer
|
boolean | OPTIONAL | If true, server pays transaction fees (default: false) |
If methodDetails.feePayer is true, the server sponsors transaction
fees per Section 7.1.1. If false or omitted, the client MUST build a
fully signed, network-ready transaction per Section 7.1.2. Fee
sponsorship is only available in pull mode (type="transaction");
push mode (type="hash") MUST NOT be used with feePayer: true.¶
Example:¶
{
"amount": "10000000",
"currency": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4W",
"recipient": "GBHEGW3KWOY2OFH767EDALFGCUTBOEVBDQMCKU",
"description": "API access fee",
"methodDetails": {
"network": "stellar:testnet",
"feePayer": true
}
}
¶
The credential in the Authorization header contains a base64url-encoded
JSON object per [I-D.httpauth-payment].¶
| Field | Type | Presence | Description |
|---|---|---|---|
challenge
|
object | REQUIRED | Echo of the challenge auth-params from WWW-Authenticate per [I-D.httpauth-payment]
|
payload
|
object | REQUIRED | Stellar-specific payload |
source
|
string | OPTIONAL | Payer DID |
The source field, if present, SHOULD use the did:pkh method [DID-PKH]
with the CAIP-2 network identifier and the payer's Stellar address (e.g.,
did:pkh:stellar:testnet:GABC...).¶
| Field | Type | Presence | Description |
|---|---|---|---|
type
|
string | REQUIRED |
"transaction"
|
transaction
|
string | REQUIRED | Base64-encoded XDR |
transactionBase64-encoded XDR of a Stellar transaction as defined in
[STELLAR-XDR]. The transaction MUST contain exactly one operation of
type invokeHostFunction.¶
When feePayer is true: the transaction source account MUST be set to
the all-zeros Stellar account
(GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF). The server
replaces it with its own address at settlement. Authorization entries
MUST be signed by the client.¶
When feePayer is false: the transaction MUST be fully signed and
network-ready, including valid sequence number, fee, timeBounds, and
source account. The server MUST submit this transaction without
modification.¶
Example:¶
{
"challenge": {
"id": "kM9xPqWvT2nJrHsY4aDfEb",
"realm": "api.example.com",
"method": "stellar",
"intent": "charge",
"request": "eyJ...",
"expires": "2025-02-05T12:05:00Z"
},
"payload": {
"type": "transaction",
"transaction": "AAAAAgAAAABriIN4..."
},
"source": "did:pkh:stellar:testnet:GABC..."
}
¶
In push mode (type="hash"), the client has already broadcast the
transaction to the Stellar network. The hash field contains the
transaction hash for the server to verify on-chain.¶
| Field | Type | Presence | Description |
|---|---|---|---|
type
|
string | REQUIRED |
"hash"
|
hash
|
string | REQUIRED | Stellar transaction hash (64-character hex string) |
Push mode MUST NOT be used when feePayer is true in the challenge
request. Since the client has already broadcast the transaction, the
server cannot act as fee sponsor. Servers MUST reject type="hash"
credentials when the challenge specifies feePayer: true.¶
Example:¶
{
"challenge": {
"id": "pT7yHnKmQ2wErXsZ5vCbNl",
"realm": "api.example.com",
"method": "stellar",
"intent": "charge",
"request": "eyJ...",
"expires": "2025-02-05T12:05:00Z"
},
"payload": {
"type": "hash",
"hash": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd"
},
"source": "did:pkh:stellar:testnet:GABC..."
}
¶
Stellar uses ledger sequence numbers for transaction and authorization
entry expiration rather than wall-clock timestamps. Clients MUST derive the
ledger expiration from the challenge expires auth-param as follows.¶
If expires is absent, clients SHOULD default to
DEFAULT_CHALLENGE_EXPIRY (5 minutes) from the current time.¶
ledgerExpiration = currentLedger + ceil((expires - now) / DEFAULT_LEDGER_CLOSE_TIME)¶
where DEFAULT_LEDGER_CLOSE_TIME is 5 seconds. currentLedger MUST be
obtained from the Stellar network via Stellar RPC getLatestLedger
[STELLAR-RPC].¶
When feePayer is true, clients MUST set the authorization entry
expiration ledger to this value.¶
When feePayer is false, clients MUST set the transaction
timeBounds.maxTime to the expires timestamp. The transaction MUST NOT
be valid beyond the challenge expiry.¶
When methodDetails.feePayer is true:¶
The client obtains currentLedger via Stellar RPC getLatestLedger
and computes the authorization entry expiration per
Section 6.¶
The client builds an invokeHostFunction transaction with the all-zeros
source account, containing a single operation calling
transfer(from, to, amount) on the [SEP-41] token contract.
The client simulates the transaction to identify the required
authorization entries.¶
The client signs the authorization entries using credential type
sorobanCredentialsAddress. The client MUST NOT sign the full
transaction.¶
The client encodes the transaction with signed authorization entries as
base64 XDR and places it in payload.transaction.¶
Upon receiving the credential, the server verifies it per Section 8, rebuilds the transaction with itself as source account, and submits it per Section 10.¶
Servers acting as fee sponsors:¶
When methodDetails.feePayer is false or absent:¶
The client sets timeBounds.maxTime to the expires auth-param value,
or DEFAULT_CHALLENGE_EXPIRY from the current time if absent. The
transaction MUST NOT be valid beyond the challenge expiry. See
Section 6.¶
The client builds a fully signed invokeHostFunction transaction
containing a single operation calling transfer(from, to, amount) on
the [SEP-41] token contract, including sequence number, fee,
and timeBounds.¶
The client encodes the complete, signed transaction as base64 XDR in
payload.transaction.¶
Upon receiving the credential, the server verifies it per Section 8 and submits the transaction without modification per Section 10.¶
Before settling a charge credential, servers MUST first validate that
payload.type is "transaction" or "hash", then proceed with the
appropriate verification path. If any check fails, the server MUST return
a verification-failed error per [I-D.httpauth-payment].¶
If the Stellar RPC is unavailable for a required simulation step, servers
MUST treat this as a server error (HTTP 5xx) rather than a
verification-failed response, and MUST NOT settle the credential.¶
The challenge id matches an outstanding, unsettled challenge issued
by this server, and the current time is before the challenge expires
auth-param.¶
The decoded transaction contains exactly one invokeHostFunction
operation with function type hostFunctionTypeInvokeContract.¶
The invoked function is transfer(from, to, amount) on the contract
matching currency. The to argument MUST equal recipient and the
amount argument MUST equal amount (as i128) from the challenge
request.¶
The transaction's network passphrase MUST correspond to
methodDetails.network.¶
The server MUST simulate the transaction via Stellar RPC. The
simulation MUST succeed and MUST emit events showing only the expected
balance changes: a decrease of amount for the payer and an increase
of amount for the recipient. Any other balance change MUST cause
verification to fail.¶
The transaction source account is the all-zeros account
(GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF).¶
Authorization entries MUST use credential type
sorobanCredentialsAddress only, and MUST NOT contain
subInvocations beyond the single [SEP-41] token transfer.¶
The authorization entry expiration MUST NOT exceed currentLedger +
ceil((expires - now) / DEFAULT_LEDGER_CLOSE_TIME).¶
The server's address MUST NOT appear as the from argument or in
any authorization entry.¶
The challenge id matches an outstanding, unsettled challenge issued
by this server, and the current time is before the challenge expires
auth-param.¶
The decoded transaction contains exactly one invokeHostFunction
operation with function type hostFunctionTypeInvokeContract.¶
The invoked function is transfer(from, to, amount) on the contract
matching currency. The to argument MUST equal recipient and the
amount argument MUST equal amount (as i128) from the challenge
request.¶
The transaction's network passphrase MUST correspond to
methodDetails.network.¶
The server MUST simulate the transaction via Stellar RPC. The
simulation MUST succeed and MUST emit events showing only the expected
balance changes: a decrease of amount for the payer and an increase
of amount for the recipient. Any other balance change MUST cause
verification to fail.¶
timeBounds.maxTime MUST NOT exceed the expires timestamp from the
challenge.¶
For push mode credentials (type="hash"), the server MUST fetch the
transaction via Stellar RPC getTransaction [STELLAR-RPC] and verify:¶
The challenge id matches an outstanding, unsettled challenge issued
by this server.¶
The transaction hash has not been previously consumed (see Section 11.2).¶
The transaction exists and has status SUCCESS.¶
The transaction contains exactly one invokeHostFunction operation
calling transfer(from, to, amount) on the contract matching
currency. The to argument MUST equal recipient and the amount
argument MUST equal amount (as i128) from the challenge request.¶
Mark the transaction hash as consumed.¶
This specification defines the following additional error code beyond those in [I-D.httpauth-payment]:¶
| Code | HTTP | Description |
|---|---|---|
settlement-failed
|
402 | Credential valid but on-chain settlement failed |
Servers MUST return settlement-failed when a credential passes
verification but the Stellar transaction fails on-chain (e.g., insufficient
funds or sequence number conflict). This is distinct from
verification-failed, which indicates the credential failed validation checks.¶
Parse the base64 XDR transaction from payload.transaction.¶
Extract all operations and authorization entries.¶
Rebuild a new transaction with:¶
Sign the rebuilt transaction with the server's key.¶
Submit via Stellar RPC sendTransaction [STELLAR-RPC].¶
Verify the submission returns PENDING status, then poll until
SUCCESS or FAILED.¶
On SUCCESS, return a receipt per Section 10.4. On FAILED, return a
settlement-failed error per Section 9.¶
Submit the received transaction as-is via Stellar RPC
sendTransaction [STELLAR-RPC]. The server MUST NOT modify the
transaction.¶
Poll until SUCCESS or FAILED.¶
On SUCCESS, return a receipt per Section 10.4. On FAILED, return a
settlement-failed error per Section 9.¶
For push mode credentials, the client has already broadcast the transaction. The server checks the transaction hash against consumed hashes per Section 11.2.2, verifies the transaction on-chain per Section 8.2, and returns a receipt per Section 10.4.¶
Limitations:¶
Upon successful settlement, servers MUST return a Payment-Receipt header
per [I-D.httpauth-payment].¶
The receipt payload fields:¶
| Field | Type | Presence | Description |
|---|---|---|---|
method
|
string | REQUIRED |
"stellar"
|
reference
|
string | REQUIRED | Transaction hash |
status
|
string | REQUIRED |
"success"
|
timestamp
|
string | REQUIRED | [RFC3339] settlement time |
externalId
|
string | OPTIONAL | Echoed from request |
When feePayer is true, the server rebuilds and signs the transaction. A
malicious client could craft a transaction to drain the server's account.¶
Servers MUST verify their own address does not appear as the from
transfer argument or in any authorization entry before signing. Servers
MUST re-simulate the rebuilt transaction and MUST reject any credential
whose simulation emits unexpected balance changes.¶
Authorization entry expiration (keyed to ledger sequence) and Stellar
sequence number consumption prevent transaction replay. Servers MUST reject
credentials referencing an expired or already-settled challenge id.¶
Servers MUST maintain a set of consumed transaction hashes. Before accepting a push mode credential, the server MUST check whether the hash has already been consumed and reject the credential if it has. After successful verification, the server MUST atomically mark the hash as consumed.¶
Clients MUST decode and verify the challenge request before signing.
Clients MUST verify that amount, currency, and recipient match their
expectations prior to authorizing any transfer.¶
The simulation requirement in Section 8 ensures the transaction behaves as specified. Servers MUST treat any unexpected balance change as a verification failure, regardless of whether it favors the server or a third party.¶
Servers offering fee sponsorship are exposed to denial-of-service attacks where adversaries submit valid-looking credentials that fail on-chain, causing the server to pay fees without receiving payment. Servers SHOULD implement rate limiting and MAY require client authentication before issuing sponsored challenges.¶
This document registers the following payment method in the "HTTP Payment Methods" registry established by [I-D.httpauth-payment]:¶
| Method Identifier | Description | Reference |
|---|---|---|
stellar
|
Stellar [SEP-41] token transfer | This document |
Contact: Stellar Development Foundation (developers@stellar.org)¶
This document registers the following payment intent in the "HTTP Payment Intents" registry established by [I-D.httpauth-payment]:¶
| Intent | Applicable Methods | Description | Reference |
|---|---|---|---|
charge
|
stellar
|
One-time [SEP-41] token transfer | This document |
stellar-charge-challenge = "Payment" 1*SP "id=" quoted-string "," "realm=" quoted-string "," "method=" DQUOTE "stellar" DQUOTE "," "intent=" DQUOTE "charge" DQUOTE "," "request=" base64url-nopad stellar-charge-credential = "Payment" 1*SP base64url-nopad ; Base64url encoding without padding per RFC 4648 Section 5 base64url-nopad = 1*( ALPHA / DIGIT / "-" / "_" )¶
Challenge:¶
HTTP/1.1 402 Payment Required WWW-Authenticate: Payment id="kM9xPqWvT2nJrHsY4aDfEb", realm="api.example.com", method="stellar", intent="charge", request="eyJhbW91bnQiOiIxMDAwMDAwMCIsImN1cnJlb...", expires="2025-02-05T12:05:00Z" Cache-Control: no-store¶
The request decodes to:¶
{
"amount": "10000000",
"currency": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4W",
"recipient": "GBHEGW3KWOY2OFH767EDALFGCUTBOEVBDQMCKU",
"methodDetails": {
"network": "stellar:testnet",
"feePayer": true
}
}
¶
This requests a transfer of 1.0 USDC (10000000 base units, assuming 7 decimal places).¶
Credential:¶
GET /api/resource HTTP/1.1 Host: api.example.com Authorization: Payment eyJjaGFsbGVuZ2Ui...¶
Decoded credential:¶
{
"challenge": {
"id": "kM9xPqWvT2nJrHsY4aDfEb",
"realm": "api.example.com",
"method": "stellar",
"intent": "charge",
"request": "eyJ...",
"expires": "2025-02-05T12:05:00Z"
},
"payload": {
"type": "transaction",
"transaction": "AAAAAgAAAABriIN4..."
},
"source": "did:pkh:stellar:testnet:GABC..."
}
¶
Receipt:¶
{
"method": "stellar",
"reference": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd",
"status": "success",
"timestamp": "2025-02-05T12:04:32Z"
}
¶
Challenge:¶
HTTP/1.1 402 Payment Required WWW-Authenticate: Payment id="pT7yHnKmQ2wErXsZ5vCbNl", realm="api.example.com", method="stellar", intent="charge", request="eyJhbW91bnQiOiIxMDAwMDAwMCIsImN1cnJlb...", expires="2025-02-05T12:05:00Z" Cache-Control: no-store¶
The request decodes to:¶
{
"amount": "10000000",
"currency": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4W",
"recipient": "GBHEGW3KWOY2OFH767EDALFGCUTBOEVBDQMCKU",
"methodDetails": {
"network": "stellar:testnet",
"feePayer": false
}
}
¶
Credential:¶
GET /api/resource HTTP/1.1 Host: api.example.com Authorization: Payment eyJjaGFsbGVuZ2Ui...¶
Decoded credential:¶
{
"challenge": {
"id": "pT7yHnKmQ2wErXsZ5vCbNl",
"realm": "api.example.com",
"method": "stellar",
"intent": "charge",
"request": "eyJ...",
"expires": "2025-02-05T12:05:00Z"
},
"payload": {
"type": "transaction",
"transaction": "AAAAAgAAAABriIN4..."
},
"source": "did:pkh:stellar:testnet:GABC..."
}
¶
Receipt:¶
{
"method": "stellar",
"reference": "b2c3d4e5f6789012345678901234567890ab1234567890123456789012345678",
"status": "success",
"timestamp": "2025-02-05T12:04:41Z"
}
¶
The client broadcasts the transaction itself and presents the confirmed hash. Cannot be used with fee sponsorship.¶
Credential:¶
{
"challenge": { "..." : "echoed challenge" },
"payload": {
"type": "hash",
"hash": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd"
}
}
¶
The author thanks the Stellar community for their input and feedback on this specification.¶