Gunjan Sharma

Blockchain · Payment Integration

Integrating XRP Payments into a Real Estate Platform: What Nobody Tells You

· Updated

The XRPL documentation is good. The xrpl.js library is well-maintained. The concepts are straightforward: wallets, transactions, ledger closes, payment streams. You read through it in an afternoon and think: this is easy.

Then you try to build a production payment system on top of it for a regulated real estate investment platform. And you discover that the documentation only covers about 60% of what you actually need to know.

Here is the other 40%.

The Architecture We Were Building

Our platform allows investors to invest in tokenized real estate properties. Each property has an associated XRP wallet. When an investor commits capital, we move XRP from their wallet to the property wallet. This triggers a token issuance event. When the property generates rental income, we distribute XRP back to investor wallets proportionally.

Simple enough conceptually. In practice: full of edge cases.

Problem 1: Destination Tags Are Not Optional

XRP wallets support destination tags — a 32-bit unsigned integer you attach to a payment to identify the recipient. On exchanges and custodial platforms (Binance, Coinbase), your deposit wallet is shared. The destination tag is how they know which user to credit.

For our platform, we had a single inbound XRP wallet that received investor deposits. When an investor sent XRP to that wallet, they were supposed to include their destination tag (which we assigned them at KYC). If they forgot the tag, we had no way to associate the incoming payment with their account.

In the first three months, this happened 23 times. Investors depositing and not including the tag. The money would arrive in our wallet and we had no record of who sent it.

Fix 1: We enabled the requireDestTag flag on our receiving wallet:

const tx = {
TransactionType: 'AccountSet',
Account: RECEIVING_WALLET_ADDRESS,
SetFlag: 1 // asfRequireDest = 1
};

With this flag set, any payment sent to our wallet without a destination tag is automatically rejected by the ledger. The XRP is returned to the sender. No lost funds, no manual reconciliation.

Fix 2: We added a pre-send check in our frontend — if an investor tried to generate a payment QR code or copy the deposit address, we always appended ?dt=THEIR_TAG and displayed the destination tag prominently in red with a warning.

Problem 2: The Base Reserve

Every XRP account must maintain a base reserve. As of the current ledger parameters, this is 10 XRP (it has changed over the years). This XRP cannot be spent. It is locked in the account forever unless you delete the account (which requires the account to be empty and has specific conditions).

For each property, we created a dedicated XRP wallet. We had 40 active properties at the time of this incident. That is 400 XRP locked across 40 wallets as base reserves — capital that is not earning yield and is not accessible.

Additionally, every trust line (if you hold XRPL tokens) and every offer (in the DEX) increases the reserve requirement by 2 XRP each. As we started issuing property tokens via XRPL's native token functionality, each wallet holding a token had its reserve effectively increased.

We had not factored this into our treasury model. Our CFO asked why 400 XRP appeared on the balance sheet as "unavailable". That was an awkward conversation.

Lesson: Model reserves explicitly before you go live. If you are creating programmatic wallets at scale (one per user, one per property), reserve requirements compound. With 10,000 users, that is 100,000 XRP locked as base reserves.

Problem 3: Transaction Finality Is Not Instant

The XRPL closes a ledger every 3-5 seconds. A transaction submitted to the network will be included in a ledger within 1-2 ledger closes under normal conditions — so 5-10 seconds. This is fast compared to Bitcoin or Ethereum.

But it is not instant. And our initial implementation treated it as instant.

Here is what we did wrong:

await client.submitAndWait(tx, { wallet });

// Immediately update database

await updateInvestmentStatus(userId, 'completed');

submitAndWait does wait for the transaction to be included in a validated ledger. But what it does not handle is network instability. If the transaction is included in a non-validated ledger that gets rolled back (this can happen during network consensus failures), your database says "completed" but the ledger says the transaction never happened.

Fix: We moved to an event-sourced model for transaction tracking. Instead of updating the database immediately after submission, we:

1. Submit the transaction and record the transaction hash + expected ledger sequence

2. Subscribe to ledger close events via WebSocket

3. On each ledger close, verify the transaction hash exists in the validated ledger

4. Only then mark the transaction as confirmed in our database

const sub = await client.request({
command: 'subscribe',
streams: ['ledger']
});
client.on('ledgerClosed', async (ledger) => {
const pendingTxs = await getPendingTransactions();
for (const tx of pendingTxs) {
const result = await client.request({
command: 'tx',
transaction: tx.hash
});
if (result.result.validated) {
await confirmTransaction(tx.id);
}
}
});

Problem 4: RPC Node Reliability

We were using the public XRPL Mainnet nodes (s1.ripple.com, s2.ripple.com) for initial development and even early production. Public nodes have rate limits and no SLA.

During one high-traffic period, we started getting connection timeouts. Our payment submission would succeed but the response never came back before the connection dropped. We did not know if the transaction was submitted or not.

Fix: We deployed our own rippled node on a dedicated AWS EC2 instance. This gave us:

- No rate limits

- Full historical ledger data (instead of the last ~32,000 ledgers on public nodes)

- Faster response times (no shared load)

- Our own WebSocket endpoint for ledger subscriptions

The rippled setup took about a day and a full sync took about 72 hours for the historical data. But it was completely worth it.

Problem 5: Amount Precision and Drops

XRP amounts in the XRPL are specified in drops, where 1 XRP = 1,000,000 drops. This is an integer field — there are no fractional XRP in the protocol, only fractional representations in drops.

Our investment amounts were in INR. We converted INR to XRP using a live price feed, then converted XRP to drops. The problem: floating point arithmetic.

const xrpAmount = inrAmount / xrpPriceInINR; // floating point

const drops = xrpAmount * 1_000_000; // can produce non-integer

When drops had a fractional component, the XRPL rejected the transaction with a temBAD_AMOUNT error. We were silently losing transactions because of floating point imprecision.

Fix:

const drops = Math.floor(xrpAmount * 1_000_000).toString();

Always floor, never round. And convert to string — the XRPL JavaScript library expects the amount as a string, not a number, for XRP payments.

What Actually Works in Production

After 14 months running XRPL payments in production, here is what I would tell anyone starting this journey:

1. Run your own rippled node. Do not build on public infrastructure.

2. Model reserves upfront. They are more significant than you think at scale.

3. Enable requireDestTag on shared wallets. Non-negotiable for multi-user platforms.

4. Never treat submitAndWait as a guarantee. Use ledger subscriptions for confirmation.

5. All amounts in drops, all drops as strings, always floor before conversion.

6. Build a transaction reconciliation job that runs every hour. Compare your database state against the ledger. They should always match. If they do not, you have a bug to find.

XRP is genuinely fast and cheap for payments. 3-5 second finality and negligible fees are remarkable for a production financial system. But the gaps between the happy path and production reality are significant. The engineers who succeed with XRPL are the ones who read the edge case documentation — not just the getting started guide.