SUBTC-CLI V1.0.0 — Bitcoin Testnet

permalink SUBTC
#btc#golang#subtc#subtc-cli#tools

# SUBTC-CLI V1.0.0 — Bitcoin Testnet

Gateway V4.2 · Stateless · Idempotent · curl-first

---

## Quick Start
./subtc key create
./subtc wallet create
./subtc wallet receive <wid>
./subtc wallet balance <wid>
./subtc send <wid> tb1qXXX 50000
./subtc wait <wid> tb1qXXX 20000
./subtc poll <wid> tb1qXXX 20000 --loop

---

## Key Management
./subtc key create # Create & save API key
./subtc key status # Verify current key

---

## Wallet Management
./subtc wallet create # Create testnet wallet
./subtc wallet list # List all wallets
./subtc wallet receive <wid> # Generate receive address
./subtc wallet balance <wid> # Show balance (SAT + BTC)

---

## Send BTC
./subtc send <wid> <addr> <sat> [idem] # Idempotent send (min 50,000 sat)

---

## Inbox / Receive
./subtc inbox <wid> <expected_sat> # Pre-configured receive inbox

---

## Wait / Poll
./subtc wait <wid> <addr> <sat> [timeout_sec] [callback_url] # Long-poll or webhook on payment
./subtc poll <wid> <addr> <sat> # Single poll check
./subtc poll <wid> <addr> <sat> --loop # Loop poll until confirmed

---

## Config
./subtc config show # Show stored config
./subtc config set-key <key> # Store API key
./subtc health # Node health check

---

## Examples
./subtc key create
./subtc wallet create
./subtc wallet receive wlt_abc123
./subtc wallet balance wlt_abc123
./subtc send wlt_abc123 tb1qXXX 50000
./subtc wait wlt_abc123 tb1qXXX 20000
./subtc poll wlt_abc123 tb1qXXX 20000 --loop

---

## Units & Fees
1 BTC = 100,000,000 SAT
Fee: 3%
Minimum send: 50,000 SAT

---

## Documentation
https://subtc.net/api

---

## Test on Linux
Install tools and build CLI:

go build -o subtc subtc-cli.go

Example run:

root@subtc:~/subtc# ./subtc key create
Key Created
────────────────────────────────────────────
key: SUBTC-KEY-3f470eb8d4d0499c12178d5c8c348a66c0c33d7ec941cdd7
✓ Key saved → ~/.subtc.json

root@subtc:~/subtc# ./subtc wallet create
Wallet Created · testnet

Code : subtc-cli.go

// subtc-cli.go — SUBTC CLI V1 · Testnet · Single File
// Gateway V4.2 · Bitcoin Only · Zero external dependencies
//
// Build:  go build -o subtc subtc-cli.go
// Usage:  ./subtc help
package main
import (
        "bytes"
        "encoding/json"
        "fmt"
        "io"
        "net/http"
        "os"
        "path/filepath"
        "strconv"
        "strings"
        "time"
)
// ─────────────────────────────────────────────────────────────────────────────
// Constants
// ─────────────────────────────────────────────────────────────────────────────
const (
        BaseURL    = "https://api.subtc.net"
        Network    = "test"
        ConfigFile = ".subtc.json"
        MinSendSat = 50_000
        Version    = "1.0.0"
)
// ─────────────────────────────────────────────────────────────────────────────
// Terminal colors
// ─────────────────────────────────────────────────────────────────────────────
const (
        cReset  = "\033[0m"
        cBold   = "\033[1m"
        cGreen  = "\033[32m"
        cYellow = "\033[33m"
        cRed    = "\033[31m"
        cCyan   = "\033[36m"
        cGray   = "\033[90m"
)
// ─────────────────────────────────────────────────────────────────────────────
// Config  (~/.subtc.json)
// ─────────────────────────────────────────────────────────────────────────────
type Config struct {
        Key string `json:"key"`
}
func configPath() string {
        home, _ := os.UserHomeDir()
        return filepath.Join(home, ConfigFile)
}
func loadConfig() Config {
        f, err := os.Open(configPath())
        if err != nil {
                return Config{}
        }
        defer f.Close()
        var c Config
        json.NewDecoder(f).Decode(&c)
        return c
}
func (c Config) save() error {
        f, err := os.OpenFile(configPath(), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
        if err != nil {
                return err
        }
        defer f.Close()
        enc := json.NewEncoder(f)
        enc.SetIndent("", "  ")
        return enc.Encode(c)
}
func (c Config) key() string {
        if v := os.Getenv("SUBTC_KEY"); v != "" {
                return v
        }
        return c.Key
}
func (c Config) requireKey() {
        if c.key() == "" {
                die("no API key — run: subtc key create")
        }
}
// ─────────────────────────────────────────────────────────────────────────────
// HTTP client
// ─────────────────────────────────────────────────────────────────────────────
var httpClient = &http.Client{Timeout: 360 * time.Second}
type J = map[string]any
func apiGET(path string) J {
        resp, err := httpClient.Get(BaseURL + path)
        must(err)
        defer resp.Body.Close()
        return decode(resp.Body)
}
func apiPOST(cfg Config, mode string, body J, idempotency string) J {
        if body == nil {
                body = J{}
        }
        raw, _ := json.Marshal(body)
        req, err := http.NewRequest(http.MethodPost,
                fmt.Sprintf("%s/v1/btc?mode=%s", BaseURL, mode),
                bytes.NewReader(raw),
        )
        must(err)
        req.Header.Set("Content-Type", "application/json")
        if k := cfg.key(); k != "" {
                req.Header.Set("X-SUBTC-KEY", k)
        }
        if idempotency != "" {
                req.Header.Set("X-SUBTC-IDEMPOTENCY", idempotency)
        }
        resp, err := httpClient.Do(req)
        must(err)
        defer resp.Body.Close()
        return decode(resp.Body)
}
func decode(r io.Reader) J {
        raw, err := io.ReadAll(r)
        must(err)
        var result J
        if err := json.Unmarshal(raw, &result); err != nil {
                die("bad JSON from server: " + string(raw))
        }
        return result
}
// ─────────────────────────────────────────────────────────────────────────────
// Display helpers
// ─────────────────────────────────────────────────────────────────────────────
func header(title string) {
        fmt.Printf("\n%s%s%s\n%s\n", cBold, title, cReset, strings.Repeat("─", 44))
}
func info(key, val string) {
        fmt.Printf("%s%-20s%s %s\n", cCyan, key+":", cReset, val)
}
func ok(msg string) {
        fmt.Printf("%s✓ %s%s\n", cGreen, msg, cReset)
}
func warn(msg string) {
        fmt.Printf("%s! %s%s\n", cYellow, msg, cReset)
}
func die(msg string) {
        fmt.Fprintf(os.Stderr, "%s✗ %s%s\n", cRed, msg, cReset)
        os.Exit(1)
}
func must(err error) {
        if err != nil {
                die(err.Error())
        }
}
func printJSON(v J) {
        b, _ := json.MarshalIndent(v, "", "  ")
        fmt.Println(cGray + string(b) + cReset)
}
func fmtSat(sat float64) string {
        return fmt.Sprintf("%.0f sat  (%.8f BTC)", sat, sat/1e8)
}
func str(v J, key string) string {
        if x, ok := v[key]; ok {
                switch t := x.(type) {
                case string:
                        return t
                case float64:
                        return fmt.Sprintf("%.0f", t)
                }
        }
        return ""
}
// nested digs into v["result"][key] — SUBTC wraps responses in {"ok":true,"result":{...}}
func nested(v J, key string) string {
        if r, ok := v["result"].(J); ok {
                return str(r, key)
        }
        return str(v, key) // fallback: flat response
}
// resultMap returns v["result"] as J, or v itself as fallback.
func resultMap(v J) J {
        if r, ok := v["result"].(J); ok {
                return r
        }
        return v
}
// ─────────────────────────────────────────────────────────────────────────────
// Commands
// ─────────────────────────────────────────────────────────────────────────────
func cmdHealth() {
        result := apiGET("/health")
        header("Health · " + Network + "net")
        printJSON(result)
}
// ── Key ───────────────────────────────────────────────────────────────────────
func cmdKeyCreate(cfg *Config) {
        result := apiPOST(*cfg, "key_create", nil, "")
        header("Key Created")
        key := nested(result, "key")
        if key == "" {
                printJSON(result)
                return
        }
        info("key", key)
        cfg.Key = key
        if err := cfg.save(); err != nil {
                warn("could not save key: " + err.Error())
        } else {
                ok("Key saved → ~/.subtc.json")
        }
}
func cmdKeyStatus(cfg Config) {
        cfg.requireKey()
        result := apiPOST(cfg, "key_status", nil, "")
        header("Key Status")
        printJSON(result)
}
// ── Wallet ────────────────────────────────────────────────────────────────────
func cmdWalletCreate(cfg Config) {
        cfg.requireKey()
        result := apiPOST(cfg, "wallet_create", J{"net": Network}, "")
        header("Wallet Created · " + Network + "net")
        if wid := nested(result, "wallet_id"); wid != "" {
                info("wallet_id", wid)
        }
        printJSON(result)
}
func cmdWalletList(cfg Config) {
        cfg.requireKey()
        result := apiPOST(cfg, "wallet_list", nil, "")
        header("Wallets")
        printJSON(result)
}
func cmdWalletReceive(cfg Config, walletID string) {
        cfg.requireKey()
        result := apiPOST(cfg, "wallet_receive", J{"wallet_id": walletID}, "")
        header("Receive Address")
        if addr := nested(result, "address"); addr != "" {
                info("address", addr)
        }
        printJSON(result)
}
func cmdWalletBalance(cfg Config, walletID string) {
        cfg.requireKey()
        result := apiPOST(cfg, "wallet_balance", J{"wallet_id": walletID}, "")
        header("Balance · " + walletID[:min(12, len(walletID))] + "…")
        res := resultMap(result)
        if s, ok := res["balance_sat"].(float64); ok {
                info("balance", fmtSat(s))
        }
        if s, ok := res["unconfirmed_sat"].(float64); ok {
                info("unconfirmed", fmtSat(s))
        }
        printJSON(result)
}
// ── Send ──────────────────────────────────────────────────────────────────────
func cmdSend(cfg Config, walletID, toAddr string, amountSat int64, idem string) {
        cfg.requireKey()
        if amountSat < MinSendSat {
                die(fmt.Sprintf("minimum send is %d sat (got %d)", MinSendSat, amountSat))
        }
        if idem == "" {
                idem = fmt.Sprintf("send-%d", time.Now().Unix())
        }
        header("Send BTC · testnet")
        info("wallet_id",   walletID)
        info("to",          toAddr)
        info("amount",      fmtSat(float64(amountSat)))
        info("idempotency", idem)
        fmt.Println()
        result := apiPOST(cfg, "wallet_send", J{
                "wallet_id":  walletID,
                "to":         toAddr,
                "amount_sat": amountSat,
        }, idem)
        if txid := nested(result, "txid"); txid != "" {
                ok("Transaction broadcast")
                info("txid", txid)
        }
        printJSON(result)
}
// ── Inbox ─────────────────────────────────────────────────────────────────────
func cmdInbox(cfg Config, walletID string, expectedSat int64) {
        cfg.requireKey()
        result := apiPOST(cfg, "inbox_create", J{
                "wallet_id":    walletID,
                "expected_sat": expectedSat,
        }, "")
        header("Inbox Created")
        if addr := nested(result, "address"); addr != "" {
                ok("Inbox ready")
                info("address", addr)
                info("expected", fmtSat(float64(expectedSat)))
        }
        printJSON(result)
}
// ── Wait (long-poll or webhook) ───────────────────────────────────────────────
func cmdWait(cfg Config, walletID, address string, expectedSat int64, timeoutSec int, callbackURL string) {
        cfg.requireKey()
        body := J{
                "wallet_id":    walletID,
                "address":      address,
                "expected_sat": expectedSat,
                "timeout_sec":  timeoutSec,
        }
        if callbackURL != "" {
                body["callback_url"] = callbackURL
        }
        header("Waiting for Funds")
        info("wallet_id",    walletID)
        info("address",      address)
        info("expected_sat", fmtSat(float64(expectedSat)))
        info("timeout",      fmt.Sprintf("%d sec", timeoutSec))
        if callbackURL != "" {
                info("callback_url", callbackURL)
                info("mode", "webhook")
        } else {
                info("mode", "long-poll (blocking…)")
        }
        fmt.Println()
        result := apiPOST(cfg, "wallet_wait_event", body, "")
        res := resultMap(result)
        if reached, _ := res["reached"].(bool); reached {
                ok("Payment confirmed!")
        }
        if s, ok2 := res["received_sat"].(float64); ok2 {
                info("received", fmtSat(s))
        }
        printJSON(result)
}
// ── Poll ──────────────────────────────────────────────────────────────────────
func cmdPoll(cfg Config, walletID, address string, expectedSat int64, loop bool, intervalSec int) {
        cfg.requireKey()
        header("Poll")
        info("wallet_id",    walletID)
        info("address",      address)
        info("expected_sat", fmtSat(float64(expectedSat)))
        if loop {
                info("mode", fmt.Sprintf("loop every %d sec — Ctrl+C to stop", intervalSec))
        }
        fmt.Println()
        poll := func() bool {
                result := apiPOST(cfg, "wallet_poll", J{
                        "wallet_id":    walletID,
                        "address":      address,
                        "expected_sat": expectedSat,
                }, "")
                res     := resultMap(result)
                reached, _ := res["reached"].(bool)
                recSat, _  := res["received_sat"].(float64)
                icon := "⏳ "
                if reached {
                        icon = "✅ "
                }
                fmt.Printf("%s  received: %-26s reached: %v\n", icon, fmtSat(recSat), reached)
                return reached
        }
        if !loop {
                poll()
                return
        }
        for {
                if poll() {
                        ok("Payment confirmed — stopping loop")
                        break
                }
                time.Sleep(time.Duration(intervalSec) * time.Second)
        }
}
// ── Config sub-commands ───────────────────────────────────────────────────────
func cmdConfigShow(cfg Config) {
        header("Config")
        keyDisplay := cfg.Key
        if len(keyDisplay) > 8 {
                keyDisplay = keyDisplay[:8] + "••••••••"
        }
        if keyDisplay == "" {
                keyDisplay = "(none — run: subtc key create)"
        }
        info("key",     keyDisplay)
        info("network", Network+" (testnet fixed in V1)")
        fmt.Println("\n  file:        ~/.subtc.json")
        fmt.Println("  env-override: SUBTC_KEY")
}
// ─────────────────────────────────────────────────────────────────────────────
// Help
// ─────────────────────────────────────────────────────────────────────────────
func printHelp() {
        fmt.Printf(`
%sSUBTC CLI V%s — Bitcoin Testnet%s
Gateway V4.2 · Stateless · Idempotent · curl-first
%sUsage:%s
  subtc <command> [args]
%sKey:%s
  subtc key create                         Create & save API key
  subtc key status                         Verify current key
%sWallet:%s
  subtc wallet create                      Create testnet wallet
  subtc wallet list                        List all wallets
  subtc wallet receive  <wid>              Generate receive address
  subtc wallet balance  <wid>              Show balance (SAT + BTC)
%sSend:%s
  subtc send <wid> <addr> <sat> [idem]     Idempotent send (min 50,000 sat)
%sInbox:%s
  subtc inbox <wid> <expected_sat>         Pre-configured receive inbox
%sWait / Poll:%s
  subtc wait <wid> <addr> <sat> [timeout_sec] [callback_url]
                                           Long-poll or webhook on payment
  subtc poll <wid> <addr> <sat>            Single poll check
  subtc poll <wid> <addr> <sat> --loop     Loop poll until confirmed
%sConfig:%s
  subtc config show                        Show stored config
  subtc config set-key <key>               Store API key
  subtc health                             Node health check
%sExamples:%s
  subtc key create
  subtc wallet create
  subtc wallet receive  wlt_abc123
  subtc wallet balance  wlt_abc123
  subtc send  wlt_abc123  tb1qXXX  50000
  subtc wait  wlt_abc123  tb1qXXX  20000  300
  subtc poll  wlt_abc123  tb1qXXX  20000  --loop
%sUnits:%s  1 BTC = 100,000,000 SAT · Fee: 3%% · Min send: 50,000 SAT
%sDocs:%s   https://subtc.net/api
`,
                cBold, Version, cReset,
                cBold, cReset,
                cCyan, cReset,
                cCyan, cReset,
                cCyan, cReset,
                cCyan, cReset,
                cCyan, cReset,
                cCyan, cReset,
                cGreen, cReset,
                cGray, cReset,
                cGray, cReset,
        )
}
// ─────────────────────────────────────────────────────────────────────────────
// Argument helpers
// ─────────────────────────────────────────────────────────────────────────────
func argSat(s, name string) int64 {
        v, err := strconv.ParseInt(s, 10, 64)
        if err != nil || v <= 0 {
                die("invalid " + name + ": " + s)
        }
        return v
}
func argInt(s string, def int) int {
        v, err := strconv.Atoi(s)
        if err != nil {
                return def
        }
        return v
}
func hasFlag(args []string, flag string) bool {
        for _, a := range args {
                if a == flag {
                        return true
                }
        }
        return false
}
func flagVal(args []string, prefix string, def string) string {
        for _, a := range args {
                if strings.HasPrefix(a, prefix) {
                        return a[len(prefix):]
                }
        }
        return def
}
func min(a, b int) int {
        if a < b {
                return a
        }
        return b
}
// ─────────────────────────────────────────────────────────────────────────────
// Main
// ─────────────────────────────────────────────────────────────────────────────
func main() {
        cfg := loadConfig()
        if len(os.Args) < 2 {
                printHelp()
                return
        }
        args := os.Args[1:]
        cmd  := args[0]
        rest := args[1:]
        switch cmd {
        // ── Health ────────────────────────────────────────────────────────────────
        case "health":
                cmdHealth()
        // ── Key ───────────────────────────────────────────────────────────────────
        case "key":
                if len(rest) == 0 {
                        die("usage: subtc key <create|status>")
                }
                switch rest[0] {
                case "create":
                        cmdKeyCreate(&cfg)
                case "status":
                        cmdKeyStatus(cfg)
                default:
                        die("unknown key sub-command: " + rest[0])
                }
        // ── Wallet ────────────────────────────────────────────────────────────────
        case "wallet":
                if len(rest) == 0 {
                        die("usage: subtc wallet <create|list|receive|balance>")
                }
                switch rest[0] {
                case "create":
                        cmdWalletCreate(cfg)
                case "list":
                        cmdWalletList(cfg)
                case "receive":
                        if len(rest) < 2 {
                                die("usage: subtc wallet receive <wallet_id>")
                        }
                        cmdWalletReceive(cfg, rest[1])
                case "balance":
                        if len(rest) < 2 {
                                die("usage: subtc wallet balance <wallet_id>")
                        }
                        cmdWalletBalance(cfg, rest[1])
                default:
                        die("unknown wallet sub-command: " + rest[0])
                }
        // ── Send ──────────────────────────────────────────────────────────────────
        case "send":
                if len(rest) < 3 {
                        die("usage: subtc send <wallet_id> <to_addr> <amount_sat> [idempotency_key]")
                }
                idem := ""
                if len(rest) >= 4 {
                        idem = rest[3]
                }
                cmdSend(cfg, rest[0], rest[1], argSat(rest[2], "amount_sat"), idem)
        // ── Inbox ─────────────────────────────────────────────────────────────────
        case "inbox":
                if len(rest) < 2 {
                        die("usage: subtc inbox <wallet_id> <expected_sat>")
                }
                cmdInbox(cfg, rest[0], argSat(rest[1], "expected_sat"))
        // ── Wait ──────────────────────────────────────────────────────────────────
        case "wait":
                if len(rest) < 3 {
                        die("usage: subtc wait <wallet_id> <address> <expected_sat> [timeout_sec] [callback_url]")
                }
                timeout     := 300
                callbackURL := ""
                if len(rest) >= 4 {
                        timeout = argInt(rest[3], 300)
                }
                if len(rest) >= 5 {
                        callbackURL = rest[4]
                }
                cmdWait(cfg, rest[0], rest[1], argSat(rest[2], "expected_sat"), timeout, callbackURL)
        // ── Poll ──────────────────────────────────────────────────────────────────
        case "poll":
                if len(rest) < 3 {
                        die("usage: subtc poll <wallet_id> <address> <expected_sat> [--loop] [--interval=N]")
                }
                loop     := hasFlag(rest[3:], "--loop") || hasFlag(rest[3:], "-l")
                interval := argInt(flagVal(rest[3:], "--interval=", "3"), 3)
                cmdPoll(cfg, rest[0], rest[1], argSat(rest[2], "expected_sat"), loop, interval)
        // ── Config ────────────────────────────────────────────────────────────────
        case "config":
                sub := ""
                if len(rest) > 0 {
                        sub = rest[0]
                }
                switch sub {
                case "", "show":
                        cmdConfigShow(cfg)
                case "set-key":
                        if len(rest) < 2 {
                                die("usage: subtc config set-key <key>")
                        }
                        cfg.Key = rest[1]
                        must(cfg.save())
                        ok("API key saved → ~/.subtc.json")
                default:
                        die("unknown config sub-command: " + sub)
                }
        case "help", "--help", "-h":
                printHelp()
        default:
                warn("unknown command: " + cmd)
                printHelp()
        }
}

# SUBTC-CLI Test Run Examples

root@subtc:~/subtc# ./subtc key create
Key Created
────────────────────────────────────────────
key:                 SUBTC-KEY-3f470eb8d4d0499c12178d5c8c348a66c0c33d7ec941cdd7
✓ Key saved → ~/.subtc.json

root@subtc:~/subtc# ./subtc wallet create
Wallet Created · testnet
────────────────────────────────────────────
wallet_id:           w_ce9e16e59a72cd59840ce2256662a2400bc114ea3758
{
  "ok": true,
  "request_id": "66d6705f7e7a1c82",
  "result": {
    "coin": "btc",
    "net": "test",
    "wallet_id": "w_ce9e16e59a72cd59840ce2256662a2400bc114ea3758"
  }
}

root@subtc:~/subtc# ./subtc wallet receive
✗ usage: subtc wallet receive <wallet_id>

root@subtc:~/subtc# ./subtc wallet receive w_ce9e16e59a72cd59840ce2256662a2400bc114ea3758
Receive Address
────────────────────────────────────────────
address:             tb1qwphc20fty0l42cn3tnwv32uz85zm9jm2thdl4y
{
  "ok": true,
  "request_id": "98683c2957cadb69",
  "result": {
    "address": "tb1qwphc20fty0l42cn3tnwv32uz85zm9jm2thdl4y",
    "addresses": [
      "tb1qwphc20fty0l42cn3tnwv32uz85zm9jm2thdl4y"
    ],
    "coin": "btc",
    "net": "test",
    "wallet_id": "w_ce9e16e59a72cd59840ce2256662a2400bc114ea3758"
  }
}

root@subtc:~/subtc# ./subtc wallet balance w_ce9e16e59a72cd59840ce2256662a2400bc114ea3758
Balance · w_ce9e16e59a…
────────────────────────────────────────────
balance:             48500 sat  (0.00048500 BTC)
unconfirmed:         0 sat  (0.00000000 BTC)
{
  "ok": true,
  "request_id": "3843d95815b06e0a",
  "result": {
    "addresses": [
      "tb1qwphc20fty0l42cn3tnwv32uz85zm9jm2thdl4y"
    ],
    "balance_sat": 48500,
    "balance_source": "getbalances",
    "coin": "btc",
    "confirmed_sat": 48500,
    "immature_sat": 0,
    "net": "test",
    "unconfirmed_sat": 0,
    "wallet_id": "w_ce9e16e59a72cd59840ce2256662a2400bc114ea3758"
  }
}

root@subtc:~/subtc# ./subtc send w_ce9e16e59a72cd59840ce2256662a2400bc114ea3758 tb1qjcr7lcucmwudwqscew4r078gxkw549ruq5r97w 30000
✗ minimum send is 50000 sat (got 30000)

root@subtc:~/subtc# ./subtc send w_ce9e16e59a72cd59840ce2256662a2400bc114ea3758 tb1qjcr7lcucmwudwqscew4r078gxkw549ruq5r97w 50000
Send BTC · testnet
────────────────────────────────────────────
wallet_id:           w_ce9e16e59a72cd59840ce2256662a2400bc114ea3758
to:                  tb1qjcr7lcucmwudwqscew4r078gxkw549ruq5r97w
amount:              50000 sat  (0.00050000 BTC)
idempotency:         send-1773768995
{
  "detail": {
    "hint": "rpc error"
  },
  "error": "SEND_FAILED",
  "ok": false
}

root@subtc:~/subtc# ./subtc wallet balance w_ce9e16e59a72cd59840ce2256662a2400bc114ea3758
Balance · w_ce9e16e59a…
────────────────────────────────────────────
balance:             97000 sat  (0.00097000 BTC)
unconfirmed:         0 sat  (0.00000000 BTC)
{
  "ok": true,
  "request_id": "52180cb3b9d20395",
  "result": {
    "addresses": [
      "tb1qwphc20fty0l42cn3tnwv32uz85zm9jm2thdl4y"
    ],
    "balance_sat": 97000,
    "balance_source": "getbalances",
    "coin": "btc",
    "confirmed_sat": 97000,
    "immature_sat": 0,
    "net": "test",
    "unconfirmed_sat": 0,
    "wallet_id": "w_ce9e16e59a72cd59840ce2256662a2400bc114ea3758"
  }
}

root@subtc:~/subtc# ./subtc send w_ce9e16e59a72cd59840ce2256662a2400bc114ea3758 tb1qjcr7lcucmwudwqscew4r078gxkw549ruq5r97w 50000
Send BTC · testnet
────────────────────────────────────────────
wallet_id:           w_ce9e16e59a72cd59840ce2256662a2400bc114ea3758
to:                  tb1qjcr7lcucmwudwqscew4r078gxkw549ruq5r97w
amount:              50000 sat  (0.00050000 BTC)
idempotency:         send-1773769023
✓ Transaction broadcast
txid:                3d72d72aa9f43be50e4dfb35971107252143fdf3da859d97d74427336b5ef8fd
{
  "ok": true,
  "request_id": "359a5d64c67a4ee3",
  "result": {
    "coin": "btc",
    "fee_addr": "tb1q2dn34v7l3jmn30zuwgkzry23td949qtrre46cw",
    "fee_bps": 300,
    "net": "test",
    "network_fee_by": "service_fee",
    "requested_sat": 50000,
    "send_method": "sendmany",
    "sent_sat": 48500,
    "service_fee_sat": 1500,
    "txid": "3d72d72aa9f43be50e4dfb35971107252143fdf3da859d97d74427336b5ef8fd",
    "wallet_id": "w_ce9e16e59a72cd59840ce2256662a2400bc114ea3758"
  }
}

root@subtc:~/subtc# ./subtc wait w_ce9e16e59a72cd59840ce2256662a2400bc114ea3758 tb1qwphc20fty0l42cn3tnwv32uz85zm9jm2thdl4y 20000
Waiting for Funds
────────────────────────────────────────────
wallet_id:           w_ce9e16e59a72cd59840ce2256662a2400bc114ea3758
address:             tb1qwphc20fty0l42cn3tnwv32uz85zm9jm2thdl4y
expected_sat:        20000 sat  (0.00020000 BTC)
timeout:             300 sec
mode:                long-poll (blocking…)
received:            97000 sat  (0.00097000 BTC)
{
  "ok": true,
  "request_id": "ae34e901e5ca030f",
  "result": {
    "address": "tb1qwphc20fty0l42cn3tnwv32uz85zm9jm2thdl4y",
    "coin": "btc",
    "expected_sat": 20000,
    "net": "test",
    "received_sat": 97000,
    "status": "confirmed",
    "wallet_id": "w_ce9e16e59a72cd59840ce2256662a2400bc114ea3758"
  }
}

root@subtc:~/subtc# ./subtc poll w_ce9e16e59a72cd59840ce2256662a2400bc114ea3758 tb1qwphc20fty0l42cn3tnwv32uz85zm9jm2thdl4y 20000 --loop
Poll
────────────────────────────────────────────
wallet_id:           w_ce9e16e59a72cd59840ce2256662a2400bc114ea3758
address:             tb1qwphc20fty0l42cn3tnwv32uz85zm9jm2thdl4y
expected_sat:        20000 sat  (0.00020000 BTC)
mode:                loop every 3 sec — Ctrl+C to stop
✅   received: 97000 sat  (0.00097000 BTC) reached: true

## https://github.com/subtc/SUBTC-CLI-V1.0.0-Bitcoin-Testnet