Skip to content

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:

  1. Detects a balance increase on the address
  2. Immediately fires wallet.deposit.detected — informational only, do not credit yet
  3. Waits for the network's confirmation threshold or settlement delay
  4. Fires wallet.deposit.confirmedthis is the signal to credit customer funds

The safe_to_credit field in the payload is the definitive credit signal — never credit on false.


Enabling Monitoring

Set enable_monitoring: true when deriving an address:

bash
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:

  1. Entering your webhook endpoint URL
  2. Toggling Enable wallet deposit webhooks on
  3. 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.

json
{
  "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_hash may start with pending_ 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.

json
{
  "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:

Eventsafe_to_creditAction
wallet.deposit.detectedfalseShow "Deposit pending" — do not credit
wallet.deposit.confirmedtrueCredit 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

NetworkConfirmation MethodSettlement Timesafe_to_credit becomes true
ETHBlock depth~3 minutesAfter 12 blocks
BSCBlock depth~30 secondsAfter 12 blocks
POLBlock depth~60 secondsAfter required block depth
BTCBlock depth (Esplora)~30 minutesAfter 6 blocks
LTCTime-based~10 minutesAfter settlement delay
BCHTime-based~30 minutesAfter settlement delay
TRXTime-based~60 secondsAfter settlement delay
SOLTime-based~15 secondsAfter settlement delay
XRPTime-based~30 secondsAfter settlement delay

For LTC, BCH, TRX, SOL, and XRP, the confirmations field in wallet.deposit.confirmed may show the required confirmation count rather than a live block-depth count. Always use safe_to_credit: true as the credit signal — never rely on confirmations > 0 alone.


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

javascript
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

ScenarioBehaviour
Endpoint returns non-200Retried up to 5 times with exponential backoff
Endpoint times out (>10s)Counted as failed, retried
All 5 attempts failDelivery dead-lettered; retryable from dashboard
Webhook not configuredEvent silently skipped — deposit still recorded in DB
Webhook enabled after a missed depositExisting undelivered deposit webhooks will not auto-resend; contact support

Manual Balance Refresh

Trigger a live on-chain balance check on-demand:

bash
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.

Rach Payments API