---
title: Onchain / Lightning swaps on Voltage using Loop
slug: onchain-lightning-swaps-on-voltage-using-loop
docTags: 
createdAt: 2025-09-30T14:10:04.714Z
---

# Lightning Loop on Voltage — Full REST Examples

## What you need (one-time)

- Your node’s **public API host** (from the Voltage dashboard).
- Your **SUPER ADMIN MACAROON** (from the Voltage dashboard).
- Requests authenticate with header **Grpc-Metadata-macaroon** carrying the **hex** of your macaroon.

*GUI alternative: You can also do all of this from Terminal Web (LiT bundles Loop). Pair via LNC from your Voltage dashboard*

## Quick env setup

:::BlockQuote
\# Required: set your host and macaroon (hex).
export HOST="\<your-node-hostname>:443"
\# If you have the file locally:
\# export MACAROON\_HEX=$(xxd -ps -u -c 1000 /path/to/loop.macaroon)

\# Sanity check:
curl -s "https\://$HOST/v1/loop/info" \\
&#x20; -H "Grpc-Metadata-macaroon: $MACAROON\_HEX" | jq .
:::

/v1/loop/info returns daemon details when auth is good. (Lightning Engineering)

## Drop-in script with both swaps and full lifecycle

Save as `loop_examples.sh`, run `chmod +x loop_examples.sh`. to make it executable.

*Requires jq and xxd.*

```bash
#!/usr/bin/env bash
set -euo pipefail

# -------- CONFIG YOU CAN TWEAK --------
: "${HOST:?Set HOST (e.g. node-123.voltageapp.io OR node-123.voltageapp.io:443)}"
: "${MACAROON_HEX:?Set MACAROON_HEX (hex of Super Admin macaroon)}"

# Fee policy defaults (simple + safe starting points).
# For server fee: ~1% of amt, hard-capped.
SWAP_FEE_PERCENT_DEFAULT=1        # 1%
SWAP_FEE_HARDCAP_SATS_DEFAULT=3000
# Miner fee cap (sweep/publish): start with quote's miner fee * 2 as a buffer.
MINER_FEE_MULTIPLIER_DEFAULT=2
# Off-chain routing fee cap (Loop Out):
ROUTE_FEE_MSAT_DEFAULT=50000      # 50k msat
# Sweep/publish conf target:
CONF_TARGET_DEFAULT=6
# Poll interval:
POLL_SECS=15
POLL_TIMEOUT_SECS=$((60*60))      # 1 hour

hdr() { echo -n "Grpc-Metadata-macaroon: $MACAROON_HEX"; }

get_json() {
  local path="$1"
  curl -sS "https://$HOST$path" -H "$(hdr)"
}

post_json() {
  local path="$1" body="$2"
  curl -sS "https://$HOST$path" -H "$(hdr)" -H 'Content-Type: application/json' -d "$body"
}

ceil_div() { awk -v n="$1" -v d="$2" 'BEGIN{printf "%d\n", (n + d - 1)/d}'; }

budget_caps_from_amt() {
  local amt_sats="$1"
  local fee_pct="${2:-$SWAP_FEE_PERCENT_DEFAULT}"
  local hardcap="${3:-$SWAP_FEE_HARDCAP_SATS_DEFAULT}"
  local pct_fee=$(( amt_sats * fee_pct / 100 ))
  if (( pct_fee > hardcap )); then echo "$hardcap"; else echo "$pct_fee"; fi
}

poll_swap() {
  local id="$1" start_ts
  start_ts="$(date +%s)"
  echo "Polling /v1/loop/swap/{id} every ${POLL_SECS}s… (timeout ${POLL_TIMEOUT_SECS}s)"
  while :; do
    local now js state
    now="$(date +%s)"
    if (( now - start_ts > POLL_TIMEOUT_SECS )); then
      echo "Timeout reached; last known state:" >&2
      get_json "/v1/loop/swap/${id}" | jq .
      exit 1
    fi
    js="$(get_json "/v1/loop/swap/${id}")"
    state="$(jq -r '.state' <<<"$js")"
    echo "$(date -Is)  state=$state"
    case "$state" in
      SUCCESS|SUCCEEDED) echo "Swap succeeded ✅"; jq . <<<"$js"; break;;
      FAILED)            echo "Swap failed ❌"; jq . <<<"$js"; exit 2;;
      *)                 sleep "$POLL_SECS";;
    esac
  done
}

# ============= LOOP OUT (Lightning -> On-chain) =============
loop_out() {
  local amt_sats="$1" dest_addr="$2"
  local conf_target="${3:-$CONF_TARGET_DEFAULT}"

  echo "== Loop Out: terms =="
  get_json "/v1/loop/out/terms" | jq .      # min/max, cltv ranges

  echo "== Loop Out: quote for ${amt_sats} sats (conf_target=$conf_target) =="
  local quote
  quote="$(get_json "/v1/loop/out/quote/${amt_sats}?conf_target=${conf_target}")"
  echo "$quote" | jq .

  local swap_fee prepay_amt sweep_fee
  swap_fee="$(jq -r '.swap_fee_sat|tonumber' <<<"$quote")"
  prepay_amt="$(jq -r '.prepay_amt_sat|tonumber' <<<"$quote")"
  sweep_fee="$(jq -r '.htlc_sweep_fee_sat|tonumber' <<<"$quote")"

  # Compute budget caps
  local max_swap_fee max_miner_fee max_route_msat
  max_swap_fee="$(budget_caps_from_amt "$amt_sats")"  # includes prepay by contract
  max_miner_fee=$(( sweep_fee * MINER_FEE_MULTIPLIER_DEFAULT ))
  max_route_msat="${ROUTE_FEE_MSAT_DEFAULT}"

  echo "== Budget check =="
  echo "Server fee quote: ${swap_fee} sats (prepay ${prepay_amt} sats is part of server fee)"
  echo "Miner (sweep) fee est: ${sweep_fee} sats; cap we will set: ${max_miner_fee} sats"
  echo "Server fee cap we will set: ${max_swap_fee} sats (policy ~1% up to ${SWAP_FEE_HARDCAP_SATS_DEFAULT})"
  if (( swap_fee > max_swap_fee )); then
    echo "✋ Quote swap_fee (${swap_fee}) exceeds cap (${max_swap_fee}). Tune caps or amount." >&2
    exit 3
  fi

  echo "== Loop Out: create swap =="
  local body created id
  body="$(jq -n --argjson amt       "$amt_sats" \
                 --arg dest         "$dest_addr" \
                 --argjson max_swap_fee        "$max_swap_fee" \
                 --argjson max_miner_fee       "$max_miner_fee" \
                 --argjson max_prepay_amt      "$prepay_amt" \
                 --argjson max_swap_routing_fee "$max_route_msat" \
                 --argjson sweep_conf_target   "$conf_target" \
                 '{amt, dest, max_swap_fee, max_miner_fee, max_prepay_amt, max_swap_routing_fee, sweep_conf_target}')"
  created="$(post_json "/v1/loop/out" "$body")"
  echo "$created" | jq .

  id="$(jq -r '.id' <<<"$created")"
  echo "== Loop Out: monitor swap id =="
  poll_swap "$id"
}

# ============= LOOP IN (On-chain -> Lightning) =============
loop_in() {
  local amt_sats="$1"
  local conf_target="${2:-$CONF_TARGET_DEFAULT}"

  echo "== Loop In: terms =="
  get_json "/v1/loop/in/terms" | jq .

  echo "== (optional) Probe routeability for ${amt_sats} sats =="
  get_json "/v1/loop/in/probe/${amt_sats}" | jq .

  echo "== Loop In: quote for ${amt_sats} sats (htlc conf_target=$conf_target) =="
  local quote
  quote="$(get_json "/v1/loop/in/quote/${amt_sats}?conf_target=${conf_target}")"
  echo "$quote" | jq .

  local swap_fee publish_fee
  swap_fee="$(jq -r '.swap_fee_sat|tonumber' <<<"$quote")"
  publish_fee="$(jq -r '.htlc_publish_fee_sat|tonumber' <<<"$quote")"

  # Compute budget caps
  local max_swap_fee max_miner_fee
  max_swap_fee="$(budget_caps_from_amt "$amt_sats")"
  max_miner_fee=$(( publish_fee * MINER_FEE_MULTIPLIER_DEFAULT ))

  echo "== Budget check =="
  echo "Server fee quote: ${swap_fee} sats"
  echo "Publish fee est: ${publish_fee} sats; cap we will set: ${max_miner_fee} sats"
  echo "Server fee cap we will set: ${max_swap_fee} sats (policy ~1% up to ${SWAP_FEE_HARDCAP_SATS_DEFAULT})"
  if (( swap_fee > max_swap_fee )); then
    echo "✋ Quote swap_fee (${swap_fee}) exceeds cap (${max_swap_fee}). Tune caps or amount." >&2
    exit 3
  fi

  echo "== Loop In: create swap =="
  local body created id
  body="$(jq -n --argjson amt "$amt_sats" \
                 --argjson max_swap_fee  "$max_swap_fee" \
                 --argjson max_miner_fee "$max_miner_fee" \
                 --argjson htlc_conf_target "$conf_target" \
                 '{amt, max_swap_fee, max_miner_fee, htlc_conf_target}')"
  created="$(post_json "/v1/loop/in" "$body")"
  echo "$created" | jq .

  id="$(jq -r '.id' <<<"$created")"
  echo "== Loop In: monitor swap id =="
  poll_swap "$id"
}

# -------- CL Interface --------
usage() {
  cat <<USAGE
Usage:
  $0 out <AMT_SATS> <ONCHAIN_DEST> [CONF_TARGET]
  $0 in  <AMT_SATS> [CONF_TARGET]

Examples:
  $0 out 250000 bc1qyourdest... 6
  $0 in  250000 6
USAGE
}

cmd="${1:-}"; shift || true
case "${cmd:-}" in
  out) : "${1:?AMT_SATS}"; : "${2:?DEST_ADDR}"; loop_out "$1" "$2" "${3:-}";;
  in)  : "${1:?AMT_SATS}"; loop_in "$1" "${2:-}";;
  *)   usage; exit 1;;
esac

```

### What this script demonstrates (mapping to Loop API)

- **Terms**: GET /v1/loop/out/terms and GET /v1/loop/in/terms for bounds (min/max, CLTV ranges). (Lightning Engineering)
- **Quote**: GET /v1/loop/out/quote/&#x7B;amt} (returns swap\_fee\_sat, prepay\_amt\_sat, htlc\_sweep\_fee\_sat) and GET /v1/loop/in/quote/&#x7B;amt} (returns swap\_fee\_sat, htlc\_publish\_fee\_sat). (Lightning Engineering)
- **Budget check**: We compare the quote vs caps you’ll actually send in the create-swap call:
  - Loop Out caps: max\_swap\_fee (**includes prepay**), max\_miner\_fee, max\_swap\_routing\_fee, sweep\_conf\_target. (Lightning Engineering)
  - Loop In caps: max\_swap\_fee, max\_miner\_fee, htlc\_conf\_target. (Lightning Engineering)
- **Create swap**: POST /v1/loop/out and POST /v1/loop/in; response includes the id you’ll use to monitor. (Lightning Engineering)
- **Poll**: GET /v1/loop/swap/&#x7B;id}. The state eventually becomes SUCCESS or FAILED; we poll until a terminal state. (These states are enumerated in Monitor/ListSwaps docs.) (Lightning Engineering)

## Fee & reliability playbook (simple policy you can adjust)

- **Always** fetch terms → quote first; populate your **caps** from the quote. (Lightning Engineering)
- Start with **≈1% of amount** for max\_swap\_fee (cap with a hard ceiling you’re comfortable with, e.g., 100–3000 sats).
- Keep max\_miner\_fee ≈ **2×** the miner fee estimate from the quote to absorb fee volatility; tune \*\_conf\_target to trade speed vs cost. (Loop Out uses sweep\_conf\_target; Loop In uses htlc\_conf\_target.) (Lightning Engineering)
- If a Loop Out fails due to routing, **retry once** with a higher max\_swap\_routing\_fee (keep server fee cap the same).
- For Loop In, you can **probe** with /v1/loop/in/probe/&#x7B;amt} to sanity-check routeability before creating the swap. (Lightning Engineering)

## Voltage-specific notes

- **TLS**: your node’s API host uses a **trusted CA-signed certificate**; you don’t need to pass a local tls.cert to curl.
- **Macaroons**: Loop requires **Super Admin Macaroon** to use it throught the API on Voltage.
- **Access control**: If you’ve enabled **IP whitelisting** on your node, be sure your client IP is allowed for both REST and gRPC APIs. (Voltage Documentation)
- **UI**: Prefer a GUI? Use **Terminal Web** (LiT bundles Loop/Pool/Faraday) via an **LNC pairing phrase** from your Voltage dashboard.

## Endpoint reference (official)

- **REST index** (all Loop endpoints): [https://lightning.engineering/api-docs/api/loop/rest-endpoints/](https://lightning.engineering/api-docs/api/loop/rest-endpoints/)
- **Loop Out**: /v1/loop/out - [https://lightning.engineering/api-docs/api/loop/swap-client/loop-out/](https://lightning.engineering/api-docs/api/loop/swap-client/loop-out/)
- **Loop In**: /v1/loop/in - [https://lightning.engineering/api-docs/api/loop/swap-client/loop-in/](https://lightning.engineering/api-docs/api/loop/swap-client/loop-in/)
- **Monitro Single Swap:&#x20;**&#x20;/v1/loop/swap/&#x7B;id} - [https://lightning.engineering/api-docs/api/loop/swap-client/swap-info/](https://lightning.engineering/api-docs/api/loop/swap-client/swap-info/)
- **Monitor Multiple Swaps**: /v1/loop/swaps - [https://lightning.engineering/api-docs/api/loop/swap-client/list-swaps/](https://lightning.engineering/api-docs/api/loop/swap-client/list-swaps/)

