This sidecar exposes a small HTTP API for LNbits to talk to the Spark L2 SDK. https://www.spark.money/
git clone https://github.com/lnbits/spark_sidecar.git
cd spark_sidecar
npm install
chmod +x server.mjs
SPARK_MNEMONIC="bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom" \
SPARK_NETWORK=MAINNET \
SPARK_SIDECAR_PORT=8765 \
SPARK_PAY_WAIT_MS=20000 \
node server.mjs
Spark Multiplicity Setting
Optional multiplicity tuning for Spark leaf optimization
Default multiplicity is 3
SPARK_MULTIPLICITY=3
Optional API Key
SPARK_SIDECAR_API_KEY="mykey"
Set the same key in LNbits as SPARK_L2_EXTERNAL_API_KEY, and point
SPARK_L2_EXTERNAL_ENDPOINT at this sidecar.
If you prefer to provide the mnemonic after startup, omit SPARK_MNEMONIC and
POST it to the sidecar:
curl -X POST http://127.0.0.1:8765/v1/mnemonic \
-H "Content-Type: application/json" \
-d '{"mnemonic":"bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom"}'Build:
nix build
Run:
SPARK_MNEMONIC="bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom" \
SPARK_NETWORK=MAINNET \
SPARK_SIDECAR_PORT=8765 \
SPARK_PAY_WAIT_MS=20000 \
nix run
Notes:
- The flake includes
flake.nixandflake.lock. Commit both. - The
resultsymlink fromnix buildshould not be committed.
POST /v1/mnemonicPOST /v1/balancePOST /v1/invoicesPOST /v1/paymentsGET /v1/invoices/stream(SSE stream of paid Lightning receive requests)GET /v1/invoices/{id}GET /v1/payments/{id}
The stream endpoint emits Server-Sent Events after the received Spark funds have
cleared. Both invoice status lookups and stream notifications check the invoice's
own incoming transfer and its leaves. A Lightning success status alone is not
enough: uncleared receipts return WAITING_FOR_FUNDS, which SparkL2 maps to pending.
The check requires a completed transfer and available leaves registered in the
SDK's local cache. Later spending, completed splits/aggregations, or a subsequent
ownership change on a single-receiver transfer also prove prior availability.
Each lookup reconstructs this evidence from Spark; there is no saved receipt
ledger. A provider outage returns pending/unavailable, never an invented success.
The availability check reads the pinned SDK's internal leaf registry; review
incoming.mjs when upgrading the SDK.
Transfer events trigger checks immediately. While a stream client is connected, polling discovers new updates and retries uncleared receipts using bounded, in-memory bookkeeping. The discovery cursor starts at process startup. It does not replay all historic invoices after every restart. LNbits' invoice-ID checks recover payments made while the sidecar was offline directly from Spark.
SSE delivery is best effort, not an acknowledgement protocol. Disconnects can lose events, and reconnects can repeat them. Invoice-ID checks are the recovery path; a GET returns status without emitting an additional SSE notification.
Example:
curl -N http://127.0.0.1:8765/v1/invoices/streamEach event payload is a JSON object:
{
"checking_id": "<receive_request_id>",
"payment_hash": "<hash>",
"status": "LIGHTNING_PAYMENT_RECEIVED"
}Optional tuning:
SPARK_STREAM_KEEPALIVE_MS(default15000)SPARK_STREAM_HEARTBEAT_MS(default30000)SPARK_TRANSFER_LOOKUP_CONCURRENCY(default20)SPARK_TRANSFER_QUEUE_MAX(default5000)SPARK_INVOICE_POLL_MS(default2000)SPARK_INVOICE_POLL_LIMIT(default100)
SPARK_OPERATION_CONCURRENCY (default 8, positive integer) limits concurrent
outgoing operation workflows. Incoming receipt checks have a separate pool of
the same size. Different operation IDs run concurrently; duplicate requests for
one ID serialize and cannot dispatch it twice. Concurrent balance refreshes share
one SDK call. Long outgoing history scans continue across polls, at most two pages
per lookup. Incoming discovery reads at most four pages per polling pass. Slow
stream clients are disconnected when their write buffers fill.
Outgoing payments check spendable funds including quoted fees before dispatch.
SPARK_FUNDS_WAIT_MS (default 20000) bounds waiting within the original POST.
If funds remain unavailable, the request fails before dispatch. Nothing is queued
for later sending: status GETs only query Spark and never initiate a payment.
This does not limit how long incoming invoices wait to be marked available.
If a send is rejected with FEE_LIMIT_EXCEEDED, the Spark quote exceeds the
max_fee_sats supplied by LNbits. The rejection message shows both amounts.
In LNbits Admin Settings → Funding, adjust the minimum fee reserve (millisats)
to cover the quote: 5000 millisats permits a 5-sat fee. LNbits defaults to a
2000-millisat minimum, whereas Spark's
Lightning withdrawal guidance
recommends a minimum 5-sat budget. Larger payments or routes may require more.
The sidecar never raises the supplied limit itself. Older SparkL2 connectors
ignore error_message on failed responses; the sidecar log still shows the reason.
SPARK_PAY_WAIT_MS (default 4000) controls how long a payment POST polls before
returning its current status; SparkL2 can continue polling pending payments.
SPARK_PAY_POLL_MS defaults to 500. Keep the POST wait below LNbits' request
timeout, allowing time for SDK calls.
Mock burst tests cover hundreds of concurrent requests, bounded overlap and provider idempotency keys. They do not establish live Spark capacity: signing, operator latency, leaf distribution, and wallet liquidity still affect throughput. Use one active sidecar per Spark wallet; removing the journal does not establish support for multiple independently running SDK wallets spending the same leaves.
LNbits is a free and open-source lightning accounts system.
Run make check for formatting, static checks, payment tests and localhost HTTP/SSE
tests. CI runs the Node tests on Linux, Windows and macOS. Replacement tests use
fresh working directories, recover status from a mock Spark service, and assert
that no sidecar files were created. Native Windows EXE packaging and live Spark
payments still require their own deployment tests.