Skip to content

Repository files navigation

TollGate — OpenWRT Router Payment Gateway

tollgate-logo

TollGate turns an OpenWRT router into a Cashu-powered payment gateway for internet access. Customers pay in sats (time- or data-based); the router gates network access and sweeps balances to configured Lightning addresses. The same binary also acts as a client to upstream TollGates, so a router can buy internet from another TollGate and resell it automatically.

Mirror, CI and releases on Nostr (ngit)

This repository is mirrored to ngit — git hosting and CI on Nostr. The mirror carries the default branch, every pr/<slug> branch and every tag, so the whole repository is reachable without GitHub.

Clone it over Nostr (the nostr:// remote speaks the ngit protocol):

git clone nostr://npub1x677kgulh6lqaa8e658f9ts4s0x692wgrhcptw535877wfkuwnsqztaquc/relay.ngit.dev/tollgate-module-basic-go
git clone https://relay.ngit.dev/npub1x677kgulh6lqaa8e658f9ts4s0x692wgrhcptw535877wfkuwnsqztaquc/tollgate-module-basic-go.git
git clone https://gitnostr.com/npub1x677kgulh6lqaa8e658f9ts4s0x692wgrhcptw535877wfkuwnsqztaquc/tollgate-module-basic-go.git

Build artifacts (Nostr, not GitHub releases)

Artifacts are published as NIP-94 kind 1063 events, not attached to GitHub releases. Each event carries one url tag per Blossom mirror and an x tag with the file's sha256 — download from any mirror and verify the hash.

No kind-1063 artifact is announced for this repo yet, so here is the exact query:

nak req -k 1063 -t "A=30617:36bdeb239fbebe0ef4f9d50e92ae1583cda2a9c81df015ba91a1fde726dc74e0:tollgate-module-basic-go" wss://relay.ngit.dev   # url + x tags

When one appears, download from any url and prove the x sha256 before use:

echo "<x-tag-sha256>  artifact" | sha256sum -c -

Generated by ngit-readme-section.sh from the live kind-30617 announcement 1eed6f1fcefb335ae49f9a6fca1f4feaea7620df2cd6c183bf563abd04b59953 (created 1788179627). Doc not be hand-edited: re-run the generator.

Core technologies

  • Cashu — ecash over mints (cashu.space)
  • Nostr — identities, discovery, advertisements, payments (NIP-01)
  • Bitcoin Lightning — payout settlement
  • NoDogSplash (ndsctl) — the captive portal / gate that authorizes MACs

Wire protocol docs live in the canonical spec repo: OpenTollGate/tollgate. The pre-built captive-portal site that ships in packaging/files/tollgate-captive-portal-site/ is generated from OpenTollGate/tollgate-captive-portal-site.

Feature highlights

  • Accept Cashu tokens for internet access, by time or by data.
  • Automatic Lightning payouts split across any number of profit-share identities.
  • Upstream autopay — a TollGate can detect an upstream TollGate and purchase access on your behalf while reselling to its own customers (reseller mode).
  • Per-mint pricing, trust allow/blocklists, configurable session increments and renewal thresholds.
  • No accounts and no user identifiers: a customer is identified by the MAC address their device uses, always resolved from the request's socket and never from a value the client sends, and an address whose client left is deauthorised instead of staying open (what that means for a customer who changes their address).

Modules

Source lives under src/. Go tooling runs from there (cd src && go build ./..., cd src && go test ./...).

Module Role
merchant Prices advertisements, validates incoming Cashu payments, and hands off started sessions to the session manager. Also drives Lightning payouts. See docs/merchant.md.
upstream_session_manager Owns the customer session lifecycle on this router — creates usage trackers (time or bytes), instructs the Valve when to open/close, handles renewal near limit. Formerly the chandler package. See docs/upstream_session_manager.md.
upstream_detector Probes WAN interfaces to discover an upstream TollGate, decides whether to buy from it, and coordinates the reseller flow. Formerly crowsnest. The cross-component flow is documented under "Cross-component flow" in docs/upstream_session_manager.md.
wireless_gateway_manager Wi-Fi gateway selection, connection/reconnection, scanning, reseller-mode network orchestration. See docs/wireless_gateway_manager.md.
valve Thin wrapper over ndsctl that opens/closes gates and authorizes/deauthorizes MACs.
config_manager Schema, loading, migrations, validation, backups of /etc/tollgate/config.json.
tollwallet Cashu wallet operations (mint client, balance tracking, melt).
lightning LNURL-p / Lightning address resolution and invoice fetching for payouts.
cli tollgate CLI for service control, wallet, private network, upstream Wi-Fi, config, and health. Entry point: src/cmd/tollgate-cli. See docs/operator-guide.md.
tollgate_protocol Wire-type definitions shared across modules.

Installation

Build artifacts produced by the CI matrix in .github/workflows/build-package.yml target both apk (OpenWrt 25.x) and ipk (OpenWrt ≤24.10) formats.

On OpenWrt 25.x:

apk add --allow-untrusted /tmp/tollgate-wrt-<version>.apk

On OpenWrt 24.10.x and earlier:

opkg install /tmp/tollgate-wrt_<version>_<arch>.ipk

For local packaging experiments use scripts/build-sdk-package.sh. It cross-compiles the target binaries locally, stages the canonical packaging/ recipe into the OpenWrt SDK, and can produce either apk or ipk artifacts.

Supported devices

Whether a package exists for a router at all is decided by the CI build matrix in .github/workflows/build-package.yml: a router is covered when its OpenWrt target and DISTRIB_ARCH match one of the rows in that matrix (check them with ubus call system board, or cat /etc/openwrt_release). That is the machine-true definition of "supported" — there is no per-model board list in the package.

Cudy WR3000 v1 (MediaTek MT7981B, 256 MB RAM) matches the matrix on mediatek/filogic / aarch64_cortex-a53, board name cudy,wr3000-v1, so the arm64 package built for that row installs on it. It was exercised on real hardware against mainline OpenWrt 25.12.5 (r33051-f5dae5ece4): the full dependency closure (37 packages, including nodogsplash 5.0.2-r2 and its kmods) installs and nodogsplash runs with the module's keepalive contract live (trusted MAC plus allow tcp port 22).

Caveat — 16 MB of flash, and the compressed variant that nonetheless fits. The WR3000 v1 has 16 MB of SPI-NOR, which is ~15.1 MB of firmware area and leaves roughly 4.6 MB of free overlay. The default build does not fit: its payload is ~20 MB uncompressed (usr/bin/tollgate-wrt 12,361,280 B plus usr/bin/tollgate 7,373,632 B) / ~8.5 MB compressed, apk add fails with failed to extract usr/bin/tollgate-wrt: No space left on device, and a custom ImageBuilder image does not fit either. The upx-ultra-brute variant that this repo's CI already builds for aarch64_cortex-a53 does fit: it shrinks the payload to 5.34 MiB (usr/bin/tollgate-wrt 3,389.5 KiB plus usr/bin/tollgate 1,823.8 KiB, plus ~256 KiB of config and captive-portal files). A real WR3000 v1 was taken through it on 2026-09-27 — installed from the compressed .apk, rebooted, and came back with tollgate-wrt running and no volatile helper, so a persistent install is possible on a 16 MB device with this variant. Two notes for such devices:

  • the 1.78 MiB tollgate CLI is only needed for provisioning, so on this class of device it can be dropped after the first boot to leave room for the nodogsplash dependency closure;
  • install the dependency closure in one apk add transaction. apk add --force-non-repository <file> performs a world sync and removes packages that were previously installed from files, which silently takes nodogsplash back out.

A volatile (tmpfs) install remains the fallback for bench work that cannot free the space. (Measured with the upx-ultra-brute dev-channel build main.200.4469994, sha256 29bb68adbb26e67c…; publishing that variant in the feed release is tracked with the packaging feed, not here.)

COMFAST CF-WR632AX (MediaTek MT7981-class SoC, compact Wi-Fi 6 travel router) matches the matrix on the same mediatek/filogic / aarch64_cortex-a53 row as the WR3000 v1, so the arm64 package built for that row installs on it unchanged — no matrix row was added for it. OpenWrt supports it officially since 25.12.0 (upstream keeps target/linux/mediatek/dts/mt7981b-comfast-cf-wr632ax.dts and publishes the image as openwrt-<version>-mediatek-filogic-comfast_cf-wr632ax-*; see the device page).

No flash-capacity caveat. The CF-WR632AX carries 128 MiB of SPI NAND (Winbond W25N01GV), unlike the 16 MB WR3000 v1 above, so the default build has room and the upx-ultra-brute variant is not required for it.

Requires OpenWrt 25.12.5 or newer with the OpenWrt U-Boot layout. A memory-speed stability issue affected that layout in 25.12.0–25.12.4 and was fixed in 25.12.5 (upstream PRs #22929 / #23416); the stock layout is unaffected. Use 25.12.5 or newer.

Not yet exercised on real hardware. Unlike the WR3000 v1 above, no CF-WR632AX has been in hand: this paragraph rests on upstream OpenWrt support and the shared target/architecture row, not on a measured result on this device. A tester with the unit is being lined up; the text here will be replaced with results when there are some. There is no persistent-install caveat for this device — the only known gap is the compressed-variant publication one, which applies to aarch64_cortex-a53 generally (only default builds are released) and is tracked with the packaging feed, not here.

Configuration

TollGate writes a default /etc/tollgate/config.json on first boot. The current schema version is v0.0.8. An abridged example:

{
  "config_version": "v0.0.8",
  "log_level": "info",
  "metric": "bytes",
  "step_size": 22020096,
  "margin": 0.1,
  "show_setup": true,
  "reseller_mode": false,
  "accepted_mints": [
    {
      "url": "https://mint.coinos.io",
      "min_balance": 64,
      "balance_tolerance_percent": 10,
      "payout_interval_seconds": 60,
      "min_payout_amount": 128,
      "price_per_step": 1,
      "price_unit": "sats",
      "purchase_min_steps": 0
    }
  ],
  "profit_share": [
    { "factor": 0.79, "identity": "owner" },
    { "factor": 0.07, "identity": "c08r4d0r" },
    { "factor": 0.07, "identity": "amperstrand" },
    { "factor": 0.07, "identity": "origami74" }
  ],
  "upstream_detector": {
    "probe_timeout": "10s",
    "probe_retry_count": 3,
    "probe_retry_delay": "2s",
    "require_valid_signature": true,
    "ignore_interfaces": ["lo", "docker0", "br-lan", "hostap0"],
    "only_interfaces": [],
    "discovery_timeout": "300s"
  },
  "upstream_session_manager": {
    "max_price_per_millisecond": 0.002777777778,
    "max_price_per_byte": 0.00003725782414,
    "trust": {
      "default_policy": "trust_all",
      "allowlist": [],
      "blocklist": []
    },
    "sessions": {
      "preferred_session_increments_milliseconds": 60000,
      "preferred_session_increments_bytes": 2500000000,
      "millisecond_renewal_offset": 10000,
      "bytes_renewal_offset": 1225000000
    },
    "usage_tracking": {
      "data_monitoring_interval": "500ms"
    }
  }
}

Key fields:

  • metric — "bytes" sells data, "milliseconds" sells time. step_size is the unit.
  • accepted_mints[*] — per-mint URL, pricing, and payout thresholds. Multiple mints are supported; the first mint holding sufficient balance wins on payout.
  • profit_share[*] — each entry references an identity that maps to an entry in /etc/tollgate/identities.json; factor values should sum to 1.0.
  • reseller_mode — when true, this router actively purchases from an upstream TollGate and resells to its own customers.
  • upstream_detector / upstream_session_manager — control the client side (buying from an upstream).

ignore_interfaces and only_interfaces gate which WAN-side interfaces are probed. ignore_interfaces typically needs to list any wireless interfaces the router itself serves on to prevent self-probing.

Testing

Unit tests, from the src/ directory:

cd src && go test -tags testenv ./...

The testenv build tag provisions a hermetic temp config dir so the main package's init() does not depend on /etc/tollgate/config.json — letting the suite run off-router (CI, dev machines). The tag is a no-op for subpackages.

A single package:

cd src && go test ./upstream_session_manager/...

End-to-end tests live in tests/ and use pytest against real router hardware:

File Purpose
tests/test_copy_images.py Transfer firmware to target routers.
tests/test_install_images.py Flash firmware (destructive).
tests/test_install_packages.py Install the tollgate-wrt package on a running router.
tests/test_network_configuration.py Verify upstream gateway connectivity.
tests/test_ecash_payment.py End-to-end buy-internet flow.
tests/test_ecash_functionality.py Wallet-level Cashu operations.
tests/test_data_measurement.py Byte accounting across a data-metered session.
tests/test_teardown.py Reset routers between runs.

See tests/README.md for how to wire up the test fleet.

Documentation

Design and protocol docs live under docs/:

License

GPL-3.0 — see LICENSE.

About

Basic tollgate implementation example

Topics

Resources

Contributing

Security policy

Stars

12 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages