# ⚡ Bandwidth Top-Up ($1 for +100GB)

This guide provides complete instructions and production-ready code examples to programmatically purchase and trigger an **instant bandwidth reset** (+100GB allowance) for your active WireGuard tunnel.

---

## 🎯 Overview

Each TunnelSats subscription includes **100 GB** of high-speed bandwidth. When your node exceeds 70% bandwidth utilization (or runs out entirely and gets disabled), you can purchase a **$1 USD Bandwidth Top-Up** in Bitcoin Lightning (sats) without waiting for your monthly subscription renewal.

Node management packages (e.g., **RaspiBlitz**, **Umbrel**, **Start9**, **Baremetal**, **LNDg**) and automation daemons can monitor usage via `/api/public/v1/subscription/status` and automatically top up bandwidth when thresholds are reached.

---

## 📡 Endpoint Specification

### `POST /api/public/v1/subscription/bandwidth-reset`

* **Authentication:** None (Anonymous / Public API)
* **Rate Limit:** 10 requests / minute per IP

### Request Headers

```http
Content-Type: application/json
```

### Request Payload

```json
{
  "wgPublicKey": "pWyZTQx8C4gzzgMVJHiN1ptI46wDtyJSMRa+wxtkXOM=",
  "serverId": "us-east"
}
```

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `wgPublicKey` | `string` | **Yes** | 44-character Base64 WireGuard public key of your node. |
| `serverId` | `string` | **Yes** | Server identifier, group domain (`eu-de`, `us-east`), or domain (`us3.tunnelsats.com`). |

---

### Response (200 OK)

```json
{
  "invoice": "lnbc30000n1p...",
  "paymentHash": "a1b2c3d4e5f6...",
  "resetId": "d7b2277f-5990-4721-a565-6816babadc59",
  "amountSats": 3000,
  "amountUsd": "1.00",
  "expiresAt": "2026-09-27T11:00:00.000Z",
  "currentUsagePercent": "85.4",
  "resetsThisMonth": 0,
  "maxResetsPerMonth": 2
}
```

| Field | Type | Description |
| --- | --- | --- |
| `invoice` | `string` | BOLT11 Lightning invoice for $1 USD equivalent in sats. |
| `paymentHash` | `string` | SHA256 payment hash for tracking invoice settlement. |
| `resetId` | `string` | Unique UUID tracking this bandwidth reset request. |
| `amountSats` | `number` | Invoice amount in satoshis calculated dynamically from real-time BTC price. |
| `amountUsd` | `string` | Fixed price (`"1.00"`). |
| `expiresAt` | `string` | ISO 8601 invoice expiry. Until then the invoice stays payable and **its monthly slot stays reserved**: reuse this invoice instead of requesting another reset. |
| `currentUsagePercent` | `string` | Current bandwidth consumption percentage on the VPN manager. |
| `resetsThisMonth` | `number` | Number of confirmed resets applied to this key in the current calendar month. |
| `maxResetsPerMonth` | `number` | Maximum allowed resets per month (`2`). |

---

## 🛑 Business Rules & Validation

| Rule | Constraint | Error Code | HTTP Status |
| --- | --- | --- | --- |
| **Usage Threshold** | Must exceed **70%** bandwidth usage (`BANDWIDTH_WARNING_THRESHOLD_PERCENT`) | `ERR_INVALID_INPUT` | `400` |
| **Active Subscription** | Subscription must not be expired | `ERR_INVALID_INPUT` | `400` |
| **Monthly Limit** | Maximum **2** confirmed resets per calendar month per key | `ERR_RATE_LIMIT_EXCEEDED` | `429` |
| **Server & Key** | Key must exist on target server | `ERR_RESOURCE_NOT_FOUND` | `404` |
| **Public Key Format** | Must be a valid 44-character Base64 string | `ERR_INVALID_INPUT` | `400` |

---

## ⚡ Settlement & Auto-Reenablement

Once the Lightning invoice is settled:

1. **LNBits Webhook** fires automatically to `/api/webhooks/bandwidth-reset`.
2. The backend contacts the respective WireGuard server manager and resets the bandwidth counter (`bwReset: true`).
3. If the peer key was disabled due to bandwidth exhaustion, it is **automatically re-enabled** immediately.

### Polling settlement: `GET /api/public/v1/subscription/{paymentHash}`

Poll with the reset's `paymentHash` to learn the outcome. If the webhook was missed, **the poll applies the paid reset itself**, so a paid reset cannot stay unapplied.

```json
{
  "paymentHash": "a1b2c3d4e5f6...",
  "type": "bandwidth_reset",
  "status": "paid",
  "resetId": "d7b2277f-5990-4721-a565-6816babadc59",
  "expiresAt": "2026-09-27T11:00:00.000Z",
  "message": "Bandwidth reset applied."
}
```

| `status` | HTTP | Meaning |
| --- | --- | --- |
| `unpaid` | `200` | Waiting for payment. |
| `processing` | `202` | Paid; the reset is being applied. Poll again. |
| `paid` | `200` | The reset was applied. |
| `failed` | `200` | Paid, but the reset could not be applied. Contact support with the payment hash. |
| `expired` | `200` | The invoice expired unpaid. |
| — | `503` | Payment state could not be verified right now. Retry later; **never treat as unpaid**. |

Always check `type == "bandwidth_reset"`: it distinguishes the reset from subscription orders and renewals polled on the same endpoint.

---

## 💻 Code Examples

<Tabs>
<TabItem value="bash" label="Bash (cURL + jq + lncli)">

:::info Prerequisites
Requires `curl`, `jq`, and an active Lightning CLI (e.g. `lncli`, `cln`, `alby-cli`, or WebLN).
:::

```bash
#!/bin/bash
set -e

API_BASE="https://tunnelsats.com/api/public/v1"
SERVER_ID="us-east"
WG_PUBKEY=$(wg show tunnelsatsv2 public-key 2>/dev/null || cat /etc/wireguard/public.key)

echo "🔍 Checking status & requesting bandwidth top-up for: $WG_PUBKEY"

# 1. Request Bandwidth Reset Invoice
RESPONSE=$(curl -s -X POST "$API_BASE/subscription/bandwidth-reset" \
  -H "Content-Type: application/json" \
  -d "{
    \"wgPublicKey\": \"$WG_PUBKEY\",
    \"serverId\": \"$SERVER_ID\"
  }")

# Check for errors
ERROR=$(echo "$RESPONSE" | jq -r '.error // empty')
if [ -n "$ERROR" ]; then
  echo "❌ Failed: $(echo "$RESPONSE" | jq -r '.message')"
  exit 1
fi

INVOICE=$(echo "$RESPONSE" | jq -r '.invoice')
PAYMENT_HASH=$(echo "$RESPONSE" | jq -r '.paymentHash')
AMOUNT_SATS=$(echo "$RESPONSE" | jq -r '.amountSats')
USAGE=$(echo "$RESPONSE" | jq -r '.currentUsagePercent')

echo "📊 Current Usage: ${USAGE}%"
echo "⚡ Paying $AMOUNT_SATS sats invoice via lncli..."

# 2. Pay Invoice (Example via LND lncli)
lncli payinvoice --force "$INVOICE"

echo "✅ Invoice paid! Payment hash: $PAYMENT_HASH"

# 3. Poll until the reset is applied (the poll also heals a missed webhook)
for i in $(seq 1 30); do
  STATUS=$(curl -s "$API_BASE/subscription/$PAYMENT_HASH" | jq -r 'select(.type == "bandwidth_reset") | .status')
  case "$STATUS" in
    paid)    echo "🎉 Bandwidth reset applied."; break ;;
    failed)  echo "❌ Paid, but the reset failed. Contact support with $PAYMENT_HASH"; exit 1 ;;
    expired) echo "⌛ Invoice expired unpaid."; exit 1 ;;
    *)       sleep 2 ;;  # unpaid, processing (202) or a retryable 503
  esac
done

# 4. Verify usage
curl -s -X POST "$API_BASE/subscription/status" \
  -H "Content-Type: application/json" \
  -d "{\"wgPublicKey\": \"$WG_PUBKEY\"}" | jq .
```

</TabItem>
<TabItem value="python" label="Python (Requests)">

```python
import requests
import time

API_BASE = "https://tunnelsats.com/api/public/v1"
WG_PUBKEY = "pWyZTQx8C4gzzgMVJHiN1ptI46wDtyJSMRa+wxtkXOM="
SERVER_ID = "us-east"

def request_bandwidth_topup(pubkey: str, server: str):
    print(f"⚡ Requesting Bandwidth Top-Up for {server}...")
    
    res = requests.post(f"{API_BASE}/subscription/bandwidth-reset", json={
        "wgPublicKey": pubkey,
        "serverId": server
    })
    
    if not res.ok:
        err = res.json()
        print(f"❌ Error [{err.get('error')}]: {err.get('message')}")
        return None
        
    data = res.json()
    print(f"✅ Top-Up Invoice Generated: {data['amountSats']} sats (${data['amountUsd']})")
    print(f"📊 Current Usage: {data['currentUsagePercent']}%")
    print(f"🔄 Resets This Month: {data['resetsThisMonth']}/{data['maxResetsPerMonth']}")
    print(f"\nBOLT11 Invoice:\n{data['invoice']}\n")
    
    return data

if __name__ == "__main__":
    topup_data = request_bandwidth_topup(WG_PUBKEY, SERVER_ID)
```

</TabItem>
</Tabs>
