Deposit Monitoring
Rach monitors blockchain addresses for incoming deposits and fires webhook events when transactions are detected and confirmed. The monitor runs continuously on-chain using native RPC — no third-party monitoring dependency.
How Monitoring Works
When you derive an address with enable_monitoring: true, the address is polled by Rach's custom blockchain monitor every ~30 seconds. The system:
- Detects a balance increase on the address
- Immediately fires
wallet.deposit.detected— informational only, do not credit yet - Waits for the network's confirmation threshold or settlement delay
- Fires
wallet.deposit.confirmed— this is the signal to credit customer funds
The
safe_to_creditfield in the payload is the definitive credit signal — never credit onfalse.
Enabling Monitoring
Set enable_monitoring: true when deriving an address:
curl -X POST '.../api/v1/wallet/user_12345/derive' \
-H 'X-API-Key: live_sk_...' \
-H 'Content-Type: application/json' \
-d '{ "network": "POL", "index": 0, "enable_monitoring": true }'Webhook Configuration
Deposit webhooks are configured from the dashboard (Settings → Webhooks). This requires a logged-in session — it cannot be set with an API key.
Enable wallet webhooks in the dashboard by:
- Entering your webhook endpoint URL
- Toggling Enable wallet deposit webhooks on
- Saving your webhook secret (shown once on first save)
Your endpoint must return HTTP 200 within 10 seconds or delivery will be retried (up to 5 attempts with exponential backoff). Failed deliveries after 5 attempts are dead-lettered and can be retried from the dashboard.
Webhook Events
wallet.deposit.detected
Fired the moment a deposit is seen on-chain. The transaction may have zero confirmations and the tx_hash may be a placeholder while the real hash is being resolved.
Use this to show a "pending deposit" status to your customer. Do not credit.
{
"event": "wallet.deposit.detected",
"data": {
"customer_id": "user_12345",
"network": "POL",
"address": "0x1a2b3c...",
"amount": "100.00",
"currency": "POL",
"tx_hash": "pending_1782847918764682996_0x1a2b",
"confirmations": 0,
"status": "detected",
"detected_at": "2026-06-30T19:31:57Z",
"detected_by": "custom",
"safe_to_credit": false
}
}
tx_hashmay start withpending_if the on-chain transaction indexer has not yet made it queryable. It will be resolved and the confirmed event will carry the real hash where available.
wallet.deposit.confirmed
Fired when the deposit has met the network's confirmation threshold or settlement delay. This is the authoritative signal to credit customer funds.
{
"event": "wallet.deposit.confirmed",
"data": {
"customer_id": "user_12345",
"network": "POL",
"address": "0x1a2b3c...",
"amount": "100.00",
"currency": "POL",
"tx_hash": "0xabc123def456...",
"confirmations": 24,
"status": "confirmed",
"detected_at": "2026-06-30T19:31:57Z",
"confirmed_at": "2026-06-30T19:33:00Z",
"safe_to_credit": true
}
}The safe_to_credit Flag
Every deposit webhook payload contains a safe_to_credit boolean. This is the simplest and most reliable way to decide whether to credit a customer:
| Event | safe_to_credit | Action |
|---|---|---|
wallet.deposit.detected | false | Show "Deposit pending" — do not credit |
wallet.deposit.confirmed | true | Credit customer account |
Always check safe_to_credit rather than inferring from confirmations or status alone. This protects you correctly across all networks, including those that use time-based settlement instead of block-depth counting.
Confirmation Thresholds by Network
| Network | Confirmation Method | Settlement Time | safe_to_credit becomes true |
|---|---|---|---|
| ETH | Block depth | ~3 minutes | After 12 blocks |
| BSC | Block depth | ~30 seconds | After 12 blocks |
| POL | Block depth | ~60 seconds | After required block depth |
| BTC | Block depth (Esplora) | ~30 minutes | After 6 blocks |
| LTC | Time-based | ~10 minutes | After settlement delay |
| BCH | Time-based | ~30 minutes | After settlement delay |
| TRX | Time-based | ~60 seconds | After settlement delay |
| SOL | Time-based | ~15 seconds | After settlement delay |
| XRP | Time-based | ~30 seconds | After settlement delay |
For LTC, BCH, TRX, SOL, and XRP, the
confirmationsfield inwallet.deposit.confirmedmay show the required confirmation count rather than a live block-depth count. Always usesafe_to_credit: trueas the credit signal — never rely onconfirmations > 0alone.
Security Guarantees
Flash Loans Cannot Trigger Webhooks
Flash loans borrow and repay within a single atomic transaction. The net balance change on your monitored address is zero. Rach detects deposits by comparing current vs previous balance — a flash loan leaves no net change and will never trigger either webhook event.
Testnet Transactions Cannot Affect Mainnet
Testnet and mainnet are entirely separate blockchains. A transaction on Mumbai testnet cannot change the balance of a Polygon mainnet address. Test API keys (test_sk_*) query testnet RPCs; live keys (live_sk_*) query mainnet RPCs. There is no cross-contamination.
EVM 0-Confirmation Safety
On EVM networks (ETH, BSC, POL), eth_getBalance returns the confirmed on-chain balance — mempool/unconfirmed transactions are not reflected. By the time Rach detects a balance increase, the transaction is already included in a block. "confirmations": 0 in the detected event means the deposit is on-chain but confirmation depth is not yet measured — the funds are real.
Two-Event Design
The separation of detected and confirmed into two distinct events with explicit safe_to_credit values means there is no ambiguous state for your integration to mishandle.
Webhook Handler Example
app.post('/webhooks/rach', express.raw({ type: '*/*' }), async (req, res) => {
// 1. Verify signature — always first
const sig = req.headers['x-webhook-signature'];
const expected = crypto
.createHmac('sha256', process.env.RACH_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).end();
}
const { event, data } = JSON.parse(req.body);
if (event === 'wallet.deposit.detected') {
// Show pending status — never credit here
await db.deposits.upsert({
customerId: data.customer_id,
amount: data.amount,
currency: data.currency,
network: data.network,
txHash: data.tx_hash,
status: 'pending',
});
await notify.depositPending(data.customer_id, data.amount, data.currency);
}
if (event === 'wallet.deposit.confirmed' && data.safe_to_credit === true) {
// safe_to_credit is the definitive signal
await db.deposits.update(
{ customerId: data.customer_id, network: data.network, status: 'pending' },
{ txHash: data.tx_hash, status: 'confirmed', confirmedAt: data.confirmed_at }
);
await wallet.credit(data.customer_id, data.amount, data.currency);
await notify.depositConfirmed(data.customer_id, data.amount, data.currency);
}
res.sendStatus(200);
});Webhook Delivery Behaviour
| Scenario | Behaviour |
|---|---|
| Endpoint returns non-200 | Retried up to 5 times with exponential backoff |
| Endpoint times out (>10s) | Counted as failed, retried |
| All 5 attempts fail | Delivery dead-lettered; retryable from dashboard |
| Webhook not configured | Event silently skipped — deposit still recorded in DB |
| Webhook enabled after a missed deposit | Existing undelivered deposit webhooks will not auto-resend; contact support |
Manual Balance Refresh
Trigger a live on-chain balance check on-demand:
GET /api/v1/wallet/user_12345/addresses?refresh=true
X-API-Key: live_sk_...Setting refresh=true forces an immediate RPC balance query for all addresses belonging to this customer. If a new deposit is detected, the full webhook flow fires as normal.
