Skip to main content

Send and receive payments

Payments Overview

A payment constitutes the transfer of a token from one account to another. On Stellar network a token can be either a Stellar asset or a custom contract token which follows the SEP-41 token standard.

There are two primary ways payments are transacted on Stellar:

The approach you should take depends on the use case:

  • If you want to make a payment of a Stellar asset between two Stellar accounts, use the payment operations. Transaction fees are cheaper when using them compared to the fees for invoking the asset's token contract directly which is referred to as the Stellar Asset Contract.
  • If you want to make a payment of a Stellar asset between a Stellar account and a contract address, or between two contract addresses, then the asset's contract must be used. Stellar's payment-related operations cannot have contract addresses as their source or destination.
  • If you want to make a payment of a custom contract token which is not a Stellar asset but follows the SEP-41 token standard, you must use the token's contract. Stellar's payment operations can only be used to transfer Stellar assets.

To learn more about the differences between Stellar assets and contract tokens, see the Tokens overview.

info

In the following code samples, proper error checking is omitted for brevity. However, you should always validate your results, as there are many ways that requests can fail.

Using Payments Example

This example highlights the approach mentioned for transacting payments using operations and assets and using the Stellar RPC with Stellar Client SDKs to perform all actions needed.

Send a Payment

Lets demonstrate a payment of an asset on Stellar. We will build a transaction with a payment operation to send 10 Lummens from a sender account to a receiver account, sign it as the sender account, and submit it to the network.

  • Submitting the transaction to the SDF-maintained public testnet instance of Stellar RPC server.
  • When submitting transactions to the RPC server it's possible that you will not receive a response from the server due to network conditions.
    • In such a situation it's impossible to determine the status of your transaction.
    • Highlights a recommendation to always save a transaction (or transaction encoded in XDR format) in a variable or a database and resubmit it if you don't know its status.
    • The transaction in serialized XDR format is idempotent meaning if the transaction has already been successfully applied to the ledger, RPC will simply return the saved result and not attempt to submit the transaction again.
    • Only in cases where a transaction's status is unknown (and thus will have a chance of being included into a ledger) will a resubmission to the network occur.
Click to view payment sending code
// send_payment.js
// follow the https://github.com/stellar/js-stellar-sdk?tab=readme-ov-file#installation

import * as StellarSdk from "@stellar/stellar-sdk";
import fs from "fs";
const rpcServer = new StellarSdk.rpc.Server(
"https://soroban-testnet.stellar.org",
);

async function sendPayments() {
const { Keypair } = StellarSdk;
const senderKeyPair = Keypair.random();
const recipientKeyPair = Keypair.random();
let senderAccount;

try {
// Request airdrop for the sender account - this creates, funds and returns the Account object
senderAccount = await rpcServer.requestAirdrop(senderKeyPair.publicKey());
console.log("Sender Account funded with airdrop");
console.log("Sender Account ID:", senderAccount.accountId());
console.log("Sender Sequence number:", senderAccount.sequence.toString());

// Note - this persistence of sender id to a file on file system is only done for demo purposes.
// It shares the created sender account with the payment monitor example which will run next.
// Since this example is using file system it therefore is intended to run only on Node.
// In a real application the stellar js sdk can be used in browser or Node.
fs.writeFileSync("sender_public.key", senderAccount.accountId());

console.log("\n\n");

await rpcServer.requestAirdrop(recipientKeyPair.publicKey());
console.log("Recipient Account funded with airdrop");
console.log("Recipient Account ID:", recipientKeyPair.publicKey());
} catch (err) {
console.error("Airdrop / Account loading failed:", err);
return;
}

// Now call sendPayment with the funded account, in a loop of once every 30 seconds
while (true) {
console.log("\n\nSending payment...");
await sendPayment(
senderKeyPair,
senderAccount,
recipientKeyPair.publicKey(),
);
await new Promise((resolve) => setTimeout(resolve, 30000)); // wait for 30 seconds
}
}

async function sendPayment(sender, senderAccount, recipient) {
// The next step is to parametrize and build the transaction object:
// Using the source account we just loaded we begin to assemble the transaction.
// We set the fee to the base fee, which is 100 stroops (0.00001 XLM).
// We also set the network passphrase to TESTNET.
const transaction = new StellarSdk.TransactionBuilder(senderAccount, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: StellarSdk.Networks.TESTNET,
})
// We then add a payment operation to the transaction oject.
// This operation will send 10 XLM to the destination account.
// Obs.: Not specifying a explicit source account here means that the
// operation will use the source account of the whole transaction, which we specified above.
.addOperation(
StellarSdk.Operation.payment({
destination: recipient,
asset: StellarSdk.Asset.native(),
amount: "10",
}),
)
// We include an optional memo which oftentimes is used to identify the transaction
// when working with pooled accounts or to facilitate reconciliation.
.addMemo(StellarSdk.Memo.id("1234567890"))
// Finally, we set a timeout for the transaction.
// This means that the transaction will not be valid anymore after 180 seconds.
.setTimeout(180)
.build();

// We sign the transaction with the source account's secret key.
transaction.sign(sender);

// Now we can send the transaction to the network.
// The sendTransaction method immediately returns a reply with the transaction hash
// and the status "PENDING". This means the transaction was received and is being processed.
const sendTransactionResponse = await rpcServer.sendTransaction(transaction);

// Here we check the status of the transaction as there are
// a possible outcomes after sending a transaction that would have
// to be handled accordingly, such as "DUPLICATE" or "TRY_AGAIN_LATER".
if (sendTransactionResponse.status !== "PENDING") {
throw new Error(
`Failed to send transaction, status: ${sendTransactionResponse.status}`,
);
}

console.log(
"Payment Transaction submitted, hash:",
sendTransactionResponse.hash,
);

// Here we poll the transaction status to await for its final result.
// We can use the transaction hash to poll the transaction status later.
const finalStatus = await rpcServer.pollTransaction(
sendTransactionResponse.hash,
);

// The pollTransaction method will return the final status of the transaction
// after the specified number of attempts or when the transaction is finalized.
// We then check the final status of the transaction and handle it accordingly.
switch (finalStatus.status) {
case StellarSdk.rpc.Api.GetTransactionStatus.FAILED:
console.error("Transaction failed, status:", finalStatus.status);
if (finalStatus.resultXdr) {
console.error(
"Transaction Result XDR (decoded):",
JSON.stringify(finalStatus.resultXdr, null, 2),
);
}

throw new Error(`Transaction failed with status: ${finalStatus.status}`);
case StellarSdk.rpc.Api.GetTransactionStatus.NOT_FOUND:
throw new Error(`Transaction failed with status: ${finalStatus.status}`);
case StellarSdk.rpc.Api.GetTransactionStatus.SUCCESS:
console.log("Success! Committed on Ledger:", finalStatus.ledger);
break;
}
}

sendPayments().catch((error) => {
console.error("Error executing sendPayments function:", error);
process.exit(1);
});

Monitoring Payments as event stream

Leverage the latest Protocol 23 (Whisk) to capture payment activity as pure events. These events can be efficiently monitored using the RPC server's getEvents method, which provides server-side filtering and real-time payment detection.

The RPC approach offers several advantages:

  • Server-side event topic filtering: Filter for specific events using topic patterns
  • Efficient polling: Use cursors and pagination for reliable event processing
  • Unified event model: supports CAP-67, all asset movements on the network are emitted as events with standardized topic names to indicate the type of movement (e.g., "transfer" for payments)
  • Better error handling: RPC stateless HTTP polling is more resilient than persistent streaming protocols like SSE or WebSockets

Demonstrate near real-time monitoring of transactions from the Stellar network by establishing an asynchronous listener to consume events from RPC and filter them by topic for just our example payments generated by our previous payments script.

Click to view payment monitoring code
// monitor_payment.js
// follow the https://github.com/stellar/js-stellar-sdk?tab=readme-ov-file#installation

import * as StellarSdk from "@stellar/stellar-sdk";
import fs from "fs";

async function monitorPayments() {
// Initialize RPC server for testnet
const rpcServer = new StellarSdk.rpc.Server(
"https://soroban-testnet.stellar.org",
);

// One-time initialization - everything encapsulated here
const monitoredFromAccount = fs
.readFileSync("sender_public.key", "utf-8")
.trim();

// create our payment event topic filter values
const transferTopicFilter =
StellarSdk.xdr.ScVal.scvSymbol("transfer").toXDR("base64");
const fromTopicFilter = StellarSdk.nativeToScVal(monitoredFromAccount, {
type: "address",
}).toXDR("base64");

// Get starting ledger
const latestLedger = await rpcServer.getLatestLedger();
console.log(
`Starting payment monitoring from ledger ${latestLedger.sequence} and from account ${monitoredFromAccount}`,
);
let currentStartLedger = latestLedger.sequence;
let currentCursor;

while (true) {
// Query for payments from our monitored account
const eventsResponse = await rpcServer.getEvents({
startLedger: currentStartLedger,
cursor: currentCursor,
filters: [
{
type: "contract",
topics: [
[
transferTopicFilter,
fromTopicFilter,
"**", // filter will match on any 'to' address and any token(SAC or SEP-41)
],
],
},
],
limit: 10,
});

// Process any events found
console.log(`Found ${eventsResponse.events.length} payment(s):`);

for (const event of eventsResponse.events) {
console.log("\n--- Payment Received ---");
console.log(`Ledger: ${event.ledger}`);
console.log(`Transaction Hash: ${event.txHash}`);
console.log(`Closed At: ${event.ledgerClosedAt}`);

// Decode addresses from topics
try {
const fromAddress = StellarSdk.scValToNative(event.topic[1]);
const toAddress = StellarSdk.scValToNative(event.topic[2]);
console.log(`Transfer: ${fromAddress}${toAddress}`);
} catch (error) {
console.log(`From/To: Unable to decode addresses`, error);
continue;
}

// Decode transfer amount from the event data
try {
// Protocol 23+ unified events model per CAP-0067
// https://github.com/stellar/stellar-protocol/blob/master/core/cap-0067.md
// Event value field is scMap with {amount: i128, to_muxed_id: u64|bytes|string} (when muxed info present)
const amount = StellarSdk.scValToNative(event.value)["amount"];
console.log(
`Amount: ${amount.toString()} stroops (${(Number(amount) / 10000000).toFixed(7)} XLM)`,
);
} catch (error) {
console.log(`Failed to decode transfer amount:`, error);
}
// Decode asset from topics[3], this is only present in events from SAC tokens
if (event.topic.length > 3) {
const asset = StellarSdk.scValToNative(event.topic[3]);
console.log(`Asset: ${asset}`);
}
}

// Update cursor to drive next query
currentCursor = eventsResponse.cursor;
currentStartLedger = null;

// Wait 5 seconds before next iteration
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}

// Start the application
monitorPayments().catch((error) => {
console.error("Failed during payment monitoring:", error);
});

Running Your Payment Pipeline with RPC

  1. Run the payment submission script in one terminal. Leave it running, it will submit a payment once every 30 seconds.
Click for command to run the payments
# js
node send_payment.js

# java
# JRE 17 or higher should be installed - https://adoptium.net/temurin/releases/?version=17&package=jdk
# download Stellar Java SDK jar, latest release on maven central
JAR_REPO='https://repo1.maven.org/maven2/network/lightsail/stellar-sdk'
JAR_VERSION="2.0.0" # replace with latest version if needed
JAR_FILE="stellar-sdk-$JAR_VERSION-uber.jar"
curl -fsSL -o "$JAR_FILE" "$JAR_REPO/$JAR_VERSION/$JAR_FILE"

javac -cp "$JAR_FILE" SendPaymentExample.java
java -cp ".:$JAR_FILE" SendPaymentExample

# go
mkdir -p payments_example/sender
cd payments_example
go mod init payments_example
go get github.com/stellar/go@latest
go get github.com/stellar/stellar-rpc@latest
# save sender.go code to sender/sender.go file
go build -o sender sender/sender.go
./sender
  1. Run the payment monitor script in a separate terminal.
Click for command to run the payments monitor
# js
node monitor_payments.js

# java
javac -cp "$JAR_FILE" MonitorPaymentExample.java
java -cp ".:$JAR_FILE" MonitorPaymentExample

# go
mkdir -p payments_example/monitor
# save monitor.go code to monitor/monitor.go file
go build -o monitor monitor/monitor.go
./monitor
  1. Observe the payment events as they're detected and displayed on console.

Summary

  • Payments from transaction operations or contract invocations on Stellar network can be monitored by leveraging the unified events model as of Protocol 23 (Whisk) and the RPC getEvents method.

    • Payments are 'transfer' events: the unified events model ensures whether a payment happened through an operation or contract invocation it will result in the same 'transfer' event being emitted.
    • Event Type: "contract" denotes payments are an application level event rather than 'system' event.
    • Transfer Topics Model: an array of four topics, specified in CAP-67 and summarized:
      • topic[0] = Event name 'transfer' determines the next 3 topics
      • topic[1] = Transfer Sender address
      • topic[2] = Transfer Recipient address
      • topic[3] = Asset identifier (only present for Stellar Assets, through their built-in contracts (SAC))
  • RPC provides precise filtering of Stellar network events and this includes event topics model defined for unified events in CAP-67. You save time by using the RPC server-side filter capabilities to focus on processing only events relative to your application such as payments from a specific account in this example.

    • Wildcards: The RPC getEvents allows to use "*" to match any value in a topic position
    • Server-side Filtering: RPC getEvents applies the filters and only matching events are returned, reducing bandwidth
    • Cursor-based Pagination: RPC getEvents retrieves event data efficiently with a paging mechanism

Guides in this category: