Pay-per-use WireGuard VPN gateway for autonomous agents. No subscriptions, no pre-auth — each access is gated by a USDC micropayment verified on Base.
Flow: agent hits /vpn → gets 402 → pays 0.01 USDC on Base Sepolia via Openfort → retries with tx hash → receives WireGuard peer config valid for 1 hour.
Tollgate is built for programmatic, pay-per-use VPN access — no accounts, no subscriptions, no human in the loop. The x402 protocol makes it native to any agent or automated system that can send a USDC transaction.
| Use case | Why x402 fits |
|---|---|
| Autonomous AI agents | An agent hits a geo-restricted API, receives a 402, pays, and retries — entirely without human intervention |
| CI/CD pipelines | Build jobs pay only for the minutes they need VPN access; no standing subscription tied to infrastructure |
| Web scraping / data collection | Each agent session gets a fresh peer IP; pay per crawl run rather than managing a pool of proxies |
| Multi-agent systems | Many agents coordinate independently — each pays for its own session with no shared credentials or API keys to rotate |
| Privacy-sensitive upstream calls | Agents mask the operator's origin IP when calling third-party APIs, without exposing a persistent VPN identity |
| IoT / embedded devices | Low-cost devices that need occasional secure tunneling; per-use micropayments are more economical than monthly plans |
| Temporary developer access | A dev needs VPN access for one hour during an incident — pays $0.01 instead of provisioning a full subscription |
Tollgate implements the x402 payment protocol. When an agent hits GET /vpn without a payment header, the server returns a 402 Payment Required with a JSON body describing exactly what to pay, to whom, and on which network. The agent pays, then retries with X-Payment: <txHash>. The server verifies on-chain before provisioning access.
The agent script uses Openfort backend wallets to send USDC programmatically — no private keys in env vars, no manual signing. Openfort handles key custody, gas sponsorship, and ERC-4337 account abstraction under the hood.
The server uses Alchemy to fetch the transaction receipt from Base Sepolia and parses the ERC-20 Transfer event logs to verify:
- Transfer was sent to
PAYMENT_RECIPIENT_ADDRESS - Amount ≥
PAYMENT_AMOUNT_USDC - USDC contract matches
USDC_CONTRACT_ADDRESS
Because Openfort uses a bundler/paymaster, there can be a few seconds of indexing lag between when the UserOperation is confirmed and when Alchemy's RPC node returns the receipt. The server retries up to 4 times with 5s intervals before rejecting.
Each tx hash is stored in Redis with a 7-day TTL. Submitting the same hash twice returns a 402.
On successful payment, the server generates a unique keypair via the wg CLI, allocates an IP from 10.0.0.10–10.0.0.209, adds the peer to the wg0 interface, stores the session in Redis, and returns the client config string.
Currently running on Base Sepolia (testnet) — chainId 84532, USDC 0x036CbD53842c5426634e7929541eC2318f3dCF7e.
Mainnet constants are commented in scripts/agent-sim.ts and src/services/payment.ts — swap them in when a live Openfort key is available.
- Node.js (ESM, LTS) · Express · TypeScript · tsx
- ethers.js v6 · ioredis · pino · express-rate-limit
- WireGuard (managed via
wgCLI, no shell —execFileSync) - Openfort (
@openfort/openfort-node) — backend wallet for agent payments - Base Sepolia (testnet) · USDC
0x036CbD53842c5426634e7929541eC2318f3dCF7e
- Node.js 20+
- Redis running locally (
brew services start redison macOS) - An Alchemy API key (Base Sepolia)
- A Base wallet address to receive payments
- An Openfort account with a backend wallet configured
npm installcp .env.example .envFill in .env. Minimum required to start:
PORT=3002
REDIS_URL=redis://127.0.0.1:6379
ALCHEMY_API_KEY=your_key_here
PAYMENT_RECIPIENT_ADDRESS=0xYourWalletAddress
BASE_CHAIN_ID=84532
USDC_CONTRACT_ADDRESS=0x036CbD53842c5426634e7929541eC2318f3dCF7e
npm start # production entry point (src/main.ts)
npm run dev # auto-restarts on file changes
npm run typecheck # type check without runningnpm test # runs all tests (uses PAYMENT_STUB and WG_STUB — no Alchemy or wg needed)Simulates an autonomous agent paying USDC via Openfort and receiving a WireGuard config.
- Tollgate server running (
npm start) - An Openfort backend wallet account funded with:
- Test ETH on Base Sepolia (gas) — Alchemy faucet
- Test USDC on Base Sepolia — Circle faucet
Create a backend wallet account (one-time setup):
node --input-type=module << 'EOF'
import 'dotenv/config';
import Openfort from '@openfort/openfort-node';
const of = new Openfort(process.env.OPENFORT_SECRET_KEY, { walletSecret: process.env.OPENFORT_WALLET_SECRET });
const account = await of.accounts.evm.backend.create({ chainId: 84532 });
console.log('OPENFORT_ACCOUNT_ID=' + account.id);
console.log('Wallet address: ' + account.address);
EOFFund the printed wallet address via the faucets above, then add the account ID to .env.
Add to .env:
OPENFORT_SECRET_KEY=sk_...
OPENFORT_WALLET_SECRET=...
OPENFORT_ACCOUNT_ID=acc_...
TOLLGATE_URL=http://your-server:3002
node --import tsx/esm scripts/agent-sim.tsExpected output:
[1] GET http://your-server:3002/vpn
[1] 402 received — payment required: 0.01 USDC to 0x...
[2] Checking USDC contract registration in Openfort...
[2] USDC already registered: con_...
[3] Sending 0.01 USDC → 0x... via Openfort...
[3] Confirmed — tx: 0x...
[4] GET http://your-server:3002/vpn with X-Payment: 0x...
[4] 200 received — WireGuard config provisioned
Session ID : <uuid>
Allowed IP : 10.0.0.x
Expires in : 3600s
[5] Tollgate access provisioned.
WireGuard config : /tmp/tollgate-client.conf
Receipt : /tmp/tollgate-receipt.json
Connect with: sudo wg-quick up /tmp/tollgate-client.conf
── Openfort ────────────────────────────────────────────────────────────
Intent ID : tin_...
Account : acc_...
sudo wg-quick up /tmp/tollgate-client.conf
# Disconnect
sudo wg-quick down /tmp/tollgate-client.confnode --import tsx/esm scripts/expire-test.ts <sessionId>sudo apt update && sudo apt install wireguard redis-server -y
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
npm install -g pm2# Generate server keypair
wg genkey | tee /etc/wireguard/server_private.key | wg pubkey > /etc/wireguard/server_public.key
# Create interface config (replace YOUR_PRIVATE_KEY and eth0 if needed)
cat > /etc/wireguard/wg0.conf << EOF
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = YOUR_PRIVATE_KEY
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
EOF
# Enable IP forwarding
echo "net.ipv4.ip_forward=1" | tee -a /etc/sysctl.conf && sysctl -p
# Start WireGuard
systemctl enable wg-quick@wg0 && systemctl start wg-quick@wg0# Copy project to server
scp -r /path/to/tollgate user@your-server:~/tollgate
# On the server
cd ~/tollgate
cp .env.example .env && nano .env # fill in all values
npm install
pm2 start src/main.ts --name tollgate --cwd ~/tollgate --interpreter tsx
pm2 save
pm2 startupufw allow 22/tcp
ufw allow 3002/tcp
ufw allow 51820/udp
ufw enableBefore going live, make sure these are set in your production .env:
| Setting | Production value | Why |
|---|---|---|
STRICT_REPLAY_CHECK |
true |
Prevents replay attacks if Redis restarts — fails closed (503) instead of open |
PUBLIC_HOST |
https://your-domain.com |
Ensures the 402 resource URL is correct behind a reverse proxy |
PAYMENT_AMOUNT_USDC |
your chosen price | Default 0.01 — adjust for mainnet economics |
Additional hardening in place out of the box:
- Rate limiting —
/vpnis limited to 20 requests per IP per minute viaexpress-rate-limit. Tunemaxinsrc/server.tsfor your expected traffic. - No shell execution — all
wgCLI calls useexecFileSyncwith explicit argument arrays, eliminating shell injection surface. - API key redaction — Alchemy RPC errors are sanitised before being returned to clients; the API key is never exposed in 402 responses.
- Peer lifecycle safety — if Redis is unavailable when storing a new session, the WireGuard peer is rolled back (
wg set … remove) before the 500 is returned, preventing zombie peers.
flowchart TD
A([Agent]) --> B[GET /vpn]
B --> C{Rate limit\nexceeded?}
C -->|Yes| D[429 Too Many Requests]
C -->|No| E{X-Payment\nheader?}
E -->|Missing| F[402 + payment JSON\nx402 middleware]
E -->|Present txHash| G[payment.ts\nAlchemy → Base Sepolia\nretry up to 4×5s]
G -->|Invalid| H[402 + reason]
G -->|Valid| I[replay.ts → Redis\nreplay check + mark used]
I -->|Duplicate TX| J[402 Already used]
I -->|Fresh TX| K[wireguard.ts\ngenkey · allocateIP · addPeer]
K --> L[session.ts → Redis\nTTL = SESSION_TTL_SECONDS]
L --> M[200 + WireGuard config]
flowchart LR
subgraph L1[Layer 1 — Network]
A[UFW Firewall\nports 22 · 3002 · 51820]
end
subgraph L2[Layer 2 — Rate Limiting]
B[express-rate-limit\n20 req / IP / min]
end
subgraph L3[Layer 3 — Payment Gate]
C[x402 middleware\n402 challenge]
end
subgraph L4[Layer 4 — On-chain Verification]
D[Alchemy RPC\nTransfer event · amount · recipient]
end
subgraph L5[Layer 5 — Replay Protection]
E[Redis\nTX hash · 7-day TTL]
end
subgraph L6[Layer 6 — Peer Safety]
F[Rollback on Redis failure\nno zombie peers]
end
A --> B --> C --> D --> E --> F
| Variable | Description |
|---|---|
BASE_CHAIN_ID |
Chain ID for payment verification. Default 84532 (Base Sepolia). Set to 8453 for mainnet. |
USDC_CONTRACT_ADDRESS |
USDC contract address for the configured chain. Default 0x036CbD... (Base Sepolia). |
PAYMENT_RECIPIENT_ADDRESS |
Wallet address that receives USDC payments. |
PAYMENT_AMOUNT_USDC |
Price per access in USDC. Default 0.01. |
SESSION_TTL_SECONDS |
How long a provisioned WireGuard peer stays active. Default 3600 (1 hour). |
STRICT_REPLAY_CHECK |
true = fail closed if Redis is down (prevents replay attacks). Default false. Set to true in production. |
PUBLIC_HOST |
Set to your public URL when behind a reverse proxy — used in the 402 resource field. |
flowchart LR
D1([Day 1 ✓\nCore Gateway]) --> D2[Day 2\nSession Lifecycle]
D2 --> a[Session expiry daemon]
D2 --> b[WireGuard peer cleanup]
D2 --> c[Rate limiting per wallet]
D2 --> d[IP pool hardening]
D2 --> e[Status dashboard]
D2 --> D3[Day 3\nMainnet & Scale]
D3 --> f[Swap to Base mainnet]
D3 --> g[Live Openfort key]
D3 --> h[WireGuard concurrency queue]
D3 --> i[Reverse proxy + TLS]