Building a Complete Application | 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
beginner60 min

Building a Complete Application

Learning Objectives

Architect a complete XRPL application with proper separation of concerns

Integrate multiple XRPL features into a cohesive system

Handle the full transaction lifecycle from initiation to confirmation

Implement operational monitoring for production readiness

Build modular, maintainable code that can evolve with requirements

  • Connections and queries (Lessons 1-4)
  • Trust lines, payments, DEX, escrow, channels (Lessons 6-11)
  • Multi-sig, WebSockets, error handling, security, performance (Lessons 12-16)

Now we combine them. A real application isn't just individual features—it's how they work together, fail gracefully, and operate reliably over time.


┌─────────────────────────────────────────────────────────────────┐
│                     Payment Processor Service                    │
├─────────────────────────────────────────────────────────────────┤
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────────┐ │
│  │   API       │  │  WebSocket  │  │     Background Jobs     │ │
│  │  Endpoints  │  │  Listeners  │  │  (confirmations, etc)   │ │
│  └──────┬──────┘  └──────┬──────┘  └───────────┬─────────────┘ │
│         │                │                      │               │
│         └────────────────┼──────────────────────┘               │
│                          │                                      │
│                          ▼                                      │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │                   Business Logic Layer                     │ │
│  │  ┌───────────────┐  ┌───────────────┐  ┌───────────────┐  │ │
│  │  │    Payment    │  │    Balance    │  │   Withdrawal  │  │ │
│  │  │    Service    │  │    Service    │  │    Service    │  │ │
│  │  └───────────────┘  └───────────────┘  └───────────────┘  │ │
│  └───────────────────────────────────────────────────────────┘ │
│                          │                                      │
│                          ▼                                      │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │                     XRPL Layer                             │ │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐│ │
│  │  │  Connection │  │ Transaction │  │   Query Service     ││ │
│  │  │    Pool     │  │  Submitter  │  │  (cached, batched)  ││ │
│  │  └─────────────┘  └─────────────┘  └─────────────────────┘│ │
│  └───────────────────────────────────────────────────────────┘ │
│                          │                                      │
│                          ▼                                      │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │                    Infrastructure                          │ │
│  │  ┌────────────┐  ┌─────────────┐  ┌───────────────────┐  │ │
│  │  │   Logging  │  │   Metrics   │  │    Config/Secrets │  │ │
│  │  └────────────┘  └─────────────┘  └───────────────────┘  │ │
│  └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
payment-processor/
├── src/
│   ├── index.js                 # Application entry point
│   ├── config/
│   │   └── index.js             # Configuration management
│   ├── xrpl/
│   │   ├── connection.js        # Connection pool
│   │   ├── transaction.js       # Transaction submission
│   │   ├── queries.js           # Query helpers
│   │   └── subscriptions.js     # WebSocket subscriptions
│   ├── services/
│   │   ├── payment.js           # Payment business logic
│   │   ├── balance.js           # Balance tracking
│   │   └── withdrawal.js        # Withdrawal processing
│   ├── api/
│   │   ├── routes.js            # API route definitions
│   │   └── middleware.js        # Validation, auth, etc.
│   ├── workers/
│   │   └── confirmation.js      # Background confirmation checker
│   └── utils/
│       ├── logger.js            # Logging utility
│       ├── metrics.js           # Performance metrics
│       └── cache.js             # Caching layer
├── tests/
│   ├── unit/
│   └── integration/
├── .env.example
├── package.json
└── README.md

// src/config/index.js
require('dotenv').config();

const config = {
xrpl: {
servers: (process.env.XRPL_SERVERS || 'wss://s.altnet.rippletest.net:51233')
.split(','),
isTestnet: process.env.XRPL_NETWORK === 'testnet',
connectionPoolSize: parseInt(process.env.XRPL_POOL_SIZE || '3'),
requestTimeoutMs: parseInt(process.env.XRPL_TIMEOUT || '30000')
},

wallet: {
// Never log or expose these
hotWalletSeed: process.env.HOT_WALLET_SEED,
warmWalletSeed: process.env.WARM_WALLET_SEED
},

limits: {
maxPaymentXRP: parseFloat(process.env.MAX_PAYMENT_XRP || '1000'),
dailyLimitXRP: parseFloat(process.env.DAILY_LIMIT_XRP || '10000'),
minPaymentXRP: parseFloat(process.env.MIN_PAYMENT_XRP || '0.001')
},

server: {
port: parseInt(process.env.PORT || '3000'),
rateLimitPerMinute: parseInt(process.env.RATE_LIMIT || '60')
},

cache: {
balanceTTLMs: parseInt(process.env.CACHE_BALANCE_TTL || '5000'),
accountInfoTTLMs: parseInt(process.env.CACHE_INFO_TTL || '30000')
}
};

// Validate required config
function validateConfig() {
const required = ['wallet.hotWalletSeed'];
const missing = required.filter(key => {
const parts = key.split('.');
let value = config;
for (const part of parts) {
value = value?.[part];
}
return !value;
});

if (missing.length > 0) {
throw new Error(Missing required config: ${missing.join(', ')});
}
}

validateConfig();

module.exports = config;
```

// src/xrpl/connection.js
const xrpl = require('xrpl');
const config = require('../config');
const logger = require('../utils/logger');

class XRPLService {
constructor() {
this.connections = [];
this.currentIndex = 0;
this.isInitialized = false;
this.wallet = null;
}

async initialize() {
if (this.isInitialized) return;

// Initialize wallet
this.wallet = xrpl.Wallet.fromSeed(config.wallet.hotWalletSeed);
logger.info(Wallet initialized: ${this.wallet.address});

// Create connection pool
for (let i = 0; i < config.xrpl.connectionPoolSize; i++) {
const client = new xrpl.Client(
config.xrpl.servers[i % config.xrpl.servers.length]
);

client.on('disconnected', () => {
logger.warn(Connection ${i} disconnected);
this.handleDisconnect(client, i);
});

client.on('error', (error) => {
logger.error(Connection ${i} error:, error);
});

await client.connect();
this.connections.push(client);
logger.info(Connection ${i} established);
}

this.isInitialized = true;
logger.info('XRPL service initialized');
}

getClient() {
// Round-robin connection selection
const client = this.connections[this.currentIndex];
this.currentIndex = (this.currentIndex + 1) % this.connections.length;
return client;
}

async request(params) {
const client = this.getClient();
const startTime = Date.now();

try {
const result = await client.request(params);
logger.debug(Request ${params.command} completed in ${Date.now() - startTime}ms);
return result;
} catch (error) {
logger.error(Request ${params.command} failed:, error);
throw error;
}
}

async handleDisconnect(client, index) {
// Attempt reconnection
for (let attempt = 1; attempt <= 5; attempt++) {
try {
await new Promise(r => setTimeout(r, 1000 * attempt));
await client.connect();
logger.info(Connection ${index} restored);
return;
} catch (error) {
logger.warn(Reconnection attempt ${attempt} failed);
}
}
logger.error(Connection ${index} failed to restore);
}

async shutdown() {
for (const client of this.connections) {
await client.disconnect();
}
this.isInitialized = false;
logger.info('XRPL service shut down');
}
}

// Singleton instance
const xrplService = new XRPLService();

module.exports = xrplService;
```

// src/services/payment.js
const xrpl = require('xrpl');
const xrplService = require('../xrpl/connection');
const config = require('../config');
const logger = require('../utils/logger');
const metrics = require('../utils/metrics');
const cache = require('../utils/cache');

class PaymentService {
constructor() {
this.dailyTotal = 0;
this.dailyResetDate = new Date().toDateString();
this.pendingPayments = new Map();
}

// Process outgoing payment
async sendPayment(destination, amountXRP, options = {}) {
const paymentId = options.paymentId || this.generatePaymentId();

logger.info(Processing payment ${paymentId}: ${amountXRP} XRP to ${destination});
metrics.paymentStarted();

try {
// Validation
await this.validatePayment(destination, amountXRP);

// Check daily limits
this.checkDailyLimit(amountXRP);

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

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

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

// Submit transaction
const result = await this.submitTransaction(payment);

// Update daily total
this.dailyTotal += amountXRP;

// Track for confirmation
this.pendingPayments.set(result.hash, {
paymentId,
destination,
amount: amountXRP,
submittedAt: new Date()
});

logger.info(Payment ${paymentId} submitted: ${result.hash});
metrics.paymentSubmitted();

return {
success: true,
paymentId,
hash: result.hash,
status: 'submitted'
};

} catch (error) {
logger.error(Payment ${paymentId} failed:, error);
metrics.paymentFailed();

return {
success: false,
paymentId,
error: error.message
};
}
}

async validatePayment(destination, amountXRP) {
// Validate amount
if (amountXRP < config.limits.minPaymentXRP) {
throw new Error(Amount below minimum: ${config.limits.minPaymentXRP} XRP);
}

if (amountXRP > config.limits.maxPaymentXRP) {
throw new Error(Amount exceeds maximum: ${config.limits.maxPaymentXRP} XRP);
}

// Validate destination
if (!xrpl.isValidAddress(destination)) {
throw new Error('Invalid destination address');
}

// Check destination exists (optional - will auto-fund if enough XRP)
try {
await xrplService.request({
command: 'account_info',
account: destination
});
} catch (error) {
if (error.data?.error === 'actNotFound') {
if (amountXRP < 10) {
throw new Error('Destination does not exist and amount is less than 10 XRP');
}
logger.info(Destination ${destination} will be created);
}
}

// Check our balance
const balance = await this.getBalance();
const required = amountXRP + 1; // Buffer for fees and reserve

if (balance.available < required) {
throw new Error(Insufficient balance: have ${balance.available}, need ${required});
}
}

checkDailyLimit(amountXRP) {
// Reset if new day
const today = new Date().toDateString();
if (today !== this.dailyResetDate) {
this.dailyTotal = 0;
this.dailyResetDate = today;
}

if (this.dailyTotal + amountXRP > config.limits.dailyLimitXRP) {
throw new Error(Would exceed daily limit of ${config.limits.dailyLimitXRP} XRP);
}
}

async submitTransaction(transaction) {
const client = xrplService.getClient();

// Autofill
const prepared = await client.autofill(transaction);

// Sign
const signed = xrplService.wallet.sign(prepared);

// Submit and wait
const result = await client.submitAndWait(signed.tx_blob);

if (result.result.meta.TransactionResult !== 'tesSUCCESS') {
throw new Error(Transaction failed: ${result.result.meta.TransactionResult});
}

return {
hash: signed.hash,
result: result.result
};
}

async getBalance() {
const cacheKey = balance:${xrplService.wallet.address};
const cached = cache.get(cacheKey);

if (cached) return cached;

const info = await xrplService.request({
command: 'account_info',
account: xrplService.wallet.address
});

const balance = Number(info.result.account_data.Balance) / 1_000_000;
const ownerCount = info.result.account_data.OwnerCount;
const reserve = 10 + (ownerCount * 2);
const available = balance - reserve;

const result = { balance, reserve, available };
cache.set(cacheKey, result, config.cache.balanceTTLMs);

return result;
}

generatePaymentId() {
return pay_${Date.now()}_${Math.random().toString(36).substr(2, 9)};
}

getPendingPayments() {
return Array.from(this.pendingPayments.entries()).map(([hash, info]) => ({
hash,
...info
}));
}

confirmPayment(hash) {
this.pendingPayments.delete(hash);
metrics.paymentConfirmed();
}
}

module.exports = new PaymentService();
```


// src/api/routes.js
const express = require('express');
const paymentService = require('../services/payment');
const xrplService = require('../xrpl/connection');
const { validatePaymentRequest, rateLimit } = require('./middleware');
const logger = require('../utils/logger');

const router = express.Router();

// Health check
router.get('/health', async (req, res) => {
try {
const balance = await paymentService.getBalance();
res.json({
status: 'healthy',
wallet: xrplService.wallet.address,
balance: balance.available,
pending: paymentService.getPendingPayments().length
});
} catch (error) {
res.status(503).json({ status: 'unhealthy', error: error.message });
}
});

// Get balance
router.get('/balance', async (req, res) => {
try {
const balance = await paymentService.getBalance();
res.json(balance);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

// Send payment
router.post('/payments', rateLimit, validatePaymentRequest, async (req, res) => {
const { destination, amount, destinationTag, memo } = req.body;

logger.info(Payment request: ${amount} XRP to ${destination});

const result = await paymentService.sendPayment(destination, amount, {
destinationTag,
memo
});

if (result.success) {
res.status(202).json(result);
} else {
res.status(400).json(result);
}
});

// Get payment status
router.get('/payments/:hash', async (req, res) => {
const { hash } = req.params;

try {
const result = await xrplService.request({
command: 'tx',
transaction: hash
});

res.json({
hash,
validated: result.result.validated,
result: result.result.meta?.TransactionResult,
ledgerIndex: result.result.ledger_index
});
} catch (error) {
if (error.data?.error === 'txnNotFound') {
res.status(404).json({ error: 'Transaction not found' });
} else {
res.status(500).json({ error: error.message });
}
}
});

// Get pending payments
router.get('/payments/pending', (req, res) => {
res.json(paymentService.getPendingPayments());
});

module.exports = router;
```

// src/api/middleware.js
const xrpl = require('xrpl');
const config = require('../config');
const logger = require('../utils/logger');

// Request rate limiting (simple in-memory)
const requestCounts = new Map();

function rateLimit(req, res, next) {
const ip = req.ip;
const now = Date.now();
const windowMs = 60000; // 1 minute

let requests = requestCounts.get(ip) || [];
requests = requests.filter(t => t > now - windowMs);

if (requests.length >= config.server.rateLimitPerMinute) {
logger.warn(Rate limit exceeded for ${ip});
return res.status(429).json({ error: 'Too many requests' });
}

requests.push(now);
requestCounts.set(ip, requests);
next();
}

// Payment request validation
function validatePaymentRequest(req, res, next) {
const { destination, amount } = req.body;
const errors = [];

if (!destination) {
errors.push('destination is required');
} else if (!xrpl.isValidAddress(destination)) {
errors.push('destination is not a valid XRPL address');
}

if (amount === undefined || amount === null) {
errors.push('amount is required');
} else if (typeof amount !== 'number' || amount <= 0) {
errors.push('amount must be a positive number');
} else if (amount < config.limits.minPaymentXRP) {
errors.push(amount must be at least ${config.limits.minPaymentXRP} XRP);
} else if (amount > config.limits.maxPaymentXRP) {
errors.push(amount must not exceed ${config.limits.maxPaymentXRP} XRP);
}

if (req.body.destinationTag !== undefined) {
const tag = req.body.destinationTag;
if (!Number.isInteger(tag) || tag < 0 || tag > 4294967295) {
errors.push('destinationTag must be an integer 0-4294967295');
}
}

if (errors.length > 0) {
return res.status(400).json({ errors });
}

next();
}

module.exports = { rateLimit, validatePaymentRequest };
```


// src/workers/confirmation.js
const xrplService = require('../xrpl/connection');
const paymentService = require('../services/payment');
const logger = require('../utils/logger');

class ConfirmationWorker {
constructor() {
this.isRunning = false;
this.checkInterval = 5000; // 5 seconds
}

start() {
if (this.isRunning) return;

this.isRunning = true;
logger.info('Confirmation worker started');

this.run();
}

stop() {
this.isRunning = false;
logger.info('Confirmation worker stopped');
}

async run() {
while (this.isRunning) {
try {
await this.checkPendingPayments();
} catch (error) {
logger.error('Confirmation check error:', error);
}

await new Promise(r => setTimeout(r, this.checkInterval));
}
}

async checkPendingPayments() {
const pending = paymentService.getPendingPayments();

if (pending.length === 0) return;

logger.debug(Checking ${pending.length} pending payments);

for (const payment of pending) {
try {
const result = await xrplService.request({
command: 'tx',
transaction: payment.hash
});

if (result.result.validated) {
const txResult = result.result.meta.TransactionResult;

if (txResult === 'tesSUCCESS') {
logger.info(Payment ${payment.paymentId} confirmed: ${payment.hash});
paymentService.confirmPayment(payment.hash);

// Emit event or callback for notification
this.onConfirmation(payment, result.result);
} else {
logger.warn(Payment ${payment.paymentId} failed: ${txResult});
paymentService.confirmPayment(payment.hash);

this.onFailure(payment, txResult);
}
}
} catch (error) {
if (error.data?.error === 'txnNotFound') {
// Check if expired
const age = Date.now() - payment.submittedAt.getTime();
if (age > 120000) { // 2 minutes
logger.warn(Payment ${payment.paymentId} expired);
paymentService.confirmPayment(payment.hash);
this.onExpiration(payment);
}
}
}
}
}

onConfirmation(payment, result) {
// Override for notifications (webhooks, etc.)
}

onFailure(payment, resultCode) {
// Override for error handling
}

onExpiration(payment) {
// Override for timeout handling
}
}

module.exports = new ConfirmationWorker();
```


// src/index.js
const express = require('express');
const config = require('./config');
const xrplService = require('./xrpl/connection');
const apiRoutes = require('./api/routes');
const confirmationWorker = require('./workers/confirmation');
const logger = require('./utils/logger');
const metrics = require('./utils/metrics');

const app = express();

// Middleware
app.use(express.json());
app.use((req, res, next) => {
logger.debug(${req.method} ${req.path});
next();
});

// Routes
app.use('/api', apiRoutes);

// Error handling
app.use((err, req, res, next) => {
logger.error('Unhandled error:', err);
res.status(500).json({ error: 'Internal server error' });
});

// Startup
async function start() {
try {
logger.info('Starting payment processor...');

// Initialize XRPL
await xrplService.initialize();

// Start confirmation worker
confirmationWorker.start();

// Start metrics collection
metrics.start();

// Start HTTP server
app.listen(config.server.port, () => {
logger.info(Server listening on port ${config.server.port});
});

} catch (error) {
logger.error('Startup failed:', error);
process.exit(1);
}
}

// Graceful shutdown
async function shutdown(signal) {
logger.info(Received ${signal}, shutting down...);

confirmationWorker.stop();
await xrplService.shutdown();

logger.info('Shutdown complete');
process.exit(0);
}

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

start();
```


This architecture provides a solid foundation but isn't production-complete. Real production requires database persistence, multi-instance coordination, comprehensive monitoring, and operational runbooks. Use this as a starting point, not a final solution.


Assignment: Build a complete payment processing service using this architecture.

Requirements:

  • Configuration management

  • XRPL connection pool

  • Payment service with validation

  • RESTful endpoints for payments

  • Input validation

  • Rate limiting

  • Confirmation worker

  • Expiration handling

  • Status updates

  • Health check endpoint

  • Logging throughout

  • Basic metrics

  • Core service functional (30%)

  • API complete and validated (25%)

  • Background processing reliable (25%)

  • Operational features present (20%)

Time investment: 5-6 hours
Value: Real, deployable payment processing infrastructure


Knowledge Check

Question 1 of 5

Why separate the XRPL layer from business logic?

  • Clean Architecture principles
  • Microservices patterns
  • Event-driven architecture
  • Database integration
  • Message queues for async processing
  • Container orchestration

For Next Lesson:
You've built a complete application structure. Lesson 18 covers testing strategies—how to test XRPL applications effectively without using mainnet or risking real funds.


End of Lesson 17

Total words: ~5,000
Estimated completion time: 60 minutes reading + 5-6 hours for deliverable

Key Takeaways

1

Layer your application

: Separate XRPL concerns from business logic from API handling.

2

Handle the full lifecycle

: From request validation through confirmation—don't just submit and hope.

3

Build for failure

: Every network call can fail. Every component can disconnect.

4

Make it observable

: Logging and metrics from day one, not retrofitted later.

5

Start simple, evolve

: This structure can grow. Don't over-engineer initially. ---