Your First XRP Payment | XRPL Development 101 | XRP Academy - XRP Academy
3 free lessons remaining this month

Free preview access resets monthly

Upgrade for Unlimited
Skip to main content
intermediate55 min

Your First XRP Payment

Learning Objectives

Construct valid Payment transactions with all required and optional fields

Sign transactions using your wallet's private key

Submit transactions and interpret the immediate response

Track transaction status from submission through validation

Handle common payment errors and implement appropriate retry logic

Every XRPL transaction follows the same lifecycle:

1. CONSTRUCT → Build the transaction object with required fields
2. PREPARE   → Add auto-fillable fields (Sequence, Fee, LastLedgerSequence)  
3. SIGN      → Create cryptographic signature with private key
4. SUBMIT    → Send signed transaction blob to the network
5. VALIDATE  → Wait for inclusion in a validated ledger
6. VERIFY    → Confirm the transaction succeeded

This might seem like a lot of steps, but modern libraries handle most of it. Understanding each step, however, is crucial for debugging production issues.

Important distinction: A submitted transaction isn't immediately confirmed. The network needs time (3-5 seconds) to reach consensus and close a ledger containing your transaction.


Every Payment transaction needs these fields:

{
    "TransactionType": "Payment",     // Type of transaction
    "Account": "rSender...",          // Sender's address (your account)
    "Destination": "rRecipient...",   // Recipient's address
    "Amount": "1000000"               // Amount in drops (1 XRP = 1,000,000 drops)
}

TransactionType: Always "Payment" for sending XRP or tokens.

Account: The address sending the payment. Must match the signing key.

Destination: The address receiving the payment. Must be an activated account (or the amount must be sufficient to activate it).

Amount: For XRP, a string of drops. For tokens, a more complex object (covered in Lesson 7).

These fields are required but typically auto-filled by the library:

{
    "Fee": "12",                      // Transaction cost in drops
    "Sequence": 1,                    // Account's transaction sequence
    "LastLedgerSequence": 45678910    // Transaction expires after this ledger
}

Fee: Network transaction cost. Usually 10-12 drops, but can spike during congestion.

Sequence: Must match your account's current Sequence value. Increments with each transaction.

LastLedgerSequence: Safety mechanism—if the transaction isn't validated by this ledger, it's permanently invalid. Prevents transactions from being stuck indefinitely.

Common optional fields for payments:

{
    "DestinationTag": 12345,          // Numeric tag for recipient identification
    "SourceTag": 67890,               // Numeric tag for sender tracking
    "InvoiceID": "ABC123...",         // 256-bit hash for invoice reference
    "Memos": [{                       // Arbitrary data attachments
        "Memo": {
            "MemoType": "746578742F706C61696E",    // "text/plain" in hex
            "MemoData": "48656C6C6F"               // "Hello" in hex
        }
    }]
}

DestinationTag: Critical for exchanges and custodians. A 32-bit unsigned integer (0 to 4,294,967,295) that identifies which customer should be credited.

SourceTag: Optional identifier on your side for tracking purposes.

Memos: Arbitrary data (must be hex-encoded). Limited to about 1KB total.


// src/payments/send-xrp.js
const xrpl = require('xrpl');

async function sendXRP(senderWallet, destinationAddress, amountXRP, options = {}) {
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
await client.connect();

try {
// Convert XRP to drops
const amountDrops = xrpl.xrpToDrops(amountXRP);

// Build the payment transaction
const payment = {
TransactionType: 'Payment',
Account: senderWallet.address,
Destination: destinationAddress,
Amount: amountDrops
};

// Add optional fields if provided
if (options.destinationTag !== undefined) {
payment.DestinationTag = options.destinationTag;
}

if (options.memo) {
payment.Memos = [{
Memo: {
MemoType: Buffer.from('text/plain').toString('hex').toUpperCase(),
MemoData: Buffer.from(options.memo).toString('hex').toUpperCase()
}
}];
}

console.log('Preparing transaction...');

// Autofill adds Sequence, Fee, LastLedgerSequence
const prepared = await client.autofill(payment);

console.log('Transaction prepared:');
console.log(' Sequence:', prepared.Sequence);
console.log(' Fee:', prepared.Fee, 'drops');
console.log(' LastLedgerSequence:', prepared.LastLedgerSequence);

// Sign the transaction
const signed = senderWallet.sign(prepared);

console.log('Transaction signed. Hash:', signed.hash);

// Submit and wait for validation
console.log('Submitting to network...');
const result = await client.submitAndWait(signed.tx_blob);

// Check the result
const txResult = result.result.meta.TransactionResult;

if (txResult === 'tesSUCCESS') {
console.log('✓ Payment successful!');
console.log(' Delivered:', xrpl.dropsToXrp(result.result.meta.delivered_amount), 'XRP');
console.log(' Ledger:', result.result.ledger_index);
return {
success: true,
hash: signed.hash,
ledger: result.result.ledger_index,
delivered: result.result.meta.delivered_amount
};
} else {
console.log('✗ Payment failed:', txResult);
return {
success: false,
hash: signed.hash,
resultCode: txResult,
error: interpretResultCode(txResult)
};
}

} finally {
await client.disconnect();
}
}

function interpretResultCode(code) {
const meanings = {
'tesSUCCESS': 'Transaction succeeded',
'tecNO_DST': 'Destination account does not exist',
'tecNO_DST_INSUF_XRP': 'Destination needs more XRP to be created',
'tecUNFUNDED_PAYMENT': 'Insufficient balance to send payment',
'tecPATH_DRY': 'No path found for cross-currency payment',
'tefBAD_AUTH': 'Transaction not authorized by account',
'terQUEUED': 'Transaction queued for future ledger',
'terPRE_SEQ': 'Sequence number already used'
};
return meanings[code] || Unknown result code: ${code};
}

// Example usage
async function main() {
// Create sender wallet (use your funded testnet wallet)
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
await client.connect();

// Generate and fund a new wallet for testing
const sender = xrpl.Wallet.generate();
console.log('Funding sender wallet...');
await client.fundWallet(sender);

// Generate recipient (will be activated by our payment)
const recipient = xrpl.Wallet.generate();
console.log('\nSender:', sender.address);
console.log('Recipient:', recipient.address);

await client.disconnect();

// Send a payment
console.log('\n--- Sending 25 XRP ---');
const result = await sendXRP(sender, recipient.address, '25', {
destinationTag: 12345,
memo: 'Test payment from XRPL Development course'
});

console.log('\nFinal result:', JSON.stringify(result, null, 2));
}

module.exports = { sendXRP, interpretResultCode };

if (require.main === module) {
main().catch(console.error);
}
```

# src/payments/send_xrp.py
import asyncio
from xrpl.asyncio.clients import AsyncWebsocketClient
from xrpl.wallet import Wallet
from xrpl.models.transactions import Payment, Memo
from xrpl.models.amounts import IssuedCurrencyAmount
from xrpl.asyncio.transaction import submit_and_wait, autofill
from xrpl.utils import xrp_to_drops, drops_to_xrp

async def send_xrp(
sender_wallet: Wallet,
destination_address: str,
amount_xrp: str,
destination_tag: int = None,
memo: str = None
) -> dict:
"""Send XRP from sender to destination.

Args:
sender_wallet: Wallet object with signing capability
destination_address: Recipient's XRPL address
amount_xrp: Amount to send as string (e.g., "25.5")
destination_tag: Optional numeric tag for recipient
memo: Optional text memo

Returns:
Dictionary with transaction result
"""
url = "wss://s.altnet.rippletest.net:51233"

async with AsyncWebsocketClient(url) as client:
# Convert XRP to drops
amount_drops = xrp_to_drops(float(amount_xrp))

Build payment transaction

    payment = Payment(
        account=sender_wallet.classic_address,
        destination=destination_address,
        amount=amount_drops
    )

Add optional fields

    if destination_tag is not None:
        payment.destination_tag = destination_tag

if memo:
payment.memos = [
Memo(
memo_type=bytes("text/plain", "utf-8").hex().upper(),
memo_data=bytes(memo, "utf-8").hex().upper()
)
]

print("Preparing transaction...")

Autofill adds Sequence, Fee, LastLedgerSequence

    prepared = await autofill(payment, client)

print(f"Transaction prepared:")
print(f" Sequence: {prepared.sequence}")
print(f" Fee: {prepared.fee} drops")
print(f" LastLedgerSequence: {prepared.last_ledger_sequence}")

Sign the transaction

    signed = sender_wallet.sign(prepared)

print(f"Transaction signed. Hash: {signed.hash}")

Submit and wait for validation

    print("Submitting to network...")
    result = await submit_and_wait(signed, client)

Check the result

    tx_result = result.result.get("meta", {}).get("TransactionResult")

if tx_result == "tesSUCCESS":
delivered = result.result["meta"].get("delivered_amount", amount_drops)
print("✓ Payment successful!")
print(f" Delivered: {drops_to_xrp(delivered)} XRP")
print(f" Ledger: {result.result.get('ledger_index')}")

return {
"success": True,
"hash": signed.hash,
"ledger": result.result.get("ledger_index"),
"delivered": delivered
}
else:
print(f"✗ Payment failed: {tx_result}")
return {
"success": False,
"hash": signed.hash,
"result_code": tx_result,
"error": interpret_result_code(tx_result)
}

def interpret_result_code(code: str) -> str:
"""Convert result code to human-readable message."""
meanings = {
"tesSUCCESS": "Transaction succeeded",
"tecNO_DST": "Destination account does not exist",
"tecNO_DST_INSUF_XRP": "Destination needs more XRP to be created",
"tecUNFUNDED_PAYMENT": "Insufficient balance to send payment",
"tecPATH_DRY": "No path found for cross-currency payment",
"tefBAD_AUTH": "Transaction not authorized by account",
"terQUEUED": "Transaction queued for future ledger",
"terPRE_SEQ": "Sequence number already used"
}
return meanings.get(code, f"Unknown result code: {code}")

async def main():
from xrpl.asyncio.clients import AsyncWebsocketClient
from xrpl.wallet import generate_faucet_wallet

url = "wss://s.altnet.rippletest.net:51233"

async with AsyncWebsocketClient(url) as client:
# Generate and fund a new wallet for testing
print("Funding sender wallet...")
sender = await generate_faucet_wallet(client)

Generate recipient

    recipient = Wallet.create()

print(f"\nSender: {sender.classic_address}")
print(f"Recipient: {recipient.classic_address}")

Send a payment

print("\n--- Sending 25 XRP ---")
result = await send_xrp(
    sender,
    recipient.classic_address,
    "25",
    destination_tag=12345,
    memo="Test payment from XRPL Development course"
)

print(f"\nFinal result: {result}")

if name == "main":
asyncio.run(main())
```


XRPL transaction results are categorized by prefix:

Prefix Meaning Transaction Fate
tes Success Applied to ledger
tec Claim (failed but fee paid) Fee consumed, no effect
tef Failed Not applied, no fee
tel Local error Not submitted
tem Malformed Not submitted
ter Retry May succeed later
  • `tesSUCCESS`: Payment delivered successfully
  • `tecNO_DST`: Destination account doesn't exist
  • `tecNO_DST_INSUF_XRP`: Amount insufficient to create destination account
  • `tecUNFUNDED_PAYMENT`: Sender doesn't have enough XRP
  • `tecDST_TAG_NEEDED`: Destination requires a destination tag
  • `tecPATH_DRY`: No payment path found (cross-currency)
  • `tefBAD_AUTH`: Signature doesn't match account
  • `tefPAST_SEQ`: Sequence number too low (already used)
  • `tefMAX_LEDGER`: Transaction expired (LastLedgerSequence passed)
  • `terQUEUED`: Transaction queued (sequence too high)
  • `terPRE_SEQ`: Sequence mismatch, may retry

Critical: Always check delivered_amount in the metadata, not the Amount field in the transaction.

// The transaction requested to send 100 XRP
const payment = {
    Amount: "100000000"  // 100 XRP in drops
};

// But the actual delivered amount might differ due to:
// - Transfer fees (for issued currencies)
// - Partial payments (if enabled)
// - Rounding

// Always check metadata for actual delivered amount
const actualDelivered = result.result.meta.delivered_amount;
console.log('Actually delivered:', xrpl.dropsToXrp(actualDelivered));
```

For XRP-to-XRP payments, these usually match. For cross-currency payments, they can differ significantly.


When you submit a transaction, it goes through several stages:

SUBMIT
  ↓
Server receives transaction blob
  ↓
Preliminary validation (signature, format, basic rules)
  ↓
If valid: Added to candidate set for next ledger
  ↓
CONSENSUS (3-5 seconds)
  ↓
Transaction either:
  - Included in validated ledger (success or tec failure)
  - Not included (will retry or expire)
  ↓
VALIDATED

The submit response is NOT confirmation. It only tells you the server accepted the transaction for processing.

// src/payments/track-transaction.js
const xrpl = require('xrpl');

async function trackTransaction(txHash, client) {
console.log(Tracking transaction: ${txHash});

const startTime = Date.now();
const maxWaitMs = 30000; // 30 seconds timeout

while (Date.now() - startTime < maxWaitMs) {
try {
const response = await client.request({
command: 'tx',
transaction: txHash
});

// Check if validated
if (response.result.validated) {
return {
status: 'validated',
result: response.result.meta.TransactionResult,
ledger: response.result.ledger_index,
response: response.result
};
}

// Not yet validated, wait and retry
console.log(' Not yet validated, waiting...');
await sleep(1000);

} catch (error) {
if (error.data?.error === 'txnNotFound') {
// Transaction not yet in any ledger
console.log(' Transaction not found, waiting...');
await sleep(1000);
} else {
throw error;
}
}
}

return {
status: 'timeout',
message: 'Transaction not validated within timeout period'
};
}

function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

module.exports = { trackTransaction };
```

The submitAndWait method handles tracking automatically:

// This handles submission and waits for validation
const result = await client.submitAndWait(signed.tx_blob);

// Under the hood, it:
// 1. Submits the transaction
// 2. Subscribes to ledger closes
// 3. Checks each new ledger for the transaction
// 4. Returns when validated or LastLedgerSequence exceeded
  • Custom retry logic
  • Progress reporting to users
  • Handling multiple pending transactions

// src/payments/robust-payment.js
const xrpl = require('xrpl');

async function sendPaymentWithRetry(wallet, destination, amount, maxRetries = 3) {
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
await client.connect();

let lastError = null;

for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
console.log(Attempt ${attempt}/${maxRetries});

const payment = {
TransactionType: 'Payment',
Account: wallet.address,
Destination: destination,
Amount: xrpl.xrpToDrops(amount)
};

const prepared = await client.autofill(payment);
const signed = wallet.sign(prepared);
const result = await client.submitAndWait(signed.tx_blob);

const txResult = result.result.meta.TransactionResult;

if (txResult === 'tesSUCCESS') {
await client.disconnect();
return { success: true, result };
}

// Check if error is retryable
if (isRetryableError(txResult)) {
console.log(Retryable error: ${txResult});
lastError = txResult;
await sleep(2000 * attempt); // Exponential backoff
continue;
}

// Non-retryable error
await client.disconnect();
return { success: false, error: txResult };

} catch (error) {
console.error(Error on attempt ${attempt}:, error.message);
lastError = error;

if (isNetworkError(error)) {
await sleep(2000 * attempt);
continue;
}

throw error;
}
}

await client.disconnect();
return { success: false, error: lastError, message: 'Max retries exceeded' };
}

function isRetryableError(code) {
const retryable = ['terQUEUED', 'terPRE_SEQ', 'tefPAST_SEQ'];
return retryable.includes(code);
}

function isNetworkError(error) {
return error.message.includes('WebSocket') ||
error.message.includes('timeout') ||
error.message.includes('ECONNREFUSED');
}

function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

module.exports = { sendPaymentWithRetry };
```

async function sendMaxPayment(wallet, destination, client) {
    // Get current balance
    const info = await client.request({
        command: 'account_info',
        account: wallet.address
    });

const balance = Number(info.result.account_data.Balance);
const ownerCount = info.result.account_data.OwnerCount;

// Calculate reserve
const baseReserve = 10_000_000; // 10 XRP in drops
const ownerReserve = ownerCount * 2_000_000; // 2 XRP per object
const reserve = baseReserve + ownerReserve;

// Calculate max sendable (leave some for fee)
const estimatedFee = 15; // 15 drops for safety
const maxSendable = balance - reserve - estimatedFee;

if (maxSendable <= 0) {
throw new Error(Insufficient balance. Have: ${balance}, Reserve: ${reserve});
}

console.log(Max sendable: ${xrpl.dropsToXrp(maxSendable.toString())} XRP);

// Send payment
const payment = {
TransactionType: 'Payment',
Account: wallet.address,
Destination: destination,
Amount: maxSendable.toString()
};

const prepared = await client.autofill(payment);
const signed = wallet.sign(prepared);
return await client.submitAndWait(signed.tx_blob);
}
```

Some accounts require destination tags. Check before sending:

async function checkDestinationTag(client, destination) {
    try {
        const info = await client.request({
            command: 'account_info',
            account: destination
        });

// Check lsfRequireDestTag flag (0x00020000 = 131072)
        const flags = info.result.account_data.Flags;
        const requiresTag = (flags & 0x00020000) !== 0;

return {
            exists: true,
            requiresDestinationTag: requiresTag
        };
    } catch (error) {
        if (error.data?.error === 'actNotFound') {
            return { exists: false, requiresDestinationTag: false };
        }
        throw error;
    }
}

// Usage
const destCheck = await checkDestinationTag(client, 'rExchange...');
if (destCheck.requiresDestinationTag && !destinationTag) {
    throw new Error('Destination requires a destination tag');
}

// src/payments/payment-service.js
const xrpl = require('xrpl');
const { XRPLConnection } = require('../connection/robust-client');

class PaymentService {
constructor(wallet, connectionOptions = {}) {
this.wallet = wallet;
this.connection = new XRPLConnection(connectionOptions);
}

async initialize() {
await this.connection.connect();
}

async close() {
await this.connection.disconnect();
}

async getBalance() {
const response = await this.connection.request({
command: 'account_info',
account: this.wallet.address
});

const data = response.result.account_data;
const balance = Number(data.Balance) / 1_000_000;
const reserve = 10 + (data.OwnerCount * 2);

return {
total: balance,
reserved: reserve,
available: Math.max(0, balance - reserve)
};
}

async validateDestination(address, amount, destinationTag = null) {
const errors = [];

// Check address format
if (!xrpl.isValidAddress(address)) {
errors.push('Invalid destination address format');
return { valid: false, errors };
}

// Check if destination exists
try {
const info = await this.connection.request({
command: 'account_info',
account: address
});

// Check destination tag requirement
const flags = info.result.account_data.Flags;
const requiresTag = (flags & 0x00020000) !== 0;

if (requiresTag && !destinationTag) {
errors.push('Destination requires a destination tag');
}
} catch (error) {
if (error.data?.error === 'actNotFound') {
// Destination doesn't exist - check if amount is enough to create
const amountDrops = xrpl.xrpToDrops(amount);
if (Number(amountDrops) < 10_000_000) {
errors.push('Amount insufficient to activate new account (minimum 10 XRP)');
}
} else {
throw error;
}
}

// Check sender has sufficient balance
const balance = await this.getBalance();
if (Number(amount) > balance.available) {
errors.push(Insufficient balance. Available: ${balance.available} XRP);
}

return {
valid: errors.length === 0,
errors
};
}

async sendPayment(destination, amount, options = {}) {
// Validate first
const validation = await this.validateDestination(
destination,
amount,
options.destinationTag
);

if (!validation.valid) {
return {
success: false,
errors: validation.errors
};
}

// Build transaction
const payment = {
TransactionType: 'Payment',
Account: this.wallet.address,
Destination: destination,
Amount: xrpl.xrpToDrops(amount)
};

if (options.destinationTag !== undefined) {
payment.DestinationTag = options.destinationTag;
}

if (options.sourceTag !== undefined) {
payment.SourceTag = options.sourceTag;
}

if (options.memo) {
payment.Memos = [{
Memo: {
MemoType: Buffer.from('text/plain').toString('hex').toUpperCase(),
MemoData: Buffer.from(options.memo).toString('hex').toUpperCase()
}
}];
}

// Submit
const client = this.connection.getClient();
const prepared = await client.autofill(payment);
const signed = this.wallet.sign(prepared);
const result = await client.submitAndWait(signed.tx_blob);

const txResult = result.result.meta.TransactionResult;

return {
success: txResult === 'tesSUCCESS',
hash: signed.hash,
resultCode: txResult,
ledger: result.result.ledger_index,
delivered: result.result.meta.delivered_amount
};
}
}

module.exports = { PaymentService };
```


XRP payments are among the simplest blockchain transactions to implement—the complexity is in handling edge cases correctly. The difference between a working demo and production-ready code is proper validation, error handling, and understanding the submit → validate distinction.


Assignment: Build a comprehensive payment application with send, receive, and status tracking capabilities.

Requirements:

  • Accept destination, amount, and optional memo/tag

  • Validate all inputs before submission

  • Display transaction progress (preparing, signing, submitting, validating)

  • Show final result with hash and ledger number

  • Display total, reserved, and available balance

  • List owned objects contributing to reserve

  • Calculate maximum sendable amount

  • Look up transaction by hash

  • Display all relevant transaction details

  • Correctly interpret and display result codes

  • Create two testnet accounts

  • Send payment from A to B

  • Verify B received correct amount

  • Send payment back from B to A

  • Verify round-trip success

  • Payment sending works correctly (30%)

  • Balance checking accurate (20%)

  • Transaction tracking comprehensive (20%)

  • Error handling robust (20%)

  • Code organization and documentation (10%)

Time investment: 2-3 hours
Value: This payment module will be extended in later lessons


1. Transaction Lifecycle Question:

You call client.submit(signed.tx_blob) and receive { "result": { "engine_result": "tesSUCCESS" }}. What does this mean?

A) The payment has been delivered to the recipient
B) The server accepted the transaction for processing, but it's not yet confirmed
C) The transaction failed but the fee was paid
D) The transaction is permanently confirmed

Correct Answer: B
Explanation: The submit response only indicates the server accepted the transaction into its candidate set. "tesSUCCESS" at submission means preliminary validation passed. Actual confirmation requires waiting for inclusion in a validated ledger. Many developers make the mistake of treating submit success as confirmation—this can lead to double-spend issues or displaying incorrect balances.


2. Result Code Question:

A payment returns tecNO_DST_INSUF_XRP. What happened?

A) The sender doesn't have enough XRP
B) The destination account doesn't exist and the payment amount is less than 10 XRP
C) Network fees consumed all the XRP
D) The destination refused the payment

Correct Answer: B
Explanation: tecNO_DST_INSUF_XRP specifically means the destination account doesn't exist AND the payment amount is insufficient to create it (less than the 10 XRP base reserve). The sender was charged a fee but the payment had no effect. To fix this, either send at least 10 XRP, or verify the destination account exists before sending smaller amounts.


3. Delivered Amount Question:

When should you check meta.delivered_amount instead of the transaction's Amount field?

A) Only for failed transactions
B) Only for cross-currency payments
C) Always, for any payment
D) Never—they're always identical

Correct Answer: C
Explanation: Always check delivered_amount in the metadata for the actual amount received. While XRP-to-XRP payments usually deliver the exact amount, discrepancies can occur with cross-currency payments (different exchange rates), partial payments (if the flag is set), and certain edge cases. Building the habit of checking delivered_amount prevents bugs when your application evolves to handle more complex payment scenarios.


4. Destination Tag Question:

You send 500 XRP to an exchange address without a destination tag. The transaction succeeds (tesSUCCESS). What happens?

A) The exchange automatically credits your account
B) The funds are returned to your account automatically
C) The funds may be lost—the exchange may not be able to credit any account
D) The transaction fails with tecDST_TAG_NEEDED

Correct Answer: C
Explanation: If an account has the RequireDestTag flag but you send without a tag, you get tecDST_TAG_NEEDED. However, many exchange addresses don't set this flag (they should, but don't). In that case, the transaction succeeds but the exchange has no way to identify which customer should be credited. Recovery may be possible by contacting support, but it's not guaranteed. Always verify destination tag requirements.


5. Error Handling Question:

Your payment fails with tefPAST_SEQ. What's the most likely cause and correct response?

A) Server error—retry with same transaction immediately
B) Sequence number already used—resync sequence and retry with new sequence
C) Insufficient funds—add more XRP and retry
D) Network congestion—wait and retry with same transaction

Correct Answer: B
Explanation: tefPAST_SEQ means the transaction's sequence number is lower than the account's current sequence—that sequence was already used by another transaction. This commonly happens when tracking sequences locally without syncing, or when a transaction succeeded but you didn't update your local tracker. The fix is to query current account info, get the correct sequence, rebuild and re-sign the transaction with the new sequence.


For Next Lesson:
Ensure your payment application works reliably. Lesson 4 will teach you to query ledger data—account histories, transaction lookups, and balance changes—which you'll use to verify your payments and build monitoring tools.


End of Lesson 3

Total words: ~5,600
Estimated completion time: 55 minutes reading + 2-3 hours for deliverable

Key Takeaways

1

Submission ≠ Confirmation

: Always wait for validation. A submitted transaction can still fail or expire. Use `submitAndWait` or implement proper tracking.

2

Check delivered_amount

: The amount requested and amount delivered can differ. This is especially critical for cross-currency payments and partial payments.

3

Validate before sending

: Check destination exists, has correct settings, and you have sufficient balance. It's cheaper to fail fast than to pay fees for failed transactions.

4

Handle destination tags correctly

: Sending to an exchange without a destination tag can mean permanent loss of funds. Always check requirements.

5

Result codes tell the full story

: Learn to interpret tes/tec/tef/ter codes. They explain exactly what happened and whether retry is appropriate. ---