Ride The RLN (RTR) is a sidecar that sits in front of your RGB Lightning Node (RLN). You talk to RTR instead of the node: same API, same paths, same payloads. RTR watches what goes through, keeps the node's transfers moving, and calls you back when something settles or fails.
flowchart LR
OP["Your backend"] -- "RLN API, unchanged<br/>+ /rtr/*" --> C["RTR<br/>(mirror DB, refresh loop, reaper)"]
C -- "webhooks<br/>settled / failed" --> OP
C -- "proxied calls<br/>+ refresh, list, fail" --> RLN["RLN node<br/>(API port not exposed)"]
Caution
Proof of concept. Not production software.
A node fronted by RTR must be used only through RTR. Never call the node's API directly, never share the node's API port, never run two RTR instances against one node. RTR's picture of your transfers is only right if it sees every single call. Break that rule and transfers can get stuck, webhooks can lie, and money can sit unclaimed with nobody noticing.
The only two supported ways to run a node: RLN alone, or RLN behind exactly one RTR.
| Without RTR | With RTR |
|---|---|
You must call /refreshtransfers in a loop, with the right body, at the right cadence, or transfers never settle (and non-donation sends are never even broadcast) |
RTR drives refresh by itself, only while something is pending |
You must remember identifiers RLN never returns again (batch_transfer_idx) to cancel expired transfers |
RTR keeps them and fails expired transfers for you |
You poll /listtransfers per asset to learn that a transfer settled |
You get a signed webhook: transfer.confirmed_pending, transfer.settled, transfer.failed |
You poll /listpayments to learn that a Lightning payment settled |
You get a signed webhook: payment.settled, payment.failed |
RLN's refresh answers {} whatever happened |
RTR diffs transfer state and reports real transitions |
Everything else is a transparent proxy: any RLN route works through RTR exactly as it does on the node, including auth (RLN keeps enforcing its own tokens).
The reference deployment is Docker Compose: node and RTR together, node API port only on the internal network, RTR on 127.0.0.1:3101.
cd deploy
cp rtr.toml.example rtr.toml # set webhook url and secret
docker compose up --build --wait
curl -s 127.0.0.1:3101/rtr/healthFrom source instead:
cargo build --release
cp sample-config.toml config.toml # set [rln] base_url, [webhook] url and secret
./target/release/rtr --config config.tomlThen point your integration at http://127.0.0.1:3101 and keep using the RLN routes as before: /init, /unlock, /rgbinvoice, /sendrgb, /nodeinfo, all of them.
You create an invoice through RTR, hand it to the payer, and forget about it. When the transfer settles, RTR POSTs to your webhook.url:
POST /hook HTTP/1.1
content-type: application/json
x-rtr-event-id: 5c0f2c6e-1a4b-4f2d-9c3e-8b7a6d5e4f30
x-rtr-signature: 9f3b7c2e1d... (hex HMAC-SHA256 of the body, keyed with webhook.secret)
{
"event_id": "5c0f2c6e-1a4b-4f2d-9c3e-8b7a6d5e4f30",
"event_type": "transfer.settled",
"transfer": {
"id": "2f9d8c1b-7e6a-4d5c-b3a2-1f0e9d8c7b6a",
"kind": "ReceiveBlind",
"status": "Settled",
"asset_id": "rgb:2dkSTbr-jFhznbPmo-TQafzswCN-av4gTsJjX-ttx6CNou5-M98k8Zd",
"recipient_id": "utxob:2FZsSxN-...",
"txid": "9f0c3b1e...",
"settled_at": 1756640420
},
"previous_status": "WaitingConfirmations",
"new_status": "Settled",
"timestamp": 1756640420
}Your handler does three things: verify the signature, skip event ids you have already seen, answer 200. That is the whole contract.
expected = hmac.new(secret, raw_body, "sha256").hexdigest()
if not hmac.compare_digest(expected, headers["x-rtr-signature"]):
return 401
if seen(headers["x-rtr-event-id"]):
return 200
mark_paid(order_for(payload["transfer"]["recipient_id"]))
return 200Five event types: transfer.confirmed_pending (broadcast, waiting for confirmations), transfer.settled, transfer.failed, plus payment.settled and payment.failed for Lightning payments. Delivery is at-least-once, in order, with retries and backoff; events that keep failing are parked and show up in health. Full details in docs/webhooks.md.
curl -s 127.0.0.1:3101/rtr/health
{"status":"ok","node":"unlocked","pending_transfers":1,"pending_payments":0,"parked_events":0,"last_full_sync_at":1756640000}status is ok only when the node is unlocked and no webhook is stuck; anything else is degraded and the other fields say why.
| Topic | Read |
|---|---|
| Deploying with Docker Compose, switching to Biscuit auth, data locations | deploy/README.md |
| Every config key, its default, CLI flags | docs/configuration.md |
| What the proxy does and which routes are intercepted | docs/proxy.md |
/rtr/* endpoints and errors |
docs/api.md |
| Webhook payload, signature, delivery guarantees | docs/webhooks.md |
| Startup, locking, migrations, what to expect in operation | docs/operations.md |
| Unit tests, regtest end-to-end suite, CI | docs/testing.md |