# Get classic account state Source: https://buckspay.mintlify.app/api-reference/account-state/get-classic-account-state /api-reference/openapi.json get /stellar/account/{publicKey} Returns on-chain state for a classic Stellar G-address: whether it exists, whether it has a USDC trustline, and its XLM and USDC balances. Used by the SDK to gate the onboarding flow and to display account state in UI. # Get contract account state Source: https://buckspay.mintlify.app/api-reference/account-state/get-contract-account-state /api-reference/openapi.json get /stellar/contract/{address} Returns on-chain state for a passkey smart-account or policy session-account (C-address). Same response shape as the classic account endpoint. The SDK routes G-addresses to `/stellar/account/:pk` and C-addresses here. # Email OTP -> Stellar key Source: https://buckspay.mintlify.app/api-reference/auth/email-otp->-stellar-key /api-reference/openapi.json post /auth/email Signer-proxy endpoint. The BFF (`createSignerProxyRoute({ provider: 'email', ... })`) forwards this request to the facilitator's `/auth/email`. The facilitator verifies the one-time code and returns the user's derived Stellar public key. **Never call the facilitator's `/auth/email` directly from the browser.** Use the BFF route: the SDK's `@buckspay/signers/email` posts to your same-origin BFF which proxies the call. The request body is provider-specific (email address + OTP code). The response `publicKey` is a Stellar G-address (ed25519 StrKey). # Social identity -> Stellar key Source: https://buckspay.mintlify.app/api-reference/auth/social-identity->-stellar-key /api-reference/openapi.json post /auth/social Signer-proxy endpoint. The BFF (`createSignerProxyRoute({ provider: 'web3auth', ... })`) forwards this request to the facilitator's `/auth/social` with the server-held API key injected. The facilitator verifies the provider identity token, derives or recovers the user's Stellar key, and returns it. **Never call the facilitator's `/auth/social` directly from the browser.** Use the BFF route: the SDK's `@buckspay/signers/social` posts to your same-origin BFF which proxies the call. The request body is provider-specific (Web3Auth / web3auth verifier). The response `publicKey` is a Stellar G-address (ed25519 StrKey). # Quote a gas-in-token fee Source: https://buckspay.mintlify.app/api-reference/fee/quote-a-gas-in-token-fee /api-reference/openapi.json post /fee/quote Request a fee quote for paying Soroban gas in a token instead of XLM. The facilitator simulates the calls, prices the XLM fee as a token amount, and returns the quote including the `FeeForwarder` contract address and a ledger-based expiry. The SDK uses this quote to build the `FeeForwarder.forward()` authorization entry before calling `POST /relay` with `feeToken` set. # API Reference Source: https://buckspay.mintlify.app/api-reference/index HTTP wire contract for the Buckspay facilitator and BFF relay route. ## Overview The Buckspay facilitator is the off-chain relay service that accepts signed Stellar authorization entries and submits them gaslessly to the network. The endpoints documented here are the HTTP wire contract that the SDK speaks internally. **The SDK calls these endpoints for you.** You do not call the facilitator directly from application code. This reference is for teams building their own BFF relay route or integrating the facilitator from a custom server-side context. See [Facilitator and BFF](/concepts/facilitator-and-bff) and [Next.js BFF](/platforms/nextjs-bff) for the standard integration pattern. ## Security model All facilitator endpoints require the `x-api-key` header. **The API key is a server-side secret and must never appear in a browser bundle.** The standard pattern is a same-origin BFF route on your server: the browser POSTs the signed payload to your BFF, which validates business rules and then calls the facilitator with the key injected server-side. The `@buckspay/nextjs` package provides `createRelayRoute` and `createSignerProxyRoute` to build compliant BFF handlers without handling the key injection yourself. ## Endpoint groups | Group | Paths | Purpose | | ----------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------ | | **Relay** | `POST /relay` | Submit a signed `RelayPayload` -> `Receipt` | | **Fee** | `POST /fee/quote` | Quote gas-in-token fee before building the `forward()` entry | | **Account State** | `GET /stellar/account/{pk}`, `GET /stellar/contract/{addr}` | Read on-chain balances and trustline status | | **Onboarding** | `POST /stellar/onboard/build`, `/submit`, `/contract/deploy`, `/session-account/deploy` | Sponsor-funded account setup and smart-contract deploy | | **Auth** | `POST /auth/social`, `POST /auth/email` | Signer-proxy: verify provider identity -> Stellar key | | **Swap** | `POST /swap/quote` | EVM gasless swap quote (requires `swapChain` config) | ## Schema sources All request and response schemas are derived directly from the SDK's own zod validators: * **`RelayPayload` / `Receipt`** - `packages/core/src/types.ts` + `packages/relayer/src/buckspay-facilitator/internals.ts` (`receiptSchema`) * **`FeeQuote`** - `packages/relayer/src/buckspay-facilitator/internals.ts` (`feeQuoteSchema`) * **`AccountState`** - `packages/relayer/src/buckspay-facilitator/internals.ts` (`accountStateSchema`) * **`AuthKeyResponse`** - `packages/core/src/types.ts` (`AuthDetails`) * **`SwapQuoteResponse`** - `packages/relayer/src/buckspay-facilitator/internals.ts` (`swapQuoteResponseSchema`) * **BFF relay / auth request validation** - `packages/nextjs/src/routes.ts` (`relayPayloadSchema`, `bodyObjectSchema`) # Build onboarding transaction Source: https://buckspay.mintlify.app/api-reference/onboarding/build-onboarding-transaction /api-reference/openapi.json post /stellar/onboard/build Build the sponsor-funded onboarding transaction for a classic G-address: creates the account if needed and establishes the USDC trustline. The facilitator returns the unsigned transaction XDR for the user to sign and submit via `POST /stellar/onboard/submit`. If the account is already fully onboarded, `nothingToDo: true` is returned and no XDR needs to be signed. # Deploy a passkey smart-account Source: https://buckspay.mintlify.app/api-reference/onboarding/deploy-a-passkey-smart-account /api-reference/openapi.json post /stellar/contract/deploy Sponsor-deploy a passkey smart-account (secp256r1 / WebAuthn). The SDK sends the passkey's 65-byte uncompressed public key; the facilitator holds the sponsor secret, enforces the pinned Wasm hash, and returns the deployed contract address. # Deploy a policy session-account Source: https://buckspay.mintlify.app/api-reference/onboarding/deploy-a-policy-session-account /api-reference/openapi.json post /stellar/session-account/deploy Sponsor-deploy a policy-scoped session-account bound to an ed25519 root key. The SDK sends the root public key (G-address); the facilitator holds the sponsor secret, enforces the pinned Wasm hash, and returns the deployed contract address. Requires `deploySessionAccount` support in the relayer. # Submit signed onboarding transaction Source: https://buckspay.mintlify.app/api-reference/onboarding/submit-signed-onboarding-transaction /api-reference/openapi.json post /stellar/onboard/submit Submit the user-signed onboarding transaction XDR from `POST /stellar/onboard/build`. The facilitator fee-bumps it (so the user does not need XLM for fees) and submits it to Stellar. # Relay a signed payment Source: https://buckspay.mintlify.app/api-reference/relay/relay-a-signed-payment /api-reference/openapi.json post /relay Submit a signed Soroban `SorobanAuthorizationEntry` (base64 XDR) to the facilitator. The facilitator wraps it in a fee-bump transaction, submits it to Stellar, and returns a `Receipt`. In gas-in-token mode, set `feeToken` and ensure `authorizationEntryXdr` encodes the `FeeForwarder.forward()` invocation (a single host-function op that includes the actual transfer as a sub-invocation). No separate fee auth entry is needed. **The API key is injected server-side by the BFF; the browser never sends this header directly.** # Get an EVM swap quote Source: https://buckspay.mintlify.app/api-reference/swap/get-an-evm-swap-quote /api-reference/openapi.json post /swap/quote Get a gasless EVM token swap quote. Requires the relayer to be configured with a `swapChain` (see `FacilitatorOptions`). Returns a quote ID, expected amounts, and EIP-712 typed data for the swap authorization. **This endpoint is available only when the EVM swap rail is configured.** If `swapChain` is absent, `BuckspayClient.swap()` fails with `SWAP_FAILED`. # Account models Source: https://buckspay.mintlify.app/concepts/account-models Classic G-addresses vs. passkey smart-contract accounts - when to use each. Buckspay separates *who signs* (`BuckspaySigner`) from *what account signs* (`AccountAdapter`). They are **orthogonal**: you can pair any signer with any account type. The two account models ship in v1. ## Comparison | | Classic | Passkey / contract | | --------------------- | --------------------------------------------- | ----------------------------------------- | | User prerequisite | A Stellar wallet (Freighter / xBull / LOBSTR) | A device with WebAuthn support | | Address | `G...` account | `C...` contract account | | Materialization | Sponsored onboarding (account + trustline) | Sponsored OZ Smart Account deploy | | Signature verified by | The network (Ed25519) | The contract's `__check_auth` (secp256r1) | | Best for | Existing Stellar users | New users and passwordless onboarding | ## Classic account (`G...`) For users who already have a Stellar wallet (Freighter, xBull, LOBSTR). Calling `connect()` resolves the `G...` address and runs sponsored onboarding - it creates the account and adds the USDC trustline - if either is missing. The network verifies the user's signature via standard Ed25519 authentication. No contract is involved. The `classicAccount()` adapter is imported from `@buckspay/accounts/classic`; the matching signer is `walletsKit` from `@buckspay/signers/wallets-kit`. ```ts theme={null} // Recipe A - classic wallet account (G...), for users who already have a Stellar wallet. import { createBuckspayClient, createRpcSimContext, type BuckspayConfig } from "@buckspay/core"; import { classicAccount } from "@buckspay/accounts/classic"; import { walletsKit } from "@buckspay/signers/wallets-kit"; import { buckspayFacilitator } from "@buckspay/relayer/buckspay-facilitator"; export const classicConfig: BuckspayConfig = { network: "testnet", account: classicAccount(), signer: walletsKit({ network: "testnet" }), relayer: buckspayFacilitator({ url: "/api/gasless", network: "testnet" }), gas: { mode: "sponsored" } }; // connect() resolves the G... address and runs sponsored onboarding (account + USDC trustline) if missing. export const classicClient = createBuckspayClient( classicConfig, createRpcSimContext("https://soroban-testnet.stellar.org") ); ``` ## Passkey / contract account (`C...`) The hero flow for new users. `connect()` creates a WebAuthn credential, derives the deterministic `C...` address, and deploys the OpenZeppelin Smart Account **sponsored** by the facilitator. The user only taps an authenticator prompt - no seed phrase, no XLM balance required. Signing happens in the device authenticator (secp256r1). The contract's `__check_auth` verifies the signature on-chain. See [Gasless on Stellar](/concepts/gasless-on-stellar) for the mechanics. The `ozContractAccount()` adapter is imported from `@buckspay/accounts/oz-contract`; `sponsorAddress` is the facilitator's public sponsor `G...` address, needed to derive the `C...` address offline. ```ts theme={null} // Recipe B - passkey smart account (C...), the hero flow. Browser only (uses window). import { createBuckspayClient, createRpcSimContext, 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"; // Public facilitator sponsor/deployer G-address (derives the deterministic C-address). const SPONSOR_G: string = "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"; export const passkeyConfig: BuckspayConfig = { network: "testnet", account: ozContractAccount({ network: "testnet", sponsorAddress: SPONSOR_G }), // create()s a passkey, derives the C-address, deploys the OZ Smart Account sponsored. signer: passkey({ rpId: window.location.hostname, rpName: "buckspay" }), relayer: buckspayFacilitator({ url: "/api/gasless", network: "testnet" }), gas: { mode: "sponsored" } }; export const passkeyClient = createBuckspayClient( passkeyConfig, createRpcSimContext("https://soroban-testnet.stellar.org") ); ``` ## The `AccountAdapter` seam `AccountAdapter` is the interface both models implement. OZ Smart Accounts is the only contract implementation in v1, but the interface is open - you can wire in any conforming adapter without changing the rest of the client. ## Next The three-phase flow every Buckspay payment follows. The native Stellar mechanisms behind sponsored and contract accounts. # Facilitator and BFF Source: https://buckspay.mintlify.app/concepts/facilitator-and-bff The relayer's role in the payment flow, and why your API key must stay server-side behind a same-origin BFF. ## The facilitator's role The Buckspay facilitator is the off-chain relay service that takes a signed Stellar authorization entry, wraps it in a fee-bump transaction, and submits it to the network. It is what makes the payment gasless: the facilitator's sponsor account sources the XLM fee so the payer does not need any. In the SDK the facilitator is represented as a `Relayer` - the third config slot in `createBuckspayClient`. You construct one with `buckspayFacilitator({ url, network })` from `@buckspay/relayer/buckspay-facilitator`. ## The same-origin BFF boundary The facilitator authenticates callers via an API key. **The API key must never reach the browser.** A browser bundle is inspectable by any user; shipping the key client-side exposes it to replay attacks and quota abuse. The standard pattern is a **Backend for Frontend (BFF)** route on your own server: 1. The browser runs `prepare()` and `sign()`, then POSTs the `SignedIntent` to your backend. 2. Your backend validates business rules (intent not expired, amount within tolerance, etc.). 3. Your backend calls `server.send(signed)` using a `createBuckspayClient` that is configured with `buckspayFacilitator({ url, apiKey, network })` - the `apiKey` is an environment variable, never bundled. The `apiKey` passed to `buckspayFacilitator` must only appear in server-side code. If you import a module containing the key in a browser bundle, any user can extract it from the network tab. The `RelayPayload` that `send()` forwards is byte-identical to what the facilitator expects - the BFF is a pass-through after your validation, not a translation layer. ## BFF example The example below is the canonical server-side pattern. It is the only Buckspay example that passes an `apiKey` - all browser examples omit it and point `url` at a same-origin route like `/api/gasless`. ```ts theme={null} // SERVER-ONLY - this file holds the facilitator API key. NEVER import it in a browser // bundle. It is the BFF boundary: the browser POSTs a SignedIntent here; this validates // and forwards to the facilitator with the secret key server-side. import { createBuckspayClient, createRpcSimContext, type Receipt, type SignedIntent } from "@buckspay/core"; import { classicAccount } from "@buckspay/accounts/classic"; import { walletsKit } from "@buckspay/signers/wallets-kit"; import { buckspayFacilitator } from "@buckspay/relayer/buckspay-facilitator"; // account/signer are unused by send() (the intent is already signed); only the relayer // (with the server-side key) and gas engine matter here. const server = createBuckspayClient( { network: "testnet", account: classicAccount(), signer: walletsKit({ network: "testnet" }), relayer: buckspayFacilitator({ url: process.env.FACILITATOR_URL ?? "http://localhost:3000", // server-side secret - never shipped to the browser ...(process.env.FACILITATOR_API_KEY ? { apiKey: process.env.FACILITATOR_API_KEY } : {}), network: "testnet" }), gas: { mode: "sponsored" } }, createRpcSimContext(process.env.SOROBAN_RPC_URL ?? "https://soroban-testnet.stellar.org") ); /** BFF handler: validate business rules, then relay. `RelayPayload` is byte-identical to the legacy body. */ export async function bffRelay(signed: SignedIntent): Promise { // ... your business validation: intent exists / not expired / amount tolerance / sponsorship budget ... return server.send(signed); } ``` ## What stays in the browser The browser-side `buckspayFacilitator` call omits `apiKey` entirely and points `url` at your BFF route: ```ts theme={null} relayer: buckspayFacilitator({ url: "/api/gasless", network: "testnet" }) ``` The SDK sends a `POST /relay` to that route with the serialized `RelayPayload`. Your BFF handler validates, then calls the real facilitator with the key. ## Migration from direct fetch If you currently `fetch` the facilitator directly from a backend route (hand-building the `SorobanRelayBody`), the migration is: * Delete the hand-rolled `signTransferAuth` / `normalizeSignature` helpers - `walletsKit` absorbs the double-encode quirk of Freighter automatically. * Replace the raw `fetch` with `server.send(signed)`, where `server` is a `createBuckspayClient` with `buckspayFacilitator({ url, apiKey, network })`. * Keep your rail schema and BFF route path unchanged. ## Next Testnet vs. pubnet, the mainnet opt-in, and USDC decimals. The three-phase flow and where the BFF slot fits in. # Gasless on Stellar Source: https://buckspay.mintlify.app/concepts/gasless-on-stellar How Stellar's native account-abstraction primitives make the payer's XLM balance irrelevant. Stellar has two orthogonal mechanisms that together make a payment genuinely gasless for the end user. Understanding them clarifies why Buckspay needs no fee-abstraction layer bolted on from the outside - it uses what the protocol already provides. ## Sponsor fee-bump and sponsored reserves Every Stellar transaction consumes a small XLM fee, and every account must maintain a minimum XLM reserve to exist on-chain. Normally that burden falls on the account owner. Stellar's **fee-bump** transaction wraps a signed inner transaction with an outer envelope that designates a different account - the *sponsor* - as the fee source. The ledger debits the sponsor, not the inner-transaction signer. The same sponsor pattern applies to **reserves**: when an account or trustline is created inside a `BEGIN_SPONSORING_FUTURE_RESERVES` / `END_SPONSORING_FUTURE_RESERVES` sandwich, the reserve is charged to the sponsor's balance, not the new account's. The payer needs zero XLM - not to send a payment, and not even to appear on-chain for the first time. Buckspay's facilitator is the sponsor for both the fee and the reserve. In Buckspay's sponsored mode the user signs **only** the `SorobanAuthorizationEntry` for their payment. The facilitator wraps that authorization in a fee-bump transaction, sources the XLM fee, and submits to the network. The user's account balance is never consulted for fees. ## `__check_auth` for contract accounts Classic Stellar accounts (`G...`) authenticate via Ed25519 - the network verifies the signature directly. Contract accounts (`C...`) follow a different path: the network calls the contract's `__check_auth` entry point with the authorization payload and the signature(s), and the contract decides whether to accept or reject. This is Soroban's **custom authentication** hook. Buckspay uses an OpenZeppelin Smart Account contract whose `__check_auth` implementation verifies a **secp256r1 (WebAuthn) signature** on-chain. That is what makes passkey-based accounts possible on Stellar: the private key never leaves the user's authenticator, and the secp256r1 check runs inside the contract, settled by the ledger. ## Putting them together For a passkey account payment: 1. The browser calls `prepare()`, which simulates the Soroban call and records the authorization entries. 2. The user's authenticator signs the authorization entry (secp256r1 via WebAuthn). 3. `send()` (via the BFF) hands the signed intent to the facilitator. 4. The facilitator wraps the transaction in a fee-bump and submits - the contract's `__check_auth` validates the passkey signature on-chain; the sponsor covers the fee. The result: the user taps an authenticator prompt and the payment settles. No seed phrases, no XLM balance, no transaction approval dialog. ```ts theme={null} // Gasless modes - v1 ships exactly one: sponsored. The facilitator's sponsor account // pays the XLM fee; the payer needs zero XLM. import type { GasConfig } from "@buckspay/core"; export const sponsored: GasConfig = { mode: "sponsored" }; // Roadmap (NOT available in v1 - do not pass these): // { mode: "token", token: "USDC:GA5..." } // pay gas in USDC via FeeForwarder // { mode: "self" } // payer pays their own fee // The GasConfig type only admits `{ mode: "sponsored" }` today, so an unimplemented // mode fails to type-check - the docs can't drift into an unsupported config. ``` ## Next Classic G-addresses vs. passkey smart-contract accounts. The three-phase flow every Buckspay payment follows. # Networks Source: https://buckspay.mintlify.app/concepts/networks Testnet vs. pubnet, the mainnet opt-in guard, USDC decimals, and the multi-network rail model. ## Testnet and pubnet Buckspay supports two Stellar networks: | Value | Network | Use 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: ```ts theme={null} 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`: ```ts theme={null} // 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 { 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 How to configure the relayer and keep the API key server-side. Classic and passkey accounts - both work on testnet and pubnet. # Prepare -> Sign -> Send Source: https://buckspay.mintlify.app/concepts/prepare-sign-send The three-phase mental model every Buckspay payment follows - and where to insert backend validation. Every Buckspay payment - regardless of account model, gas mode, or network - follows the same three-phase flow. Understanding this model makes the rest of the API predictable. ### Prepare `prepare(calls)` simulates the Soroban transaction against the network's RPC and records the authorization entries the signer must approve. It returns a `PreparedIntent` - a typed value that carries the simulation result, the ledger footprint, and the entries awaiting signature. No network write happens at this step. `createRpcSimContext` (or `mainnetSimContext` for pubnet) provides the Soroban RPC simulator that `prepare` uses. Pass it once when constructing the client. ### Sign `sign(prepared)` passes the authorization entries to the configured `BuckspaySigner`. For a classic wallet the signer invokes the wallet extension; for a passkey account it triggers a WebAuthn assertion in the device authenticator. The result is a `SignedIntent` - the same entries, now carrying the cryptographic authorization. **This is the only user-interaction point.** The user sees a single signing prompt, regardless of how many calls are batched. ### Send `send(signed)` serializes the `SignedIntent` into a `RelayPayload` and posts it to the configured `Relayer`. For browser apps the relayer URL points at your own backend route (the BFF), which validates the intent and forwards it to the Buckspay facilitator with the API key server-side. The facilitator wraps the transaction in a fee-bump and submits it to Stellar. `send` resolves with a `Receipt` once the transaction is finalized on-chain. ## The `pay` shortcut `client.pay(calls)` chains all three phases in a single call. It is equivalent to: ```ts theme={null} const prepared = await client.prepare(calls); const signed = await client.sign(prepared); const receipt = await client.send(signed); ``` Use the three-step form when you need to insert backend validation between `sign` and `send` - for example, to check that the intent has not expired or that the amount is within a pre-approved tolerance. ## Where the BFF fits The `SignedIntent` that comes out of `sign()` is safe to POST to your backend: it carries no private key material. Your server validates business rules, then calls `server.send(signed)` - using a server-side `createBuckspayClient` configured with the `buckspayFacilitator` API key. The API key never leaves the server. See [Facilitator and BFF](/concepts/facilitator-and-bff) for the full pattern. ## Next How the relayer works and why the API key must stay server-side. Which account type to use and how they affect the sign step. # Atomic batch Source: https://buckspay.mintlify.app/features/atomic-batch Settle multiple transfers all-or-nothing in one Stellar transaction with a single signature. `sendCalls([...])` settles N calls **all-or-nothing in one transaction**, authorized with a **single** signature. It is the EIP-5792-style alias of `pay` - same input shape, same `Receipt` - with the atomicity guarantee made explicit. ## Quick example ```ts theme={null} // Recipe 10 - ATOMIC BATCH (sendCalls). N USDC transfers settle all-or-nothing in ONE tx via the // pinned Multicall router's `batch_transfer` - the smart account (or classic wallet) authorizes the // whole batch with a SINGLE signature. A batch of 1 is exactly pay([call]). import { createBuckspayClient, batch, MAX_BATCH_CALLS, BuckspayError, 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"; const SPONSOR_G = "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"; const USDC_SAC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; const A = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"; const B = "GBPYQYRH62E6NLRGXHBT4I3ZPTEHVBQMYTSH44YLOAPTCDYNAXDOLJRY"; export const batchConfig: BuckspayConfig = { network: "testnet", account: ozContractAccount({ network: "testnet", sponsorAddress: SPONSOR_G }), signer: passkey({ rpId: "localhost", rpName: "buckspay" }), relayer: buckspayFacilitator({ url: "/api/gasless", network: "testnet" }), gas: { mode: "sponsored" } }; export const client = createBuckspayClient(batchConfig); export async function payManyAtomically(): Promise { await client.connect(); // Collect calls with the pure builder (enforces MAX_BATCH_CALLS on build()). const calls = batch() .add(client.transfer({ token: USDC_SAC, to: A, amount: "1.00" })) .add(client.transfer({ token: USDC_SAC, to: B, amount: "2.50" })) .build(); // sendCalls = atomic, all-or-nothing. Either BOTH transfers land or NEITHER does. const receipt = await client.sendCalls(calls); console.log(receipt.transferTx); // one settlement tx for the whole batch } export async function guardOversizeBatch(): Promise { const tooMany = Array.from({ length: MAX_BATCH_CALLS + 1 }, () => client.transfer({ token: USDC_SAC, to: A, amount: "0.01" }) ); try { await client.sendCalls(tooMany); } catch (e) { if (e instanceof BuckspayError && e.code === "BATCH_TOO_LARGE") { console.error(`batch capped at ${MAX_BATCH_CALLS} calls`); return; } throw e; } } ``` ## The `batch()` builder `batch()` is a pure, fluent builder that collects calls and enforces the size cap on `build()`: ```ts theme={null} import { batch, MAX_BATCH_CALLS } from "@buckspay/core"; const calls = batch() .add(client.transfer({ token: USDC_SAC, to: A, amount: "1.00" })) .add(client.transfer({ token: USDC_SAC, to: B, amount: "2.50" })) .build(); // throws BATCH_TOO_LARGE if > MAX_BATCH_CALLS const receipt = await client.sendCalls(calls); ``` You can also pass an array directly to `sendCalls([...])` - the builder is optional. ## Native mechanism The atomic unit is always the **transaction**; partial application is impossible at the protocol level - not enforced by retry logic in the SDK. | Account type | Mechanism | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | Classic (`G...`) | Multi-operation Stellar transaction: operations share one envelope, so all land or none do. | | Contract (`C...`) | The pinned Multicall router's `batch_transfer`: the smart account authorizes the whole batch in `__check_auth` with one signature. | ## Size cap `MAX_BATCH_CALLS` is **16**. `batch().build()` and `sendCalls` directly both reject an oversized batch with `BuckspayError("BATCH_TOO_LARGE")`. Keep batches bounded so a single authorization entry stays within simulation and fee limits. ## Parity invariant A **batch of one is byte-identical to the single-call entry** produced by `pay([call])`. Batching adds a wrapper only for N > 1; it never changes the one-call path. This invariant is pinned by a regression golden so the core path cannot drift. **Throughput for independent payments.** Settling many independent payments fast is a separate concern from atomic batching. The facilitator spreads independent calls across a pool of channel accounts with non-sequential nonces - transparently, with no API change to how you call `pay`. ## Next Grant scoped session keys so users can pay within fixed limits without a root prompt per action. Let users pay transaction fees in USDC instead of XLM. # Gas in token Source: https://buckspay.mintlify.app/features/gas-in-token Let users pay Soroban transaction fees in USDC instead of XLM via the FeeForwarder contract. `gas: { mode: "token", token }` lets the payer settle the Soroban fee in USDC rather than holding XLM. The facilitator still fronts the actual XLM gas; the `FeeForwarder` contract reimburses it from the payer's stablecoin balance atomically. Choose this mode when **you want the user, not your sponsor, to cover the fee** in the asset they already hold. For the sponsor-pays default, see [Gasless modes](/features/gasless-modes). ## Configuration ```ts theme={null} import type { BuckspayConfig } from "@buckspay/core"; const config: BuckspayConfig = { network: "testnet", // ...account, signer, relayer... gas: { mode: "token", token: USDC_SAC, // C-address of the fee token maxFee: "2000000" // optional ceiling in stroops (here: 0.2 USDC) } }; ``` ## How it settles - one signature `prepare()` calls `POST /fee/quote`, which returns a `FeeQuote`: | Field | Type | Description | | ----------------- | -------- | -------------------------------------------------- | | `forwarder` | `string` | `FeeForwarder` contract address that pulls the fee | | `collector` | `string` | Destination for the relayer's reimbursement | | `token` | `string` | The fee token (echoes your config) | | `estimatedXlmFee` | `string` | XLM the relayer fronts for the transaction fee | | `tokenAmount` | `string` | Amount owed by the payer in `token` | | `expiresAtLedger` | `number` | Ledger number after which the quote is invalid | The engine builds a **single** `FeeForwarder.forward(payer, token, merchant, payment, collector, fee)` invocation - one authorization entry that pays the merchant **and** reimburses the relayer's XLM gas at the same time. The payer signs once; there is no second entry to approve. ## The `maxFee` ceiling `maxFee` (optional, in stroops) is a hard cap on the quoted fee. If `tokenAmount` from the quote exceeds `maxFee`, `prepare()` throws `BuckspayError("TOKEN_GAS_REJECTED")` **before** anything is signed. A fee surge can never surprise the user at signing time. Omit `maxFee` to accept the facilitator's quote as-is. ## Full example ```ts theme={null} // Recipe 09 - GAS IN TOKEN. The payer pays Soroban gas in USDC instead of XLM. The SDK does NOT relay // the direct transfer - it relays a SINGLE FeeForwarder `forward(payer, token, merchant, payment, // collector, fee)` invocation that pays the merchant AND the relayer's gas in one auth entry, so the // user signs ONCE. Sponsored mode (recipe 03) needs none of this; this is the opt-in for "the user holds // USDC but no XLM". import { createBuckspayClient, createRpcSimContext, type BuckspayConfig } from "@buckspay/core"; import { classicAccount } from "@buckspay/accounts/classic"; import { walletsKit } from "@buckspay/signers/wallets-kit"; import { buckspayFacilitator } from "@buckspay/relayer/buckspay-facilitator"; // Testnet USDC SAC (C-address). The caller passes it; the SDK is asset-agnostic. The fee token here is the // SAME USDC the user transfers - they pay both the value and the gas in it. const USDC_SAC_TESTNET: string = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; const MERCHANT: string = "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"; export const tokenGasConfig: BuckspayConfig = { network: "testnet", account: classicAccount(), signer: walletsKit({ network: "testnet" }), // url points at YOUR backend, which forwards to the facilitator with the key server-side. relayer: buckspayFacilitator({ url: "/api/gasless", network: "testnet" }), // Pay gas in USDC. prepare() quotes the fee + forwarder + collector, builds ONE forward() entry, and // refuses a quote above `maxFee` (stroops) with BuckspayError("TOKEN_GAS_REJECTED"). gas: { mode: "token", token: USDC_SAC_TESTNET, maxFee: "2000000" } // 0.2 USDC ceiling }; export const tokenGasClient = createBuckspayClient( tokenGasConfig, createRpcSimContext("https://soroban-testnet.stellar.org") ); export async function payGasInUsdc(): Promise { await tokenGasClient.connect(); const call = tokenGasClient.transfer({ token: USDC_SAC_TESTNET, to: MERCHANT, amount: "1.50" }); // prepare -> feeQuote -> build the single forward() entry -> sign once -> relay. Settles on testnet. const receipt = await tokenGasClient.pay([call]); console.log(receipt.transferTx); } ``` ## Next Overview of the GasConfig union and when to use each mode. Settle multiple transfers in one all-or-nothing transaction. # Gasless modes Source: https://buckspay.mintlify.app/features/gasless-modes Sponsored vs. token-fee gas abstraction - choose who pays for the transaction fee. `GasConfig` controls who covers the Stellar transaction fee. The type is a discriminated union with exactly two members - any other value fails to type-check: ```ts theme={null} type GasConfig = | { mode: "sponsored" } | { mode: "token"; token: string; maxFee?: string }; ``` ## Sponsored mode The facilitator's sponsor account pays the XLM fee on behalf of the payer. The payer signs only the `SorobanAuthorizationEntry` off-chain; the facilitator wraps it in a fee-bump transaction, supplies the XLM, and submits to the network. **The payer needs zero XLM** - not to transact, and not even to exist on-chain. This is the default for most applications. See [Onboarding](/features/onboarding) to understand how an account with no XLM balance is materialized in the first place. ```ts theme={null} // Gasless modes - v1 ships exactly one: sponsored. The facilitator's sponsor account // pays the XLM fee; the payer needs zero XLM. import type { GasConfig } from "@buckspay/core"; export const sponsored: GasConfig = { mode: "sponsored" }; // Roadmap (NOT available in v1 - do not pass these): // { mode: "token", token: "USDC:GA5..." } // pay gas in USDC via FeeForwarder // { mode: "self" } // payer pays their own fee // The GasConfig type only admits `{ mode: "sponsored" }` today, so an unimplemented // mode fails to type-check - the docs can't drift into an unsupported config. ``` ## Token-fee mode `{ mode: "token", token }` lets the payer cover the fee in USDC instead of XLM. The facilitator still fronts the XLM gas; the `FeeForwarder` contract collects reimbursement from the payer's stablecoin balance in a single authorization entry - the payer signs once. Use token-fee when **you want the user, not your sponsor, to bear the gas cost** in the asset they already hold. See [Gas in token](/features/gas-in-token) for the full details: the `FeeQuote` fields, the `maxFee` ceiling, and the `TOKEN_GAS_REJECTED` error code. ## Choosing a mode | Scenario | Recommended mode | | -------------------------------------- | ----------------------------------- | | Your app subsidizes fees (UX priority) | `sponsored` | | User holds USDC, no sponsor budget | `token` | | Any other token | not yet supported - use `sponsored` | The `GasConfig` type only admits these two members. Passing an undeclared mode is a compile-time error - you cannot accidentally configure something unsupported. ## Next Full reference for the USDC fee-forwarding mode: FeeQuote, maxFee, and error codes. How a payer with zero XLM gets a payment-ready account, sponsored end-to-end. # Feature coverage Source: https://buckspay.mintlify.app/features/index Every Buckspay feature mapped to its native Stellar mechanism, SDK surface, runnable example, and guide page. Every Buckspay feature is an extension of an existing primitive that maps to a **native Stellar mechanism** - "adding an auth method is an adapter, not a rewrite." This table is the index from feature -> mechanism -> SDK surface -> runnable example -> guide. | Feature | Native Stellar mechanism | SDK surface | Example | Guide | | ---------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------- | ------------------------------------------------ | | Gasless (sponsored) | sponsor fee-bump (CAP-15) + sponsored reserves (CAP-33) | `gas: { mode: "sponsored" }` | `03-gasless-sponsored.ts` | [Gasless modes](/features/gasless-modes) | | Passkey smart account | secp256r1 in `__check_auth` (CAP-51) | `@buckspay/accounts/oz-contract` + `passkey` | `02b-passkey-account.ts` | [Account models](/concepts/account-models) | | Gas in stablecoin | token-forwarding contract via relayer (FeeForwarder) | `gas: { mode: "token", token }` | `09-gas-in-token.ts` | [Gas in token](/features/gas-in-token) | | Atomic batch | multi-op tx (classic) / Multicall contract (Soroban) | `sendCalls([...])` / `batch()` | `10-batch.ts` | [Atomic batch](/features/atomic-batch) | | Sessions | policy signers in `__check_auth` (spend limit + allowlist on-chain) | `grantSession` / `revokeSession` | `11-sessions.ts` | [Sessions overview](/features/sessions/overview) | | Parallel (transparent) | channel-account pool + non-sequential nonces (Protocol 23) | none (facilitator throughput) | - | [Atomic batch](/features/atomic-batch) | | Social login | external provider -> ed25519 key -> smart-account signer | `socialSigner({ provider: "web3auth" })` | `12-social-login.ts` | [Social login](/signers/social-login) | | Email/SMS login | external provider -> derived ed25519 key | `emailSigner({ proxyUrl })` | `16-email-login.ts` | [Social login](/signers/social-login) | | Mobile (React Native) | native passkey (WebAuthn) + secure storage | `@buckspay/react-native` `nativePasskey` | `13-react-native.tsx` | [React Native](/platforms/react-native) | | Server bindings (BFF) | server-side relay + signer proxy | `@buckspay/nextjs` `createRelayRoute` / `createSignerProxyRoute` | `15-nextjs-bff.ts` | [Next.js BFF](/platforms/nextjs-bff) | | Swaps (optional) | native DEX / Soroswap (reuses the facilitator `/swap/*` rail) | `quoteSwap` / `swap` | `14-swap.ts` | [Swaps](/features/swaps) | > **Swaps is optional** - the first surface deferred if the cycle tightens. The core (connect, > gasless pay, gas-in-token, batch, sessions, social/email login, React Native) does not depend on > it. ## Swaps - cut decision **The EVM/Stellar seam.** The facilitator's existing `/swap/*` rail is the EVM Calibur (EIP-7702) path: `/swap/submit` consumes an EIP-712 `batchSignature` from the payer's **EVM wallet**, not the SDK's Soroban `BuckspaySigner`. `quoteSwap` is signer-agnostic and ships end-to-end; the `swap` **submit** leg needs the EVM wallet path (the app BFF -> `/swap/submit`). > **Decided: KEEP - 2026-06-30** **As KEPT (current state):** * `BuckspayClient.quoteSwap` ships end-to-end (delegates to `/swap/quote`, maps to `SwapQuote`). * `BuckspayClient.swap` enforces the `minOut` floor before submit and fails closed (`SWAP_FAILED`) until the EVM wallet submit path (app BFF -> `/swap/submit`) is wired; the full submit leg / a native Stellar (Soroswap) rail is the future swap work. * `buckspayFacilitator({ swapChain })` exposes `quoteSwap`/`swap`; without `swapChain` both are omitted. `docs/examples/14-swap.ts` and the gated `swap.e2e.test.ts` cover the quote path. **If later cut:** revert this surface - the `Receipt.chain` widening, `Relayer.quoteSwap?`/`swap?`, the `BuckspayClient.quoteSwap`/`swap` methods, the `swapChain` option, `docs/examples/14-swap.ts`, the gated `swap.e2e.test.ts`, and this section's KEEP detail - and move swaps to a native DEX / Soroswap rail on the Stellar side (removing the EVM seam). # Onboarding Source: https://buckspay.mintlify.app/features/onboarding Sponsored account creation and trustline setup - the payer needs zero XLM, not even to exist on-chain. `connect()` calls `account.ensureReady(...)` to materialize a payment-ready account - sponsored end-to-end, so the payer needs no XLM. This covers both the classic `G...` account (funded reserves + USDC trustline) and the contract `C...` account (smart-account deployment). ## Read state first Check what the account still needs before calling `connect()`: ```ts theme={null} import type { AccountState } from "@buckspay/core"; const state: AccountState = await client.getAccountState(address); // { exists, hasUsdcTrustline, xlmBalance?, usdcBalance? } if (!state.exists || !state.hasUsdcTrustline) { await client.connect(); // materializes the account, sponsored } ``` ## Full example ```ts theme={null} // Onboarding - read account state first, then connect() materializes it (sponsored). import type { AccountState } from "@buckspay/core"; import { classicClient } from "./02a-classic-account.js"; export async function ensureOnboarded(address: string): Promise { const state: AccountState = await classicClient.getAccountState(address); // For classic: missing account or no USDC trustline -> connect() runs the sponsored // sandwich (createAccount + changeTrust), the sponsor covers the XLM reserves (CAP-0033). if (!state.exists || !state.hasUsdcTrustline) { await classicClient.connect(); } // For the contract model, connect() deploys the OZ Smart Account sponsored instead. } export function describeState(state: AccountState): string { const xlm = state.xlmBalance ?? "0"; const usdc = state.usdcBalance ?? "0"; return `exists=${String(state.exists)} trustline=${String(state.hasUsdcTrustline)} xlm=${xlm} usdc=${usdc}`; } ``` ## Classic onboarding (`G...`) If the `G...` account does not exist or has no USDC trustline, the SDK: 1. Fetches an unsigned sponsor-sandwich transaction from the facilitator (`relayer.buildOnboard`). 2. The wallet signs it. 3. `relayer.submitOnboard` co-signs and submits. The sponsor covers the XLM reserves using Stellar's sponsored-reserves mechanism (CAP-0033). **The payer contributes zero XLM** - not for reserves, not for the fee. ## Contract deploy (`C...`) `ensureReady` deploys the OpenZeppelin Smart Account contract via `relayer.deployContract({ passkeyPublicKey })`, sponsored by the facilitator. The contract address (`C...`) is returned and stored in the account adapter. `connect()` is **idempotent** - calling it again on a fully onboarded account is a no-op. The payer needs zero XLM - not to transact, and not even to appear on-chain for the first time. The facilitator acts as sponsor for both the fee and the on-chain reserves. ## Next How sponsored and token-fee gas modes work after onboarding. Classic G-addresses vs. passkey smart-contract accounts. # Allowlist policy Source: https://buckspay.mintlify.app/features/sessions/allowlist Restrict a session key to a fixed set of contracts - enforced on-chain in __check_auth. `allowlist` restricts which contracts a session key may call. Any call to a contract not in the list is rejected **on-chain** in `__check_auth`. A stolen session blob cannot be used to interact with arbitrary contracts - it is scoped to exactly the set you declare. ## Usage ```ts theme={null} import { allowlist } from "@buckspay/accounts/policy"; const policy = allowlist([ APP_CONTRACT, // C-address of an allowed contract USDC_SAC // another allowed contract (e.g. the token itself) ]); ``` Pass the policy in the `policies` array when calling `grantSession`: ```ts theme={null} const { session } = await client.grantSession({ sessionKey: { type: "ed25519", publicKey: sessionKp.publicKey() }, policies: [allowlist([APP_CONTRACT])], expiresAt: Date.now() + 86_400_000 }); ``` ## What happens when a call targets an unlisted contract The contract rejects the authorization during `__check_auth`. The facilitator maps the on-chain rejection to `BuckspayError("SESSION_POLICY_VIOLATION")`. The allowlist check runs inside the contract - not in the SDK, not in the facilitator. A session key with an allowlist of `[APP_CONTRACT]` cannot be used to call any other contract, even if the session blob is extracted from memory. ## Combining policies `allowlist` is commonly paired with `spendLimit` to scope the session both by callable contracts and by cumulative spend. See [Sessions overview](/features/sessions/overview) for the full lifecycle example, which uses both policies together. ## Next Cap cumulative token spend over a rolling period. Full lifecycle: grant, serialize, use, and revoke a session. # Sessions Source: https://buckspay.mintlify.app/features/sessions/overview Scoped, expiring session keys that let users pay within fixed on-chain policies without a root prompt per action. A **session** is a scoped, expiring key the app mints so the user can pay within fixed limits **without a root prompt per action**. The session key is never the account's root key - it carries on-chain policies (a spend limit, an allowlist) and is revocable at any time. Sessions are a **contract (`C...`) account** feature only. On the classic model, `grantSession` throws `BuckspayError("INVALID_CONFIG")` - a classic `G...` key has no on-chain policy surface to scope. ## Lifecycle ```ts theme={null} import { serializeSession, deserializeSession } from "@buckspay/core"; import { spendLimit, allowlist } from "@buckspay/accounts/policy"; // 1. Grant - root-signed; installs the session key + policies on-chain. const { session, receipt } = await client.grantSession({ sessionKey: { type: "ed25519", publicKey: sessionKp.publicKey() }, policies: [ spendLimit({ token: USDC_SAC, max: "100", period: "day" }), allowlist([APP_CONTRACT]) ], expiresAt: Date.now() + 86_400_000 // 24 h }); // 2. Serialize to secure storage; rehydrate with deserializeSession. const blob = serializeSession(session); // 3. The session key pays within its policies - no per-action root prompts. // 4. Revoke when done - root-signed; the key authorizes nothing afterward. await client.revokeSession(session); ``` ## Full example ```ts theme={null} // Recipe 11 - SESSIONS (contract/passkey accounts only). Grant a scoped session key with on-chain // policies (spend limit + allowlist), serialize it for later, then revoke it. Sessions are refused on // the classic model: grantSession throws BuckspayError("INVALID_CONFIG"). import { Keypair } from "@stellar/stellar-sdk"; import { createBuckspayClient, createRpcSimContext, serializeSession, type BuckspayConfig } from "@buckspay/core"; import { ozContractAccount } from "@buckspay/accounts/oz-contract"; import { spendLimit, allowlist } from "@buckspay/accounts/policy"; import { passkey } from "@buckspay/signers/passkey"; import { buckspayFacilitator } from "@buckspay/relayer/buckspay-facilitator"; const SPONSOR_G = "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"; const USDC_SAC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; const APP_CONTRACT = "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75"; const config: BuckspayConfig = { network: "testnet", account: ozContractAccount({ network: "testnet", sponsorAddress: SPONSOR_G }), signer: passkey({ rpId: "localhost", rpName: "buckspay" }), // the ROOT signer authorizes the install relayer: buckspayFacilitator({ url: "/api/gasless", network: "testnet" }), gas: { mode: "sponsored" } }; const client = createBuckspayClient( config, createRpcSimContext("https://soroban-testnet.stellar.org", { simSource: SPONSOR_G }) ); export async function runSession(): Promise { await client.connect(); // The session key is the ONE key the app deliberately mints - scoped + revocable, never the root // account key. Store its secret in secure storage; the SDK only ever needs its public key. const sessionKp = Keypair.random(); const { session, receipt } = await client.grantSession({ sessionKey: { type: "ed25519", publicKey: sessionKp.publicKey() }, policies: [spendLimit({ token: USDC_SAC, max: "100", period: "day" }), allowlist([APP_CONTRACT])], expiresAt: Date.now() + 86_400_000 // 24h }); console.log("granted", session.id, receipt.transferTx); // Persist the session (e.g. to secure storage) and rehydrate it later with deserializeSession. const blob = serializeSession(session); console.log("serialized session blob length", blob.length); // ...the session key now pays within its policies, with no per-action root prompts... // Revoke when done (root-signed); the session key no longer authorizes anything. const revokeReceipt = await client.revokeSession(session); console.log("revoked", revokeReceipt.transferTx); } ``` ## The native mechanism The policies compile to **policy signers evaluated on-chain in `__check_auth`**. The contract - not the SDK - enforces them, so a stolen session blob still cannot exceed its limits or call outside its allowlist. ## Error codes | Code | Meaning | | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `SESSION_EXPIRED` | The session is past `expiresAt`. The SDK rejects it before the relay; the contract would reject it too. | | `SESSION_POLICY_VIOLATION` | A payment breaches a policy - over the spend limit, or a contract outside the allowlist. The facilitator maps the on-chain rejection to this code. | | `INVALID_CONFIG` | `grantSession` was called on a classic (`G...`) account. | ## Install and revoke use the gated relay `grantSession` and `revokeSession` are ordinary root-signed operations over the same gated `/relay` path as any payment. They inherit its nonce and expiration replay protection. There is no separate, weaker channel for session management. ## Policies Cap cumulative spend of a token over a rolling period (day, week, month, or total). Restrict which contracts the session key may call. # Spend limit policy Source: https://buckspay.mintlify.app/features/sessions/spend-limit Cap a session key's cumulative token spend over a rolling period - enforced on-chain in __check_auth. `spendLimit` caps how much of a given token a session key may spend over a rolling time window. The limit is enforced **on-chain** in the contract's `__check_auth` - not in the SDK, not in the facilitator. A stolen session blob still cannot exceed its configured cap. ## Usage ```ts theme={null} import { spendLimit } from "@buckspay/accounts/policy"; const policy = spendLimit({ token: USDC_SAC, // C-address of the token to cap max: "100", // maximum spend in display units (e.g. "100" = 100 USDC) period: "day" // rolling window: "day" | "week" | "month" | "total" }); ``` Pass the policy in the `policies` array when calling `grantSession`: ```ts theme={null} const { session } = await client.grantSession({ sessionKey: { type: "ed25519", publicKey: sessionKp.publicKey() }, policies: [spendLimit({ token: USDC_SAC, max: "100", period: "day" })], expiresAt: Date.now() + 86_400_000 }); ``` ## Period values | Value | Window | | --------- | --------------------------------------- | | `"day"` | Rolling 24-hour window | | `"week"` | Rolling 7-day window | | `"month"` | Rolling 30-day window | | `"total"` | Lifetime cap for the session (no reset) | ## What happens when the limit is exceeded A payment that would exceed the cap is rejected by the contract during `__check_auth`. The facilitator maps the on-chain rejection to `BuckspayError("SESSION_POLICY_VIOLATION")`. The limit is per-session, per-token. Two separate sessions each with `max: "100"` can each spend up to 100 USDC independently. ## Combining policies `spendLimit` is commonly paired with `allowlist` so the session key is scoped both by spend and by callable contracts. See [Sessions overview](/features/sessions/overview) for the full lifecycle example. ## Next Restrict which contracts the session key may call. Full lifecycle: grant, serialize, use, and revoke a session. # Swaps Source: https://buckspay.mintlify.app/features/swaps Optional token-swap surface via the facilitator's swap rail - quoteSwap ships end-to-end; swap enforces a minOut floor. Swaps is an **optional** feature surface. `quoteSwap` works end-to-end. `swap` enforces the `minOut` floor before submit and fails closed (`SWAP_FAILED`) until the full EVM wallet submit path is wired through your BFF. The core SDK (connect, pay, batch, sessions) does not depend on this surface. ## Enabling swaps Pass `swapChain` to `buckspayFacilitator`. Without it, `quoteSwap` and `swap` are omitted from the relayer and both methods throw if called: ```ts theme={null} import { buckspayFacilitator } from "@buckspay/relayer/buckspay-facilitator"; const relayer = buckspayFacilitator({ url: "/api/gasless", // your BFF - the facilitator API key stays server-side network: "testnet", swapChain: "base-sepolia" }); ``` ## `quoteSwap` - ships end-to-end `quoteSwap` is signer-agnostic and delegates to `/swap/quote`: ```ts theme={null} const quote = await client.quoteSwap({ tokenIn: "0xSellTokenAddress", tokenOut: "0xBuyTokenAddress", amount: "1000000" }); // quote: SwapQuote ``` ## `swap` - enforces the floor, fails closed `swap` checks `minOut` before any submit. If the live quote falls below `minOut`, it throws `BuckspayError("SWAP_FAILED")` without signing or submitting anything: ```ts theme={null} const receipt = await client.swap({ tokenIn: "0xSellTokenAddress", tokenOut: "0xBuyTokenAddress", amount: "1000000", minOut: "985000" // floor: reject if the received amount would be below this }); ``` ## Full example ```ts theme={null} // Recipe 14 - SWAP (STRETCH, optional). Gasless token swap via the facilitator's existing // /swap rail. NOTE: this is the first feature cut if the cycle tightens - the core surface does // NOT depend on it. The submit leg uses the EVM wallet typed-data signature through your BFF. import { createBuckspayClient, createRpcSimContext, type SwapQuote } from "@buckspay/core"; import { classicAccount } from "@buckspay/accounts/classic"; import { walletsKit } from "@buckspay/signers/wallets-kit"; import { buckspayFacilitator } from "@buckspay/relayer/buckspay-facilitator"; const client = createBuckspayClient( { network: "testnet", account: classicAccount(), signer: walletsKit({ network: "testnet" }), // `swapChain` enables quoteSwap/swap; without it the relayer omits them (fails closed). relayer: buckspayFacilitator({ url: "/api/gasless", // your BFF - the facilitator API key stays server-side network: "testnet", swapChain: "base-sepolia" }), gas: { mode: "sponsored" } }, createRpcSimContext("https://soroban-testnet.stellar.org") ); export async function quoteOnly(): Promise { await client.connect(); // quoteSwap is signer-agnostic and works end-to-end today. return client.quoteSwap({ tokenIn: "0xSellTokenAddress", tokenOut: "0xBuyTokenAddress", amount: "1000000" }); } export async function swapWithFloor(): Promise { await client.connect(); // minOut is enforced BEFORE any submit; a below-floor quote throws SWAP_FAILED. const receipt = await client.swap({ tokenIn: "0xSellTokenAddress", tokenOut: "0xBuyTokenAddress", amount: "1000000", minOut: "985000" }); console.log(receipt.transferTx); } ``` ## Next Settle multiple transfers all-or-nothing in one transaction. How a user with zero XLM gets a payment-ready account. # Installation Source: https://buckspay.mintlify.app/get-started/installation Add Buckspay SDK packages to your project. ## Requirements * **Node.js ≥ 20** (LTS recommended) * `@stellar/stellar-sdk` is a peer dependency of `@buckspay/core` - install it alongside the SDK ## Core package Every Buckspay integration starts with `@buckspay/core`. Install it with your preferred package manager: ```bash pnpm theme={null} pnpm add @buckspay/core @stellar/stellar-sdk ``` ```bash npm theme={null} npm install @buckspay/core @stellar/stellar-sdk ``` ```bash yarn theme={null} yarn add @buckspay/core @stellar/stellar-sdk ``` ```bash bun theme={null} bun add @buckspay/core @stellar/stellar-sdk ``` ## Surface packages Add the surface packages for your target platform. You only need the ones you use. ### Web (browser) ```bash pnpm theme={null} pnpm add @buckspay/react @buckspay/signers @buckspay/accounts @buckspay/relayer ``` ```bash npm theme={null} npm install @buckspay/react @buckspay/signers @buckspay/accounts @buckspay/relayer ``` ```bash yarn theme={null} yarn add @buckspay/react @buckspay/signers @buckspay/accounts @buckspay/relayer ``` ```bash bun theme={null} bun add @buckspay/react @buckspay/signers @buckspay/accounts @buckspay/relayer ``` ### Mobile (React Native) ```bash pnpm theme={null} pnpm add @buckspay/react-native @buckspay/signers @buckspay/accounts @buckspay/relayer ``` ```bash npm theme={null} npm install @buckspay/react-native @buckspay/signers @buckspay/accounts @buckspay/relayer ``` ```bash yarn theme={null} yarn add @buckspay/react-native @buckspay/signers @buckspay/accounts @buckspay/relayer ``` ```bash bun theme={null} bun add @buckspay/react-native @buckspay/signers @buckspay/accounts @buckspay/relayer ``` ### Next.js BFF (server-side route) ```bash pnpm theme={null} pnpm add @buckspay/nextjs @buckspay/relayer ``` ```bash npm theme={null} npm install @buckspay/nextjs @buckspay/relayer ``` ```bash yarn theme={null} yarn add @buckspay/nextjs @buckspay/relayer ``` ```bash bun theme={null} bun add @buckspay/nextjs @buckspay/relayer ``` ## Package overview Not sure which packages you need? See [Which package do I need?](/get-started/which-package) for a one-line summary of each. # Quickstart Source: https://buckspay.mintlify.app/get-started/quickstart Send a gasless USDC transfer on Stellar in under five minutes. Buckspay makes a gasless Stellar USDC payment a three-call affair. The user signs with a wallet they already have (Freighter, xBull, LOBSTR); Buckspay's facilitator pays the XLM fee. **No private keys ever touch your app.** ### Install the packages Add the core client plus the account, signer, and relayer packages you need for a classic Stellar address flow. ```bash theme={null} pnpm add @buckspay/core @buckspay/accounts @buckspay/signers @buckspay/relayer ``` ### Run the full example The snippet below is the complete, runnable file - compiled and type-checked in CI. It: 1. **Creates a client** with a classic G-address account, a Wallets Kit browser signer, and your backend BFF route as the relayer. `createRpcSimContext` gives `prepare()` a recording simulator over the Soroban RPC. 2. **Connects** the user's wallet and ensures the account is ready on-chain. 3. **Calls `transfer`** to build the payment intent. 4. **Calls `pay`** - which runs prepare -> sign -> send in one call. 5. **Logs the settled receipt** (`receipt.transferTx`). ```ts theme={null} // Quickstart - classic gasless USDC transfer (browser; NO apiKey). import { createBuckspayClient, createRpcSimContext } from "@buckspay/core"; import { classicAccount } from "@buckspay/accounts/classic"; import { walletsKit } from "@buckspay/signers/wallets-kit"; import { buckspayFacilitator } from "@buckspay/relayer/buckspay-facilitator"; const USDC_SAC: string = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; const MERCHANT: string = "GA6HCMBLTZS5VYYBCATRBR5VBZJEH5C2OON6XQGB3RNYDDAQ7JZ65YQH"; export async function quickstart(): Promise { const buckspay = createBuckspayClient( { network: "testnet", account: classicAccount(), signer: walletsKit({ network: "testnet" }), // url points at YOUR backend, which forwards to the facilitator with the key server-side. relayer: buckspayFacilitator({ url: "/api/gasless", network: "testnet" }), gas: { mode: "sponsored" } }, // prepare() simulates against the Soroban RPC. createRpcSimContext("https://soroban-testnet.stellar.org") ); await buckspay.connect(); // wallet + ensureReady const call = buckspay.transfer({ token: USDC_SAC, to: MERCHANT, amount: "1.50" }); const receipt = await buckspay.pay([call]); // prepare -> sign -> send console.log(receipt.transferTx); // settled on testnet } ``` > **No API key in the browser.** `url: "/api/gasless"` points at *your* backend route, which > forwards the request to the facilitator with the secret key server-side. > See [BFF / backend integration](/concepts/facilitator-and-bff) for the route implementation. ### Confirm on-chain `receipt` is the facilitator's settlement response: ```ts theme={null} { ok: true, via: "buckspay_self", token: "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA", chain: "stellar-testnet", transferTx: "a1b2c3...", // transaction hash - verify on Stellar Expert ledger: 54321, status: "SUCCESS" } ``` Paste `transferTx` into [Stellar Expert (testnet)](https://testnet.stellar.expert) to confirm the payment landed. ## Next steps Classic G-addresses vs. smart-contract accounts (passkey and multi-sig). Sponsored, fee-forwarding, and session-based gas strategies. Drop-in hooks for React apps - `useWallet`, `useStellarPay`, and `BuckspayProvider`. # Which package do I need? Source: https://buckspay.mintlify.app/get-started/which-package A one-line guide to every published Buckspay SDK package. All packages are published under the `@buckspay` scope on npm and follow the same version number. Start with `@buckspay/core`; add surface packages for your target platform. The composable client - `createBuckspayClient`, context, and shared types. Required by every integration. Account adapters: `classicAccount()` for G-addresses and `ozContractAccount()` for smart-contract wallets. Browser and mobile signing strategies: Wallets Kit (Freighter, xBull, LOBSTR) and passkey-based signers. Relayer adapters that forward signed transactions to a gasless facilitator - use with your own BFF backend route. React hooks (`useWallet`, `useStellarPay`) and provider components for browser apps. Next.js App Router helpers and server-side BFF route handler for the relayer. React Native bindings with passkey and wallet-kit support for iOS and Android. # Buckspay SDK Source: https://buckspay.mintlify.app/index Gasless USDC payments on Stellar, in a few lines - on web and mobile. Send USDC on Stellar (Soroban) without your users ever touching gas. The Buckspay SDK wraps account management, signing, and gasless relaying into a single composable client - for browsers, React Native, and server-side Next.js BFFs. Send your first gasless USDC transfer in under five minutes. Classic (G-address) vs. smart-contract accounts (passkey + OZ). Sponsored, fee-forwarding, and session-based gas strategies. Passkey and wallets-kit signers on iOS and Android. # Platforms Source: https://buckspay.mintlify.app/platforms/index The Buckspay SDK core is platform-agnostic. Each platform binding swaps only the host layer and the signer - the account model, gas engine, and payment flow are identical everywhere. The core (`@buckspay/core`) owns the prepare -> sign -> send pipeline, the gas-abstraction engine, and the Soroban wire format. Platform packages layer on top: they swap the signer transport (browser WebAuthn, native enclave, injected wallet) and the React host (web DOM vs. React Native primitives), while re-exporting the same hooks and status machine without forking them. Choose your platform: `BuckspayProvider`, `useWallet`, and `useStellarPay` for React web apps. Works with any browser signer (Wallets Kit, passkey). The same hooks, re-exported unchanged, plus `nativePasskey` (iOS / Android secure enclave) and secure-storage adapters. Use the SDK directly - no React. Ideal for server scripts, CLI tools, and backend workers that build and relay intents programmatically. App Router route factories: `createRelayRoute` and `createSignerProxyRoute`. The API key and provider secrets stay server-side behind a same-origin BFF. Game engines, embedded, and additional server runtimes - coming soon. # Next.js BFF - @buckspay/nextjs Source: https://buckspay.mintlify.app/platforms/nextjs-bff App Router route factories that keep the facilitator API key and provider secrets server-side behind same-origin BFF routes. `@buckspay/nextjs` exports two App Router route factories. They act as the BFF (Backend for Frontend) boundary described in [Facilitator and BFF](/concepts/facilitator-and-bff): the browser POSTs to your same-origin routes; the routes forward to the facilitator with the secret credentials server-side. `@buckspay/nextjs` is **server-only**. Never import it in a Client Component or a file that ends up in the browser bundle. The API key and provider secrets you pass to these factories are captured in a server closure and are never serialized or echoed to the client. ### Install ```bash theme={null} pnpm add @buckspay/nextjs @buckspay/core @buckspay/relayer ``` ### Create the relay route Create `app/api/buckspay/relay/route.ts`. `createRelayRoute` returns a `(req: Request) => Promise` handler - assign it to `POST` as required by App Router conventions. ```ts theme={null} // app/api/buckspay/relay/route.ts - SERVER-ONLY import { createRelayRoute } from "@buckspay/nextjs"; export const POST = createRelayRoute({ facilitatorUrl: process.env.BUCKSPAY_FACILITATOR_URL!, apiKey: process.env.BUCKSPAY_FACILITATOR_API_KEY!, // SERVER-SIDE ONLY network: "testnet" }); ``` `createRelayRoute` accepts a `CreateRelayRouteOptions` object: | Option | Type | Description | | ---------------- | --------- | ------------------------------------------------------------ | | `facilitatorUrl` | `string` | Base URL of the Buckspay facilitator | | `apiKey` | `string` | Facilitator API key - from a server env var, never a literal | | `network` | `Network` | `"testnet"` or `"mainnet"` | The route validates the incoming `RelayPayload` with a strict Zod schema before forwarding, so malformed requests are rejected at the BFF layer. On error it returns a coded JSON response (`{ error: BuckspayErrorCode }`) without leaking the upstream message. ### Create signer proxy routes (social and email login) If you use social or email-based signers, create proxy routes that forward the auth body to the facilitator's `/auth/*` endpoints while injecting the API key server-side. ```ts theme={null} // app/api/buckspay/auth/social/route.ts - SERVER-ONLY import { createSignerProxyRoute } from "@buckspay/nextjs"; export const POST = createSignerProxyRoute({ provider: "web3auth", network: "testnet" }); ``` ```ts theme={null} // app/api/buckspay/auth/email/route.ts - SERVER-ONLY import { createSignerProxyRoute } from "@buckspay/nextjs"; export const POST = createSignerProxyRoute({ provider: "email", network: "testnet" }); ``` `createSignerProxyRoute` reads `BUCKSPAY_FACILITATOR_URL` and `BUCKSPAY_FACILITATOR_API_KEY` from the server environment by default. You can override both via the optional second argument (`CreateSignerProxyRouteDeps`). ### Point the browser client at your BFF routes In your browser (or React) code, configure the relayer without an API key - it points at your same-origin BFF routes: ```ts theme={null} import { buckspayFacilitator } from "@buckspay/relayer/buckspay-facilitator"; const relayer = buckspayFacilitator({ url: "/api/buckspay/relay", network: "testnet" }); // No apiKey here - the key lives server-side in the route handler above. ``` ## Full example The snippet below shows all three route handlers in a single file for reference. In production, each goes in its own `route.ts` under `app/`. ```ts theme={null} // Recipe 15 - NEXT.JS BFF (App Router). Three server-only route handlers, packaged. // // These are the same-origin endpoints your browser code calls (`/api/buckspay/...`). The // facilitator apiKey and the provider secrets stay SERVER-SIDE - they are never in the client // bundle. Each `export const POST` below lives in its own `route.ts` under app/. import { createRelayRoute, createSignerProxyRoute } from "@buckspay/nextjs"; // app/api/buckspay/relay/route.ts - the gasless relay BFF (the dashboard pattern, packaged). export const relayPOST = createRelayRoute({ facilitatorUrl: process.env.BUCKSPAY_FACILITATOR_URL ?? "https://facilitator.example", apiKey: process.env.BUCKSPAY_FACILITATOR_API_KEY ?? "", // SERVER-SIDE ONLY network: "testnet" }); // app/api/buckspay/auth/social/route.ts - social signer-proxy (forwards to /auth/social). // Reads BUCKSPAY_FACILITATOR_URL + BUCKSPAY_FACILITATOR_API_KEY from server env. export const socialPOST = createSignerProxyRoute({ provider: "web3auth", network: "testnet" }); // app/api/buckspay/auth/email/route.ts - email signer-proxy (forwards to /auth/email). export const emailPOST = createSignerProxyRoute({ provider: "email", network: "testnet" }); ``` ## Next Why the API key must stay server-side and how the BFF slot fits the payment flow. The browser-side hooks that POST to your BFF relay route. # Node / Core - @buckspay/core Source: https://buckspay.mintlify.app/platforms/node-core Use createBuckspayClient directly - no React. Suitable for server scripts, CLI tools, and backend workers. `@buckspay/core` is the platform-agnostic heart of the SDK. You can use it directly - without any React layer - in Node.js scripts, backend workers, or any environment where you control the signer manually. The surface is the same three-phase flow described in [Prepare -> Sign -> Send](/concepts/prepare-sign-send): `createBuckspayClient` assembles the client; `createRpcSimContext` gives `prepare()` a Soroban simulator; then `connect()` -> `pay()` (or the split `prepare` / `sign` / `send`) executes the payment. ### Install ```bash theme={null} pnpm add @buckspay/core @buckspay/accounts @buckspay/signers @buckspay/relayer ``` ### Create the client ```ts theme={null} import { createBuckspayClient, createRpcSimContext } from "@buckspay/core"; import { classicAccount } from "@buckspay/accounts/classic"; import { walletsKit } from "@buckspay/signers/wallets-kit"; import { buckspayFacilitator } from "@buckspay/relayer/buckspay-facilitator"; const client = createBuckspayClient( { network: "testnet", account: classicAccount(), signer: walletsKit({ network: "testnet" }), relayer: buckspayFacilitator({ url: "/api/gasless", network: "testnet" }), gas: { mode: "sponsored" } }, createRpcSimContext("https://soroban-testnet.stellar.org") ); ``` ### Connect and pay ```ts theme={null} await client.connect(); const call = client.transfer({ token: USDC_SAC, to: MERCHANT, amount: "1.50" }); const receipt = await client.pay([call]); console.log(receipt.transferTx); ``` ## When to use the core directly | Scenario | Recommended approach | | -------------------------------------- | ------------------------------------------------------------------------------------------------ | | React / React Native app | [`@buckspay/react`](/platforms/web-react) or [`@buckspay/react-native`](/platforms/react-native) | | Next.js App Router BFF route | [`@buckspay/nextjs`](/platforms/nextjs-bff) | | Node.js script, backend worker, or CLI | `@buckspay/core` directly | | Server-side relay (any framework) | `createBuckspayClient` + `buckspayFacilitator({ apiKey })` - server-only | When using `@buckspay/core` in a server-side relay, pass `apiKey` to `buckspayFacilitator` as an environment variable. The API key must never appear in a browser bundle. ## Full quickstart example ```ts theme={null} // Quickstart - classic gasless USDC transfer (browser; NO apiKey). import { createBuckspayClient, createRpcSimContext } from "@buckspay/core"; import { classicAccount } from "@buckspay/accounts/classic"; import { walletsKit } from "@buckspay/signers/wallets-kit"; import { buckspayFacilitator } from "@buckspay/relayer/buckspay-facilitator"; const USDC_SAC: string = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; const MERCHANT: string = "GA6HCMBLTZS5VYYBCATRBR5VBZJEH5C2OON6XQGB3RNYDDAQ7JZ65YQH"; export async function quickstart(): Promise { const buckspay = createBuckspayClient( { network: "testnet", account: classicAccount(), signer: walletsKit({ network: "testnet" }), // url points at YOUR backend, which forwards to the facilitator with the key server-side. relayer: buckspayFacilitator({ url: "/api/gasless", network: "testnet" }), gas: { mode: "sponsored" } }, // prepare() simulates against the Soroban RPC. createRpcSimContext("https://soroban-testnet.stellar.org") ); await buckspay.connect(); // wallet + ensureReady const call = buckspay.transfer({ token: USDC_SAC, to: MERCHANT, amount: "1.50" }); const receipt = await buckspay.pay([call]); // prepare -> sign -> send console.log(receipt.transferTx); // settled on testnet } ``` ## Next The full quickstart walkthrough with a classic G-address account. The three-phase flow and where the BFF slot fits. # React Native - @buckspay/react-native Source: https://buckspay.mintlify.app/platforms/react-native The same BuckspayProvider and hooks on iOS and Android, with nativePasskey (device secure enclave) and secure-storage adapters. `@buckspay/react-native` brings the SDK to iOS and Android. The core pipeline (auth-entry build, fee-bump, prepare -> sign -> send) is platform-agnostic. This package adds three things on top: * **`nativePasskey`** - a `WebAuthnLike` backed by `react-native-passkey` that drives the device secure enclave instead of `navigator.credentials`. The private key never leaves the device. * **Secure-storage adapters** - `memorySecureStore`, `expoSecureStore`, `keychainSecureStore` for persisting session blobs and credential IDs. * **Hermes polyfills** - installed as a side-effect on import, before `@stellar/stellar-sdk` is used. `BuckspayProvider`, `useWallet`, and `useStellarPay` are re-exported **by reference** from `@buckspay/react` - the same store, same status machine, zero fork. Your screen logic is identical to a web component; only the host primitives (`` / `` / ``) and the signer differ. ### Install the package and peer dependencies Install `@buckspay/react-native` plus the native peers your app requires: ```bash theme={null} pnpm add @buckspay/react-native @buckspay/react @buckspay/core @buckspay/accounts @buckspay/relayer # Native peers - install in the app, not in the SDK pnpm add react-native react-native-passkey react-native-get-random-values ``` The peer modules (`react-native-passkey`, `react-native-get-random-values`) are optional: each is lazily loaded only if you use the feature that depends on it, so apps that skip `nativePasskey` never pull in the native module. ### Build the config with `nativePasskey` The config shape is identical to web. The only mobile-specific change is the signer: ```tsx theme={null} import { nativePasskey } from "@buckspay/react-native"; import { ozContractAccount } from "@buckspay/accounts/oz-contract"; import { buckspayFacilitator } from "@buckspay/relayer/buckspay-facilitator"; import type { BuckspayConfig } from "@buckspay/core"; const config: BuckspayConfig = { network: "testnet", account: ozContractAccount({ network: "testnet", sponsorAddress: "GA...SPONSOR" }), signer: nativePasskey({ rpId: "app.example.com", rpName: "My App" }), relayer: buckspayFacilitator({ url: "/api/gasless", network: "testnet" }), gas: { mode: "sponsored" } }; ``` `nativePasskey` accepts: | Option | Type | Required | Description | | -------- | -------- | -------- | ----------------------------------------------------- | | `rpId` | `string` | Yes | Relying-party domain, bound at registration | | `rpName` | `string` | No | Human-readable name shown in the authenticator prompt | ### Wrap the tree with `BuckspayProvider` Exactly the same as web - pass `sim` so `pay()` can simulate: ```tsx theme={null} import { BuckspayProvider } from "@buckspay/react-native"; import { createRpcSimContext } from "@buckspay/core"; export function App() { return ( {/* your screens */} ); } ``` ### Connect and pay - identical to web `useWallet` and `useStellarPay` work exactly as on web. Replace DOM elements with React Native primitives: ```tsx theme={null} import { useWallet, useStellarPay } from "@buckspay/react-native"; import { View, Text, Pressable } from "react-native"; function PayScreen() { const { wallet, connect, status } = useWallet(); const { pay, status: payStatus, receipt } = useStellarPay(); if (!wallet) { return void connect()} disabled={status === "connecting"}> {status === "connecting" ? "Connecting..." : "Connect"} ; } return ( void pay([/* calls */])} disabled={payStatus === "relaying"}> Pay 1.50 USDC (free) {receipt && Settled: {receipt.transferTx}} ); } ``` ## Secure storage Session blobs and credential IDs are persisted via a `SecureStore` port. Three adapters are exported from `@buckspay/react-native`: | Adapter | Use | | ---------------------------------- | ------------------------------------------- | | `memorySecureStore()` | Tests and ephemeral in-memory sessions | | `expoSecureStore()` | Expo managed workflow (`expo-secure-store`) | | `keychainSecureStore({ service })` | Bare React Native (`react-native-keychain`) | Each adapter lazily imports its peer, so apps that do not use a given adapter never pull the native module into their bundle. ## Full example ```tsx theme={null} // Recipe 13 - REACT NATIVE. The mobile binding re-exports the SAME @buckspay/react hooks and only // swaps the signer (native passkey / secure enclave), storage, and Hermes polyfills. The screen // logic is identical to web (recipe 05); only the host components (/) and the signer // differ. This example pins the @buckspay/react-native surface; the View/Text layer is the app's, // so it is omitted here (it would pull react-native intrinsics) - fragments stand in for it. import { BuckspayProvider, useWallet, useStellarPay, nativePasskey, memorySecureStore, type SecureStore } from "@buckspay/react-native"; import { ozContractAccount } from "@buckspay/accounts/oz-contract"; import { buckspayFacilitator } from "@buckspay/relayer/buckspay-facilitator"; import { createRpcSimContext, type BuckspayConfig, type Call } from "@buckspay/core"; const SPONSOR_G = "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"; const USDC_SAC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; const MERCHANT = "GA6HCMBLTZS5VYYBCATRBR5VBZJEH5C2OON6XQGB3RNYDDAQ7JZ65YQH"; // Same config shape as web - the ONLY mobile-specific line is the native passkey signer. export const mobileConfig: BuckspayConfig = { network: "testnet", account: ozContractAccount({ network: "testnet", sponsorAddress: SPONSOR_G }), signer: nativePasskey({ rpId: "app.buckspay.dev", rpName: "buckspay" }), relayer: buckspayFacilitator({ url: "/api/gasless", network: "testnet" }), gas: { mode: "sponsored" } }; // Session blobs / credential ids persist via a SecureStore port (keychain/expo on device). export const store: SecureStore = memorySecureStore(); // The hooks are byte-identical to @buckspay/react - no fork (recipe 05). export function PayScreen() { const { wallet, address, connect } = useWallet(); const { pay, status, receipt } = useStellarPay(); if (!wallet) { // On device this is a inside a . return <>{"Connect"}; } const call: Call = { contract: USDC_SAC, fn: "transfer", args: [] // built from address -> MERCHANT in the real screen; see recipe 05 }; void address; void (() => pay([call])); return <>{status === "success" && receipt ? `settled: ${receipt.transferTx}` : "Pay 1.50 USDC (free)"}; } // Wrap the tree once, exactly like web - pass `sim` so useStellarPay().pay() can simulate. export function App() { return ( ); } ``` ## Next How `nativePasskey` delegates to the shared passkey crypto pipeline. The web counterpart - same hooks, browser authenticator transport. # Web - @buckspay/react Source: https://buckspay.mintlify.app/platforms/web-react BuckspayProvider, useWallet, and useStellarPay - React hooks for gasless USDC payments in web apps. `@buckspay/react` wraps the core client in idiomatic hooks. Wrap your component tree once with `BuckspayProvider`; every component inside can then connect a wallet and send gasless payments with two hooks. ### Install ```bash theme={null} pnpm add @buckspay/react @buckspay/core @buckspay/accounts @buckspay/signers @buckspay/relayer ``` ### Wrap the tree with `BuckspayProvider` Place `BuckspayProvider` at the root of the tree (or the subtree that needs payment). Pass a `BuckspayConfig` and a simulator context so `useStellarPay().pay()` can simulate the transaction before signing. ```tsx theme={null} import { BuckspayProvider } from "@buckspay/react"; import { createBuckspayClient, createRpcSimContext } from "@buckspay/core"; import { classicAccount } from "@buckspay/accounts/classic"; import { walletsKit } from "@buckspay/signers/wallets-kit"; import { buckspayFacilitator } from "@buckspay/relayer/buckspay-facilitator"; const config = { network: "testnet", account: classicAccount(), signer: walletsKit({ network: "testnet" }), relayer: buckspayFacilitator({ url: "/api/gasless", network: "testnet" }), gas: { mode: "sponsored" as const } }; export function App() { return ( {/* your app */} ); } ``` Pass `sim` (a `createRpcSimContext(rpcUrl)`) so `pay()` can record authorization entries against the Soroban RPC. Omit it only in connect-only apps that never call `pay()`. ### Connect with `useWallet` `useWallet()` returns `{ wallet, address, connect, status, error }`. Gate the payment UI on `wallet !== null`: ```tsx theme={null} import { useWallet } from "@buckspay/react"; function ConnectButton() { const { wallet, connect, status } = useWallet(); if (wallet) return null; return ( ); } ``` ### Pay with `useStellarPay` `useStellarPay()` returns `{ pay, prepare, sign, status, receipt, error, reset }`. * `pay(calls)` - one-shot: prepare -> sign -> send in a single call. * `prepare(calls)` / `sign(intent)` - split flow for apps that validate the signed intent in a BFF before sending. See [Facilitator and BFF](/concepts/facilitator-and-bff). * `reset()` - returns the store to `idle` so the user can retry. #### Status machine `status` follows the path: ``` idle -> connecting -> ready -> signing -> relaying -> success | error ``` Render `error.code` (a `BuckspayErrorCode`) for user-facing messages, and announce state changes with `aria-live` for accessibility. ## Full example The component below is the compiled, type-checked reference implementation. ```tsx theme={null} // React hooks - full pay component. Compiles against @types/react 19 + the real hook types. import { useMemo, type CSSProperties } from "react"; import { Address, nativeToScVal } from "@stellar/stellar-sdk"; import { useWallet, useStellarPay } from "@buckspay/react"; import type { Call } from "@buckspay/core"; const USDC_SAC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; const MERCHANT = "GA6HCMBLTZS5VYYBCATRBR5VBZJEH5C2OON6XQGB3RNYDDAQ7JZ65YQH"; const btn: CSSProperties = { padding: "0.75rem 1.25rem", borderRadius: 8, cursor: "pointer" }; export function PayButton() { const { wallet, address, connect, status: wStatus, error: wErr } = useWallet(); const { pay, status, receipt, error } = useStellarPay(); const transferCall = useMemo(() => { if (!address) return null; return { contract: USDC_SAC, fn: "transfer", args: [new Address(address).toScVal(), new Address(MERCHANT).toScVal(), nativeToScVal(15000000n, { type: "i128" })] }; }, [address]); const busy = status === "signing" || status === "relaying"; const shownError = error ?? wErr; if (!wallet) { return ( ); } return (
{receipt &&

settled: {receipt.transferTx}

} {shownError && (

{shownError.code}: {shownError.message}

)}
); } ``` ## Next The same hooks on iOS and Android, with `nativePasskey` and secure-storage adapters. Route factories that keep the API key server-side. # Mainnet cutover runbook Source: https://buckspay.mintlify.app/production/mainnet-cutover The deliberate, reversible, audited procedure to enable Stellar pubnet for the Buckspay SDK and facilitator. **Invariant:** nothing touches pubnet until the GO/NO-GO gate passes, and every step has a rollback. Mainnet is OFF by default - the cutover is "turn the deliberate opt-in on, watched", and the rollback is "turn it back off." **Owner:** buckspay eng lead, **On-call:** the rotation named in the team runbook (PagerDuty), **Security sign-off:** per [Audit prep](/production/security/audit-prep). *** ## Pre-flight checklist Every row maps to a concrete verification command. **All must be green before the GO/NO-GO gate.** | Item | Owner | Verify | | ---------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Release gate PASS | eng | `bash scripts/release-gate.sh` exits 0 (all hard guards PASS) | | Secrets rotated | security | Sponsor secret + facilitator API key rotated post-incident; old keys revoked; `bash scripts/check-no-committed-env.sh` exits 0 (no tracked `.env*`, sponsor ≠ leaked `G`) | | Sponsor funded + low-balance alert armed | ops | Pubnet sponsor XLM ≥ the configured floor (`SPONSOR_MIN_XLM`); the low-balance alert fires below it (tested in staging) | | OZ Wasm installed on pubnet + pin parity | eng | Pubnet-installed Wasm hash == `OZ_SMART_ACCOUNT_WASM_HASH`; `node scripts/verify-wasm-hash.mjs` + `bash scripts/check-pin-parity.sh` pass | | Dedicated RPC | ops | `SOROBAN_RPC_URL_PUBNET` points at a dedicated/consistent provider (NOT a public load balancer); a health probe returns the latest ledger | | USDC allow-list set | eng | Only Circle's pubnet USDC SAC is accepted by the facilitator relay allow-list | | Spend caps + kill-switch armed | ops | Per-tx + rolling spend caps configured; the kill-switch is reachable and tested in staging | | Guarded mainnet smoke green | eng | `BUCKSPAY_E2E_MAINNET=1 BUCKSPAY_E2E=1 pnpm e2e` runs **both** pubnet smokes green with tiny funds (0.0001 USDC) | ### Extended surface (gas-in-token / batch / sessions / login / parallel) Every row inherits the controls in the base checklist above (spend caps, kill-switch, token allow-list, redacted observability) - these rows add only what the extended surface introduces. | Item | Owner | Verify | | -------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Multicall router pinned on pubnet + parity | eng | Pubnet-installed router address == the SDK pin; `pnpm --filter @buckspay/accounts exec vitest run batch-entry` + `bash scripts/check-pin-parity.sh` pass | | Session policy-account Wasm pinned | eng | `pnpm --filter @buckspay/accounts exec vitest run policy-account` passes (the on-chain spend-limit + allowlist authorizer hash is pinned) | | Contract decision gates GO | eng | `bash scripts/check-contract-decisions.sh` exits 0 (the gas-forwarder / batch-router / policy-signer decisions are GO + fixtures pinned) | | Fee token on the relay allow-list | eng | Only Circle's pubnet USDC SAC is accepted as the gas fee token, so `gas: { mode: "token" }` cannot forward an arbitrary token | | Signer-proxy secrets server-side | security | web3auth client secret + the OTP backend secret are in the server vault, NOT in any bundle; `POST /auth/social` + `POST /auth/email` are reachable **only** through the app BFF (`@buckspay/nextjs`) | | Channel-account pool funded + boot-validated | ops | The parallel channel accounts exist on pubnet, are funded above the floor, and are validated at facilitator boot; per-channel low-balance alerts armed | | Cross-feature smoke green | eng | `BUCKSPAY_E2E_MAINNET=1 BUCKSPAY_E2E=1 pnpm e2e` runs the session + batch + gas-in-token smoke green with tiny funds | | Feature docs + examples present | eng | `bash scripts/check-feature-docs.sh` exits 0 (every feature guide page + compiled example present, coverage table complete) | ## GO / NO-GO gate The single decision point. **GO only if ALL hold:** * Every pre-flight row is checked green. * `bash scripts/release-gate.sh` exits 0 (all hard guards PASS). * The guarded mainnet smoke (classic + contract) is green. * Two named approvers have signed: the **eng owner** and the **security sign-off**. **NO-GO if ANY of these is true** (explicit triggers): * `release-gate.sh` FAILs. * Either mainnet smoke is red. * Pubnet sponsor XLM below the floor. * Pubnet Wasm hash ≠ `OZ_SMART_ACCOUNT_WASM_HASH` (pin mismatch). * USDC allow-list unset (facilitator would sponsor an arbitrary token). * Kill-switch unreachable / untested. | Approver | Role | Date | GO / NO-GO | | ----------- | ----------------- | ---- | ---------- | | *(pending)* | eng owner | | | | *(pending)* | security sign-off | | | ## Rollback Reverse the cutover with **no fund loss** (pubnet config is OFF by default, so rollback = "turn the deliberate flag back off"): 1. **Flip the opt-in off.** SDK: stop passing `allowMainnet: true` (browser) / unset `BUCKSPAY_ALLOW_MAINNET` (Node) -> `resolveNetwork("pubnet", ...)` throws again. Facilitator: set `STELLAR_NETWORK=testnet` / disable the pubnet config -> the relay refuses pubnet payloads. 2. **Drain in-flight intents.** Let signed, unexpired auth entries settle or expire (`signatureExpirationLedger` bounds them); do not accept new pubnet relays (kill-switch). 3. **Revert the docs "supported" change** if the GA changeset already shipped - publish a follow-up `patch` re-gating note (mainnet remains technically opt-in; the docs walk it back to "experimental" until re-cutover). 4. **Re-point clients to testnet** and confirm no pubnet traffic reaches the facilitator. ## Sponsor-refill procedure The pubnet sponsor holds **only operational float**, never custody. * **Funding source:** the buckspay treasury account (named in the ops vault), NOT a user account. * **Threshold + target:** refill when sponsor XLM \< `SPONSOR_MIN_XLM` (the low-balance auto-pause floor); top up to the configured target balance (≈ N days of expected gas). * **Transfer:** the ops owner submits a payment from the treasury to the sponsor `G...` for the top-up amount; record the tx hash in the ops log. * **Confirm:** after the refill confirms, verify the low-balance alert **clears** and the facilitator resumes relays (auto-pause lifts once balance ≥ floor). ## Emergency pause (kill-switch) Immediately stop pubnet relays without a full rollback (seconds, reversible). * **Trip it:** `POST /admin/relays/pause` with `x-admin-api-key` - or set `RELAYS_ENABLED=false` and restart. `/relay` then returns `503 relays_paused`. * **Who:** any on-call engineer with the admin key (SEPARATE from the relay `API_KEY`). * **Propagation:** effective immediately on the instance(s); confirm across all replicas. * **Verify tripped:** a pubnet relay attempt is refused with `503 relays_paused`; `submitStellarTransfer` is never reached. * **Pause vs rollback:** pause is reversible in seconds (`/admin/relays/resume`); rollback is the full cutover reversal above. Pause first, then decide. ## Incident response * **Severities:** SEV-1 (sponsor draining / funds at risk), SEV-2 (relays failing / degraded), SEV-3 (elevated errors, no fund risk). * **Escalation:** on-call -> eng lead -> security sign-off. Page on SEV-1/2. * **First five minutes:** (1) **trip the kill-switch** (emergency pause above); (2) assess the sponsor balance + spend-cap counters; (3) check facilitator logs for the offending intent (PII-free structured records - chain/token/outcome, hashed address tags); (4) snapshot RPC/ledger state for the post-mortem. * **Comms:** status update to the incident channel every 30 min until resolved; customer-facing note if user funds were affected. * **Post-incident:** a written review within 48h (timeline, root cause, the guard that should have caught it, the follow-up). The relayer-trust + sponsor-exposure surfaces are in the [threat model](/production/security/threat-model). ## Go-live sequence (GA) The human/machine boundary is explicit: **Claude prepares + stages; the maintainer publishes.** 1. **Claude** - ensure a changeset exists for every change since the last release (`.changeset/mainnet-ga.md` + any others) and stage it. 2. **Claude / CI** - run the go/no-go gate: `bash scripts/release-gate.sh` **must exit 0** (all hard guards PASS); `bash scripts/check-cutover-runbook.sh` -> exit 0; `bash scripts/check-feature-docs.sh` -> exit 0; the guarded mainnet smoke green (`BUCKSPAY_E2E_MAINNET=1 BUCKSPAY_E2E=1 pnpm e2e`). Security sign-off finalized. This is the GO/NO-GO gate above. 3. **MAINTAINER (user)** - run `pnpm changeset version` (computes the lockstep bump + CHANGELOGs - never hand-edit a `version`), then `pnpm -r build` + `pnpm -r publish --dry-run` (confirm each tarball is `dist/`-only), then `pnpm changeset publish` + `git push --follow-tags`. **Claude never runs version/publish** - it is outward, irreversible, and needs npm auth (see [Versioning and migration](/production/versioning-and-migration)). 4. **Flip pubnet on** per the pre-flight + GO gate; watch the sponsor balance + spend caps for the first window; keep the kill-switch one command away. ## Extended surface go-live Bringing up gas-in-token, atomic batch, sessions, social/email login, and the parallel channel pool is **additive** to the base cutover - the GO/NO-GO gate, kill-switch, and rollback above are unchanged and now cover the new flows. Sequence the additions; do not weaken the gate. 1. **New endpoints** (`POST /fee/quote`, `POST /auth/social`, `POST /auth/email`) - enable per the extended pre-flight. Each inherits the base controls (spend caps, kill-switch, token allow-list, redacted observability); it does not get a parallel control plane. The `/auth/*` routes are reachable **only** through the app BFF (`@buckspay/nextjs`) - provider secrets stay server-side, never in a browser or mobile bundle. 2. **Pinned contracts** - confirm the pubnet-installed **Multicall** router address and the **policy-account** (session authorizer) Wasm hash match the SDK pins, with the same pin-parity discipline as the passkey smart-account hash. A mismatch is a **NO-GO** trigger. The FeeForwarder is facilitator-side (returned by `/fee/quote`); its fee token must be on the relay allow-list. 3. **Channel-account pool** - bring the pool up funded and boot-validated; it is transparent to the SDK (parallel throughput, no API surface). Watch the per-channel balances alongside the sponsor for the first window. 4. **Rollback addendum** - disabling the extended surface = unset the FeeForwarder / Multicall / channel config + the `/auth/*` routes (the relay then refuses token-gas / batched / auth payloads); the sponsored single-call path keeps working. The kill-switch still stops **all** pubnet relays in seconds. # Audit prep Source: https://buckspay.mintlify.app/production/security/audit-prep Scope, release-blocker checklist, and sign-off table for the Buckspay SDK security audit. Scopes a v1 audit and the release-blocker gate. Every blocker maps to a verification command. ## Scope (in) The SDK packages - `core`, `accounts/oz-contract`, `signers/passkey`, `relayer/buckspay-facilitator`, `react` - and the facilitator's new `contract` endpoints + the relay `from`=C-address path. **Focus areas:** auth-entry construction, the `__check_auth` secp256r1 signature format, expiration/nonce handling, the relayer trust boundary, and the BFF key boundary. Also **in scope: the deployed `minimal-passkey-account` smart-account wrapper** (`spikes/passkey-contract/contract/src/lib.rs`) - buckspay-authored `__constructor`/`__check_auth` around OZ's audited verifier; see [Contract provenance](/production/security/contract-provenance). ## Scope (out / deferred) OpenZeppelin's audited WebAuthn/secp256r1 **verifier** (`stellar_accounts::verifiers::webauthn::verify`) * we pin the wasm that calls it and do not re-audit OZ's verifier; the buckspay wrapper around it is **in** scope (above). Also out of scope: the wallet/authenticator implementations (Freighter, platform WebAuthn) and the gasless features (token-gas, batch, sessions, social login). ## Contracts vs SDK boundary buckspay **deploys + authorizes against** a smart-account contract. It does **not** author the cryptographic verifier (OZ's audited `verify`), but it **does** author the thin `minimal-passkey-account` wrapper around it (`__constructor`/`__check_auth`) - that wrapper is in audit scope (see [Contract provenance](/production/security/contract-provenance)). The SDK's off-chain responsibility ends at producing correct, bounded, validated auth entries and never holding secrets. The on-chain `__check_auth` (wrapper) -> `verify` (OZ) is the sole signature verifier. ## Transitive risks from `@creit.tech/stellar-wallets-kit` (status) Its multi-wallet connectors (Trezor/xBull/LOBSTR/Near/Hot/WalletConnect/ethereumjs) pull deps that are **not buckspay code**: * **Advisories - RESOLVED (high + critical).** All 5 high + 1 critical were `protobufjs <7.6.1`. Pinned to the patched `>=7.6.1 <8` via a `pnpm overrides` entry in `pnpm-workspace.yaml` (kept on the 7.x line to avoid a major bump). `pnpm audit --audit-level=high --prod` is now clean; the full workspace suite stays green. Remaining: **1 low + 1 moderate**, accepted - not high-severity, no patch path that doesn't churn the connector tree; re-checked weekly by CI. * **Copyleft/Unknown licenses - ACCEPTED (confined + CI-locked).** AGPL `ua-parser-js`, LGPL `rpc-websockets`, GPL `@lobstrco/*`, MPL `@ethereumjs/*`, plus several `Unknown` (Trezor/Near/ Hot/xBull connectors). All are **hard transitive deps of `@creit.tech/stellar-wallets-kit`** - connector modules the Stellar gasless path never exercises; they are not redistributed as part of any `@buckspay/*` package. **Finding:** narrowing the runtime module set does **not** remove them - they are declared by the umbrella package, so importing it pulls the whole tree regardless of which modules are instantiated (the signer already wires only Freighter/xBull/ LOBSTR). The license gate exempts exactly this set with a per-package reason (`scripts/check-licenses.mjs`), and a guard locks the acceptance: any copyleft/Unknown dep **outside** the known wallets-kit set fails CI (`pnpm --filter @buckspay/signers exec vitest run no-unexpected-copyleft`). **Clean-slate fix (deferred, deliberate product decision):** vendor a Freighter-only connector via `@stellar/freighter-api` and drop `@creit.tech/stellar-wallets-kit` entirely - this removes the copyleft tree but loses multi-wallet (xBull/LOBSTR) support, so it was not taken. ## Release-blocker checklist | Blocker | Verification command | | --------------------------------------------------------------- | ------------------------------------------------------------------- | | Threat model present | `bash scripts/check-threat-model.sh` | | No secrets in src | `bash scripts/check-no-secrets-in-src.sh` | | No secret in react bundle | `pnpm --filter @buckspay/react exec vitest run no-secret-in-bundle` | | Expiration bounded | `pnpm --filter @buckspay/core exec vitest run expiration-bounded` | | Licenses within allow-list | `pnpm licenses` | | OZ Wasm hash pinned (real pinned value) | `pnpm --filter @buckspay/accounts exec vitest run oz-wasm-pin` | | Mainnet gated | `pnpm --filter @buckspay/core exec vitest run network-gate` | | e2e green on testnet (classic + contract/passkey flows) | `BUCKSPAY_E2E=1 pnpm e2e` | | `pnpm audit` reviewed (transitive advisories accepted/resolved) | `pnpm audit --audit-level=high --prod` | Run all at once: `bash scripts/release-gate.sh`. ## Mainnet release blockers Extends the checklist above with the mainnet-cutover items. Each maps 1:1 to a line in `scripts/release-gate.sh`. | Blocker | Verification command | Status | | ------------------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------ | | Secrets rotated (no tracked `.env*`, sponsor ≠ leaked `G`) | `bash scripts/check-no-committed-env.sh` | guard | | Secret-rotation runbook present | `test -f docs/security/secret-rotation.md` | doc | | Licenses within allow-list (copyleft confined to wallets-kit) | `node scripts/check-licenses.mjs` | guard | | No **unexpected** copyleft in prod tree | `pnpm --filter @buckspay/signers exec vitest run no-unexpected-copyleft` | guard | | OZ wasm-hash pin reproducible | `node scripts/verify-wasm-hash.mjs` | guard | | Cross-repo wasm-pin parity | `bash scripts/check-pin-parity.sh` | guard | | Mainnet cutover runbook present | `bash scripts/check-cutover-runbook.sh` | guard | | Gated mainnet smoke | `BUCKSPAY_E2E_MAINNET=1 BUCKSPAY_E2E=1 pnpm e2e` | gated (skips when unset) | This gate enforces "no copyleft **outside** the accepted wallets-kit tree", not "zero copyleft" - the multi-wallet connectors are an accepted, confined risk (see the transitive-risks section above). Flipping to zero-copyleft requires the deferred Freighter-only vendoring. ## Sign-off The audit sign-off is filled by the auditor once `bash scripts/release-gate.sh` is green end-to-end. The **scope hash** ties a specific audit to a specific tree state - recompute with `bash scripts/audit-scope-hash.sh`; if it no longer matches, in-scope code changed after sign-off and the audit must be re-validated. | Auditor | Date | Scope hash | Result | | ------------------------------------ | ---- | ------------------------------------------------------------------ | ----------- | | *(pending - engagement not yet run)* | | `963d5e9e36f38dcb16719da305372ae6af2ea0cb981ff96fbe8a506637f944d9` | *(pending)* | # Contract provenance Source: https://buckspay.mintlify.app/production/security/contract-provenance Which wasm Buckspay deploys, where it comes from, how to reproduce it, and what that means for the audit scope. Records exactly **which** contract wasm buckspay deploys/authorizes against, **where it comes from**, **how to reproduce it byte-for-byte**, and **what that means for the audit scope**. The wasm IS the on-chain `__check_auth` authorizer, so "the code we deploy == the code we audited" must be a checkable fact - enforced by `scripts/verify-wasm-hash.mjs`, `scripts/check-pin-parity.sh`, and this document. **Status:** active, **Pinned hash:** `bf1aa9b2a4f8c05e1e5226009800bf69bd9ab7375ad160a1113d8d31a6ffdc69`, **Last reviewed:** 2026-06-28 *** ## Deployed wasm The deployed contract is the **custom `minimal-passkey-account` wrapper** - single-signer, buckspay-authored, built locally. **It is NOT the official OpenZeppelin Smart Account release.** It binds the account to **one** secp256r1 (WebAuthn) public key (`pubkey: BytesN<65>`, the 65-byte uncompressed `0x04‖x‖y`) at construction, and on every authorization it calls OpenZeppelin's **audited** WebAuthn/secp256r1 verifier `stellar_accounts::verifiers::webauthn::verify` (stellar-accounts 0.7.1) for the cryptography. The `__constructor` + `__check_auth` wrapper around that verifier is buckspay code (`spikes/passkey-contract/contract/src/lib.rs`). It deliberately skips the OZ composable framework (context rules / policies / external verifier contracts) - the initial gate only needed to prove the `secp256r1 -> __check_auth -> relay` path with a single deploy. * **Pinned hash:** `bf1aa9b2a4f8c05e1e5226009800bf69bd9ab7375ad160a1113d8d31a6ffdc69` * The hash is the **sha256 of the wasm bytes**, so it is **network-independent** - identical on testnet and pubnet. The bytes must be **installed once per network** (`uploadContractWasm`); see [Pin & install](#pin--install) below. ## Source & commit * **Crate:** `spikes/passkey-contract/contract` - package `minimal-passkey-account`, `crate-type = ["cdylib"]`, `publish = false`. * **Dependencies:** `soroban-sdk 26.1.0`, `stellar-accounts 0.7.1` (both via the build workspace; the workspace `Cargo.lock` is the determinism anchor - do **not** bump these without re-pinning). * **Source files:** `src/lib.rs` (the `__constructor` + `CustomAccountInterface::__check_auth` impl) and `Cargo.toml`. * **Git commit of the artifact build:** `77d7b4327cc8870c36f64312bb74ad6bdba96256` (`git -C buckspay-sdk rev-parse HEAD` at the time the installed/pinned wasm was produced). * **Pre-built artifact:** `spikes/passkey-contract/wasm/minimal_passkey_account.wasm` (14,897 bytes) - byte-identical to the installed wasm; committed as the reproducibility fixture at `packages/accounts/test/fixtures/oz-smart-account.wasm`. ## Reproducible build Anyone with the toolchain + commit reproduces the **byte-identical** wasm and the same hash. * **Toolchain:** `stellar` CLI **25.2.0**, `cargo`/`rustc` **1.93**. * **Commands** (from the crate root): ```bash theme={null} cd spikes/passkey-contract/contract stellar contract build # -> target/wasm32v1-none/release/minimal_passkey_account.wasm stellar contract optimize --wasm target/wasm32v1-none/release/minimal_passkey_account.wasm # -> target/wasm32v1-none/release/minimal_passkey_account.optimized.wasm shasum -a 256 target/wasm32v1-none/release/minimal_passkey_account.optimized.wasm # -> bf1aa9b2a4f8c05e1e5226009800bf69bd9ab7375ad160a1113d8d31a6ffdc69 ``` * **Verify without a Rust toolchain:** `node scripts/verify-wasm-hash.mjs` hashes the committed fixture and asserts it equals the pin (`pnpm --filter @buckspay/accounts exec vitest run test/wasm-hash-bytes.test.ts` does the same in CI). ## Audit-scope consequence Because the deployed wasm is a **custom wrapper** (not the official audited OZ Smart Account release), the wrapper's logic **IS in audit scope**. Be precise about what is and isn't inherited: * **Inherited as audited (out of buckspay scope):** only OZ's `stellar_accounts::verifiers::webauthn::verify` * the WebAuthn/secp256r1 verifier. We pin the wasm that calls it; we do not re-audit OZ's verifier. * **In scope (the auditor MUST review `src/lib.rs`):** 1. **Single-signer storage** - `pubkey: BytesN<65>` set once in `__constructor`, read in `__check_auth`; there is **no key-rotation, multi-sig, recovery, or admin/upgrade path**. 2. **Payload conversion** - `signature_payload: Hash<32>` -> `to_bytes().to_bytes()` -> the `payload` passed to `verify`. Confirm it matches what the verifier expects. 3. **No in-contract replay protection** - the wrapper relies on the **Soroban host nonce** + the SDK-bounded `signatureExpirationLedger` for anti-replay, not on contract-side state. Confirm that boundary is sound. 4. **Failure = panic** - `verify` panics on a bad signature (auth rejected); confirm there is no path that returns `Ok(())` without a successful verify. * **Residual risk / re-scope trigger:** adopting a future "real OZ Smart Account release" (or any wasm change) **re-scopes** this - it re-runs the build + hash procedure, re-pins, updates this doc, and re-audits the new bytes. ## Pin & install * **SDK pin:** `packages/accounts/src/oz-contract/wasm-pin.ts` - `OZ_SMART_ACCOUNT_WASM_HASH` (single value; network-independent) + `assertPinnedWasmHash`. * **Facilitator config:** `facilitator/src/stellarContract.ts` - `OZ_SMART_ACCOUNT_WASM_HASH` (`Record`, both `stellar-testnet` and `stellar-pubnet` = the pin) + `isPinnedWasm`. The cross-repo guard `scripts/check-pin-parity.sh` (and `packages/accounts/test/pin-parity.test.ts`) asserts the SDK pin === the facilitator's pubnet hash byte-for-byte. * **Per-network install:** the wasm is installed once per network via `uploadContractWasm`. Testnet is already installed + proven (`spikes/passkey-contract/DECISION.md` = GO). The pubnet install is `facilitator/scripts/install-wasm-pubnet.ts` - gated by `BUCKSPAY_ALLOW_MAINNET=1` + a funded sponsor, idempotent, asserts the installed hash == the pin. ## Sign-off | Auditor | Date | Wasm hash | Commit | | ----------- | ---- | ------------------------------------------------------------------ | ------------------------------------------ | | *(pending)* | | `bf1aa9b2a4f8c05e1e5226009800bf69bd9ab7375ad160a1113d8d31a6ffdc69` | `77d7b4327cc8870c36f64312bb74ad6bdba96256` | # Key handling Source: https://buckspay.mintlify.app/production/security/key-handling Where every key and secret may live, where it must not, and the operational rules that enforce the boundary. Every place a key/secret could appear, and the rule for each. Enforced by `scripts/check-no-secrets-in-src.sh` (no secret-shaped material in `packages/*/src`) and the react bundle guard (`no-secret-in-bundle.test.ts`). | Asset | MAY live in | MUST NOT live in | Enforced by | | ------------------------------------------- | --------------------------------------- | ------------------------------------------ | --------------------------------------------------- | | Stellar secret seed (`S...`) | the user's wallet, test-only `e2e/` env | any `packages/*` source, any client bundle | `check-no-secrets-in-src.sh` + react bundle guard | | Passkey private key (secp256r1) | the platform authenticator only | exportable storage, any JS variable | WebAuthn (non-extractable by spec) | | Facilitator API key | server env / BFF | any browser bundle | react bundle guard (`x-api-key`, `apiKey` patterns) | | Sponsor secret (`STELLAR_SPONSOR_SECRET_*`) | the facilitator env only | referenced by the SDK **at all** | `check-no-secrets-in-src.sh` | ## Operational rules * **Rotate** the sponsor secret and the facilitator API key on a schedule; treat anything that ever entered git history as compromised (rotate immediately). See the [secret rotation runbook](/production/security/secret-rotation). * The sponsor account holds **only operational float** (enough for fees + reserves), never a treasury balance. * **Never log** a raw `S...`/`C...`/`G...` address or a signature; logs carry codes, not PII. * **Mainnet is gated:** `pubnet` is refused unless `BUCKSPAY_ALLOW_MAINNET=1` (`core/src/network-gate.ts`), so a default config cannot move real funds. * The browser **never** holds the facilitator key - authenticated calls go through the app's same-origin BFF (`/api/gasless/relay`), which injects `x-api-key` server-side. # Secret rotation runbook Source: https://buckspay.mintlify.app/production/security/secret-rotation Enumeration and rotation procedure for every burned secret from the .env.bak incident. A `.env.bak` file containing **real** secrets was committed to the facilitator repo. Removing it from `HEAD` did **not** un-leak the values - they live in git history and in every clone. The only remediation is **rotation**. This runbook enumerates every burned secret and the exact procedure to rotate it. Enforced by `scripts/check-no-committed-env.sh`. **Status:** active, **Incident date:** 2026-06, **Owner:** buckspay security *** ## Incident A `.env.bak` carrying production-grade secrets was committed to `github.com/bucks-pay/facilitator` (present in commits `25bd513` and `4e8c83b`). It was later removed from tracking with `git rm --cached .env.bak` and `.env.bak` is now in `.gitignore`. **Removal from `HEAD` is NOT remediation:** the values remain in git history and in every existing clone/fork, in any CI log that printed them, and with every current/former collaborator who pulled. Anyone with repo history has **every value below**. The blast radius is the full set of secrets listed in the Burned secrets section below. The only fix is to rotate each to brand-new material and abandon the old. ## Burned secrets (rotate ALL) Exact keys present in the leaked `.env.bak` (verified from the file; values intentionally not reproduced here). | Secret | What it controls | Rotation action | New home | Old-value disposition | | ---------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ------------------------------------- | | `STELLAR_SPONSOR_SECRET_TESTNET` | Sponsors/pays for ALL testnet gasless Soroban txs. **Leaked sponsor public key: `GDKACWHUTPRUFHENFT56SL7XPM5FJU25DARE76OG43Q73XCATTUX4RPI`.** | Generate a brand-new sponsor keypair on a clean host; **drain** the leaked G's XLM to the new sponsor (or treasury); abandon the old G permanently - never re-fund it. | Secret manager (`KmsSecretProvider`) | Drained to 0, abandoned, never reused | | `RELAYER_PRIVATE_KEY` | EVM relayer signer - pays gas / submits on AVAX/Polygon/Base/Celo (`chain.ts` `privateKeyToAccount(env.RELAYER_PRIVATE_KEY)`). | Generate a new EOA; move any operational float off the leaked address; repoint `chain.ts` to the new key. | Secret manager | Float swept out, abandoned | | `DEPLOYER_PRIVATE_KEY` | Contract-deployer EOA. | Rotate to a new deployer; the leaked one must hold nothing and never deploy again. | Secret manager | Emptied, never deploys again | | `API_KEY` | Facilitator inbound auth key (`env.API_KEY`, checked on `/relay`). | Issue a new key; distribute to legitimate callers (dashboard BFF); revoke the old at the gateway so the burned value `401`s. | Secret manager / BFF env | Revoked at gateway | | `ZEROEX_API_KEY` | 0x swap-provider key. | Rotate in the 0x dashboard (revoke old, issue new); update env. | Secret manager | Revoked in 0x dashboard | | `ONEINCH_API_KEY` | 1inch swap-provider key. | Rotate in the 1inch dashboard (revoke old, issue new); update env. | Secret manager | Revoked in 1inch dashboard | | `CALIBUR_*` (8: `CALIBUR_AVALANCHE`, `_POLYGON`, `_BASE`, `_CELO`, `_AVALANCHE_FUJI`, `_POLYGON_AMOY`, `_BASE_SEPOLIA`, `_CELO_SEPOLIA`) | EIP-7702 Calibur delegate addresses/keys (mainnet + testnet sets). | Rotate per the Calibur key-rotation procedure; treat every leaked delegate as compromised. | Secret manager | Treated as compromised, rotated | | `AVAX_RPC_URL`, `BASE_RPC_URL`, `CELO_RPC_URL`, `POLYGON_RPC_URL` | RPC endpoints. | If any URL embeds a provider API key, rotate that provider key; otherwise these are **public endpoints, no secret** - recorded here so the row is not skipped. | env | Provider key rotated iff embedded | Non-secret values also present (no rotation needed): `PORT`, `SWAP_DEFAULT_SLIPPAGE_BPS`, `SWAP_MAX_SLIPPAGE_BPS`, `SWAP_QUOTE_TTL_SECONDS`. The mainnet sponsor `STELLAR_SPONSOR_SECRET_PUBNET` was **NOT** in `.env.bak`, but it MUST be generated fresh on a clean host post-incident and MUST differ from the leaked address above - enforced by `scripts/check-no-committed-env.sh` Guard 2. ## Rotation procedure Apply these four steps to **every** row above: 1. **Generate** new material on a clean host. Never paste an old value into a new file; never reuse a leaked key on a "fresh" account. 2. **Deploy** to the secret manager / deploy target - the facilitator's `SecretProvider`: `KmsSecretProvider` in prod, a gitignored `.env` in dev. **Never** into a tracked file. 3. **Revoke / abandon** the old value at its source: gateway revoke for API keys (0x/1inch/ `API_KEY`); for on-chain keys, sweep all funds out and never reuse the address. 4. **Verify** the new value works and the old value fails: old `API_KEY` -> `401`; leaked sponsor G -> zero balance and no longer referenced anywhere. **Drain the leaked sponsor (specific):** build a payment of the full XLM balance (minus the base reserve) from the leaked `GDKACWHU...4RPI` to the new sponsor (or treasury), submit it **once**, then record the leaked G as abandoned in this doc. Never re-fund it. ## Never commit a secret file again (controls) 1. **`.gitignore`** in both repos ignores `.env` and `.env.*` and explicitly `.env.bak` / `.env.local`, allowing ONLY `.env.example`. (SDK: `.env`, `.env.*`, `!.env.example`; facilitator: `.env`, `.env.bak`, `.env.local`, `.env.*.local`.) 2. **`scripts/check-no-committed-env.sh`** runs in CI and the release gate - fails if any `.env*` (≠ `.env.example`) is tracked, and fails if the configured pubnet sponsor equals the leaked address. 3. **Pre-commit / CI secret scanner** (`gitleaks` or `trufflehog`) blocks secret-shaped strings before they land. This extends `scripts/check-no-secrets-in-src.sh` (blocks `S...` seeds / PEM markers / `apiKey` literals in `packages/*/src`) to **tracked files anywhere**, not just `src`. ## Post-rotation verification * [ ] Leaked sponsor `GDKACWHU...4RPI` balance == 0 (drained + abandoned). * [ ] New sponsor funded and referenced via the secret manager (never a tracked file). * [ ] Old `API_KEY` returns `401` at the gateway; new key distributed to the BFF. * [ ] `bash scripts/check-no-committed-env.sh` exits 0. * [ ] No `.env*` (except `.env.example`) tracked in either repo: `git ls-files | grep -E '(^|/)\.env'` shows only `*.env.example`. * [ ] 0x / 1inch / Calibur / EVM relayer + deployer keys rotated and old ones revoked/emptied. # Threat model Source: https://buckspay.mintlify.app/production/security/threat-model STRIDE-style analysis of the six security surfaces in the Buckspay SDK. STRIDE-style rows for the six surfaces. Status ∈ . Verified present by `scripts/check-threat-model.sh`. ## 1. Signer custody **Threat:** key extraction / unauthorized signing. **Mitigation:** keys live only in the wallet (Wallets Kit) or the authenticator (WebAuthn secp256r1). `BuckspaySigner` returns **only** public keys + signatures - there is no code path that reads a secret. The Ed25519 signer used in tests is a test-only double under `e2e/`. **Status: mitigated by design.** ## 2. Relayer trust **Threat:** a malicious/compromised facilitator returns a fake receipt or substitutes the transfer. **Mitigation:** the facilitator can only *submit* what the payer signed - the on-chain auth entry binds `from/to/amount/contract` (`facilitator/src/stellarSubmit.ts validateInvocation`), so the relayer **cannot alter** the transfer. The SDK validates the `Receipt` shape with zod before trusting `transferTx`. `Relayer` is an interface, so a neutral relayer is droppable later. **Status: mitigated** (single-provider risk **accepted** for v1; relayer neutrality is planned for a future release). ## 3. Sponsor-key exposure **Threat:** the facilitator's sponsor secret (`STELLAR_SPONSOR_SECRET_*`) leaks. **Mitigation:** the sponsor secret lives only in the facilitator env - never in the SDK or any client; the SDK never references it. Onboarding/deploy txs are sponsor-sandwich validated server-side (`facilitator/src/stellar.ts ALLOWED_ONBOARD_OPS`). **Status: mitigated**; key rotation + spend caps tracked in [Key handling](/production/security/key-handling). ## 4. Replay / expiration **Threat:** a captured signed auth entry is replayed. **Mitigation:** each entry carries a random `nonce` and a `signatureExpirationLedger`; the facilitator rejects expired entries (`stellarSubmit.ts`) and the network rejects nonce reuse. The SDK **caps** the expiration ledger to `MAX_EXPIRATION_LEDGERS` (`core/src/expiration.ts`, guard test `expiration-bounded.test.ts`), so the replay window can't be widened. **Status: mitigated.** ## 5. Passkey phishing (rpId binding) **Threat:** a phishing origin tries to drive the user's passkey. **Mitigation:** WebAuthn binds credentials to the `rpId`; the SDK requires `passkey({ rpId })` and the demo derives `rpId` from `window.location.hostname`. A credential created for `app.buckspay.dev` **cannot** be asserted from `evil.example`. Rule: never accept a caller-supplied `rpId` that doesn't match the page origin. **Status: mitigated** by WebAuthn + correct rpId usage. ## 6. Tenant isolation **Threat:** one tenant's payment/intent leaks into another's flow. **Mitigation:** the SDK is stateless per `BuckspayClient` instance (one config = one network/account/relayer); there is no shared module-level mutable state across tenants. The dashboard BFF scopes intents per tenant (out of SDK scope, but the integrator's responsibility). **Status: mitigated in SDK**; integrator-responsibility boundary stated. ## Out of scope for v1 / deferred to audit The audited OpenZeppelin Smart Account *contract* (we pin its Wasm, we don't re-audit OZ), the wallet/authenticator implementations, and the gasless features (token-gas, batch, sessions, social login). See [Audit prep](/production/security/audit-prep). # Versioning and migration Source: https://buckspay.mintlify.app/production/versioning-and-migration How @buckspay/* packages are versioned, what your ^ pin actually resolves, and how to migrate across breaking changes. ## Versioning policy All `@buckspay/*` packages follow [SemVer 2.0.0](https://semver.org/) and are managed with [Changesets](https://github.com/changesets/changesets) in a monorepo. ### Lockstep releases **All five packages share a single version and are released together:** * `@buckspay/core` * `@buckspay/signers` * `@buckspay/accounts` * `@buckspay/relayer` * `@buckspay/react` `@buckspay/react@0.3.0` always pairs with `@buckspay/core@0.3.0`. This removes the compatibility matrix - one version number answers "which packages work together?" ### Bump rules while pre-1.0 The SDK is currently in `0.x`. The bump rules below apply **until `1.0.0`**: | Change | Bump | Example | | ----------------------------------------------------- | --------- | ---------------- | | Breaking API change (rename, remove, behavior change) | **minor** | `0.3.0 -> 0.4.0` | | New backward-compatible feature | patch | `0.3.0 -> 0.3.1` | | Bug fix | patch | `0.3.0 -> 0.3.1` | `major` is reserved for the one-time graduation to `1.0.0`. It is never used before then. ### How `^` behaves on a `0.x` version npm's caret on a `0.x` range **behaves like a tilde - it allows patch updates only**: | Pin | Resolves to | | -------- | ------------------------------------- | | `^0.3.0` | `>=0.3.0 <0.4.0` (patch updates only) | | `~0.3.0` | `>=0.3.0 <0.4.0` (same result) | A consumer on `@buckspay/react@^0.3.0` automatically gets bug fixes and new features within `0.3.x`, but **must explicitly update** to `0.4.0` when a breaking change ships. This is intentional: you can't accidentally take a breaking change from a `^` range. **Recommendation:** pin all five packages with `^`: ```bash theme={null} pnpm add @buckspay/core@^0.x.y @buckspay/react@^0.x.y ``` Because versions are lockstep, a single version number covers the whole suite. ### Changelogs Each package ships a `CHANGELOG.md` generated from changeset summaries. Read it before upgrading across a `minor` bump to understand what the breaking change requires. *** ## Migrating to a new version ### Patch updates (`0.x.y -> 0.x.z`) No action required. Your `^0.x.y` pin resolves the new patch automatically. No public API changed. ### Minor updates (`0.x.y -> 0.(x+1).0`) - breaking changes A minor bump signals a **breaking API change** under the pre-1.0 rules. Steps: 1. **Read the CHANGELOG** for each affected package. The changeset summary describes exactly what changed and what the replacement looks like. 2. **Update your pin** in `package.json` from `^0.x.y` to `^0.(x+1).0`. 3. **Update all five packages together** - because they are lockstep, mixing versions (e.g. `core@0.4.0` with `react@0.3.x`) is unsupported and may cause type errors at build time. 4. **Fix call sites** per the CHANGELOG. Breaking changes are accompanied by a migration note that shows the before/after. ### Future: upgrading to `1.0.0` When `1.0.0` ships, the `^` caret regains its usual meaning: `^1.0.0` resolves `>=1.0.0 <2.0.0`. Packages will graduate from `0.x` lockstep at the same time, so the migration from `0.x` to `1.0.0` is a single coordinated bump across the suite. Migration notes will be published in each package's `CHANGELOG.md`. Breaking changes in `1.x+` cost a `major` bump, so `^1.0.0` will never silently take a breaking change - this is the standard SemVer guarantee that `0.x` does not provide. # accounts/src/classic Source: https://buckspay.mintlify.app/sdk-reference/accounts/src/classic/index # accounts/src/classic ## Interfaces * [ClassicAccountOptions](/sdk-reference/accounts/src/classic/interfaces/ClassicAccountOptions) ## Variables * [MULTICALL\_CONTRACT\_ID](/sdk-reference/accounts/src/classic/variables/MULTICALL_CONTRACT_ID) ## Functions * [buildBatchTransferEntry](/sdk-reference/accounts/src/classic/functions/buildBatchTransferEntry) * [classicAccount](/sdk-reference/accounts/src/classic/functions/classicAccount) * [resolveMulticallContract](/sdk-reference/accounts/src/classic/functions/resolveMulticallContract) # accounts/src/oz-contract Source: https://buckspay.mintlify.app/sdk-reference/accounts/src/oz-contract/index # accounts/src/oz-contract ## Interfaces * [OzContractOptions](/sdk-reference/accounts/src/oz-contract/interfaces/OzContractOptions) ## Variables * [OZ\_SMART\_ACCOUNT\_WASM\_HASH](/sdk-reference/accounts/src/oz-contract/variables/OZ_SMART_ACCOUNT_WASM_HASH) ## Functions * [assertPinnedWasmHash](/sdk-reference/accounts/src/oz-contract/functions/assertPinnedWasmHash) * [contractSalt](/sdk-reference/accounts/src/oz-contract/functions/contractSalt) * [deriveContractAddress](/sdk-reference/accounts/src/oz-contract/functions/deriveContractAddress) * [ozContractAccount](/sdk-reference/accounts/src/oz-contract/functions/ozContractAccount) ## References ### buildBatchTransferEntry Re-exports [buildBatchTransferEntry](/sdk-reference/accounts/src/classic/functions/buildBatchTransferEntry) *** ### MULTICALL\_CONTRACT\_ID Re-exports [MULTICALL\_CONTRACT\_ID](/sdk-reference/accounts/src/classic/variables/MULTICALL_CONTRACT_ID) *** ### resolveMulticallContract Re-exports [resolveMulticallContract](/sdk-reference/accounts/src/classic/functions/resolveMulticallContract) # accounts/src/policy-account Source: https://buckspay.mintlify.app/sdk-reference/accounts/src/policy-account/index # accounts/src/policy-account ## Interfaces * [PolicyAccountOptions](/sdk-reference/accounts/src/policy-account/interfaces/PolicyAccountOptions) ## Variables * [POLICY\_ACCOUNT\_WASM\_HASH](/sdk-reference/accounts/src/policy-account/variables/POLICY_ACCOUNT_WASM_HASH) ## Functions * [assertPinnedPolicyWasm](/sdk-reference/accounts/src/policy-account/functions/assertPinnedPolicyWasm) * [policyAccount](/sdk-reference/accounts/src/policy-account/functions/policyAccount) * [resolvePolicyAccountAddress](/sdk-reference/accounts/src/policy-account/functions/resolvePolicyAccountAddress) # accounts/src/policy Source: https://buckspay.mintlify.app/sdk-reference/accounts/src/policy/index # accounts/src/policy ## Type Aliases * [SessionPolicy](/sdk-reference/accounts/src/policy/type-aliases/SessionPolicy) ## Functions * [allowlist](/sdk-reference/accounts/src/policy/functions/allowlist) * [buildInstallArgs](/sdk-reference/accounts/src/policy/functions/buildInstallArgs) * [buildRevokeArgs](/sdk-reference/accounts/src/policy/functions/buildRevokeArgs) * [compilePolicies](/sdk-reference/accounts/src/policy/functions/compilePolicies) * [spendLimit](/sdk-reference/accounts/src/policy/functions/spendLimit) # core/src Source: https://buckspay.mintlify.app/sdk-reference/core/src/index # core/src ## Classes * [BuckspayClient](/sdk-reference/core/src/classes/BuckspayClient) * [BuckspayError](/sdk-reference/core/src/classes/BuckspayError) * [GasAbstractionEngine](/sdk-reference/core/src/classes/GasAbstractionEngine) ## Interfaces * [AccountAdapter](/sdk-reference/core/src/interfaces/AccountAdapter) * [AccountSimContext](/sdk-reference/core/src/interfaces/AccountSimContext) * [AccountState](/sdk-reference/core/src/interfaces/AccountState) * [AssembleInput](/sdk-reference/core/src/interfaces/AssembleInput) * [AuthDetails](/sdk-reference/core/src/interfaces/AuthDetails) * [AuthEntryPayload](/sdk-reference/core/src/interfaces/AuthEntryPayload) * [BatchBuilder](/sdk-reference/core/src/interfaces/BatchBuilder) * [BuckspayConfig](/sdk-reference/core/src/interfaces/BuckspayConfig) * [BuckspaySigner](/sdk-reference/core/src/interfaces/BuckspaySigner) * [BuckspayState](/sdk-reference/core/src/interfaces/BuckspayState) * [BuckspayWallet](/sdk-reference/core/src/interfaces/BuckspayWallet) * [BuildBatchEntryInput](/sdk-reference/core/src/interfaces/BuildBatchEntryInput) * [BuildEntryInput](/sdk-reference/core/src/interfaces/BuildEntryInput) * [Call](/sdk-reference/core/src/interfaces/Call) * [EnsureReadyInput](/sdk-reference/core/src/interfaces/EnsureReadyInput) * [FeeQuote](/sdk-reference/core/src/interfaces/FeeQuote) * [PreparedIntent](/sdk-reference/core/src/interfaces/PreparedIntent) * [Receipt](/sdk-reference/core/src/interfaces/Receipt) * [RecordingResult](/sdk-reference/core/src/interfaces/RecordingResult) * [Relayer](/sdk-reference/core/src/interfaces/Relayer) * [RelayPayload](/sdk-reference/core/src/interfaces/RelayPayload) * [Session](/sdk-reference/core/src/interfaces/Session) * [SessionGrant](/sdk-reference/core/src/interfaces/SessionGrant) * [SessionInstallInput](/sdk-reference/core/src/interfaces/SessionInstallInput) * [SessionManager](/sdk-reference/core/src/interfaces/SessionManager) * [SessionManagerDeps](/sdk-reference/core/src/interfaces/SessionManagerDeps) * [SessionRevokeInput](/sdk-reference/core/src/interfaces/SessionRevokeInput) * [Signature](/sdk-reference/core/src/interfaces/Signature) * [SignedIntent](/sdk-reference/core/src/interfaces/SignedIntent) * [SignerKey](/sdk-reference/core/src/interfaces/SignerKey) * [SorobanSimulateRaw](/sdk-reference/core/src/interfaces/SorobanSimulateRaw) * [SorobanSimulator](/sdk-reference/core/src/interfaces/SorobanSimulator) * [SubInvocation](/sdk-reference/core/src/interfaces/SubInvocation) * [SwapQuote](/sdk-reference/core/src/interfaces/SwapQuote) * [SwapQuoteRequest](/sdk-reference/core/src/interfaces/SwapQuoteRequest) * [SwapRequest](/sdk-reference/core/src/interfaces/SwapRequest) ## Type Aliases * [AccountModel](/sdk-reference/core/src/type-aliases/AccountModel) * [BuckspayErrorCode](/sdk-reference/core/src/type-aliases/BuckspayErrorCode) * [FacilitatorChain](/sdk-reference/core/src/type-aliases/FacilitatorChain) * [GasConfig](/sdk-reference/core/src/type-aliases/GasConfig) * [Network](/sdk-reference/core/src/type-aliases/Network) * [RpcFetch](/sdk-reference/core/src/type-aliases/RpcFetch) * [SessionPolicy](/sdk-reference/core/src/type-aliases/SessionPolicy) * [SignerType](/sdk-reference/core/src/type-aliases/SignerType) * [SwapChain](/sdk-reference/core/src/type-aliases/SwapChain) ## Variables * [MAX\_BATCH\_CALLS](/sdk-reference/core/src/variables/MAX_BATCH_CALLS) * [USDC\_DECIMALS](/sdk-reference/core/src/variables/USDC_DECIMALS) ## Functions * [batch](/sdk-reference/core/src/functions/batch) * [buildUnsignedCallEntry](/sdk-reference/core/src/functions/buildUnsignedCallEntry) * [buildUnsignedEntry](/sdk-reference/core/src/functions/buildUnsignedEntry) * [createBuckspayClient](/sdk-reference/core/src/functions/createBuckspayClient) * [createBuckspayConfig](/sdk-reference/core/src/functions/createBuckspayConfig) * [createRpcSimContext](/sdk-reference/core/src/functions/createRpcSimContext) * [createSessionManager](/sdk-reference/core/src/functions/createSessionManager) * [createSorobanSimulator](/sdk-reference/core/src/functions/createSorobanSimulator) * [deserializeSession](/sdk-reference/core/src/functions/deserializeSession) * [getLatestLedger](/sdk-reference/core/src/functions/getLatestLedger) * [mainnetSimContext](/sdk-reference/core/src/functions/mainnetSimContext) * [randomNonce](/sdk-reference/core/src/functions/randomNonce) * [serializeSession](/sdk-reference/core/src/functions/serializeSession) * [sessionId](/sdk-reference/core/src/functions/sessionId) * [simulateRecording](/sdk-reference/core/src/functions/simulateRecording) * [toStroops](/sdk-reference/core/src/functions/toStroops) # nextjs/src Source: https://buckspay.mintlify.app/sdk-reference/nextjs/src/index # nextjs/src ## Interfaces * [CreateRelayRouteDeps](/sdk-reference/nextjs/src/interfaces/CreateRelayRouteDeps) * [CreateRelayRouteOptions](/sdk-reference/nextjs/src/interfaces/CreateRelayRouteOptions) * [CreateSignerProxyRouteDeps](/sdk-reference/nextjs/src/interfaces/CreateSignerProxyRouteDeps) * [CreateSignerProxyRouteOptions](/sdk-reference/nextjs/src/interfaces/CreateSignerProxyRouteOptions) ## Type Aliases * [Network](/sdk-reference/nextjs/src/type-aliases/Network) ## Functions * [createRelayRoute](/sdk-reference/nextjs/src/functions/createRelayRoute) * [createSignerProxyRoute](/sdk-reference/nextjs/src/functions/createSignerProxyRoute) # react-native/src Source: https://buckspay.mintlify.app/sdk-reference/react-native/src/index # react-native/src ## Interfaces * [BuckspayProviderProps](/sdk-reference/react-native/src/interfaces/BuckspayProviderProps) * [NativePasskeyOptions](/sdk-reference/react-native/src/interfaces/NativePasskeyOptions) * [SecureStore](/sdk-reference/react-native/src/interfaces/SecureStore) * [UseStellarPayResult](/sdk-reference/react-native/src/interfaces/UseStellarPayResult) * [UseWalletResult](/sdk-reference/react-native/src/interfaces/UseWalletResult) ## Functions * [BuckspayProvider](/sdk-reference/react-native/src/functions/BuckspayProvider) * [expoSecureStore](/sdk-reference/react-native/src/functions/expoSecureStore) * [keychainSecureStore](/sdk-reference/react-native/src/functions/keychainSecureStore) * [memorySecureStore](/sdk-reference/react-native/src/functions/memorySecureStore) * [nativePasskey](/sdk-reference/react-native/src/functions/nativePasskey) * [useStellarPay](/sdk-reference/react-native/src/functions/useStellarPay) * [useWallet](/sdk-reference/react-native/src/functions/useWallet) # react/src Source: https://buckspay.mintlify.app/sdk-reference/react/src/index # react/src ## Interfaces * [BuckspayProviderProps](/sdk-reference/react/src/interfaces/BuckspayProviderProps) * [UseStellarPayResult](/sdk-reference/react/src/interfaces/UseStellarPayResult) * [UseWalletResult](/sdk-reference/react/src/interfaces/UseWalletResult) ## Functions * [BuckspayProvider](/sdk-reference/react/src/functions/BuckspayProvider) * [useStellarPay](/sdk-reference/react/src/functions/useStellarPay) * [useWallet](/sdk-reference/react/src/functions/useWallet) # relayer/src/buckspay-facilitator Source: https://buckspay.mintlify.app/sdk-reference/relayer/src/buckspay-facilitator/index # relayer/src/buckspay-facilitator ## Interfaces * [FacilitatorOptions](/sdk-reference/relayer/src/buckspay-facilitator/interfaces/FacilitatorOptions) ## Functions * [buckspayFacilitator](/sdk-reference/relayer/src/buckspay-facilitator/functions/buckspayFacilitator) # signers/src/email Source: https://buckspay.mintlify.app/sdk-reference/signers/src/email/index # signers/src/email ## Interfaces * [EmailSigner](/sdk-reference/signers/src/email/interfaces/EmailSigner) * [EmailSignerDeps](/sdk-reference/signers/src/email/interfaces/EmailSignerDeps) * [EmailSignerOptions](/sdk-reference/signers/src/email/interfaces/EmailSignerOptions) ## Functions * [emailSigner](/sdk-reference/signers/src/email/functions/emailSigner) # signers/src/passkey Source: https://buckspay.mintlify.app/sdk-reference/signers/src/passkey/index # signers/src/passkey ## Interfaces * [CheckAuthParts](/sdk-reference/signers/src/passkey/interfaces/CheckAuthParts) * [CreateInput](/sdk-reference/signers/src/passkey/interfaces/CreateInput) * [CreateOutput](/sdk-reference/signers/src/passkey/interfaces/CreateOutput) * [GetInput](/sdk-reference/signers/src/passkey/interfaces/GetInput) * [GetOutput](/sdk-reference/signers/src/passkey/interfaces/GetOutput) * [PasskeyOptions](/sdk-reference/signers/src/passkey/interfaces/PasskeyOptions) * [WebAuthnLike](/sdk-reference/signers/src/passkey/interfaces/WebAuthnLike) ## Functions * [coseToUncompressed](/sdk-reference/signers/src/passkey/functions/coseToUncompressed) * [decodeCheckAuthSignature](/sdk-reference/signers/src/passkey/functions/decodeCheckAuthSignature) * [extractCoseKey](/sdk-reference/signers/src/passkey/functions/extractCoseKey) * [formatCheckAuthSignature](/sdk-reference/signers/src/passkey/functions/formatCheckAuthSignature) * [passkey](/sdk-reference/signers/src/passkey/functions/passkey) # signers/src/social Source: https://buckspay.mintlify.app/sdk-reference/signers/src/social/index # signers/src/social ## Interfaces * [AuthDetails](/sdk-reference/signers/src/social/interfaces/AuthDetails) * [BuckspaySigner](/sdk-reference/signers/src/social/interfaces/BuckspaySigner) * [SocialProvider](/sdk-reference/signers/src/social/interfaces/SocialProvider) * [SocialSignerOptions](/sdk-reference/signers/src/social/interfaces/SocialSignerOptions) * [Web3AuthLike](/sdk-reference/signers/src/social/interfaces/Web3AuthLike) ## Type Aliases * [Web3AuthLoader](/sdk-reference/signers/src/social/type-aliases/Web3AuthLoader) ## Functions * [socialSigner](/sdk-reference/signers/src/social/functions/socialSigner) # signers/src/wallets-kit Source: https://buckspay.mintlify.app/sdk-reference/signers/src/wallets-kit/index # signers/src/wallets-kit ## Interfaces * [KitOptions](/sdk-reference/signers/src/wallets-kit/interfaces/KitOptions) * [WalletsKitLike](/sdk-reference/signers/src/wallets-kit/interfaces/WalletsKitLike) ## Functions * [normalizeSignature](/sdk-reference/signers/src/wallets-kit/functions/normalizeSignature) * [walletsKit](/sdk-reference/signers/src/wallets-kit/functions/walletsKit) # Email OTP Source: https://buckspay.mintlify.app/signers/email-otp Authenticate users with a one-time code sent to their inbox - no wallet, no seed phrase, no OAuth account required. The email signer authenticates users with a one-time password (OTP) sent to their email address. The OTP backend and the derived Stellar ed25519 key are custodied server-side by the facilitator; the browser only ever sees the public key and 64-byte signatures, delivered through your same-origin signer-proxy. ## When to use Choose `emailSigner` when you want the lowest-friction onboarding: the user enters their email, receives a code, and pays - no wallet install, no seed phrase, no OAuth account. The resulting key backs a classic `G...` account. The OTP backend secret and the custodied private key are **never** in the browser bundle. Configure a same-origin `proxyUrl` pointing at your backend signer-proxy, created with `createSignerProxyRoute` from `@buckspay/nextjs`. The private key never reaches client-side code. ## Factory ```ts theme={null} import { emailSigner } from "@buckspay/signers/email"; const signer = emailSigner({ proxyUrl: "/api/buckspay/auth/email", network: "testnet" }); ``` `emailSigner(opts)` accepts an `EmailSignerOptions` object: | Option | Type | Required | Description | | ---------- | ----------------------- | -------- | -------------------------------------------------------- | | `proxyUrl` | `string` | Yes | Same-origin route to the `@buckspay/nextjs` signer-proxy | | `network` | `"testnet" \| "pubnet"` | Yes | Stellar network to target | ## Two-step authentication The email signer uses a two-step flow before the payment: **Step 1 - Request the OTP** ```ts theme={null} await signer.requestOtp(email); // Sends the one-time code to the user's inbox. ``` **Step 2 - Verify and authenticate** ```ts theme={null} const details = await signer.authenticate?.({ email, otp: codeFromInbox }); // details: AuthDetails { publicKey: "G...", provider: "email" } ``` After `authenticate()` resolves, the signer's `getPublicKey()` and `signAuthEntry()` are ready and the payment can proceed with the normal `prepare -> sign -> send` flow. ## Server-side proxy Mount the signer-proxy in your backend: ```ts theme={null} // app/api/buckspay/auth/email/route.ts import { createSignerProxyRoute } from "@buckspay/nextjs"; export const POST = createSignerProxyRoute({ provider: "email", network: "testnet" }); ``` The proxy forwards the `issue` and `verify` actions to the facilitator with the backend secret. ## Example ```ts theme={null} // Recipe 16 - EMAIL / OTP LOGIN -> gasless USDC. Browser app. // // The user enters their email, receives a one-time code, and verifies it. The OTP backend // and the derived ed25519 key are custodied by the facilitator; the browser only ever sees // the public key + signatures, reached through YOUR signer-proxy. No secret ships client-side. import { createBuckspayClient, createRpcSimContext, type BuckspayConfig } from "@buckspay/core"; import { classicAccount } from "@buckspay/accounts/classic"; import { emailSigner } from "@buckspay/signers/email"; import { buckspayFacilitator } from "@buckspay/relayer/buckspay-facilitator"; const USDC_SAC_TESTNET = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; const MERCHANT = "GA6HCMBLTZS5VYYBCATRBR5VBZJEH5C2OON6XQGB3RNYDDAQ7JZ65YQH"; const signer = emailSigner({ // YOUR same-origin signer-proxy route; forwards to the facilitator with creds server-side. proxyUrl: "/api/buckspay/auth/email", network: "testnet" }); export const emailConfig: BuckspayConfig = { network: "testnet", account: classicAccount(), signer, relayer: buckspayFacilitator({ url: "/api/buckspay/relay", network: "testnet" }), gas: { mode: "sponsored" } }; export const emailClient = createBuckspayClient( emailConfig, createRpcSimContext("https://soroban-testnet.stellar.org") ); export async function payWithEmailLogin(email: string, otpFromInbox: string): Promise { // 1. Issue the code (UI then collects it from the user's inbox). await signer.requestOtp(email); // 2. Verify the code -> resolves the custodied Stellar ed25519 key. const details = await signer.authenticate?.({ email, otp: otpFromInbox }); console.log("signed in as", details?.publicKey, "via", details?.provider); // 3. Ordinary gasless payment - accounts/relayer/engine are untouched. await emailClient.connect(); const call = emailClient.transfer({ token: USDC_SAC_TESTNET, to: MERCHANT, amount: "2.00" }); const receipt = await emailClient.pay([call]); console.log(receipt.transferTx); } ``` ## Next Use an OAuth provider (Google, Apple, Discord) instead of an email code. How the signer-proxy and API key work together server-side. # Native passkey Source: https://buckspay.mintlify.app/signers/native-passkey iOS and Android WebAuthn via the device secure enclave - the same secp256r1 crypto as the web passkey signer, only the authenticator transport differs. `nativePasskey` from `@buckspay/react-native` brings passkey signing to iOS and Android. It delegates to the exact same crypto pipeline as the web `passkey` signer - the secp256r1 signatures, the COSE key extraction, and the OZ `__check_auth` payload are byte-for-byte identical. The only difference is the authenticator transport: `react-native-passkey` drives the platform's secure enclave instead of `navigator.credentials`. ## When to use Choose `nativePasskey` for mobile React Native apps where you want the same passwordless, seed-phrase-free experience as the web passkey flow. The private key never leaves the device secure enclave. Like its web counterpart, it must be paired with `ozContractAccount`. ## Factory ```ts theme={null} import { nativePasskey } from "@buckspay/react-native"; const signer = nativePasskey({ rpId: "app.example.com", rpName: "My App" }); ``` `nativePasskey(opts)` accepts a `NativePasskeyOptions` object: | Option | Type | Required | Description | | -------- | -------- | -------- | ---------------------------------------------------------------------------- | | `rpId` | `string` | Yes | Relying party domain (same rules as the web passkey - bound at registration) | | `rpName` | `string` | No | Human-readable name shown in the authenticator prompt | ## The same hooks, same flow `@buckspay/react-native` re-exports `BuckspayProvider`, `useWallet`, and `useStellarPay` by reference from `@buckspay/react`. The store and status machine are identical - your screen logic is the same as a web component. Only the host primitives (`` / ``) and the signer differ. ## Secure storage Session blobs and credential IDs are persisted via a `SecureStore` port. Three adapters are available: | Adapter | Use | | ----------------------- | ------------------------------------------------ | | `memorySecureStore()` | Tests and ephemeral sessions | | `expoSecureStore()` | Expo managed workflow (uses `expo-secure-store`) | | `keychainSecureStore()` | Bare React Native (uses `react-native-keychain`) | Each adapter lazily imports its optional native peer, so apps that do not use a given adapter never pull its native module into the bundle. ## Peer dependencies Install these in your React Native app (not in the SDK): * `react-native` - the React Native runtime * `react-native-passkey` - the native WebAuthn module * `react-native-get-random-values` - Hermes crypto polyfill ## Example ```tsx theme={null} // Recipe 13 - REACT NATIVE. The mobile binding re-exports the SAME @buckspay/react hooks and only // swaps the signer (native passkey / secure enclave), storage, and Hermes polyfills. The screen // logic is identical to web (recipe 05); only the host components (/) and the signer // differ. This example pins the @buckspay/react-native surface; the View/Text layer is the app's, // so it is omitted here (it would pull react-native intrinsics) - fragments stand in for it. import { BuckspayProvider, useWallet, useStellarPay, nativePasskey, memorySecureStore, type SecureStore } from "@buckspay/react-native"; import { ozContractAccount } from "@buckspay/accounts/oz-contract"; import { buckspayFacilitator } from "@buckspay/relayer/buckspay-facilitator"; import { createRpcSimContext, type BuckspayConfig, type Call } from "@buckspay/core"; const SPONSOR_G = "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"; const USDC_SAC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; const MERCHANT = "GA6HCMBLTZS5VYYBCATRBR5VBZJEH5C2OON6XQGB3RNYDDAQ7JZ65YQH"; // Same config shape as web - the ONLY mobile-specific line is the native passkey signer. export const mobileConfig: BuckspayConfig = { network: "testnet", account: ozContractAccount({ network: "testnet", sponsorAddress: SPONSOR_G }), signer: nativePasskey({ rpId: "app.buckspay.dev", rpName: "buckspay" }), relayer: buckspayFacilitator({ url: "/api/gasless", network: "testnet" }), gas: { mode: "sponsored" } }; // Session blobs / credential ids persist via a SecureStore port (keychain/expo on device). export const store: SecureStore = memorySecureStore(); // The hooks are byte-identical to @buckspay/react - no fork (recipe 05). export function PayScreen() { const { wallet, address, connect } = useWallet(); const { pay, status, receipt } = useStellarPay(); if (!wallet) { // On device this is a inside a . return <>{"Connect"}; } const call: Call = { contract: USDC_SAC, fn: "transfer", args: [] // built from address -> MERCHANT in the real screen; see recipe 05 }; void address; void (() => pay([call])); return <>{status === "success" && receipt ? `settled: ${receipt.transferTx}` : "Pay 1.50 USDC (free)"}; } // Wrap the tree once, exactly like web - pass `sim` so useStellarPay().pay() can simulate. export function App() { return ( ); } ``` ## Next The web counterpart - identical crypto, browser authenticator transport. How every signer feeds into the same client and payment flow. # Signers Source: https://buckspay.mintlify.app/signers/overview Every signer factory returns a BuckspaySigner that plugs into the same client and the same prepare -> sign -> send flow. Only how you obtain the signer differs. The central idea behind Buckspay signers: **the payment flow never changes**. Whether the user authenticates with a browser wallet, a device passkey, a social login, or a mobile secure enclave, every signer factory returns a `BuckspaySigner` value that drops into the same `createBuckspayClient` call and the same `prepare -> sign -> send` pipeline. ```ts theme={null} // The ONLY line that changes across all signer types: const signer = walletsKit({ network: "testnet" }); // passkey({ rpId: "app.example.com" }) // socialSigner({ provider: "web3auth", clientId, network, proxyUrl }) // emailSigner({ proxyUrl, network }) // nativePasskey({ rpId: "app.example.com" }) // @buckspay/react-native // Everything below is identical regardless of which signer you chose: const client = createBuckspayClient({ network, account, signer, relayer, gas }); await client.connect(); const receipt = await client.pay([client.transfer({ token, to, amount })]); ``` ## How it works A signer factory is a function that returns a `BuckspaySigner` object. The object implements two methods the engine calls internally: * **`getPublicKey()`** - resolves the Stellar account address backing this signer. * **`signAuthEntry(payload)`** - signs a Soroban authorization entry for the current payment. The engine calls these during `sign()` (the second phase of `prepare -> sign -> send`). All user interaction - wallet pop-ups, WebAuthn prompts, OAuth redirects, OTP verification - happens inside this one call. The rest of the pipeline is silent. Some signers also expose an optional **`authenticate?()`** step for social and email flows, which runs the identity-resolution phase before the payment itself. ## Choosing a signer | Signer | Package | Best for | | -------------- | ------------------------------- | ---------------------------------------------- | | Wallets Kit | `@buckspay/signers/wallets-kit` | Users with Freighter, xBull, or LOBSTR | | Passkey | `@buckspay/signers/passkey` | Passwordless new-user flows (browser) | | Social login | `@buckspay/signers/social` | Sign in with Google/Apple/Discord via web3auth | | Email OTP | `@buckspay/signers/email` | Email-code verification without a wallet | | Native passkey | `@buckspay/react-native` | iOS and Android (secure enclave) | Browser injected wallets - Freighter, xBull, LOBSTR. WebAuthn / secp256r1 on a smart contract account. Google, Apple, Discord via web3auth - secrets stay server-side. One-time code over email - no wallet, no seed phrase. iOS/Android secure enclave via `@buckspay/react-native`. The shared flow every signer feeds into. # Passkey Source: https://buckspay.mintlify.app/signers/passkey WebAuthn / secp256r1 signing on a smart contract account - the private key never leaves the authenticator. The passkey signer lets users authenticate with their device's built-in WebAuthn authenticator (Touch ID, Face ID, Windows Hello, a hardware security key). The private key never leaves the authenticator; the Soroban contract's `__check_auth` verifies the secp256r1 signature on-chain. ## When to use Choose `passkey` for the **hero new-user flow**: no seed phrase, no existing Stellar wallet, no XLM balance required. The signer must be paired with a contract account (`ozContractAccount`) because the secp256r1 verification runs inside the contract, not at the network layer. ## Factory ```ts theme={null} import { passkey } from "@buckspay/signers/passkey"; const signer = passkey({ rpId: "app.example.com", rpName: "My App" }); ``` `passkey(opts)` accepts a `PasskeyOptions` object: | Option | Type | Required | Description | | -------- | -------- | -------- | ------------------------------------------------------------- | | `rpId` | `string` | Yes | Relying party domain (must match the page's effective domain) | | `rpName` | `string` | No | Human-readable name shown in the authenticator prompt | `rpId` is bound permanently to the credential at registration time. If you change it, existing users cannot authenticate - their credential is tied to the original domain. Use `window.location.hostname` for consistency, or a stable parent domain for cross-subdomain apps. This is the WebAuthn anti-phishing mechanism: a credential registered at `app.example.com` cannot be used on `evil.example.net`. ## Required account type `passkey` must be paired with `ozContractAccount`. The contract's `__check_auth` method verifies the WebAuthn signature on-chain; a classic `G...` account cannot accept secp256r1 signatures. ```ts theme={null} import { ozContractAccount } from "@buckspay/accounts/oz-contract"; import { passkey } from "@buckspay/signers/passkey"; const account = ozContractAccount({ network: "testnet", sponsorAddress: SPONSOR_G }); const signer = passkey({ rpId: "app.example.com", rpName: "My App" }); ``` `sponsorAddress` is the facilitator's public `G...` address, needed to derive the deterministic `C...` address offline before the contract is deployed. ## Example The snippet below shows atomic batch transfers using `passkey` and `ozContractAccount`. A batch of one call is equivalent to `pay([call])`. ```ts theme={null} // Recipe 10 - ATOMIC BATCH (sendCalls). N USDC transfers settle all-or-nothing in ONE tx via the // pinned Multicall router's `batch_transfer` - the smart account (or classic wallet) authorizes the // whole batch with a SINGLE signature. A batch of 1 is exactly pay([call]). import { createBuckspayClient, batch, MAX_BATCH_CALLS, BuckspayError, 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"; const SPONSOR_G = "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"; const USDC_SAC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; const A = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"; const B = "GBPYQYRH62E6NLRGXHBT4I3ZPTEHVBQMYTSH44YLOAPTCDYNAXDOLJRY"; export const batchConfig: BuckspayConfig = { network: "testnet", account: ozContractAccount({ network: "testnet", sponsorAddress: SPONSOR_G }), signer: passkey({ rpId: "localhost", rpName: "buckspay" }), relayer: buckspayFacilitator({ url: "/api/gasless", network: "testnet" }), gas: { mode: "sponsored" } }; export const client = createBuckspayClient(batchConfig); export async function payManyAtomically(): Promise { await client.connect(); // Collect calls with the pure builder (enforces MAX_BATCH_CALLS on build()). const calls = batch() .add(client.transfer({ token: USDC_SAC, to: A, amount: "1.00" })) .add(client.transfer({ token: USDC_SAC, to: B, amount: "2.50" })) .build(); // sendCalls = atomic, all-or-nothing. Either BOTH transfers land or NEITHER does. const receipt = await client.sendCalls(calls); console.log(receipt.transferTx); // one settlement tx for the whole batch } export async function guardOversizeBatch(): Promise { const tooMany = Array.from({ length: MAX_BATCH_CALLS + 1 }, () => client.transfer({ token: USDC_SAC, to: A, amount: "0.01" }) ); try { await client.sendCalls(tooMany); } catch (e) { if (e instanceof BuckspayError && e.code === "BATCH_TOO_LARGE") { console.error(`batch capped at ${MAX_BATCH_CALLS} calls`); return; } throw e; } } ``` ## Next The same secp256r1 crypto on iOS and Android via the secure enclave. When to use ozContractAccount vs. classicAccount. # Social login Source: https://buckspay.mintlify.app/signers/social-login Sign users in with Google, Apple, or Discord via web3auth - the provider secret stays server-side, never in the browser bundle. The social signer lets users authenticate with an OAuth provider (Google, Apple, Discord) through web3auth. The OAuth public flow runs in the browser; the provider secret and verifier callback are handled by your backend signer-proxy. A Stellar ed25519 key backs the resulting account - the user never manages a seed phrase. ## When to use Choose `socialSigner` when you want a familiar sign-in button (Google, Apple, Discord) without requiring your users to install a wallet or remember a seed phrase. The resulting key backs a classic `G...` account. The web3auth **client secret** must never be included in a browser bundle. Configure a same-origin `proxyUrl` pointing at your backend signer-proxy, which is created with `createSignerProxyRoute` from `@buckspay/nextjs`. Only the public OAuth flow runs in the browser; the secret callback and verifier go through your server. ## Factory ```ts theme={null} import { socialSigner } from "@buckspay/signers/social"; const signer = socialSigner({ provider: "web3auth", clientId: "BNxxxx-your-web3auth-client-id", network: "testnet", proxyUrl: "/api/buckspay/auth/social" }); ``` `socialSigner(opts)` accepts a `SocialSignerOptions` object: | Option | Type | Required | Description | | ---------- | ----------------------- | -------- | ---------------------------------------------------------- | | `provider` | `"web3auth"` | Yes | OAuth provider backend (only `"web3auth"` in v1) | | `clientId` | `string` | Yes | Your web3auth project client ID (public - safe in browser) | | `network` | `"testnet" \| "pubnet"` | Yes | Stellar network to target | | `proxyUrl` | `string` | No | Same-origin route to the `@buckspay/nextjs` signer-proxy | ## Authentication Call `signer.authenticate?.(params)` before the payment. It runs the provider's OAuth flow and resolves an `AuthDetails` object: ```ts theme={null} const details = await signer.authenticate?.({ loginProvider: "google" }); // details: AuthDetails { publicKey: "G...", provider: "web3auth" } ``` After `authenticate()` returns, the signer's `getPublicKey()` and `signAuthEntry()` are ready. Provider or network failures surface as `BuckspayError` with code `AUTH_PROVIDER_ERROR`. ## Server-side proxy Mount the signer-proxy in your backend (for example, a Next.js App Router route): ```ts theme={null} // app/api/buckspay/auth/social/route.ts import { createSignerProxyRoute } from "@buckspay/nextjs"; export const POST = createSignerProxyRoute({ provider: "web3auth", network: "testnet" }); ``` The proxy receives the OAuth callback and calls the facilitator's `POST /auth/social` endpoint with the secret - only public key material is returned to the browser. ## Example ```ts theme={null} // Recipe 12 - SOCIAL LOGIN (web3auth) -> gasless USDC. Browser app. // // The user signs in with Google/Apple/Discord via web3auth. The PUBLIC OAuth runs in the // browser; the SECRET verifier callback is completed by YOUR backend's signer-proxy // (`@buckspay/nextjs` createSignerProxyRoute -> facilitator POST /auth/social). web3auth // hands back a Stellar ed25519 key - the smart-account signer. No web3auth secret and no // private key ever reach this bundle. import { createBuckspayClient, createRpcSimContext, type BuckspayConfig } from "@buckspay/core"; import { classicAccount } from "@buckspay/accounts/classic"; import { socialSigner } from "@buckspay/signers/social"; import { buckspayFacilitator } from "@buckspay/relayer/buckspay-facilitator"; const USDC_SAC_TESTNET = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; const MERCHANT = "GA6HCMBLTZS5VYYBCATRBR5VBZJEH5C2OON6XQGB3RNYDDAQ7JZ65YQH"; // The social ed25519 key backs the classic account (its G-address IS the account). const signer = socialSigner({ provider: "web3auth", clientId: "BNxxxx-your-web3auth-client-id", network: "testnet", // YOUR same-origin signer-proxy route; it forwards to the facilitator with the secret server-side. proxyUrl: "/api/buckspay/auth/social" }); export const socialConfig: BuckspayConfig = { network: "testnet", account: classicAccount(), signer, // url points at YOUR backend relay route (apiKey server-side), never the raw facilitator key. relayer: buckspayFacilitator({ url: "/api/buckspay/relay", network: "testnet" }), gas: { mode: "sponsored" } }; export const socialClient = createBuckspayClient( socialConfig, createRpcSimContext("https://soroban-testnet.stellar.org") ); export async function payWithSocialLogin(): Promise { // 1. Run the provider OAuth flow -> resolves the Stellar ed25519 key. const details = await signer.authenticate?.({ loginProvider: "google" }); console.log("signed in as", details?.publicKey, "via", details?.provider); // 2. From here it is an ordinary gasless payment - accounts/relayer/engine are untouched. await socialClient.connect(); const call = socialClient.transfer({ token: USDC_SAC_TESTNET, to: MERCHANT, amount: "1.50" }); const receipt = await socialClient.pay([call]); console.log(receipt.transferTx); } ``` ## Next No OAuth required - authenticate with a code sent to the user's inbox. How the signer-proxy and API key work together server-side. # Wallets Kit Source: https://buckspay.mintlify.app/signers/wallets-kit Connect Freighter, xBull, or LOBSTR with a single factory - the signer plugs into the same client and payment flow as every other Buckspay signer. The Wallets Kit signer wraps [Stellar Wallets Kit](https://github.com/creit-tech/StellarWalletsKit) and gives users their preferred browser wallet - Freighter, xBull, or LOBSTR - without any wallet-specific branching in your application code. ## When to use Choose `walletsKit` when your users already have a Stellar wallet installed. It suits classic `G...` accounts backed by an existing keypair. For new users who have no wallet, consider the [passkey signer](/signers/passkey) instead. ## Factory ```ts theme={null} import { walletsKit } from "@buckspay/signers/wallets-kit"; const signer = walletsKit({ network: "testnet" }); // network: "pubnet" - for production ``` `walletsKit(opts)` accepts a `KitOptions` object: | Option | Type | Required | Description | | ------------------ | ----------------------- | -------- | ---------------------------------------- | | `network` | `"testnet" \| "pubnet"` | Yes | Which Stellar network to target | | `selectedWalletId` | `string` | No | Pre-select a wallet (default: Freighter) | The factory returns a `BuckspaySigner`. The browser wallet library is loaded lazily on first use so it never runs during server-side rendering. ## Example The snippet below shows a complete classic gasless transfer using `walletsKit` paired with `classicAccount`. This is the canonical starting point for users who already have a wallet. ```ts theme={null} // Quickstart - classic gasless USDC transfer (browser; NO apiKey). import { createBuckspayClient, createRpcSimContext } from "@buckspay/core"; import { classicAccount } from "@buckspay/accounts/classic"; import { walletsKit } from "@buckspay/signers/wallets-kit"; import { buckspayFacilitator } from "@buckspay/relayer/buckspay-facilitator"; const USDC_SAC: string = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; const MERCHANT: string = "GA6HCMBLTZS5VYYBCATRBR5VBZJEH5C2OON6XQGB3RNYDDAQ7JZ65YQH"; export async function quickstart(): Promise { const buckspay = createBuckspayClient( { network: "testnet", account: classicAccount(), signer: walletsKit({ network: "testnet" }), // url points at YOUR backend, which forwards to the facilitator with the key server-side. relayer: buckspayFacilitator({ url: "/api/gasless", network: "testnet" }), gas: { mode: "sponsored" } }, // prepare() simulates against the Soroban RPC. createRpcSimContext("https://soroban-testnet.stellar.org") ); await buckspay.connect(); // wallet + ensureReady const call = buckspay.transfer({ token: USDC_SAC, to: MERCHANT, amount: "1.50" }); const receipt = await buckspay.pay([call]); // prepare -> sign -> send console.log(receipt.transferTx); // settled on testnet } ``` ## How signatures work Freighter historically returned a double-encoded auth entry in some versions. The Wallets Kit signer applies `normalizeSignature` internally before returning, so your code always receives a clean 64-byte signature regardless of which wallet or version the user has installed. ## Next Passwordless flow for new users - no wallet required. Pair walletsKit with classicAccount or ozContractAccount.