Get SatGate running and see the Economic Firewall in action. Three policies, three curl commands, sixty seconds.
- macOS/Linux (or Windows WSL)
- curl (for testing)
Option A: Pre-built Binary (fastest)
# macOS (Apple Silicon)
curl -L https://github.com/satgate-io/satgate/releases/latest/download/satgate-darwin-arm64 -o satgate
# macOS (Intel)
curl -L https://github.com/satgate-io/satgate/releases/latest/download/satgate-darwin-amd64 -o satgate
# Linux (x86_64)
curl -L https://github.com/satgate-io/satgate/releases/latest/download/satgate-linux-amd64 -o satgate
chmod +x satgateOption B: Build from Source
git clone https://github.com/satgate-io/satgate.git
cd satgate
go build -o satgate ./cmd/satgateOption C: Docker
git clone https://github.com/satgate-io/satgate.git
cd satgate
docker compose up -d
# Gateway is now running on http://localhost:8080# Set an admin token (or one will be auto-generated)
export ADMIN_TOKEN=my-secret-admin-token
# Use the mock Lightning provider for this demo
export LIGHTNING_BACKEND=mock
# Start with the example config
./satgate --config examples/gateway.yamlYou should see:
INF Starting SatGate OSS Gateway version=dev
WRN CAPABILITY_ROOT_KEY not set - using auto-generated key (demo mode)
INF Macaroon service initialized
INF Governance service initialized (in-memory)
INF Lightning provider initialized provider=mock
INF Routes configured routes=8
INF Gateway listening listen=:8080
INF Supported policies: public, capability, l402
Note: Demo mode auto-generates a root key. Tokens won't persist across restarts. Set
CAPABILITY_ROOT_KEYfor production (see Configuration).
Open a new terminal and try these:
curl http://localhost:8080/health{"status":"healthy","service":"satgate-oss","routes":8}No token needed. Public routes are explicitly opted out of protection.
Without a token — blocked:
curl http://localhost:8080/protected/getAuthorization required
Mint a capability token:
curl -s -X POST http://localhost:8080/api/capability/mint \
-H "X-Admin-Token: my-secret-admin-token" \
-H "Content-Type: application/json" \
-d '{"scope": "api:read", "duration": "1h"}'{
"token": "eyJ2IjoxLCJs...",
"scope": "api:read",
"expiresAt": "2026-02-03T13:00:00Z",
"signature": "e05e049c..."
}Save the token:
export TOKEN="eyJ2IjoxLCJs..."With a token — access granted:
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8080/api/capability/ping{
"status": "ok",
"message": "Token validated successfully",
"caveats": ["expires = ...", "scope = api:read"],
"validated": "2026-02-03T12:00:00Z"
}Your request was cryptographically verified using a Macaroon bearer token with embedded caveats.
curl http://localhost:8080/api/micro{
"error": "payment_required",
"invoice": "lnbc1u1p...",
"amount_sats": 1,
"payment_hash": "5348..."
}HTTP 402 — Payment Required. The gateway issued a Lightning invoice. Pay it, submit the preimage, and access is granted. That's the L402 protocol.
One of the most powerful features — create a child token with fewer permissions, without contacting the server:
curl -s -X POST http://localhost:8080/api/capability/delegate \
-H "Content-Type: application/json" \
-d "{\"parentToken\": \"$TOKEN\", \"caveats\": [\"scope = api:read:limited\"]}"{
"token": "eyJ2IjoxLCJs...",
"caveats": ["expires = ...", "scope = api:read", "scope = api:read:limited"],
"signature": "ec91c4d8..."
}The child token inherits the parent's caveats plus the new restriction. Caveats can only be added, never removed. This is how agent swarms work — a master agent delegates restricted tokens to workers.
Instant kill switch — ban a token by its signature:
curl -s -X POST http://localhost:8080/api/governance/ban \
-H "X-Admin-Token: my-secret-admin-token" \
-H "Content-Type: application/json" \
-d '{"tokenSignature": "e05e049c...", "reason": "Compromised"}'{
"status": "banned",
"tokenSignature": "e05e049c...",
"reason": "Compromised",
"bannedAt": "2026-02-03T12:05:00Z"
}The token is now dead. Any request using it — or any child token delegated from it — will be rejected.
curl http://localhost:8080/api/governance/graphReturns a full lineage graph of all minted and delegated tokens, their status (active/banned), usage counts, and relationships.
In under a minute, you:
- Started an API gateway with zero dependencies
- Protected routes with cryptographic capability tokens
- Monetized routes with Lightning micropayments (L402)
- Delegated tokens with restricted permissions
- Revoked a compromised token instantly
- Inspected the full token lineage graph
No database. No Redis. No Docker. Just a single binary.
| Guide | Description |
|---|---|
| Core Concepts | Understand Macaroons, caveats, and delegation |
| Add Your API | Protect your own backend endpoints |
| Configuration Reference | Full YAML reference |
| Architecture | How SatGate works under the hood |
| Self-Hosted Deployment | Production deployment guide |
The example config (examples/gateway.yaml) sets up:
| Route | Path | Policy | Strip Prefix | Description |
|---|---|---|---|---|
| health | /health |
Built-in | — | Always available |
| public-demo | /public/* |
public |
✓ | No auth required |
| protected-demo | /protected/* |
capability |
✓ | Requires valid Macaroon |
| premium-demo | /premium/* |
l402 (100 sats) |
✓ | Lightning payment |
| api-micro | /api/micro |
l402 (1 sat) |
— | Lightning micropayment |
| api-basic | /api/basic |
l402 (10 sats) |
— | Lightning payment |
| api-standard | /api/standard |
l402 (100 sats) |
— | Lightning payment |
| api-premium | /api/premium |
l402 (1000 sats) |
— | Lightning payment |
Tip:
stripPrefix: trueremoves the matched path prefix before proxying. So/public/get→ upstream receives/get. Userewritefor static path replacement instead.
Just kill the process (Ctrl+C). No state to clean up — everything is in-memory in demo mode.