> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://mpp.dev/api/mcp` to find what you need.

# 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

```mermaid
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',
      // [!code hl:start]
      feePayer: {
        envelopeSigner: Keypair.fromSecret(process.env.FEE_PAYER_SECRET),
        feeBumpSigner: Keypair.fromSecret(process.env.FEE_BUMP_SECRET), // optional
      },
      // [!code hl:end]
    }),
  ],
})
```

### 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 |
