QALS Wiki · the qalarc network⌂ qalarc.com/projects/qals
generated 2026-09-16 · qalcode autonomous research

Village Starlink share: pay-per-MB backhaul, zero-rated messaging · qalhotspot/README.md

qalhotspot — the metered-access gateway ("village Starlink share")

Status: PAPER v0 (2026-09-13) — the accounting + messaging integration is the deliverable. No real traffic shaping exists yet: metering is accounting-only (a client script reports bytes; in production this hooks iptables/nftables counters). The money math is real paper-loopd math: escrowed holds, integer-cent settlement, exact refunds, chain-anchored receipts. Port :8844 · zero-dependency stdlib · qalhotspot.py.

bash qalhotspot/test_qalhotspot.sh    # full suite, all green
bash qalhotspot/client.sh guesta villageop 500 8   # guest-side demo

The village model — the network pays for its own backhaul

Founder intent (verbatim): "people on a network can on-sell WiFi connection, or have metered WiFi, if one person in a village is supplying Starlink. It would also enable communications."

One payer buys the expensive uplink (Starlink, LTE, long-range PtP). Everyone else prepays small amounts of QALS into their own loopd agents. The gateway escrows a hold on the guest's agent at connect (115% pre-auth, the petrol-card model), meters bytes, and at disconnect settles the hold exactly: actual usage pays the operator (loopd settle to_agent_id = beneficiary), the remainder refunds to the guest, and the receipt is anchored via qalpipe. The mesh (qalchat) carries every message. Nobody needs a bank account, a contract, or even working internet at the far end — prepaid escrow + local mesh is enough.

                 THE VILLAGE CIRCULAR ECONOMY
                 ============================

      AU$ (Starlink bill)                QALS (prepaid credit)
           ▲                                     │
           │ payer buys                          │ guests prepay
           │ backhaul                            ▼
   ┌─────────────────┐      sells bytes      ┌─────────────────┐
   │  OPERATOR       │◄──────────────────────│   GUESTS        │
   │ "village        │   2c/MB (their price) │ own loopd agents│
   │  Starlink"      │──────────────────────►│ escrowed holds  │
   └────────┬────────┘  metered internet    └────────┬────────┘
            │ earns QALS                             │ qalchat DMs
            │                                        │ (welcome/warn/receipt)
            ▼                                        ▼
   ┌─────────────────────────────────────────────────────────┐
   │        qalchat MESH — ALWAYS FREE (zero-rated)          │
   │  encrypted envelopes via the local relay :8830          │
   │  the village can ALWAYS talk — even at 0 quota          │
   └─────────────────────────────────────────────────────────┘
            │ conversation Merkle roots
            ▼
        qalpipe anchor → the chain remembers (receipts, proofs)

The loop closes: guest prepay funds the operator; the operator's Starlink revenue keeps the mesh's backhaul alive; the mesh keeps the village on. The network funds its own backhaul from the demand on it.

Pricing freedom — operators set the rate, the network takes ZERO

Per content/ground_truth.md (F16): chain fees are zero — the network never charges. A hotspot is the same doctrine at the access layer:

The zero-rated messaging principle

The expensive thing is the backhaul (Starlink quota). The cheap thing is local wireless. So the gateway splits traffic into two classes at the accounting layer (v0) and the firewall layer (production):

class what priced
internet general traffic → paid uplink operator's c/MB rate
mesh qalchat envelopes to/from the local relay always 0c — allowlisted

The gateway's own welcome/warning/receipt DMs are mesh-class by definition: the gateway is a qalchat client (subprocess to qalchat.py, the gmux-bridge pattern — loopd and qalchat files untouched, HTTP/CLI only). Even a guest at 0 balance keeps encrypted messaging, group chat, and payment messages — a village that can't pay for YouTube can still call the doctor. That is the "it would also enable communications" clause, taken literally and made non-negotiable: GET /mesh-status{"messaging": "always on"}.

The money path (exact, tested)

  1. connect — guest posts prepay + payment pre-auth (agent_id, api_secret, the paper stand-in for card/NFC pre-auth). Gateway places a loopd hold on the guest's agent: 500c prepay → 575c escrowed (115%), debited at placement.
  2. meter — integer math only: usage_cents = bytes × rate // 1_000_000. Warning DM at warn_pct (default 80) of prepay. At 0 remaining the session auto-cuts and settles clamped at the prepay — the guest never owes more than they committed.
  3. disconnect — loopd settle at actual usage, round_to_cents: 0 (exact, no doof rounding on wifi), to_agent_id pays the operator. Receipt JSON → qalpipe anchor (purpose: wifi:{op}:{guest}:{mb}MB) → receipt DM.

Proven line from test_qalhotspot.sh:

guest paid 300c (150MB × 2c) · guest 425→700c · operator 0→300c
refund itemised: 200c unused + 75c margin = 275c · network fee: 0c

Honest edges (documented, paper phase): guest pre-auth secrets live in gateway memory only, never at rest; loopd holds expire after 5 min (production adds keepalive/re-auth); the per-user daily cap is enforced per-session in v0.

API

route what
POST /register operator: {handle, name?, price_cents_per_mb?=2, warn_pct?=80, daily_cap_mb?=500} — handle must own a qalchat identity (beneficiary agent auto-looked-up)
POST /connect {operator, guest_handle, prepay_cents, agent_id, api_secret} → session token + welcome DM — or {mode:"autopay", auth_token, operator, ts?} (the auto-pay key IS the connection)
POST /vouchers/create operator-auth: {handle, mb_limit, rate_note?, agent_id, api_secret}VCH-XXXX-XXXX-XXXX + operator-funded escrow
POST /vouchers/redeem {code, guest_handle} → session on the voucher's escrow; single-use (replay 409, unknown 404)
POST /autopay/authorize guest: {guest_handle, agent_id, secret, max_cents_per_day, operator}ap_… key (secret memory-only)
GET /voucher/{code}/qr · GET /autopay/{token}/qr inline-SVG QR (zero-dep encoder qrsvg.py) for the physical card / phone
GET /autopay/{token} spend status {spent_today_cents, remaining_today_cents, active_hold_cents}
POST /meter/{token} {bytes, class?: internet\|mesh} → usage, warning, auto-cut
POST /disconnect/{token} settle exact → refund → anchor → receipt DM
GET /session/{token} GET /mesh-status GET /report[/{op}] GET /health observability

The voucher key system — two ways in, one escrow engine

Founder ask: "someone pays in cash and the owner gives them a key which they use to put in the Qals app … or they get paid automatically as it goes in Qals." Both modes shipped (2026-09-13), proven end-to-end with exact math in test_vouchers.sh (102 assertions, green).

MODE A — prepaid vouchers (cash → key → data)

 walk-in guest          operator (shopop)              the ledger (loopd)
 ─────────────         ─────────────────              ───────────────────
  pays AU$2 cash ────► POST /vouchers/create
   at the door          {mb_limit:100} ─────────────► hold 229c on the
                        ◄─ VCH-K4M2-8XQZ-1P7D          OPERATOR's agent
                        GET /voucher/CODE/qr           (int(200×1.15))
                        ◄─ SVG QR → PRINT ON A CARD
  types code into
  the Qals app ──────► POST /vouchers/redeem
                        {code, guest_handle} ───────► session on the
                        ◄─ wh_… token, 100MB           voucher's escrow
                          @ 2c/MB                      (guest needs NO
  browses 80MB ──────► POST /meter … → 160c used       loopd balance)
  disconnect ────────► POST /disconnect
                        ◄─ charged 160c, refund 69c → receipt anchored
                           (qalpipe PASS) → receipt DM

The card is a funded promise: the operator escrows the face value upfront (cash already changed hands), the code is single-use, and settlement unwinds to the operator's own agent — net-zero Qals movement, pure accounting. The guest touches money zero times.

Exact-math line from the suite:

cash AU$2 in hand · 80MB → 160c of the 200c card burned
operator escrow 771→1000c net-zero (69c released + 160c self-settled)
walkin loopd balance: 0 before, 0 after · network fee: 0c

MODE B — auto-pay keys (pay-as-you-go, billed in Qals)

 subscriber             gateway                        the ledger (loopd)
 ─────────────         ─────────────────              ───────────────────
  POST /autopay/
  authorize ─────────► ap_<30-hex> key ◄── shows QR at the door
  (cap AU$5/day)        GET /autopay/{tok}/qr
  POST /connect ─────► hold = remaining daily budget
  {mode:"autopay"}       (500c → 575c escrowed)
  session 1: 100MB ──► settle 200c ────────────────► operator +200c
  session 2: 50MB  ──► settle 100c (day total 300 < 500)
  session 3: 250MB ──► AUTO-CUT at remaining 200c — cap enforced MID-SESSION
  4th connect      ──► 402 daily cap reached
  next day (ts)    ──► fresh 500c budget (resets at midnight; ts-mockable)

Exact-math line from the suite:

day total 200c + 100c + 200c = 500c = AU$5.00 cap — enforced mid-session
subguest 2000→1500c · operator +500c across 3 sessions · network fee: 0c

The key IS the connection: authorize once, connect forever (within the cap), billed automatically in Qals. Secrets stay memory-only — never at rest.

The cash-card model (physical ↔ digital)

A voucher card is physical money with a digital twin: the QR on the card is the escrow key. Mint it, sell it for cash, scratch it (redeem), done — no accounts, no onboarding, no smartphone required beyond the Qals app to type the code. The qrsvg.py encoder is zero-dep (byte mode, versions 1-5, ECC-L, penalty-optimal masking) and the suite decodes its SVG output with an independent zxing reader — byte-exact.

Office pattern (same keys, different meter)

Swap "MB" for "GPU-minutes" and the whole system ports to compute: a visitor buys a cash card at the office kiosk → redeems it against the qmarket pool for N GPU-minutes (voucher mode); a resident agent holds an auto-pay key capped at AU$/day → metered agent spend with the same mid-session cap enforcement (autopay mode). Same escrow engine, same exact settlement, same anchored receipts — 07_compute_marketplace is the natural next stop.

Production path (v1+)

  1. Real metering — nftables per-station counters (nft add rule … counter per MAC/SSID client), gateway reads deltas; the /meter accounting interface stays identical, so v0 clients keep working.
  2. Captive portal — TLS on :8844, the welcome DM is the portal (guest needs only qalchat); web fallback page for bootstrap.
  3. Voucher cards at scale — ✅ paper phase DONE (vouchers, auto-pay keys, QRs, daily caps); production adds batch card printing (the SVG QR endpoint feeds a print sheet), voucher expiry + transfer, and a card-revocation list for lost cards.
  4. Hold keepalive — re-auth before loopd's 5-min TTL for long sessions; ✅ cross-session daily caps now enforced for auto-pay keys.
  5. Roaming — operators cross-accept each other's guests (settle operator-to-operator via to_agent_id chains); the pricing-freedom rule makes bilateral rates trivial.

Next step: the carrier plugin (from content/mesh-pipelines.md)

The hotspot proves the HTTP relay carrier end-to-end with money. The same gateway pattern generalises: a carrier is just put(envelope)/poll(handle), and the mesh-pipelines doc sketches lora (a 32-byte Merkle anchor crosses a mesh in one LoRa frame — receipts and conversation proofs ride for free), dtn, ipfs, ble. A village whose only link is one Starlink dish plus a LoRa ring is exactly the deployment this is built for: metered backhaul for the internet class, free mesh for the messaging class, anchors whenever any node touches the chain. LoRa carrier = the next build.

Runbook

python3 qalhotspot/qalhotspot.py            # gateway on :8844 (LAN-facing)
# needs: loopd :8823, qalchat relay :8830 (and qalnet :9000 for anchoring)
# env:   QALHOTSPOT_LOOPD, QALCHAT_RELAY, QALCHAT_DIR (defaults match the stack)
#
# suites (serialised via /tmp lock; each archives data first — never deletes):
bash qalhotspot/test_qalhotspot.sh          # classic prepaid sessions (11 sections)
bash qalhotspot/test_vouchers.sh            # voucher keys: MODE A + MODE B (102 asserts)

systemd unit pattern (user unit — the stack runs as the operator's user; do not install without review):

# ~/.config/systemd/user/qalhotspot.service   (generated pattern — paper phase)
[Unit]
Description=qalhotspot — metered-access gateway (village Starlink share)
After=network-online.target
Wants=network-online.target

[Service]
WorkingDirectory=%h/projects/GLM_projects/crypto_currency/qalhotspot
ExecStart=/usr/bin/python3 %h/projects/GLM_projects/crypto_currency/qalhotspot/qalhotspot.py
Environment=QALCHAT_RELAY=http://127.0.0.1:8830
Environment=QALHOTSPOT_LOOPD=http://127.0.0.1:8823
Restart=on-failure
RestartSec=3

[Install]
WantedBy=default.target

Registration elsewhere in the repo: DIRECTORY.md (dir + port 8844), scripts/build_docs.py PAGES (wiki: Metered WiFi), website/ui-codex/hub.html (System card). Data lives in qalhotspot/data/ — archived on test runs, never deleted.