Documentation: spare-sdk

Documentation

spare-sdk

Add a per-transaction USDC micro-donation to any Stellar app. The donation goes straight to the foundation's account; Spare never holds funds. Circle USDC only, domain-verified foundations only.

Install

pnpm add spare-sdk @stellar/stellar-sdk

@stellar/stellar-sdk is a peer dependency: install it next to spare-sdk (one copy shared with your app).

Classic transaction: addDonation

import { addDonation, HorizonAccountPort } from "spare-sdk";

const result = await addDonation(unsignedTx, {
  foundation: { account: "G...", domain: "foundation.org" }, // must be listed in its stellar.toml
  amount: "0.50",            // or: roundUp: true (only when the purchase is USDC)
  appId: "my-app",           // <= 22 ASCII bytes, memo becomes "spare:my-app"
  maxAmount: "5",            // your cap per donation
  accounts: new HorizonAccountPort("https://horizon.stellar.org"),
});

switch (result.status) {
  case "offered":      // ask the user "Add US$<result.donation.amount>?", then sign result.transaction
  case "not-offered":  // sign the original tx; result.reason says why (e.g. "insufficient-balance")
  case "passthrough":  // signed / fee-bump / multisig / extra signers / Soroban: sign the original tx
}

offered returns a new unsigned transaction (new hash, fee raised by one operation, memo set only if it was empty and no destination requires one). Nothing is added without the user's explicit yes.

Round-up

Only when the whole transaction is a Circle-USDC purchase (payments, pathPaymentStrictSend, sell offers). The SDK reports what it used, so show these instead of recomputing: donation.purchase / purchaseStroops (the USDC purchase) and roundedTotal / roundedTotalStroops (purchase plus donation). They are set on round-up only.

  • Already whole (for example 5.00): round-up-already-whole.
  • pathPaymentStrictReceive, XLM or other-asset operations, or unmodelled operations in the same transaction: round-up-needs-usdc-purchase (the real spend is unknown up front, so Spare never rounds sendMax).

A Soroban transaction holds one operation, so the donation is a separate payment after success. It needs a second signature: tell the user before they accept.

import { donateAfter } from "spare-sdk";

const follow = await donateAfter(
  { successful: true, sourceAccount: userAccount, hash },
  { foundation, amount: "0.50", appId: "my-app", maxAmount: "5",
    accounts, networkPassphrase: Networks.PUBLIC },
);
if (follow.status === "offered") { /* confirm, then sign follow.transaction */ }

The follow-up is a single payment (medium threshold). If the user's account needs more than its master key to sign that, nothing is offered (multisig).

Results and reason codes

StatusMeaning
offeredA new unsigned transaction, the donation info and the fee. Ask the user, then sign it.
not-offeredThe transaction is fine but no donation must be offered. reason says why. Sign the original.
passthroughSpare cannot safely modify the transaction. It is left untouched. Sign the original.

passthrough reasons

ReasonMeaning
fee-bumpThe transaction is a fee-bump.
already-signedThe transaction already carries signatures.
sorobanA Soroban transaction: use donateAfter.
multiple-sourcesMore than one source account.
multisigThe master key alone cannot meet the transaction's threshold: medium, or high with setOptions / accountMerge.
extra-signersThe CAP-21 extraSigners precondition is set.

not-offered reasons

ReasonMeaning
invalid-optionsOptions are malformed, including a malformed baseFee.
invalid-app-idappId is not valid (at most 22 ASCII bytes).
invalid-amountThe amount is not a valid USDC amount.
invalid-foundationThe foundation is malformed.
amount-over-capThe donation is above your maxAmount.
round-up-needs-usdc-purchaseRound-up needs a Circle-USDC purchase (see above).
round-up-already-wholeThe purchase is already a whole amount.
foundation-not-verifiedThe foundation account is not listed in its website's stellar.toml.
foundation-trustline-missingThe foundation has no USDC trustline.
foundation-trustline-unauthorizedThe foundation's USDC trustline is not authorized.
foundation-trustline-fullNo room under the foundation's trustline limit.
user-trustline-missingThe user has no USDC trustline.
user-trustline-unauthorizedThe user's USDC trustline is not authorized.
unsupported-operationThe transaction has operations that can spend funds in ways Spare does not model, for example manageBuyOffer, createClaimableBalance, liquidityPoolDeposit, accountMerge.
multisigFrom donateAfter only: the follow-up needs more than the master key.
insufficient-balanceNot enough USDC, or XLM for the fee plus any XLM the transaction sends.
account-lookup-failedThe account could not be loaded.
main-tx-not-successfuldonateAfter only: the main transaction did not succeed.

DonationInfo

FieldDescription
amountAmount in USDC, 7 decimals. Show this to the user before signing.
amountStroopsThe same amount in stroops (bigint).
foundationThe foundation account.
attributedFalse when the transaction already had a memo, so the donation cannot be tagged for the app.
purchase, purchaseStroopsRound-up only: the USDC purchase the transaction already makes.
roundedTotal, roundedTotalStroopsRound-up only: the purchase plus the donation.

Options

OptionDescription
foundation{ account, domain }: the G... account and the bare hostname whose stellar.toml must list it.
appIdAt most 22 ASCII bytes. The memo becomes spare:<appId>.
maxAmountYour cap per donation, in USDC.
accountsAn AccountPort (for example HorizonAccountPort).
fetchOptional. Injectable fetch, defaults to the global one.
baseFeeOptional. Per-operation fee in stroops, digits only, minimum 100.

Account port

AccountPort.loadAccount(id) returns an AccountSnapshot, or null when the account does not exist. HorizonAccountPort fills it from Horizon (headroom = limit - balance - buying liabilities; memoRequired from the config.memo_required data entry). Spare decides about multiple signatures from masterWeight and thresholds per transaction.

FieldDescription
sequenceCurrent sequence number (the next transaction uses sequence + 1).
xlmAvailableXLM spendable on fees: balance minus reserve and selling liabilities.
usdcCircle-USDC trustline { balance, authorized, headroom }, or null when there is none.
masterWeightWeight of the account's own key; 0 when it is disabled.
thresholds{ low, medium, high }, 0-255.
memoRequiredSEP-29: the account expects a memo on incoming payments.

Memo, SEP-29 and fees

If a payment, path payment or merge destination has config.memo_required set, Spare does not set its memo, so your app's own checkMemoRequired still fires. The donation is still offered, with donation.attributed === false. A destination that cannot be read is treated as memo-required.

baseFee is per operation, digits only, minimum 100 stroops. A malformed value gives not-offered / invalid-options; it never throws. A transaction with a fee of 0 is raised to 100 per operation, and the same number is used for the balance check and the built transaction.

Helpers

  • verifyFoundation(foundation) and listVerifiedFoundations(candidates): the SEP-1 ACCOUNTS check (https only, no redirects, 5 s timeout, body capped at 100 KB).
  • parseUsdc, formatStroops, roundUpToWholeDollar: amount helpers.
  • isValidAppId, memoFor: app id validation and the memo.
  • USDC_ISSUER, USDC_CODE: the Circle USDC constants.
  • Network access (fetch, AccountPort) is injectable.