Course Capstone - Production-Grade Integration | XRPL APIs & Integration | XRP Academy - XRP Academy
3 free lessons remaining this month

Free preview access resets monthly

Upgrade for Unlimited
Skip to main content
advanced60 min

Course Capstone - Production-Grade Integration

Learning Objectives

Built a complete, production-ready payment processing system

Integrated all major concepts from the course

Demonstrated mastery of XRPL API patterns

Created a reference implementation for future projects

Validated your skills through comprehensive testing

XRP Payment Gateway - A complete system for sending and receiving XRP payments:

┌─────────────────────────────────────────────────────────────────┐
│                    XRP Payment Gateway                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐      │
│  │   Outgoing   │    │   Incoming   │    │   Admin      │      │
│  │   Payments   │    │   Monitor    │    │   Dashboard  │      │
│  └──────────────┘    └──────────────┘    └──────────────┘      │
│         │                   │                   │               │
│         └───────────────────┴───────────────────┘               │
│                             │                                    │
│  ┌──────────────────────────┴──────────────────────────┐       │
│  │              Core Integration Layer                  │       │
│  │  • Connection Pool    • Error Handling              │       │
│  │  • Retry Logic        • Caching                     │       │
│  │  • Rate Limiting      • Metrics                     │       │
│  └──────────────────────────────────────────────────────┘       │
│                             │                                    │
│  ┌──────────────────────────┴──────────────────────────┐       │
│  │                   XRPL Network                       │       │
│  └──────────────────────────────────────────────────────┘       │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Functional Requirements:

  1. Send Payments

  2. Receive Payments

  3. Query Operations

Non-Functional Requirements:

  1. Reliability

  2. Security

  3. Observability

  4. Performance


// Project Structure
src/
├── config/
│   ├── index.js           // Environment configuration
│   └── xrpl.js            // XRPL-specific config
├── core/
│   ├── connection-pool.js // Connection management
│   ├── cache.js           // Caching layer
│   ├── error-handler.js   // Error classification
│   └── retry.js           // Retry logic
├── services/
│   ├── payment-sender.js  // Outgoing payments
│   ├── payment-monitor.js // Incoming payment detection
│   └── query-service.js   // Balance/history queries
├── api/
│   ├── routes.js          // API endpoints
│   ├── validation.js      // Input validation
│   └── middleware.js      // Auth, rate limiting
├── monitoring/
│   ├── metrics.js         // Metrics collection
│   ├── logger.js          // Structured logging
│   └── health.js          // Health check
├── webhooks/
│   └── delivery.js        // Webhook delivery
└── app.js                 // Application entry point
OUTGOING PAYMENT FLOW:
──────────────────────
  1. API Request
  2. Validation
  3. Payment Service
  4. Submission with Retry
  5. Response

INCOMING PAYMENT FLOW:
──────────────────────

  1. Subscription
  2. Transaction Event
  3. Validation
  4. Processing
  5. Webhook Delivery

// src/core/xrpl-gateway.js
const xrpl = require('xrpl')
const { ConnectionPool } = require('./connection-pool')
const { Cache } = require('./cache')
const { ErrorHandler } = require('./error-handler')
const { RetryPolicy } = require('./retry')

class XRPLGateway {
constructor(config) {
this.config = config
this.pool = new ConnectionPool(config.servers, {
poolSize: config.poolSize || 3,
healthCheckInterval: 30000
})
this.cache = new Cache(config.redis)
this.errorHandler = new ErrorHandler()
this.retryPolicy = new RetryPolicy({
maxRetries: 3,
baseDelay: 1000,
maxDelay: 30000
})
}

async initialize() {
await this.pool.initialize()
await this.verifyNetwork()
}

async verifyNetwork() {
const conn = await this.pool.acquire()
try {
const info = await conn.client.request({ command: 'server_info' })
const networkId = info.result.info.network_id

if (this.config.expectedNetwork === 'mainnet' && networkId !== 0) {
throw new Error('Expected mainnet but connected to testnet')
}
if (this.config.expectedNetwork === 'testnet' && networkId !== 1) {
throw new Error('Expected testnet but connected to mainnet')
}
} finally {
this.pool.release(conn)
}
}

async request(requestData) {
// Check cache for cacheable requests
if (this.isCacheable(requestData)) {
const cached = await this.cache.get(requestData)
if (cached) return cached
}

// Execute with retry
const result = await this.retryPolicy.execute(async () => {
const conn = await this.pool.acquire()
try {
return await conn.client.request(requestData)
} finally {
this.pool.release(conn)
}
})

// Cache result
if (this.isCacheable(requestData)) {
await this.cache.set(requestData, result)
}

return result
}

isCacheable(request) {
const nonCacheable = ['submit', 'subscribe', 'unsubscribe']
return !nonCacheable.includes(request.command)
}

async shutdown() {
await this.pool.shutdown()
}
}

module.exports = { XRPLGateway }
```

// src/services/payment-sender.js
class PaymentSender {
  constructor(gateway, wallet, metrics) {
    this.gateway = gateway
    this.wallet = wallet
    this.metrics = metrics
  }

async send(destination, amountXRP, options = {}) {
const startTime = Date.now()

// Validate
this.validate(destination, amountXRP, options)

// Check destination requires tag
await this.checkDestinationTagRequired(destination, options.destinationTag)

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

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

// Autofill
const prepared = await this.autofill(payment)

// Sign
const signed = this.wallet.sign(prepared)

// Submit and wait
const result = await this.submitAndWait(signed)

// Record metrics
this.metrics.recordPayment({
success: result.success,
amount: amountXRP,
latency: Date.now() - startTime
})

return result
}

validate(destination, amount, options) {
if (!xrpl.isValidAddress(destination)) {
throw new ValidationError('Invalid destination address')
}

const numAmount = parseFloat(amount)
if (isNaN(numAmount) || numAmount <= 0) {
throw new ValidationError('Invalid amount')
}

if (numAmount > this.config.maxPayment) {
throw new ValidationError(Amount exceeds maximum (${this.config.maxPayment}))
}
}

async checkDestinationTagRequired(destination, tag) {
const info = await this.gateway.request({
command: 'account_info',
account: destination,
ledger_index: 'validated'
})

const flags = info.result.account_data.Flags
const lsfRequireDestTag = 0x00020000

if ((flags & lsfRequireDestTag) && tag === undefined) {
throw new ValidationError('Destination requires a destination tag')
}
}

async autofill(payment) {
const conn = await this.gateway.pool.acquire()
try {
return await conn.client.autofill(payment)
} finally {
this.gateway.pool.release(conn)
}
}

async submitAndWait(signed) {
const conn = await this.gateway.pool.acquire()
try {
const result = await conn.client.submitAndWait(signed.tx_blob)

const txResult = result.result.meta.TransactionResult

return {
success: txResult === 'tesSUCCESS',
hash: result.result.hash,
result: txResult,
deliveredAmount: result.result.meta.delivered_amount,
ledger: result.result.ledger_index,
fee: result.result.Fee
}
} finally {
this.gateway.pool.release(conn)
}
}
}
```

// src/services/payment-monitor.js
class PaymentMonitor {
  constructor(gateway, config) {
    this.gateway = gateway
    this.config = config
    this.webhookDelivery = new WebhookDelivery(config.webhookUrl)
    this.deduplicator = new Deduplicator(config.redis)
    this.lastLedger = null
    this.client = null
  }

async start() {
this.client = new xrpl.Client(this.config.servers[0])

this.client.on('disconnected', () => this.handleDisconnect())
this.client.on('ledgerClosed', (l) => this.handleLedger(l))
this.client.on('transaction', (tx) => this.handleTransaction(tx))

await this.client.connect()
await this.subscribe()
}

async subscribe() {
await this.client.request({
command: 'subscribe',
accounts: [this.config.monitorAddress],
streams: ['ledger']
})
}

async handleLedger(ledger) {
const current = ledger.ledger_index

// Gap detection
if (this.lastLedger && current > this.lastLedger + 1) {
await this.fillGap(this.lastLedger + 1, current - 1)
}

this.lastLedger = current
}

async handleTransaction(tx) {
// Must be a Payment
if (tx.transaction.TransactionType !== 'Payment') return

// Must be incoming (to our address)
if (tx.transaction.Destination !== this.config.monitorAddress) return

// Must have succeeded
if (tx.meta?.TransactionResult !== 'tesSUCCESS') return

// Reject partial payments
if (this.isPartialPayment(tx.transaction)) {
console.warn(Rejecting partial payment: ${tx.transaction.hash})
return
}

// Deduplicate
const isNew = await this.deduplicator.checkAndMark(tx.transaction.hash)
if (!isNew) return

// Process payment
const payment = this.extractPayment(tx)

// Deliver webhook
await this.webhookDelivery.deliver(payment)
}

isPartialPayment(tx) {
const tfPartialPayment = 0x00020000
return (tx.Flags & tfPartialPayment) !== 0
}

extractPayment(tx) {
const delivered = tx.meta?.delivered_amount || tx.transaction.Amount

return {
hash: tx.transaction.hash,
from: tx.transaction.Account,
destinationTag: tx.transaction.DestinationTag,
amount: typeof delivered === 'string'
? parseInt(delivered) / 1_000_000
: parseFloat(delivered.value),
currency: typeof delivered === 'string' ? 'XRP' : delivered.currency,
ledger: tx.ledger_index,
timestamp: new Date().toISOString()
}
}

async fillGap(start, end) {
console.log(Filling gap: ledgers ${start}-${end})

const transactions = await this.gateway.request({
command: 'account_tx',
account: this.config.monitorAddress,
ledger_index_min: start,
ledger_index_max: end
})

for (const tx of transactions.result.transactions) {
await this.handleTransaction({
transaction: tx.tx,
meta: tx.meta,
ledger_index: tx.tx.ledger_index
})
}
}

async handleDisconnect() {
console.log('Monitor disconnected, reconnecting...')
setTimeout(() => this.start(), 5000)
}
}
```

// src/api/routes.js
const express = require('express')
const router = express.Router()

module.exports = function(paymentSender, queryService, metrics) {

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

const result = await paymentSender.send(destination, amount, {
destinationTag
})

res.json(result)

} catch (error) {
if (error instanceof ValidationError) {
res.status(400).json({ error: error.message })
} else {
console.error('Payment error:', error)
res.status(500).json({ error: 'Internal error' })
}
}
})

// Get balance
router.get('/balance/:address', async (req, res) => {
try {
const balance = await queryService.getBalance(req.params.address)
res.json(balance)
} catch (error) {
if (error.data?.error === 'actNotFound') {
res.status(404).json({ error: 'Account not found' })
} else {
res.status(500).json({ error: 'Internal error' })
}
}
})

// Get transaction status
router.get('/transactions/:hash', async (req, res) => {
try {
const tx = await queryService.getTransaction(req.params.hash)
res.json(tx)
} catch (error) {
if (error.data?.error === 'txnNotFound') {
res.status(404).json({ error: 'Transaction not found' })
} else {
res.status(500).json({ error: 'Internal error' })
}
}
})

// Health check
router.get('/health', async (req, res) => {
const health = await metrics.getHealth()
res.status(health.status === 'healthy' ? 200 : 503).json(health)
})

return router
}
```


Your submission must include tests for:

  1. Unit Tests

  2. Integration Tests (on testnet)

  3. System Tests

# Run all tests
npm test

Run with coverage

npm run test:coverage

Run integration tests (requires testnet)

npm run test:integration
```


Component Weight Criteria
Core Functionality 30% Payments send/receive correctly, queries work
Reliability 20% Reconnection, retry logic, gap detection
Security 20% Input validation, partial payment protection, rate limiting
Code Quality 15% Clean architecture, documentation, error handling
Testing 15% Coverage, test quality, edge cases

To pass, your submission must:

  • Successfully send XRP payments on testnet
  • Detect incoming payments with correct amount
  • Reject partial payments
  • Handle connection failures with reconnection
  • Pass all required test cases
  • Include health check endpoint

For distinction, also include:

  • Connection pooling with health checks
  • Comprehensive caching strategy
  • Full metrics implementation
  • Webhook delivery with retry
  • Audit logging
  • Docker deployment configuration

  1. **Source Code** - Complete project with all components
  2. **README** - Setup instructions, architecture overview
  3. **Test Results** - Output from test run with coverage
  4. **Demo** - Screen recording or live demo of functionality

Your README should enable setup with:

# Clone and install
git clone <your-repo>
cd xrp-payment-gateway
npm install

# Configure
cp .env.example .env
# Edit .env with testnet credentials

# Run tests
npm test

# Start
npm start

This capstone project represents the culmination of your XRPL API integration learning. You've progressed from understanding basic queries to building a production-grade payment system. The patterns and practices you've implemented here form the foundation for any serious XRPL application.

  • Deep understanding of XRPL API architecture
  • Production-ready payment handling
  • Comprehensive error handling and reliability
  • Security-first integration practices
  • Observable and maintainable code
  • Consider running your own XRPL nodes for production
  • Explore advanced features (escrow, payment channels, hooks)
  • Contribute to the XRPL ecosystem
  • Build your own applications on this foundation

Congratulations on completing the XRPL APIs & Integration course!


Time Investment: 8-12 hours for full implementation

Submission Deadline: As specified by instructor


End of Lesson 18 - Course Capstone

Key Takeaways