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 roundssendMax).
Soroban call: donateAfter
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
| Status | Meaning |
|---|---|
offered | A new unsigned transaction, the donation info and the fee. Ask the user, then sign it. |
not-offered | The transaction is fine but no donation must be offered. reason says why. Sign the original. |
passthrough | Spare cannot safely modify the transaction. It is left untouched. Sign the original. |
passthrough reasons
| Reason | Meaning |
|---|---|
fee-bump | The transaction is a fee-bump. |
already-signed | The transaction already carries signatures. |
soroban | A Soroban transaction: use donateAfter. |
multiple-sources | More than one source account. |
multisig | The master key alone cannot meet the transaction's threshold: medium, or high with setOptions / accountMerge. |
extra-signers | The CAP-21 extraSigners precondition is set. |
not-offered reasons
| Reason | Meaning |
|---|---|
invalid-options | Options are malformed, including a malformed baseFee. |
invalid-app-id | appId is not valid (at most 22 ASCII bytes). |
invalid-amount | The amount is not a valid USDC amount. |
invalid-foundation | The foundation is malformed. |
amount-over-cap | The donation is above your maxAmount. |
round-up-needs-usdc-purchase | Round-up needs a Circle-USDC purchase (see above). |
round-up-already-whole | The purchase is already a whole amount. |
foundation-not-verified | The foundation account is not listed in its website's stellar.toml. |
foundation-trustline-missing | The foundation has no USDC trustline. |
foundation-trustline-unauthorized | The foundation's USDC trustline is not authorized. |
foundation-trustline-full | No room under the foundation's trustline limit. |
user-trustline-missing | The user has no USDC trustline. |
user-trustline-unauthorized | The user's USDC trustline is not authorized. |
unsupported-operation | The transaction has operations that can spend funds in ways Spare does not model, for example manageBuyOffer, createClaimableBalance, liquidityPoolDeposit, accountMerge. |
multisig | From donateAfter only: the follow-up needs more than the master key. |
insufficient-balance | Not enough USDC, or XLM for the fee plus any XLM the transaction sends. |
account-lookup-failed | The account could not be loaded. |
main-tx-not-successful | donateAfter only: the main transaction did not succeed. |
DonationInfo
| Field | Description |
|---|---|
amount | Amount in USDC, 7 decimals. Show this to the user before signing. |
amountStroops | The same amount in stroops (bigint). |
foundation | The foundation account. |
attributed | False when the transaction already had a memo, so the donation cannot be tagged for the app. |
purchase, purchaseStroops | Round-up only: the USDC purchase the transaction already makes. |
roundedTotal, roundedTotalStroops | Round-up only: the purchase plus the donation. |
Options
| Option | Description |
|---|---|
foundation | { account, domain }: the G... account and the bare hostname whose stellar.toml must list it. |
appId | At most 22 ASCII bytes. The memo becomes spare:<appId>. |
maxAmount | Your cap per donation, in USDC. |
accounts | An AccountPort (for example HorizonAccountPort). |
fetch | Optional. Injectable fetch, defaults to the global one. |
baseFee | Optional. 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.
| Field | Description |
|---|---|
sequence | Current sequence number (the next transaction uses sequence + 1). |
xlmAvailable | XLM spendable on fees: balance minus reserve and selling liabilities. |
usdc | Circle-USDC trustline { balance, authorized, headroom }, or null when there is none. |
masterWeight | Weight of the account's own key; 0 when it is disabled. |
thresholds | { low, medium, high }, 0-255. |
memoRequired | SEP-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)andlistVerifiedFoundations(candidates): the SEP-1ACCOUNTScheck (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.