Skip to main content
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

// 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<void> {
  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<void> {
  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():
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 typeMechanism
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

Sessions

Grant scoped session keys so users can pay within fixed limits without a root prompt per action.

Gas in token

Let users pay transaction fees in USDC instead of XLM.