Skip to main content

Testnet and pubnet

Buckspay supports two Stellar networks:
ValueNetworkUse case
"testnet"Stellar testnet (Fuji)Development and integration testing
"pubnet"Stellar pubnet (mainnet)Production - real funds
Pass the network field in BuckspayConfig. The same field is forwarded to buckspayFacilitator, which sets the FacilitatorChain header - "stellar-testnet" or "stellar-pubnet" (hyphen-separated).

Mainnet opt-in guard

Mainnet is supported and off by default. Without an explicit opt-in, constructing a client on "pubnet" throws BuckspayError("INVALID_CONFIG"). This makes it impossible for a misconfigured or forgotten config to move real funds.
Constructing a createBuckspayClient with network: "pubnet" without opting in throws BuckspayError("INVALID_CONFIG"). Add the opt-in deliberately, not as a drive-by change.
The opt-in is deliberately different in each environment so neither can accidentally inherit the other’s setting:
  • Browser: allowMainnet: true in the BuckspayConfig.
  • Node / server: BUCKSPAY_ALLOW_MAINNET=1 environment variable.

USDC and the asset-agnostic design

Buckspay does not hardcode any USDC address. You pass the Stellar Asset Contract (SAC) address when constructing a transfer call:
client.transfer({ token: USDC_SAC, to: MERCHANT, amount: "1.50" })
This keeps the SDK asset-agnostic - the same client handles any Soroban token. USDC = 7 decimals on Stellar. Circle’s implementation uses seven decimal places, not the six common on EVM chains. Amounts in the SDK are expressed as human-readable strings ("1.50", "0.000001") and converted internally - you do not deal with raw stroops. On pubnet, the USDC SAC is Circle’s official contract address. On testnet, use the Circle testnet USDC SAC.

Dedicated Soroban RPC

createRpcSimContext(rpcUrl) connects the SDK’s simulation engine to a Soroban RPC node. On pubnet, use a dedicated, consistent RPC endpoint rather than the shared public load balancer, which is eventually-consistent and can cause simulation failures when ledger state lags across nodes. For pubnet with a contract account, mainnetSimContext(rpcUrl, { sponsorAddress }) is the correct preset - it sets the funded sponsor G... address as the simSource that the facilitator uses when recording the transaction.

Pubnet example

The example below shows a complete pubnet configuration with the allowMainnet: true opt-in, a dedicated RPC URL, and mainnetSimContext:
// Recipe 08 - MAINNET (pubnet) gasless USDC, contract/passkey account. Browser only.
//
// Mainnet is gated: it runs ONLY because `allowMainnet: true` is set in config (the
// browser opt-in, equivalent to Node `BUCKSPAY_ALLOW_MAINNET=1`). Without it,
// constructing the client on "pubnet" throws BuckspayError("INVALID_CONFIG").
import {
  createBuckspayClient,
  mainnetSimContext,
  type BuckspayConfig
} from "@buckspay/core";
import { ozContractAccount } from "@buckspay/accounts/oz-contract";
import { passkey } from "@buckspay/signers/passkey";
import { buckspayFacilitator } from "@buckspay/relayer/buckspay-facilitator";

// The facilitator sponsor's PUBLIC G-address (used to derive the C-address AND to frame
// the recording sim on pubnet). PUBLIC only - the sponsor secret lives in the facilitator.
const SPONSOR_G: string = "GDVEU3DD4KOFECV66VIHWEZOYX4ZKR3WV27L464SIIPOU2IUI3JCZA57";
// Mainnet USDC SAC (C-address). Differs per network - the caller passes it; the SDK is asset-agnostic.
const USDC_SAC_PUBNET: string = "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7EQI5VS577FRESC2GUDOAAEZ3";
const MERCHANT: string = "GAMX62ZD4FWIKMWGVPEDR6WNL2TYTPQMO2ZJEAZUAON7VCZ5G2GWDF7W";
const SOROBAN_RPC_PUBNET = "https://mainnet.sorobanrpc.com";

export const mainnetConfig: BuckspayConfig = {
  network: "pubnet",
  // Explicit browser opt-in - without this, the client refuses to construct on pubnet.
  allowMainnet: true,
  account: ozContractAccount({ network: "pubnet", sponsorAddress: SPONSOR_G }),
  signer: passkey({ rpId: window.location.hostname, rpName: "buckspay" }),
  // url points at YOUR backend, which forwards to the facilitator with the key server-side.
  relayer: buckspayFacilitator({ url: "/api/gasless", network: "pubnet" }),
  gas: { mode: "sponsored" }
};

export const mainnetClient = createBuckspayClient(
  mainnetConfig,
  // Contract model on pubnet MUST carry the funded sponsor G as `simSource`; this preset forces it.
  mainnetSimContext(SOROBAN_RPC_PUBNET, { sponsorAddress: SPONSOR_G })
);

export async function payOnMainnet(): Promise<void> {
  await mainnetClient.connect(); // derive C-address + ensureReady (sponsored deploy if needed)
  const call = mainnetClient.transfer({ token: USDC_SAC_PUBNET, to: MERCHANT, amount: "1.50" });
  const receipt = await mainnetClient.pay([call]); // prepare -> sign -> send
  console.log(receipt.transferTx); // settled on pubnet
}

Multi-network rail model

A single asset can be offered over multiple rails: the payer chooses which network to settle on. The FacilitatorChain value ("stellar-testnet" | "stellar-pubnet") in the relay payload tells the facilitator which network to submit to. The SDK resolves the rail from the client’s network field - no extra wiring needed.

Next

Facilitator and BFF

How to configure the relayer and keep the API key server-side.

Account models

Classic and passkey accounts - both work on testnet and pubnet.