- Website: tollgate.me
- Release manager (firmware + package builds): releases.tollgate.me
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.
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- Browse / open PRs: https://gitworkshop.dev/npub1xh6njjxpze2vwvn0d863dmrxpll0ds04k38t0wqrg5fcr6nf3ursk3xqw3/git.orangesync.tech/tollgate-module-basic-go
- CI: every push is built by ngit-CI — the workflows run from the ngit side, so the mirror above is the build of record.
- Announcement (source of truth for the URLs above): kind
30617,d=tollgate-module-basic-go, by36bdeb239fbebe0ef4f9d50e92ae1583cda2a9c81df015ba91a1fde726dc74e0, relays:wss://relay.ngit.dev wss://gitnostr.com
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 tagsWhen 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.
- 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.
- 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).
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. |
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>.apkOn OpenWrt 24.10.x and earlier:
opkg install /tmp/tollgate-wrt_<version>_<arch>.ipkFor 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.
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
tollgateCLI is only needed for provisioning, so on this class of device it can be dropped after the first boot to leave room for thenodogsplashdependency closure; - install the dependency closure in one
apk addtransaction.apk add --force-non-repository <file>performs a world sync and removes packages that were previously installed from files, which silently takesnodogsplashback 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.
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_sizeis 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 anidentitythat maps to an entry in/etc/tollgate/identities.json;factorvalues 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.
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.
Design and protocol docs live under docs/:
- docs/operator-guide.md — practical CLI reference for router operators, including client identity, MAC addresses, and what changing one does
- docs/rc-tester-guide.md — install/upgrade/rollback/report guide for the alpha release candidate
- docs/tester-intake.md — the single intake channel, the report template, and the triage/severity rules for alpha testers
- docs/merchant.md
- docs/upstream_session_manager.md — module internals + end-to-end cross-component flow
- docs/data-session-management.md
- docs/wireless_gateway_manager.md
GPL-3.0 — see LICENSE.
