Docs

x402 payments

Price an HTTP endpoint in USDC, take payment from a Stellar wallet, and watch it settle.


WebSoroban Pay turns a normal HTTP endpoint into an x402 v2 resource settled in USDC on Stellar. You create and monitor the endpoint in Payment Studio. A buyer opens its public checkout, approves one authorization in their wallet, and gets the protected response with an on-chain receipt.

Testnet and Mainnet

Endpoints run on Stellar Testnet (test USDC) or Stellar Mainnet (real Circle USDC). New endpoints default to testnet. Every mainnet screen is marked in amber before a wallet asks for approval. Both networks settle through the OpenZeppelin x402 facilitator, which sponsors the network fee.

Create a paid endpoint#

  1. Sign in and open Payment Studio.
  2. Pick the network, then a name and URL slug.
  3. Set a USDC price, a payout address, and the JSON response message.
  4. Studio checks that the payout address exists and holds a USDC trustline. Settlement to an address without one always fails, so creation is blocked until the check passes.
  5. Create the endpoint and store its endpoint key. The key is shown once, and the backend stores only its SHA-256 hash. Testnet keys start with wsp_test_ and mainnet keys with wsp_live_.
  6. Use Inspect 402 to confirm the resource returns HTTP 402 and a PAYMENT-REQUIRED header.

Hosted resources use this shape:

code
GET https://YOUR_API/api/pay/resource/YOUR_SLUG        # testnet
GET https://YOUR_API/api/pay/live/resource/YOUR_SLUG   # mainnet

Opening the resource in a browser shows a WebSoroban Pay handoff to the public checkout, so the buyer does not need a WebSoroban account.

Protect your own API#

code
npm install @web-soroban/pay @x402/core @x402/express @x402/stellar express
code
import { createWebSorobanPay } from '@web-soroban/pay'

const pay = createWebSorobanPay({
  apiUrl: process.env.WEBSOROBAN_API_URL!,
  apiKey: process.env.WEBSOROBAN_PAY_KEY,
  endpointId: process.env.WEBSOROBAN_ENDPOINT_ID!,
  payTo: process.env.STELLAR_PAY_TO!,
})

app.use(pay.protect('GET /brief', '$0.01'))

const report = await pay.preflight() // key accepted? payout has a USDC trustline?

The network comes from the key prefix, so a testnet key can never guard a mainnet route. The fastest start is the Paid API endpoint template. It runs the preflight on startup and includes pnpm pay, which makes a real paid testnet request from your terminal.

Go live on mainnet#

  1. In Payment Studio, create a new endpoint with Mainnet selected. A testnet endpoint cannot be switched; its key stays testnet-only.
  2. Use a payout address that exists on mainnet and holds a trustline to Circle USDC (issuer GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN).
  3. Swap your server's WEBSOROBAN_PAY_KEY and WEBSOROBAN_ENDPOINT_ID for the wsp_live_ key and new endpoint id. pay.preflight() confirms the key and the trustline on mainnet before you take traffic.
  4. Make one small real payment (for example $0.01) from a mainnet wallet and check the receipt on Stellar Expert before announcing the endpoint.

Buyers need mainnet USDC and a USDC trustline. There is no faucet on mainnet.

Pay from a wallet#

x402 on Stellar asks the wallet to sign one Soroban authorization entry.

WalletHosted checkout
Freighter (browser extension)Yes
HanaYes
HOTYes
KleverYes
Albedo, OneKeySign from your own x402 client
Freighter MobileNot supported (cannot sign auth entries)
  1. Open the endpoint's Open checkout action, or continue from its raw 402 Payment Required page.
  2. Choose a wallet. Set it to the network shown on the checkout.
  3. The checkout checks the paying account's USDC trustline and balance before asking for anything.
  4. Review the exact asset, amount, recipient, and network, then approve.
  5. Wait for Payment settled, then open the receipt on Stellar Expert.

The facilitator sponsors the network fee. The authorization binds the exact token transfer: the facilitator can submit it, but cannot change the asset, amount, payer, or recipient. The checkout also verifies each signature against the exact authorization and your account before sending it.

Use an x402 client#

Programmatic clients read the PAYMENT-REQUIRED response, create an exact Stellar payment payload, then retry with PAYMENT-SIGNATURE. Successful responses expose PAYMENT-RESPONSE, which includes the settlement transaction.

code
npm install @x402/core@^2.25.0 @x402/fetch@^2.25.0 \
  @x402/stellar@^2.25.0 @stellar/freighter-api@^6.0.1

CORS requirements

A cross-origin resource must allow PAYMENT-SIGNATURE and expose PAYMENT-REQUIRED, PAYMENT-RESPONSE, and PAYMENT-ERROR. WebSoroban's hosted endpoints are already configured for these headers.

Reliability and failures#

Payment Studio shows the settlement success rate for the last 24 hours, 7 days, and 30 days. It also lists why payments failed, each reason with its fix. Failures that never reached the facilitator, such as spend caps and rate limits, are listed but do not count against the success rate.

  • No USDC trustline: add USDC from the wallet's asset list, then retry.
  • Insufficient USDC: fund the paying account. Testnet USDC is available at faucet.circle.com; on mainnet, buy or transfer Circle USDC.
  • Fee below minimum: the network fee rose between quote and settlement. The checkout retries once automatically; otherwise approve again.
  • Switch Freighter to Testnet/Mainnet: change the active network in the extension and approve again.
  • Signature for a different authorization: reconnect the active account and approve again.
  • Network mismatch: the key, wallet, or client is set to the other network.
  • Failed to fetch after approval: the protected API's CORS policy is blocking the signed retry. Check the headers above.
  • Spend cap: an agent signer over its daily USDC limit is rejected before settlement. Raise the cap or wait for the window to reset.

Webhooks#

Register payment.settled, payment.failed, or wallet.spend_limit_exceeded in Payment Studio, then use Test to send a signed webhook.test delivery. Deliveries are retried for about two hours. Each one carries X-WebSoroban-Signature (t=<unix>,v1=<hmac>). Verify it against the raw body:

code
import { constructWebhookEvent } from '@web-soroban/pay'

app.post('/webhooks/pay', express.raw({ type: 'application/json' }), (req, res) => {
  const event = constructWebhookEvent(secret, req.header('X-WebSoroban-Signature'), req.body)
  res.sendStatus(204)
})

Next#