I've been using Coinos as a Lightning wallet for receiving small payments, but I wanted to automatically sweep the balance to my own Lightning node instead of keeping sats on a custodial service.
What This Script DoesWhat This Script Does
This bash script automatically:
- Checks your Coinos balance via their API
- Creates a Lightning invoice on your node for the available amount
- Pays that invoice through Coinos, effectively withdrawing to your node
- Keeps a configurable reserve amount in your Coinos account
PrerequisitesPrerequisites
- A running LND Lightning node accessible via Docker
- Coinos account with API access
curl,jq, anddockerinstalled on your system
Setup InstructionsSetup Instructions
- Get your Coinos API token:
- Go to https://coinos.io/docs
- Copy the token
- Configure the script:
- Replace
TOKEN="secret"with your actual API token - Adjust
RESERVE_AMOUNT(default: 2000 sats) - amount to keep in Coinos - Adjust
MIN_WITHDRAWAL(default: 100 sats) - minimum amount to withdraw - Change
LND_CONTAINERif your Docker container has a different name
- Replace
- Make executable and test:
chmod +x coinos_payout.sh ./coinos_payout.sh - Automate with cron (optional):
# Run every hour 0 * * * * /path/to/coinos_payout.sh
Configuration OptionsConfiguration Options
All the important settings are at the top of the script:
TOKEN: Your Coinos API tokenRESERVE_AMOUNT: Sats to keep in Coinos (default: 2000)MIN_WITHDRAWAL: Minimum withdrawal threshold (default: 100 sats)LND_CONTAINER: Docker container name for your LND nodeLOG_FILE: Where to store logs
Why Use This?Why Use This?
- Self-custody: Automatically move sats from custodial Coinos to your node
- Automation: Set it and forget it with cron
- Reserve buffer: Keeps some sats in Coinos for immediate spending
The script includes error handling and logging, so you can monitor what's happening and troubleshoot if needed.
Security NotesSecurity Notes
- Keep your API token secure and never commit it to version control
- Review the code before running to understand what it does
- The script has input validation and other security measures. But might not be bulletproof.
NotesNotes
- Coinos does have an autowithdrawal feature to a Bitcoin or LN address. But after #1001011 I'd rather have my custom script.
- This assumes you're running LND in Docker. If you have a different setup, you'll need to modify the
lnclicommand accordingly.
#!/bin/bash
# =============================================================================
# Coinos Auto-Payout Script (Security Hardened)
# =============================================================================
# This script automatically withdraws your Coinos balance via Lightning Network
# by creating an invoice on your Lightning node and paying it through Coinos API
#
# Requirements:
# - curl, jq, docker installed
# - Running LND node accessible via docker
# - Coinos API token with payment permissions
# =============================================================================
# Exit on any error, undefined variable, or pipe failure
set -euo pipefail
# =============================================================================
# CONFIGURATION - MODIFY THESE VALUES
# =============================================================================
# Your Coinos API token (get from https://coinos.io/docs)
# IMPORTANT: Replace "secret" with your actual token
TOKEN="secret"
# Coinos API base URL (usually no need to change)
API_BASE="https://coinos.io/api"
# Amount to keep as reserve in your Coinos account (in sats)
# This prevents withdrawing everything and accounts for potential fees
RESERVE_AMOUNT=2000
# Minimum withdrawal amount (in sats)
# Won't create withdrawal if available amount is below this threshold
MIN_WITHDRAWAL=100
# Log file location (in a secure directory)
LOG_FILE="$HOME/.coinos_payout.log"
# Docker container name for your LND node
# Change this if your LND container has a different name
LND_CONTAINER="lnd"
# Invoice memo/description
INVOICE_MEMO="coinos withdrawal"
# API timeout in seconds
API_TIMEOUT=30
# =============================================================================
# SCRIPT LOGIC - DO NOT MODIFY BELOW THIS LINE
# =============================================================================
# Function to log with timestamp
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}
# Function to handle errors
error_exit() {
log "ERROR: $1"
exit 1
}
# Function to validate numeric input
validate_number() {
local value="$1"
local name="$2"
if ! [[ "$value" =~ ^[0-9]+$ ]]; then
error_exit "Invalid $name format: must be a positive integer"
fi
}
# Function to validate container name
validate_container_name() {
local container="$1"
if ! [[ "$container" =~ ^[a-zA-Z0-9_-]+$ ]]; then
error_exit "Invalid container name: must contain only alphanumeric characters, underscores, and hyphens"
fi
}
# Function to validate Lightning invoice format
validate_invoice() {
local invoice="$1"
# Basic Lightning invoice validation (should start with ln)
if ! [[ "$invoice" =~ ^ln[a-zA-Z0-9]+$ ]]; then
error_exit "Invalid Lightning invoice format"
fi
# Check reasonable length (Lightning invoices are typically 200-400 characters)
local len=${#invoice}
if [ "$len" -lt 100 ] || [ "$len" -gt 1000 ]; then
error_exit "Lightning invoice has unusual length: $len characters"
fi
}
# Function to validate API token
validate_token() {
local token="$1"
if [ "$token" = "secret" ]; then
error_exit "Please set your actual API token in the TOKEN variable"
fi
if [ ${#token} -lt 10 ]; then
error_exit "API token appears to be too short"
fi
}
# Function to make secure API call
make_api_call() {
local endpoint="$1"
local method="${2:-GET}"
local data="${3:-}"
local curl_opts=(
-s
-f # Fail on HTTP errors
--max-time "$API_TIMEOUT"
--connect-timeout 10
-H "content-type: application/json"
-H "Authorization: Bearer $TOKEN"
)
if [ "$method" = "POST" ] && [ -n "$data" ]; then
curl_opts+=(-d "$data")
fi
local response
if ! response=$(curl "${curl_opts[@]}" "$endpoint" 2>/dev/null); then
error_exit "API call failed: $endpoint"
fi
echo "$response"
}
# Function to safely execute docker command
execute_docker_command() {
local container="$1"
local amount="$2"
local memo="$3"
# Validate inputs
validate_container_name "$container"
validate_number "$amount" "invoice amount"
# Sanitize memo (remove potentially dangerous characters)
local safe_memo
safe_memo=$(echo "$memo" | tr -cd '[:alnum:][:space:].-_')
# Execute with proper quoting
local response
if ! response=$(docker exec "$container" lncli addinvoice --amt "$amount" --memo "$safe_memo" 2>/dev/null); then
error_exit "Failed to create Lightning invoice (check if LND container '$container' is running)"
fi
echo "$response"
}
# Initialize secure log file
init_log_file() {
# Create log file with restricted permissions
touch "$LOG_FILE"
chmod 600 "$LOG_FILE"
}
# =============================================================================
# MAIN EXECUTION
# =============================================================================
# Check if required tools are available
command -v curl >/dev/null 2>&1 || error_exit "curl is required but not installed"
command -v jq >/dev/null 2>&1 || error_exit "jq is required but not installed"
command -v docker >/dev/null 2>&1 || error_exit "docker is required but not installed"
# Initialize secure log file
init_log_file
# Validate configuration
validate_token "$TOKEN"
validate_container_name "$LND_CONTAINER"
validate_number "$RESERVE_AMOUNT" "reserve amount"
validate_number "$MIN_WITHDRAWAL" "minimum withdrawal"
log "Starting Coinos payout process..."
# Get current balance
log "Getting balance..."
BALANCE_RESPONSE=$(make_api_call "$API_BASE/me")
# Validate and extract balance
if ! echo "$BALANCE_RESPONSE" | jq -e '.balance' >/dev/null 2>&1; then
error_exit "Invalid API response format - missing balance field"
fi
BALANCE=$(echo "$BALANCE_RESPONSE" | jq -r '.balance')
validate_number "$BALANCE" "balance"
log "Current balance: $BALANCE sats"
# Calculate amount to withdraw (balance minus reserve)
AMOUNT=$((BALANCE - RESERVE_AMOUNT))
log "Amount to withdraw: $AMOUNT sats (keeping $RESERVE_AMOUNT sats as reserve)"
# Check if there's enough to withdraw
if [ "$AMOUNT" -le "$MIN_WITHDRAWAL" ]; then
log "No funds available to withdraw (available: $AMOUNT sats, minimum: $MIN_WITHDRAWAL sats)"
exit 0
fi
# Validate withdrawal amount
if [ "$AMOUNT" -le 0 ]; then
error_exit "Invalid withdrawal amount: $AMOUNT sats"
fi
# Create Lightning invoice
log "Creating Lightning invoice for $AMOUNT sats..."
INVOICE_RESPONSE=$(execute_docker_command "$LND_CONTAINER" "$AMOUNT" "$INVOICE_MEMO")
log "Raw invoice response received"
# Validate and extract payment request
if ! echo "$INVOICE_RESPONSE" | jq -e '.payment_request' >/dev/null 2>&1; then
error_exit "Invalid invoice response format - missing payment_request field"
fi
INVOICE=$(echo "$INVOICE_RESPONSE" | jq -r '.payment_request' | tr -d '\r\n"' | sed 's/[[:space:]]//g')
if [ "$INVOICE" = "null" ] || [ -z "$INVOICE" ]; then
error_exit "Could not extract payment request from invoice response"
fi
# Validate invoice format
validate_invoice "$INVOICE"
log "Invoice created: ${INVOICE:0:50}..."
# Prepare payment data
PAYMENT_DATA=$(jq -n --arg payreq "$INVOICE" '{payreq: $payreq}')
# Pay the invoice through Coinos
log "Submitting payment request to Coinos..."
PAYMENT_RESPONSE=$(make_api_call "$API_BASE/payments" "POST" "$PAYMENT_DATA")
# Validate payment response
if ! echo "$PAYMENT_RESPONSE" | jq -e '.' >/dev/null 2>&1; then
error_exit "Invalid payment response format"
fi
# Check if payment was successful (this depends on the API structure)
if echo "$PAYMENT_RESPONSE" | jq -e '.error' >/dev/null 2>&1; then
ERROR_MSG=$(echo "$PAYMENT_RESPONSE" | jq -r '.error')
error_exit "Payment failed: $ERROR_MSG"
fi
log "Payment response: $PAYMENT_RESPONSE"
log "Payout process completed successfully"
Nice setup super practical. I did something similar.
You're basically bridging Coinos to your own node with a smart sweep script. Keeps some sats liquid, the rest self-custodied. Clean, automated, and secure can’t ask for more. 👌
Could you explain to me all this like a baby
Coinos is a custodial wallet service. They custody their users' Bitcoin and keep a record of each user's balance.
You can use it to receive payments even if you are offline or connect it to sites like stacker.news for sending and receiving Bitcoin payments.
Because you don't have full control of your Bitcoin deposited there, you want to keep just a small amount.
This script regularly checks your balance in Coinos and automatically transfers whatever exceeds a threshold to a Bitcoin wallet that only you control.
Helpful but I think I need to do further research
Pretty sure Coinos had that, but I checked real quick and didn’t see it. Guess they removed it! cc/@adam_coinos_io
Check the notes section:
Oh, I missed that part! Makes sense now, that’s probably why they got rid of it. I’d totally forgotten!