Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/components/cards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,39 @@ export function LightningSessionCard() {
);
}

export function StellarMethodCard() {
return (
<Card
description="Smart contract payments on Stellar"
icon="simple-icons:stellar"
title="Stellar"
to="/payment-methods/stellar"
/>
);
}

export function StellarChargeCard() {
return (
<Card
description="One-time SAC token transfers settled on-chain"
icon="simple-icons:stellar"
title="Stellar charge"
to="/payment-methods/stellar/charge"
/>
);
}

export function StellarChannelCard() {
return (
<Card
description="Pay-as-you-go payments over one-way payment channels"
icon="simple-icons:stellar"
title="Stellar channel"
to="/payment-methods/stellar/session"
/>
);
}

export function SolanaMethodCard() {
return (
<Card
Expand Down
9 changes: 8 additions & 1 deletion src/pages.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ type Page =
| { path: '/quickstart/server'; render: 'static' }
| { path: '/protocol/challenges'; render: 'static' }
| { path: '/protocol/credentials'; render: 'static' }
| { path: '/protocol/discovery'; render: 'static' }
| { path: '/protocol/http-402'; render: 'static' }
| { path: '/protocol'; render: 'static' }
| { path: '/protocol/receipts'; render: 'static' }
Expand All @@ -102,6 +101,9 @@ type Page =
| { path: '/payment-methods/tempo/session'; render: 'static' }
| { path: '/payment-methods/stripe/charge'; render: 'static' }
| { path: '/payment-methods/stripe'; render: 'static' }
| { path: '/payment-methods/stellar/charge'; render: 'static' }
| { path: '/payment-methods/stellar'; render: 'static' }
| { path: '/payment-methods/stellar/session'; render: 'static' }
| { path: '/payment-methods/solana/charge'; render: 'static' }
| { path: '/payment-methods/solana'; render: 'static' }
| { path: '/payment-methods/lightning/charge'; render: 'static' }
Expand All @@ -114,7 +116,12 @@ type Page =
| { path: '/guides/multiple-payment-methods'; render: 'static' }
| { path: '/guides/one-time-payments'; render: 'static' }
| { path: '/guides/pay-as-you-go'; render: 'static' }
| { path: '/guides/proxy-existing-service'; render: 'static' }
| { path: '/guides/split-payments'; render: 'static' }
| { path: '/guides/streamed-payments'; render: 'static' }
| { path: '/advanced/discovery'; render: 'static' }
| { path: '/advanced/identity'; render: 'static' }
| { path: '/advanced/refunds'; render: 'static' }
| { path: '/_api/api/og'; render: 'static' };

// prettier-ignore
Expand Down
9 changes: 8 additions & 1 deletion src/pages/payment-methods/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ imageDescription: "Choose a payment method for your service"
---

import { Cards, Tab, Tabs } from 'vocs'
import { TempoMethodCard, CardMethodCard, LightningMethodCard, SolanaMethodCard, StripeMethodCard, CustomMethodCard } from '../../components/cards'
import { TempoMethodCard, CardMethodCard, LightningMethodCard, StellarMethodCard, SolanaMethodCard, StripeMethodCard, CustomMethodCard } from '../../components/cards'

# Payment methods [Available methods and how to choose one]

Expand Down Expand Up @@ -42,6 +42,12 @@ WWW-Authenticate: Payment method="lightning", intent="charge", ...
```http
HTTP/1.1 402 Payment Required
WWW-Authenticate: Payment method="solana", intent="charge", ...
```
</Tab>
<Tab title="Stellar">
```http
HTTP/1.1 402 Payment Required
WWW-Authenticate: Payment method="stellar", intent="charge", ...
```
</Tab>
</Tabs>
Expand All @@ -54,5 +60,6 @@ WWW-Authenticate: Payment method="solana", intent="charge", ...
<CardMethodCard />
<LightningMethodCard />
<SolanaMethodCard />
<StellarMethodCard />
<CustomMethodCard />
</Cards>
221 changes: 221 additions & 0 deletions src/pages/payment-methods/stellar/charge.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
---
imageDescription: "One-time stablecoin payments on Stellar"
---

import { Cards, Tab, Tabs } from 'vocs'
import { MermaidDiagram } from '../../../components/MermaidDiagram'

# Stellar charge [One-time SEP-41 token transfers]

The Stellar implementation of the [charge](/intents/charge) intent.

The client signs a SEP-41 `transfer` invocation, and the server verifies and broadcasts the transaction on-chain. Settlement completes in ~5 seconds with deterministic finality.

Stellar charge supports two credential modes:

- **Pull mode** (default): The client signs Stellar auth entries and sends the transaction XDR. The server broadcasts it. Two variants: sponsored (server holds an envelope signer and optionally wraps with a fee bump) or unsponsored (server broadcasts the transaction as-is, without modification).
- **Push mode**: The client builds, signs, and broadcasts the transaction itself, then sends the transaction hash. The server polls for confirmation.

This method is best for single API calls, content access, or one-off purchases.

## How it works

<MermaidDiagram chart={`sequenceDiagram
participant Client
participant Server
participant Stellar
Client->>Server: (1) GET /resource
Server-->>Client: 402 + Challenge (amount, currency, recipient)
Client->>Client: (2) Build SEP-41 transfer, sign auth entries
Client->>Server: (3) Retry with Credential (signed XDR)
Server->>Stellar: (4) Simulate + broadcast transaction
Stellar-->>Server: Transaction confirmed
Server-->>Client: 200 OK + Receipt
`} />

## Server

Use `stellar.charge` to gate any endpoint behind a one-time SEP-41 token payment. The method handles Challenge generation, Credential verification, transaction broadcast, and Receipt creation.

```ts
import express from 'express'
import { Mppx } from 'mppx/server'
import { stellar } from '@stellar/mpp/charge/server'
import { USDC_SAC_TESTNET } from '@stellar/mpp'

const mppx = Mppx.create({
secretKey: process.env.MPP_SECRET_KEY,
methods: [
stellar.charge({
recipient: process.env.STELLAR_RECIPIENT,
currency: USDC_SAC_TESTNET,
network: 'stellar:testnet',
}),
],
})

const app = express()

app.get('/my-service', async (req, res) => {
const webReq = new Request(`http://localhost:3000${req.url}`, {
method: req.method,
headers: new Headers(req.headers as Record<string, string>),
})

const result = await mppx.charge({
amount: '0.01',
description: 'Premium API access',
})(webReq)

if (result.status === 402) {
const challenge = result.challenge
challenge.headers.forEach((value, key) => res.setHeader(key, value))
return res.status(402).send(await challenge.text())
}

const response = result.withReceipt(
Response.json({ message: 'Payment verified' }),
)
response.headers.forEach((value, key) => res.setHeader(key, value))
return res.status(response.status).send(await response.text())
})

app.listen(3000)
```

### With fee sponsorship

When `feePayer` is configured, the server adds its own source account and optionally wraps the transaction in a fee bump before broadcasting. The client doesn't need XLM for gas – it signs only the Stellar auth entries.

```ts
import { Mppx } from 'mppx/server'
import { stellar } from '@stellar/mpp/charge/server'
import { USDC_SAC_TESTNET } from '@stellar/mpp'
import { Keypair } from '@stellar/stellar-sdk'

const mppx = Mppx.create({
secretKey: process.env.MPP_SECRET_KEY,
methods: [
stellar.charge({
recipient: process.env.STELLAR_RECIPIENT,
currency: USDC_SAC_TESTNET,
network: 'stellar:testnet',
feePayer: { // [!code hl]
envelopeSigner: Keypair.fromSecret(process.env.FEE_PAYER_SECRET), // [!code hl]
feeBumpSigner: Keypair.fromSecret(process.env.FEE_BUMP_SECRET), // optional // [!code hl]
}, // [!code hl]
}),
],
})
```

### Server configuration

| Parameter | Type | Default | Description |
|---|---|---|---|
| `currency` | `string` | – | SEP-41 token contract address |
| `decimals` | `number` | `7` | Token decimal precision |
| `feePayer` | `object` | – | Fee sponsorship configuration |
| `maxFeeBumpStroops` | `number` | `10,000,000` | Maximum fee bump amount in stroops |
| `network` | `string` | – | `'stellar:testnet'` or `'stellar:pubnet'` |
| `pollDelayMs` | `number` | `1,000` | Delay between transaction poll attempts |
| `pollMaxAttempts` | `number` | `30` | Maximum transaction poll attempts |
| `pollTimeoutMs` | `number` | `30,000` | Total poll timeout |
| `recipient` | `string` | – | Stellar public key (`G...`) or contract (`C...`) |
| `rpcUrl` | `string` | – | Stellar RPC endpoint |
| `simulationTimeoutMs` | `number` | `10,000` | Simulation timeout |
| `store` | `Store` | – | State store for replay protection |

## Client

Use `stellar.charge` with `Mppx.create` to automatically handle `402` responses. The client signs SEP-41 transfer auth entries and retries with the Credential.

```ts
import { Keypair } from '@stellar/stellar-sdk'
import { Mppx } from 'mppx/client'
import { stellar } from '@stellar/mpp/charge/client'

Mppx.create({
methods: [
stellar.charge({
keypair: Keypair.fromSecret('S...'),
mode: 'pull',
onProgress(event) {
console.log(event.type) // challenge → signing → signed → paying → confirming → paid
},
}),
],
})

const response = await fetch('http://localhost:3000/my-service')
const data = await response.json()
```

### Push mode

In push mode, the client broadcasts the transaction and sends the hash for server verification:

```ts
import { Keypair } from '@stellar/stellar-sdk'
import { Mppx } from 'mppx/client'
import { stellar } from '@stellar/mpp/charge/client'

Mppx.create({
methods: [
stellar.charge({
keypair: Keypair.fromSecret('S...'),
mode: 'push', // [!code hl]
}),
],
})
```

### Without polyfill

If you don't want to patch `globalThis.fetch`, use `mppx.fetch` directly:

```ts
import { Keypair } from '@stellar/stellar-sdk'
import { Mppx } from 'mppx/client'
import { stellar } from '@stellar/mpp/charge/client'

const mppx = Mppx.create({
methods: [
stellar.charge({
keypair: Keypair.fromSecret('S...'),
}),
],
polyfill: false,
})

const response = await mppx.fetch('http://localhost:3000/my-service')
```

### Client configuration

| Parameter | Type | Default | Description |
|---|---|---|---|
| `decimals` | `number` | `7` | Token decimal precision |
| `keypair` | `Keypair` | – | Stellar keypair for signing |
| `mode` | `'pull' \| 'push'` | `'pull'` | Credential mode |
| `onProgress` | `function` | – | Lifecycle event callback |
| `pollDelayMs` | `number` | `1,000` | Delay between poll attempts (push mode) |
| `pollMaxAttempts` | `number` | `30` | Maximum poll attempts (push mode) |
| `pollTimeoutMs` | `number` | `30,000` | Total poll timeout (push mode) |
| `rpcUrl` | `string` | – | Stellar RPC endpoint |
| `secretKey` | `string` | – | Stellar secret key (alternative to `keypair`) |
| `simulationTimeoutMs` | `number` | `10,000` | Simulation timeout |
| `timeout` | `number` | `180` | Transaction timeout in seconds |

### Progress events

The `onProgress` callback fires at each stage of the charge flow:

| Event | Description |
|---|---|
| `challenge` | Challenge received from server |
| `signing` | Building and signing SEP-41 transfer |
| `signed` | Transaction signed |
| `paying` | Sending Credential to server |
| `confirming` | Waiting for on-chain confirmation (push mode) |
| `paid` | Payment confirmed, Receipt received |
Loading
Loading