Trigger icon
Schedule icon
Query icon
Request icon
If icon
Fail icon
Exit icon
Loop icon
Log icon
SlackIncomingWebhook icon
Pause icon

Batch Bitcoin Payouts When Fees Are Low, with Human Approval

Batch queued Bitcoin payouts into one transaction when mempool fees drop below your cap, with address checks, approval, and txid tracking.

Categories
BusinessData

Paying users, sellers, or affiliates in Bitcoin one transaction at a time wastes fees, and paying when the mempool is congested wastes more. This blueprint turns a payout table into a fee-aware batching pipeline: payouts queue up in Postgres, the flow waits until mempool.space reports fees below your cap, rejects invalid addresses, asks a human to approve the batch, and pays every recipient in a single Bitcoin Core sendmany transaction. Each payout row records its txid, so your application always knows what was paid and when.

It defaults to signet, and it asks the node which chain it is on before doing anything: on mainnet it refuses to run unless you set allow_mainnet.

How it works

  1. ensure_table creates the payouts queue on first run. Your application inserts rows with an address and an amount; new rows start as pending.
  2. node_info calls getblockchaininfo, and mainnet_guard (If) fails the run on mainnet unless allow_mainnet is true.
  3. load_queue reads the oldest pending payouts. queue_empty exits cleanly when there is nothing to pay.
  4. validate_addresses (Loop) checks every address with validateaddress and marks invalid ones rejected, so one bad address cannot block the queue.
  5. fees reads the mempool.space estimate, and fee_gate (If) defers the run when the chosen estimate is above max_fee_rate. The queue is untouched and the next trigger tries again.
  6. lock_batch claims up to max_outputs payouts for this execution (FOR UPDATE SKIP LOCKED), and batch_summary totals them and merges payouts to the same address.
  7. value_cap fails batches above max_batch_btc before anyone is asked to approve them.
  8. approval posts the batch to Slack with a link, and wait_for_approval (Pause) waits up to 24 hours for an approver to resume it with approved set. A rejected batch goes back to the queue.
  9. mark_broadcasting, send_batch (sendmany at the approved fee rate, replaceable), and mark_sent record the txid on every payout, then notify_sent posts the explorer link.
  10. The errors block returns claimed-but-unsent payouts to the queue. Payouts already marked broadcasting are never retried automatically, so a failure after broadcast cannot cause a double payment.

Triggers: a io.kestra.plugin.jdbc.postgresql.Trigger that starts a batch when 10 payouts are waiting, and a 30-minute Schedule that sweeps smaller queues. Both ship disabled. The flow runs with concurrency.limit: 1, so two batches never run at once.

Payout lifecycle

pending → locked → broadcasting → sent, or rejected for invalid addresses. A batch that is deferred, rejected by the approver, or over the value cap leaves its payouts pending.

Prerequisites

  • A Bitcoin Core node (v24 or newer) with RPC enabled and a funded wallet named by the wallet input. Start on signet or testnet4.
  • A Postgres database for the payout queue.
  • A Slack incoming webhook for approval requests and notifications.
  • Outbound access from the worker to mempool.space, or your own fee API in the same format.

Secrets

  • BITCOIN_RPC_USER, BITCOIN_RPC_PASSWORD: Bitcoin Core RPC credentials.
  • POSTGRES_URL, POSTGRES_USER, POSTGRES_PASSWORD: JDBC URL (for example jdbc:postgresql://db:5432/payouts) and credentials for the queue.
  • SLACK_WEBHOOK_URL: Slack incoming webhook URL.

Inputs

  • rpc_url (STRING) and wallet (STRING): node endpoint and funding wallet.
  • allow_mainnet (BOOL, default false): required to send on mainnet.
  • fee_api_url (URI), fee_target (SELECT, default hourFee), max_fee_rate (FLOAT sat/vB, default 5): where fees come from, which estimate to pay, and the cap.
  • max_outputs (INT, default 50) and max_batch_btc (FLOAT, default 0.1): batch size and value cap.
  • require_approval (BOOL, default true), kestra_url, explorer_tx_url (STRING): approval and links.

Outputs

  • txid (STRING): the broadcast transaction, empty when nothing was sent.
  • batch (JSON): payout and address counts, total BTC, and the amounts sent.

Quick start

  1. Add the secrets (in the open-source edition, as base64-encoded SECRET_* environment variables).
  2. Run the flow once; it creates the payouts table and exits because the queue is empty.
  3. Insert a few signet payouts: INSERT INTO payouts (address, amount_btc) VALUES ('tb1q...', 0.001);
  4. Run it again, approve the batch from the Slack link, and follow the txid on mempool.space.
  5. Enable queue_threshold and every_30_minutes when you are happy with the results.

How to extend

  • Notify each recipient: add a Loop over the batch after mark_sent that calls your user-facing API with the txid.
  • Bump stuck batches: a scheduled flow can find sent payouts unconfirmed after an hour and call bumpfee on their txid (transactions are sent replaceable).
  • Replace Slack approval with your own policy: skip require_approval for batches under a small value and keep it for large ones.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.