pull down to refresh

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 Does

This bash script automatically:
  1. Checks your Coinos balance via their API
  2. Creates a Lightning invoice on your node for the available amount
  3. Pays that invoice through Coinos, effectively withdrawing to your node
  4. Keeps a configurable reserve amount in your Coinos account

Prerequisites

  • A running LND Lightning node accessible via Docker
  • Coinos account with API access
  • curl, jq, and docker installed on your system

Setup Instructions

  1. Get your Coinos API token:
  2. 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_CONTAINER if your Docker container has a different name
  3. Make executable and test:
    chmod +x coinos_payout.sh
    ./coinos_payout.sh
    
  4. Automate with cron (optional):
    # Run every hour
    0 * * * * /path/to/coinos_payout.sh
    

Configuration Options

All the important settings are at the top of the script:
  • TOKEN: Your Coinos API token
  • RESERVE_AMOUNT: Sats to keep in Coinos (default: 2000)
  • MIN_WITHDRAWAL: Minimum withdrawal threshold (default: 100 sats)
  • LND_CONTAINER: Docker container name for your LND node
  • LOG_FILE: Where to store logs

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

Notes

  • 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 lncli command 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"
Could you explain to me all this like a baby
reply
0 sats \ 1 reply \ @klk OP 1h
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.
reply
0 sats \ 0 replies \ @Danni 45m
Helpful but I think I need to do further research
reply
Pretty sure Coinos had that, but I checked real quick and didn’t see it. Guess they removed it! cc/@adam_coinos_io
reply
0 sats \ 1 reply \ @klk OP 13h
Check the notes section:
Coinos does have an autowithdrawal feature to a Bitcoin or LN address. But after #1001011 I'd rather have my custom script.
reply
Oh, I missed that part! Makes sense now, that’s probably why they got rid of it. I’d totally forgotten!
reply
stackers have outlawed this. turn on wild west mode in your /settings to see outlawed content.