Path Payments
!req glossary ennbtry with jed diction:
"cross-asset transactions"
path p ayments let accounts send one asset to a recipeint , whoi receiveds a differnt asset. The secure conversion takes advantagfe of the netowrk's natie liquidity to ewxchange using istant avaliable trades.
The operations specify a minimum amount which the market must meet or excceed. For instance, if you send 10 bananas and rquire a receiver to get at last 15 appleess, then the trahsfer will seek the best exxchange at or above 1.5 each.
TRhis let accoutns transfer value without mandating a single universal currency. You converting one asset ito another at the point of transferring value.
Routign logic
!see below qupte!
Pay payments use the DEX or AMM pools whcih standy readyt t oswap assets at the time of a trnasaction. They cna only consume existing liquidity by drawing on offers previously posted.
Tje tramsfer suicceeds oiF nd only if there's enough eavlaible interest in the ledger submitted, at the minimum rate. Without this liquiddyty, the cohnversion fails and returns an errro code.
To prepatre for
Path Hops
When sending path payments, your transfer can hop between up to six order books or AMMs to find the best price.
Validators perform this arithmatic automatically, allowing you to specify only the lowest total amount you will accept.
At each step in the path, the network calculates the optimal source of liquidity to convert through given your destination asset.
Both the order book and AMMs coexist, providing multiple avenues for liquidity.
Instead of having to choose whether to go through the order book or an AMM, the pathfinding algorithm automatically checks both sources of liquidity and executes new trades using whichever offers the better rate.
It also exchanges with an AMM over an order book at each step if the entire conversation happens at a price equal to or better than limit offers.
paths
s when you call this ou;ll notice differnt soruce_amoutns in /data/apis/horizon/api-reference/list-strict-receive-payment-paths or differnt destination_amount in docs/data/apis/horizon/api-reference/list-strict-send-payment-paths
Converting Path Paymtns
offers or AMMs which convert to a minimum specified amount or better.
Transfers automatically use up to six different orderbooks to get the best price, as fully defined in [the Encyclopedia page here] .
Some assets will have a small or nonexistent order book between them. In these cases, Stellar facilitates path payments, which we’ll discuss later.
Atomicity
payh maymentn operations take adntage of every teerasnaction's exclusive ability to succeed i n full or fail.
This means that your transfer off 10 or more AstroDollars will not
In a path payment, the asset received differs from the asset sent. Rather than the operation transferring assets directly from one account to another, path payments cross through
before arriving at the destination account.
For the path payment to succeed, there has to be enough liquidity path in existence. Conversions can take up to six independent hops to succeed at the best avaliable price.
Example
Account A sells XLM → [buy XLM / sell ETH → buy ETH / sell BTC → buy BTC / sell USDC] → Account B receives USDC
It is possible for path payments to fail if there are no viable exchange paths.
Path payments - more info
- Path payments don’t allow intermediate offers to be from the source account as this would yield a worse exchange rate. You’ll need to either split the path payment into two smaller path payments or ensure that the source account’s offers are not at the top of the order book.
- Balances are settled at the very end of the operation.
- This is especially important when (
Destination, Destination Asset) == (Source, Send Asset) as this provides a functionality equivalent to getting a no-interest loan for the duration of the operation.
- This is especially important when (
Destination minis a protective measure, it allows you to specify a lower bound for an acceptable conversion. If offers in the order books are not favorable enough for the operation to deliver that amount, the operation will fail.
Operations
Path payments use the Path Payment Strict Send or Path Payment Strict Receive operations.
Path Payment Strict Send
errors Allows a user to specify the amount of the asset to send. The amount received will vary based on offers in the order books and/or liquidity pools.
Path Payment Strict Receive
errors Allows a user to specify the amount of the asset received. The amount sent will vary based on the offers in the order books/liquidity pools.
Example
First, ensure the receiver has a trustline established for the asset they will receive. In this example, we will use USDC as the asset received. The sender will send XLM, which will be converted to USDC through the path payment operation.
- Python
- JavaScript
- Java
- Go
from stellar_sdk import Server, Keypair, Asset, TransactionBuilder, Network, Memo
USDC_ISSUER = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" # USDC issuer on Testnet
USDC_ASSET = Asset("USDC", USDC_ISSUER)
RECEIVER_SECRET = "S..." # Receiver secret
SENDER_SECRET = "S..." # Sender secret
HORIZON_SERVER = Server("https://horizon-testnet.stellar.org")
receiverKP = Keypair.from_secret(RECEIVER_SECRET)
receiverAccount = HORIZON_SERVER.load_account(receiverKP.public_key)
trustTx = (
TransactionBuilder(
source_account=receiverAccount,
network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE,
base_fee=HORIZON_SERVER.fetch_base_fee()
)
.append_change_trust_op(
asset=USDC_ASSET,
limit="10"
)
.add_memo(Memo.text("Trusting USDC"))
.set_timeout(30)
.build()
)
trustTx.sign(receiverKP)
trustResp = HORIZON_SERVER.submit_transaction(trustTx)
print("Trustline response:", trustResp)
import {
Horizon,
Asset,
Keypair,
TransactionBuilder,
Networks,
BASE_FEE,
Operation,
Memo,
} from "@stellar/stellar-sdk";
const USDC_ISSUER = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"; // USDC issuer on Stellar Testnet
const USDC_ASSET = new Asset("USDC", USDC_ISSUER); // USDC asset on Stellar Testnet
const RECEIVER_SECRET = "S..."; // Receiver's secret key
const SENDER_SECRET = "S..."; // Sender's secret key
const horizonServer = new Horizon.Server("https://horizon-testnet.stellar.org");
// Create a USDC trustline for the receiver
const receiverKP = Keypair.fromSecret(RECEIVER_SECRET);
let account = await horizonServer.loadAccount(receiverKP.publicKey());
let transaction = new TransactionBuilder(account, {
fee: BASE_FEE * 100,
networkPassphrase: Networks.TESTNET,
})
.addOperation(
Operation.changeTrust({
asset: USDC_ASSET,
limit: "10",
}),
)
.addMemo(Memo.text("Trusting USDC"))
.setTimeout(30)
.build();
transaction.sign(receiverKP);
const resp = await SERVER.submitTransaction(transaction);
console.log("resp", resp);
import org.stellar.sdk.*;
import org.stellar.sdk.requests.AccountsRequestBuilder;
import org.stellar.sdk.responses.AccountResponse;
public class pathPay {
static final String HORIZON_SERVER_URL = "https://horizon-testnet.stellar.org";
static final String USDC_ISSUER = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"; // Testnet issuer
static final String RECEIVER_SECRET = "S..."; // Receiver secret seed
static final String SENDER_SECRET = "S..."; // Sender secret seed
public static void main(String[] args) {
Server server = new Server(HORIZON_SERVER_URL);
// Assets (use ChangeTrustAsset for trustline; Asset for payments)
ChangeTrustAsset usdcTrust = ChangeTrustAsset.createNonNativeAsset("USDC", USDC_ISSUER);
Asset usdcAsset = Asset.createNonNativeAsset("USDC", USDC_ISSUER);
KeyPair receiverKP = KeyPair.fromSecretSeed(RECEIVER_SECRET);
AccountResponse receiverAccount = server.accounts().account(receiverKP.getAccountId());
Transaction trustTx = new TransactionBuilder(receiverAccount, Network.TESTNET)
.setBaseFee(Transaction.MIN_BASE_FEE) // you can raise this if network load is high
.setTimeout(30)
.addOperation(
new ChangeTrustOperation.Builder(usdcTrust, "10").build()
)
.addMemo(Memo.text("Trusting USDC"))
.build();
trustTx.sign(receiverKP);
SubmitTransactionResponse trustResp = server.submitTransaction(trustTx);
System.out.println("Trustline response: " + trustResp.isSuccess());
...
package main
import (
"context"
"fmt"
"log"
"github.com/stellar/go-stellar-sdk/clients/horizonclient"
"github.com/stellar/go-stellar-sdk/keypair"
"github.com/stellar/go-stellar-sdk/network"
"github.com/stellar/go-stellar-sdk/txnbuild"
)
const HORIZON_SERVER_URL = "https://horizon-testnet.stellar.org"
const USDC_ISSUER = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" // Testnet USDC issuer
const RECEIVER_SECRET = "S..." // Receiver secret seed
const SENDER_SECRET = "S..." // Sender secret seed
func main() {
ctx := context.Background()
client := &horizonclient.Client{
HorizonURL: HORIZON_SERVER_URL,
}
// USDC asset definition
usdcAsset := txnbuild.CreditAsset{Code: "USDC", Issuer: USDC_ISSUER}
// ==========================================================
// Step 1: Receiver creates a trustline to USDC (limit 10)
// ==========================================================
receiverKP, err := keypair.ParseFull(RECEIVER_SECRET)
check(err)
receiverReq := horizonclient.AccountRequest{AccountID: receiverKP.Address()}
receiverAccount, err := client.AccountDetail(receiverReq)
check(err)
trustOp := &txnbuild.ChangeTrust{
Line: usdcAsset,
Limit: "10",
}
trustTx, err := txnbuild.NewTransaction(txnbuild.TransactionParams{
SourceAccount: &receiverAccount,
IncrementSequenceNum: true,
BaseFee: txnbuild.MinBaseFee * 100, // similar to BASE_FEE * 100 in your JS
Timebounds: txnbuild.NewTimeout(30),
Operations: []txnbuild.Operation{trustOp},
Memo: txnbuild.MemoText("Trusting USDC"),
})
check(err)
trustTx, err = trustTx.Sign(network.TestNetworkPassphrase, receiverKP)
check(err)
trustTxB64, err := trustTx.Base64()
check(err)
trustResp, err := client.SubmitTransactionXDR(ctx, trustTxB64)
check(err)
fmt.Println("Trustline response: ", trustResp.Hash)
...
Now let's send a path payment from the sender to the receiver, converting XLM to USDC:
- Python
- JavaScript
- Java
- Go
senderKP = Keypair.from_secret(SENDER_SECRET)
senderAccount = HORIZON_SERVER.load_account(senderKP.public_key)
paymentTx = (
TransactionBuilder(
source_account=senderAccount,
network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE,
base_fee=HORIZON_SERVER.fetch_base_fee()
)
.append_path_payment_strict_receive_op(
send_asset=Asset.native(), # Send XLM
send_max="10", # Max XLM to spend
destination=receiverKP.public_key, # Receiver
dest_asset=USDC_ASSET, # Receiver gets USDC
dest_amount="1" # Exactly 1 USDC
# path=[] # Optional explicit path
)
.add_memo(Memo.text("XLM to USDC"))
.set_timeout(30)
.build()
)
paymentTx.sign(senderKP)
payResp = HORIZON_SERVER.submit_transaction(paymentTx)
print("Payment response:", payResp)
// Use path payment to send XLM from the receiver to the sender, who receives USDC
let senderKP = Keypair.fromSecret(SENDER_SECRET);
let account = await horizonServer.loadAccount(senderKP.publicKey());
let transaction = new TransactionBuilder(account, {
fee: BASE_FEE * 100,
networkPassphrase: Networks.TESTNET,
})
.addOperation(
Operation.pathPaymentStrictReceive({
sendAsset: Asset.native(), // Sending XLM
sendMax: "10", // Maximum amount of XLM to send
destAsset: USDC_ASSET, // Receiving USDC
destAmount: "1", // Amount of USDC to receive
destination: receiverKP.publicKey(), // Receiver's public key
}),
)
.addMemo(Memo.text("XLM to USDC"))
.setTimeout(30)
.build();
transaction.sign(senderKP);
const resp = await horizonServer.submitTransaction(transaction);
console.log("resp", resp);
...
KeyPair senderKP = KeyPair.fromSecretSeed(SENDER_SECRET);
AccountResponse senderAccount = server.accounts().account(senderKP.getAccountId());
Transaction payTx = new TransactionBuilder(senderAccount, Network.TESTNET)
.setBaseFee(Transaction.MIN_BASE_FEE)
.setTimeout(30)
.addOperation(
new PathPaymentStrictReceiveOperation.Builder(
Asset.createNativeAsset(), // sendAsset (XLM)
"10", // sendMax (max XLM to spend)
receiverKP.getAccountId(), // destination
usdcAsset, // destAsset (USDC)
"1" // destAmount (exactly 1 USDC to receiver)
)
// .setPath(List.of(...)) // optional explicit path if you want to force hops
.build()
)
.addMemo(Memo.text("XLM to USDC"))
.build();
payTx.sign(senderKP);
SubmitTransactionResponse payResp = server.submitTransaction(payTx);
System.out.println("Payment response: " + payResp.isSuccess());
}
}
...
senderKP, err := keypair.ParseFull(SENDER_SECRET)
check(err)
senderReq := horizonclient.AccountRequest{AccountID: senderKP.Address()}
senderAccount, err := client.AccountDetail(senderReq)
check(err)
payOp := &txnbuild.PathPaymentStrictReceive{
SendAsset: txnbuild.NativeAsset{}, // sending XLM
SendMax: "10", // max XLM to spend
Destination: receiverKP.Address(), // receiver account
DestAsset: usdcAsset, // receiver gets USDC
DestAmount: "1", // exactly 1 USDC
// Path: []txnbuild.Asset{...}, // optional explicit path
}
payTx, err := txnbuild.NewTransaction(txnbuild.TransactionParams{
SourceAccount: &senderAccount,
IncrementSequenceNum: true,
BaseFee: txnbuild.MinBaseFee * 100,
Timebounds: txnbuild.NewTimeout(30),
Operations: []txnbuild.Operation{payOp},
Memo: txnbuild.MemoText("XLM to USDC"),
})
check(err)
payTx, err = payTx.Sign(network.TestNetworkPassphrase, senderKP)
check(err)
payTxB64, err := payTx.Base64()
check(err)
payResp, err := client.SubmitTransactionXDR(ctx, payTxB64)
check(err)
fmt.Println("Payment response: ", payResp.Hash)
}
https://developers.stellar.org/docs/learn/fundamentals
also we need to clairfy chanign API v openApia which si from #991 docs/data/apis/horizon/api-reference/structure/response-format.mdx#L170 \
_contra- openapi/horizon/components/examples/responses/Offers/GetAllOffers.yml#L28 ++ openapi/horizon/components/examples/responses/Offers/GetAllOffers.yml#L28
Merge starts from new ex and leads into @Mootz12 remarks:
DEX prices sources are calculated via Horizon's "find strict receive payment path" endpoint. The specified
destAmountwill be received via thesourceAssetasset, and the price will be computed such thatsourceAmount / destAmount, or the average price for the full path payment operation on that block. This can be useful to fetch prices for assets that are not on centralized exchanges.
_Muust reconcile with base challenge at _ https://github.com/stellar/stellar-docs/issues/1529#issuecomment-3325034550
Guides in this category:
📄 Create an Account
Learn about creating Stellar accounts, keypairs, funding, and account basics.
📄 Send to and receive payments from Contract Accounts
Learn to send payments to and receive payments from Contract Accounts on the Stellar network.
📄 Send and receive payments
Learn to send payments and watch for received payments on the Stellar network.
📄 Channel Accounts
Create channel accounts to submit transactions to the network at a high rate.
📄 Signing Soroban contract invocations
Learn two methods to sign Soroban smart contract invocations: full transaction signing for G-accounts and auth-entry signing for G or C-accounts with sponsored fees.
📄 Claimable Balances
Split a payment into two parts by creating a claimable balance.
📄 Clawbacks
Use clawbacks to burn a specific amount of a clawback-enabled asset from a trustline or claimable balance.
📄 Fee-bump Transactions
Use fee-bump transactions to pay for transaction fees on behalf of another account without re-signing the transaction.
📄 Sponsored Reserves
Use sponsored reserves to pay for base reserves on behalf of another account.
📄 Path Payments
Send a payment where the asset received differs from the asset sent.
📄 Pooled accounts: muxed accounts and memos
Use muxed accounts to differentiate between individual accounts in a pooled account.
📄 Install and deploy a smart contract with code
Install and deploy a smart contract with code.
📄 Invoke a contract function in a transaction using SDKs
Use the Stellar SDK to create, simulate, and assemble a transaction.
📄 simulateTransaction RPC method guide
simulateTransaction examples and tutorials guide.
📄 Submit a transaction to Stellar RPC using the JavaScript SDK
Use a looping mechanism to submit a transaction to the RPC.
📄 Upload WebAssembly (Wasm) bytecode using code
Upload the Wasm of the contract using js-stellar-sdk.