A FastAPI service for Swarm network access with x402 payments, notary signing, and stamp pool management.
⚠️ ALPHA SOFTWARE - PROOF OF CONCEPT This software is in Alpha stage and should be considered a Proof of Concept. Use for testing and experimentation only. Not recommended for production use.
⚠️ DATA PERSISTENCE WARNING Storage on Swarm is rented storage with limited time periods. The default configuration uses very short rental periods (approximately 1 day). Do not expect uploaded data to persist longer than the rental period. Data will become unavailable when the postage stamp expires.
swarm_connect/
├── app/ # Main application package
│ ├── main.py # FastAPI app instantiation and router inclusion
│ ├── api/
│ │ ├── endpoints/ # API route definitions
│ │ │ ├── stamps.py # Stamp management (purchase, extend, list, health check)
│ │ │ ├── data.py # Data upload/download (single + manifest)
│ │ │ ├── wallet.py # Wallet and chequebook info
│ │ │ ├── pool.py # Stamp pool (low-latency provisioning)
│ │ │ └── notary.py # Notary signing status
│ │ └── models/ # Pydantic request/response models
│ ├── core/
│ │ ├── config.py # Configuration (environment variables)
│ │ └── version.py # Version from build
│ ├── middleware/
│ │ └── rate_limit.py # Per-IP sliding window rate limiter
│ ├── services/
│ │ ├── swarm_api.py # Swarm Bee API client
│ │ ├── stamp_pool.py # Stamp pool background manager
│ │ ├── provenance.py # Document processing for signing
│ │ └── signing.py # Ethereum signing infrastructure
│ └── x402/ # x402 payment protocol (optional)
│ ├── dependency.py # FastAPI dependency for payment checks
│ ├── middleware.py # Post-response settlement + headers
│ ├── pricing.py # BZZ → USD price calculation
│ ├── access.py # IP list parsing (blocklist)
│ ├── audit.py # Transaction audit logging
│ └── ratelimit.py # Per-IP rate limiting for x402
│
├── scripts/ # Operator utility scripts
│ └── generate_notary_key.py # Generate Ethereum keypair for notary signing
├── docs/ # Feature documentation
├── tests/ # Unit and integration tests (700+)
│
├── .env.example # Example environment file
├── docker-compose.yml # Docker deployment (production + staging)
├── Dockerfile # Container image (Python 3.10)
├── requirements.txt # Python dependencies
└── run.py # Development server
Data retention. Swarm keeps data only while the stamp it was uploaded with is valid. Stamp purchase, extension, pool acquire and upload responses include
expires_at: an estimate (UTC, at today's storage price) of when the stamp runs out. Extend the stamp (PATCH /api/v1/stamps/{id}/extend) well before then, or the data can disappear. Pool stamps come with at least about a day left; purchased stamps last the requested duration. Service terms: TERMS.md (draft; linked asterms_urlfromGET /onceTERMS_URLis set).
Running this on a server rather than locally? See the Deployment Guide for requirements, funding, firewalling and the failure modes worth knowing about.
# Create virtual environment
python3 -m venv venv
# Activate virtual environment (Linux/Mac)
source venv/bin/activate
# On Windows: venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt
# Set up environment file
cp .env.example .env
# Edit .env and ensure SWARM_BEE_API_URL points to your Bee node's API endpoint
# (e.g., http://localhost:1633 or the public gateway https://api.gateway.ethswarm.org)# Start the development server (with auto-reload)
python run.py
# Optional: Use different port if 8000 is taken
PORT=8001 python run.py
# For HTTPS development (requires SSL certificates)
SSL_KEYFILE=./localhost+2-key.pem SSL_CERTFILE=./localhost+2.pem python run.pyThe server will be available at:
- HTTP: http://127.0.0.1:8000
- API Documentation: http://127.0.0.1:8000/docs
- Alternative docs: http://127.0.0.1:8000/redoc
docker-compose.yml reads a few variables from the compose .env file so one
image can serve hosts with different topologies. All are optional — leave them
unset and the stack behaves as it always has.
| Variable | Default | Purpose |
|---|---|---|
SWARM_BEE_API_URL |
required | Bee endpoint for the production gateway |
SWARM_BEE_API_URL_DEV |
required | Bee endpoint for the staging gateway |
COMPOSE_PROFILES |
(unset) | set to bee to also run the bundled Bee nodes |
BEE_VERSION |
2.8.1 |
Bee image tag |
BEE_RPC_ENDPOINT |
(unset) | Gnosis RPC endpoint (required with the bee profile) |
BEE_DEV_RPC_ENDPOINT |
falls back to BEE_RPC_ENDPOINT |
Separate RPC for the staging node, so its chain load cannot rate-limit production |
BEE_PASSWORD / BEE_DEV_PASSWORD |
(unset) | keystore passwords |
BEE_NAT_ADDR / BEE_DEV_NAT_ADDR |
(unset) | public host:port each node advertises |
BEE_P2P_PORT / BEE_DEV_P2P_PORT |
1634 / 1734 |
host p2p ports |
BEE_FULL_NODE / BEE_DEV_FULL_NODE |
false |
run as a full node instead of light |
GATEWAY_BIND |
127.0.0.1 |
host interface the gateways bind to; only the local reverse proxy should reach them. Set 0.0.0.0 only on a host without one |
HOST_LABEL |
(empty) | value of the host label on metrics; set when more than one host runs the stack |
Both Bee endpoints are required — compose refuses to start without them, naming the variable it needs. A default was previously baked in, which silently became wrong on any host that did not share that one node.
docker-compose.yml defines the gateways, deployed per branch.
docker-compose.host.yml defines the host-level services — the bundled Bee
nodes and the metrics agent — which are not branch-specific and are applied
with docker compose -f docker-compose.host.yml up -d. Both use the same
COMPOSE_PROJECT_NAME, so they share one network and one set of volumes.
Running the gateway and its Bee node on one host — set COMPOSE_PROFILES=bee
and point each gateway at its own node (http://bee:1633, http://bee-dev:1633).
The Bee API is never published to the host: it has no authentication, so it must
not be reachable from outside the compose network. Only the p2p ports are exposed,
and BEE_NAT_ADDR must advertise the matching public host:port or peers cannot
dial back — since Bee 2.7.0 an invalid value fails startup rather than degrading
silently.
The deploy workflow appends /opt/swarm_connect_host.env to the compose .env if
that file exists, so host-specific values survive a redeploy.
Swarm Connect is a FastAPI-based API gateway that provides comprehensive access to Ethereum Swarm (distributed storage network) functionality. It offers complete postage stamp management and data operations through a clean, RESTful interface, eliminating the need for clients to interact directly with complex Swarm Bee node APIs.
┌─────────────────────────────────────────────────────────────────────────────┐
│ CLIENT APPLICATIONS │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Web Apps │ │ Mobile App │ │ CLI Tools │ │ Third-party Apps │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
HTTP/HTTPS Requests
│
┌─────────────────────────────────────────────────────────────────────────────┐
│ SWARM CONNECT API GATEWAY │
│ (FastAPI) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ API LAYER │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────┐ │ │
│ │ │ Health Check │ │ OpenAPI Docs │ │ Stamps & Data APIs │ │ │
│ │ │ GET / │ │ /docs /redoc │ │ Complete CRUD │ │ │
│ │ └─────────────────┘ └─────────────────┘ └─────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ VALIDATION LAYER │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────┐ │ │
│ │ │ Request/Response│ │ StampDetails │ │ Error Handling & │ │ │
│ │ │ Validation │ │ Pydantic Model│ │ HTTP Status Codes │ │ │
│ │ └─────────────────┘ └─────────────────┘ └─────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ BUSINESS LOGIC LAYER │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────┐ │ │
│ │ │ Stamp Filtering│ │ TTL Calculation│ │ Expiration Time │ │ │
│ │ │ by Batch ID │ │ & Processing │ │ Formatting │ │ │
│ │ └─────────────────┘ └─────────────────┘ └─────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ SERVICE LAYER │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────┐ │ │
│ │ │ Swarm API │ │ HTTP Client │ │ Error Recovery & │ │ │
│ │ │ Integration │ │ (httpx Async) │ │ Retry Logic │ │ │
│ │ └─────────────────┘ └─────────────────┘ └─────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ CONFIGURATION LAYER │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────┐ │ │
│ │ │ Environment │ │ Settings │ │ URL Validation & │ │ │
│ │ │ Variables │ │ Management │ │ Caching │ │ │
│ │ └─────────────────┘ └─────────────────┘ └─────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
HTTP Requests (10s timeout)
│
┌─────────────────────────────────────────────────────────────────────────────┐
│ SWARM BEE NODE │
│ (localhost:1633) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ BEE API ENDPOINTS │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────┐ │ │
│ │ │ GET /batches │ │ Stamp Data │ │ Blockchain │ │ │
│ │ │ (All Stamps) │ │ Repository │ │ Integration │ │ │
│ │ └─────────────────┘ └─────────────────┘ └─────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
- Duration-Based Purchasing: Specify stamp duration in hours instead of raw PLUR amounts
- Dynamic Price Calculation: Automatically calculates costs based on current network price
- Wallet Balance Verification: Checks sufficient funds before purchase/extension with clear error messages
- Purchase Stamps: Create new postage stamps with duration (hours) or legacy amount
- Extend Stamps: Add time to existing stamps using duration or legacy amount
- List All Stamps: Retrieve comprehensive list of all available stamps with enhanced data
- Get Stamp Details: Fetch specific stamp information by batch ID
- Expiration Calculation: Automatically calculates stamp expiration time (current time + TTL)
- Utilization Percentage: Calculates human-readable stamp usage percentage (0-100%)
- Utilization Status Warnings: Status levels ("ok", "warning", "critical", "full") with actionable messages
- Usable Flag: Stamps at 100% utilization automatically marked as unusable
- Data Merging: Merges global network data with local node information for complete stamp details
- Local Ownership Detection: Identifies stamps owned/managed by the connected node
- Enhanced Field Mapping: Handles different field names between global and local APIs
- Unified Data Upload: Single endpoint handles both JSON and binary data automatically
- Collection/Manifest Upload: Upload multiple files as TAR archive for 15x performance improvement
- Deferred Upload Mode: Optional deferred mode for faster upload response (data syncs to network asynchronously)
- Configurable Redundancy: Optional erasure coding level (0-4) for data durability vs storage cost tradeoff
- Pre-Upload Stamp Validation: Optional validation to check stamp usability before upload
- Performance Timing: W3C Server-Timing headers and optional JSON timing breakdown for latency profiling
- Enhanced Error Messages: Detailed feedback when uploads fail due to stamp capacity
- SWIP-Compliant Examples: Pre-filled with SWIP standard provenance data structure
- Content-Type Detection: Automatic handling based on Content-Type header
- Raw Data Download: Download data as binary stream or base64-encoded JSON
- Reference-Based Access: Access data using Swarm reference hashes
- Provenance Support: Built-in examples for data lineage and provenance tracking
- Notary Signing: Optional cryptographic signatures proving data existed at upload time (see Notary Guide)
- FastAPI Framework: Modern, fast web framework with automatic OpenAPI documentation
- Auto-Documentation: Interactive API docs at
/docsand/redoc - Type Validation: Pydantic models ensure data integrity and type safety
- Error Handling: Comprehensive error responses with appropriate HTTP status codes
- Configuration Management: Environment-based settings with validation
- Development Server: Hot-reload development server with SSL support
- Binary Data Support: Direct binary upload/download with optional JSON wrapping
- Modular Design: Separate endpoints for stamps and data operations
- Upload Size Limits: Configurable maximum upload size (default: 10 MB) with clear 413 errors
- Download Size Limits:
GET /api/v1/data/{ref}streams from Bee and refuses content overMAX_DOWNLOAD_SIZE_MB(default 25 MB) with 413DOWNLOAD_TOO_LARGE; a transfer taking longer thanDOWNLOAD_TIMEOUT_SECONDS(default 120) gets 504 - Global Rate Limiting: Per-IP sliding window rate limiter with burst capacity (default: 60 req/min + 10 burst)
- Spending Limits: Stamp purchases and extensions are bounded per request and per caller per day, so no caller can drain the gateway's BZZ. All spending (purchases, extensions, pool buys and top-ups, for-owner batches) also counts against one gateway-wide daily ceiling,
GATEWAY_DAILY_BZZ_CEILING(default 20 BZZ), of which unpaid spending may useGATEWAY_DAILY_BZZ_FREE_CEILING(default 10 BZZ), so the pool keeps headroom; 503GATEWAY_DAILY_SPEND_CEILINGwhen reached. Limits are reserved when a request is admitted, so concurrent requests cannot overrun them, and are given back only when the spend certainly did not happen (a timeout after the request was sent stays counted). Metrics:gateway_spend_bzz_today{scope},gateway_spend_ceiling_bzz{scope}andgateway_spend_uncertain_bzz_total{operation}(charges kept after an uncertain failure). A spend admitted just before midnight UTC counts on the day it was admitted. To clear kept charges before midnight (after confirming on-chain that nothing was spent), stop the gateway, remove or lower the"(gateway total)"/"(gateway giveaway)"entries indata/stamp_spend_budget.json, and start it again. - Input Validation: Strict regex validation on stamp IDs (64-char hex) and references (64-128 char hex)
- Error Sanitization: Internal details (IPs, ports, file paths) are never exposed in error responses
- Server Header Suppression:
Serverheader removed to prevent version fingerprinting - Rate Limit Headers:
X-RateLimit-LimitandX-RateLimit-Remainingon rate-limited routes (free-tier writes report the stricter free-tier limit)
- Request Timeouts: 10-second timeout for external API calls
- Error Recovery: Multiple layers of exception handling
- Flexible Response Parsing: Handles different Swarm API response formats
- Logging: Structured logging for debugging and monitoring
- Health Checks: Basic health check endpoint for monitoring
- Loads environment variables on startup
- Validates Swarm Bee API URL format
- Provides cached settings to all components
- Receives HTTP requests and routes them
- Applies path parameters and validation
- Returns structured JSON responses
- Makes HTTP calls to Swarm Bee node (both
/batchesand/stampsendpoints) - Handles network errors and timeouts
- Parses and normalizes API responses
- Data Merging Logic: Combines global stamp data with local node information
- Field Mapping: Handles different field names between endpoints (
immutablevsimmutableFlag) - Usability Calculation: Determines stamp usability based on TTL, depth, and immutability
- Utilization Calculation: Computes stamp usage as percentage:
(utilization / 2^(depth-bucketDepth)) * 100 - Local Detection: Identifies stamps owned by the connected node
- Validates response data structure with enhanced fields
- Handles optional fields and type conversion
- Local Ownership Field: Boolean indicator for node-owned stamps
- Enhanced Nullable Fields: Proper handling of potentially missing data from different endpoints
- Formats output for API consumers
1. Client → FastAPI Router → Endpoint Handler
2. Endpoint → Service Layer → External Swarm API
3. Service → Business Logic → Data Processing
4. Response ← Pydantic Model ← Formatted Data
GET /(and/health): Health check. Always includes abee_nodesection describing the Bee node — identity and build (overlay,version,api_version,bee_status), connectivity (mode,connected_peers,population,reachability,network_availability(Available/Unavailable/Unknown)), reserve and radius (storage_radius,committed_depth,reserve_size,reserve_size_within_radius,pullsync_rate,batch_commitment), chain sync (last_synced_block,chain_tip,chain_sync_lag_blocks), pluswarming_up, ahealthyflag, andwarnings.statusbecomesdegradedwhennetwork_availabilityisUnavailable(the node can't reach the storer network, so uploads may return success without propagating); other warnings — a low peer count, a large chain-sync lag, or a non-ok Bee status — are advisory and do not changestatus. When x402 is enabled, also includes thex402wallet section.
POST /api/v1/stamps/: Purchase new postage stamps with time-based or advanced parametersGET /api/v1/stamps/: List stamps (default: local only;?global=truefor all;?wallet=0x...for wallet-filtered)GET /api/v1/stamps/{stamp_id}: Retrieve specific stamp batch detailsGET /api/v1/stamps/{stamp_id}/check: Check stamp health for uploads (errors and warnings)PATCH /api/v1/stamps/{stamp_id}/extend: Extend existing stamps with additional fundsPOST /api/v1/stamps/for-owner: Create a postage batch owned by an arbitrary address (Flow B). CallsPostageStamp.createBatch(owner=…)on Gnosis so the owner can sign its own stamps off-node. Body:owner(0x address),size/depth,duration_hours, optionalimmutable. ReturnsbatchID+txHash. Spends the gateway's Gnosis funds, so it is OFF by default (STAMP_PURCHASE_FOR_OTHERS_ENABLED; 404 when off) and guarded by an owner allow-list (STAMP_FOR_OTHERS_REQUIRE_WHITELIST/_OWNER_WHITELIST) and hard caps (STAMP_FOR_OTHERS_MAX_DEPTH/_MAX_BZZ/_MAX_DURATION_HOURS), all enforced before any spend. When x402 is enabled the caller pays for the service (free tier off by default); before the on-chain write the gateway's signer wallet is preflighted so it never attempts a batch it can't fund. See the Buy-Batch-for-Owner Guide for key custody, funding, and metrics.
POST /api/v1/data/?stamp_id={id}&content_type={type}: Upload raw data to SwarmPOST /api/v1/data/manifest?stamp_id={id}: Upload TAR archive as collection/manifest (15x faster for batch uploads)GET /api/v1/data/{reference}: Download raw data from Swarm (returns bytes directly)GET /api/v1/data/{reference}/json: Download data with JSON metadata (base64-encoded)
GET /api/v1/debug/bee/{path}: signature-gated read-only proxy to allow-listed Bee diagnostic endpoints (topology,addresses,peers,status,chainstate, …) for operators who only have gateway access. Disabled (404) unlessDEBUG_ALLOWED_ADDRESSESis set; requires an EIP-191 signature from an allow-listed address (X-Debug-Timestamp+X-Debug-Signatureoverswarm-connect-debug:<unix_ts>). No shared secret is stored.
POST /api/v1/chunks/: Forward a single client-supplied pre-stamped chunk to Swarm. Send the raw chunk as the body and the marshaled stamp in theSwarm-Postage-Stampheader. The client owns the postage batch and stamps locally; the gateway is a thin forwarder (does not verify the stamp — Bee does). Optional?deferred=true. RequiresCHUNK_UPLOAD_ENABLED=true(returns 404 when disabled). When x402 is enabled, the upload spends prepaid bandwidth credit: present the bearer token from the top-up in theX-Bandwidth-Credit-Tokenheader (the chunk's byte length is debited). A free tier is also available — sendX-Payment-Mode: freeto draw from a per-IP daily byte quota (CHUNK_UPLOAD_FREE_TIER_MB_PER_DAY); exceeding it returns429.POST /api/v1/chunks/credit?mb={n}: Add prepaid bandwidth credit with a single x402 payment (priced atX402_BANDWIDTH_USD_PER_GB, minimumBANDWIDTH_CREDIT_MIN_TOPUP_MB). Returns a bearer credit token bound to the payer wallet; reuse it across many chunk uploads so per-chunk requests never hit the minimum-price floor.
See the Chunk Forwarding Guide for operator configuration and client integration.
GET /api/v1/wallet: Get the wallet address and BZZ balance of the Bee nodeGET /api/v1/chequebook: Get the chequebook address and balance information of the Bee node
GET /api/v1/notary/info: Check notary availability and get public address for verificationGET /api/v1/notary/status: Simplified notary status for health checksPOST /api/v1/data/?sign=notary: Upload with notary signature (see Notary Guide)
- Complete Gateway Solution: Full stamp lifecycle and data operations in one service
- Simplified Interface: Clean REST API vs complex Swarm protocols
- Enhanced Data: Adds calculated expiration times to raw stamp data
- Reliability: Robust error handling and timeout management
- Developer Experience: Auto-generated docs and type safety
- Flexibility: Configurable for different Swarm node endpoints
- Binary Support: Native handling of raw data with multiple access patterns
- x402 Payment Support: Optional pay-per-request monetization via USDC
The gateway supports optional x402 payment integration, enabling pay-per-request access without user accounts.
When X402_ENABLED=true, protected endpoints (POST /stamps/, POST /data/) require USDC payment on Base chain. The gateway uses received payments to fund Swarm operations.
┌──────────────┐ USDC ┌──────────────┐ xBZZ ┌──────────────┐
│ Client │ ────────────> │ Gateway │ ────────────> │ Swarm Bee │
│ (Base chain)│ │ (2 wallets) │ │ (Gnosis chain)│
└──────────────┘ └──────────────┘ └──────────────┘
-
Enable x402 in
.env:X402_ENABLED=true X402_PAY_TO_ADDRESS=0xYourBaseWallet
-
Without payment, protected endpoints return HTTP 402:
curl -X POST http://localhost:8000/api/v1/stamps/ # Returns 402 with payment requirements -
With payment (using x402 client):
curl -X POST http://localhost:8000/api/v1/stamps/ \ -H "X-PAYMENT: <base64-encoded-payment>" \ -H "Content-Type: application/json" -d '{"size": "small", "duration_hours": 24}' # Returns 201 with {"batchID": ..., "message": ...} -
Free tier (when
X402_FREE_TIER_ENABLED=true, the default): sendX-Payment-Mode: freeinstead of a payment. Limited toX402_FREE_TIER_RATE_LIMITrequests per minute per IP (default 3), then 429. Responses to free-tier requests carryX-Payment-Mode: free-tier; sendfreein requests.
- Pay-per-request: No accounts, no subscriptions
- Access control: IP blocklist (
X402_BLACKLIST_IPS, IPs and CIDR ranges, 403 on every route); there is no payment allowlist - Rate limiting: x402 free tier 3 writes/min per client (IPv6 grouped by /64); global per-client limit
RATE_LIMIT_PER_MINUTE+RATE_LIMIT_BURST(installed alongside x402 once #401 is deployed) - Audit logging: JSON lines format for all transactions
- Pre-flight checks: Validates gateway wallet balances before accepting payments
| Endpoint | Payment Required |
|---|---|
POST /api/v1/stamps/ |
Yes |
POST /api/v1/data/ |
Yes |
POST /api/v1/data/manifest |
Yes |
GET /api/v1/data/{ref} |
No (free) |
GET /api/v1/stamps/ |
No (free) |
GET /api/v1/pricing |
No (free): current price quotes for the paid operations, so a client can learn a price without triggering a 402 |
# Core
X402_ENABLED=true
X402_PAY_TO_ADDRESS=0x... # Your USDC receiving wallet
X402_NETWORK=base-sepolia # or "base" for mainnet
# Pricing
X402_BZZ_USD_RATE=0.50 # BZZ to USD rate
X402_MARKUP_PERCENT=50 # Profit margin
X402_MIN_PRICE_USD=0.01 # Minimum charge
X402_BANDWIDTH_USD_PER_GB=0.10 # Paid uploads and chunk credit (bandwidth)
# Access Control
X402_BLACKLIST_IPS= # Blocked IPs / CIDR ranges (403 on every route)
# Audit
X402_AUDIT_LOG_PATH=logs/x402_audit.jsonlSee x402 Operator Guide for complete setup instructions.
Every error carries code and message at the top level; the codes are listed in Error codes.
The gateway has built-in Prometheus monitoring with a Grafana Cloud dashboard.
Architecture:
Gateway ──/metrics──> Grafana Alloy ──remote write──> Grafana Cloud
(Docker) (dashboards + alerts)
- Gateway exposes
GET /metricswith Prometheus text format - Grafana Alloy runs as a sidecar container, scrapes metrics every 15s
- Metrics are pushed to Grafana Cloud (free tier: 10k series, 14-day retention)
- Dashboard: datafund.grafana.net/d/gateway-overview
What's tracked:
- HTTP request rate, latency percentiles (p50/p95/p99), and error rate
- Upload/download counts and bytes transferred
- Stamp purchases and pool acquisitions
- Wallet balances (BZZ, xDAI, chequebook, Base ETH) with alert thresholds
- Stamp pool availability by size and minimum TTL
- x402 payment mode breakdown (paid/free/rejected)
- Bee node API error rate by endpoint
- Rate limit rejections
Configuration:
METRICS_ENABLED=true # Expose /metrics (default: true)
METRICS_BALANCE_POLL_SECONDS=60 # Balance polling interval (default: 60s)
GATEWAY_ENVIRONMENT=development # Environment label: development or mainLocal development:
# Run local Prometheus + Grafana
docker compose -f monitoring/docker-compose.monitoring.yml up -d
# Prometheus: http://localhost:9090
# Grafana: http://localhost:3000 (admin/admin)Grafana Cloud setup (for new deployments):
- Create a Grafana Cloud account (free tier)
- Create an access policy with
metrics:writescope, generate a token - Set GitHub secrets:
GRAFANA_CLOUD_PROM_USERNAME(instance ID) andGRAFANA_CLOUD_API_TOKEN - Alloy is defined in
docker-compose.host.yml; the deploy workflow starts it, and recreates it on every push so config changes take effect
See the monitoring epic for full details.
Purchase a new postage stamp with duration-based or legacy amount pricing.
Simple usage with size presets (recommended):
{"duration_hours": 48, "size": "small", "label": "my-stamp"}Size presets:
| Size | Use case |
|---|---|
small |
One small document (default) |
medium |
Several medium documents |
large |
Several large documents |
Parameters:
duration_hours: Desired stamp duration in hours (default: 25)size: Storage size preset - "small", "medium", or "large" (default: "small")depth: Advanced - explicit depth value 16-32 (overridden by size if both provided)label: Optional user-defined label
Using defaults (25 hours, size small):
{}Legacy amount mode:
{"amount": 8000000000, "depth": 17}Response: {"batchID": "...", "message": "Postage stamp purchased successfully"}
Error (insufficient funds):
{
"detail": "Insufficient funds to purchase stamp. Required: 1.50 BZZ, Available: 0.25 BZZ, Shortfall: 1.25 BZZ"
}List postage stamps. Default returns only local stamps (usable for uploads).
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
global |
bool | false |
If true, return all stamps including non-local |
wallet |
string | - | Filter to stamps accessible by this wallet (requires x402 enabled) |
exclusive |
bool | false |
With wallet: only stamps purchased by this wallet (excludes shared/untracked) |
- Response:
{"stamps": [...], "total_count": N} - Each stamp includes
accessMode:"owned"(exclusive),"shared"(free tier),"pool"(pool inventory), ornull(untracked)
Get detailed information about a specific stamp.
- Response: Detailed stamp information with calculated expiration time
Stamp Utilization Status Fields:
| Field | Description |
|---|---|
utilizationPercent |
Usage percentage (0-100%) |
utilizationStatus |
Status level: "ok", "warning", "critical", "full" |
utilizationWarning |
Actionable message when status is elevated |
usable |
Boolean - false when stamp is expired, invalid, or 100% full |
Status Thresholds:
| Status | Range | Meaning |
|---|---|---|
ok |
0-80% | Plenty of capacity |
warning |
80-95% | Approaching full capacity |
critical |
95-99.99% | Nearly full, action recommended |
full |
100% | Cannot accept more data |
Example response with warning:
{
"batchID": "abc123...",
"utilizationPercent": 87.5,
"utilizationStatus": "warning",
"utilizationWarning": "Stamp is approaching full capacity (87.5% utilized). Monitor usage and consider purchasing additional stamps.",
"usable": true
}Extend an existing stamp by adding more time.
Duration-based (recommended):
{"duration_hours": 48}Using default (25 hours):
{}Legacy amount mode:
{"amount": 8000000000}Response: {"batchID": "...", "message": "Postage stamp extended successfully"}
POST /api/v1/data/?stamp_id={id}&content_type={type}&validate_stamp={bool}&deferred={bool}&include_timing={bool}&redundancy={level}
Upload data to Swarm (JSON or binary).
- Request Body: JSON data (default) or raw binary data
- Content-Type:
application/json(default) orapplication/octet-streamfor binary - Size Limit: Maximum upload size is configurable (default: 10 MB). Returns 413 if exceeded.
- validate_stamp: Optional (default: false) - Pre-validate stamp before upload
- deferred: Optional (default: false) - Use deferred upload mode
- include_timing: Optional (default: false) - Include timing breakdown in response
- redundancy: Optional (default: 2) - Erasure coding level (0-4)
- Response:
{"reference": "...", "message": "Data uploaded successfully", "timing": null} - Features: Pre-filled with SWIP-compliant provenance data example structure
Deferred vs Direct Upload:
| Mode | Parameter | Description |
|---|---|---|
| Direct | deferred=false (default) |
Chunks uploaded directly to network. Ensures immediate availability. Safer for gateway use cases. |
| Deferred | deferred=true |
Data goes to local node first, syncs to network asynchronously. Faster upload response but data may not be immediately retrievable. |
Redundancy Levels (Erasure Coding):
| Level | Name | Chunk Loss Tolerance | Use Case |
|---|---|---|---|
| 0 | none | 0% | Maximum storage efficiency, no redundancy |
| 1 | medium | 1% | Basic redundancy for stable networks |
| 2 | strong | 5% | Default - Good balance of durability and cost |
| 3 | insane | 10% | High durability for important data |
| 4 | paranoid | 50% | Maximum durability for critical data |
Higher redundancy levels increase data durability at the cost of more storage space (and stamp usage).
Examples:
# Standard upload (direct mode, default redundancy=2)
curl -X POST "http://localhost:8000/api/v1/data/?stamp_id=YOUR_STAMP_ID" \
-F "file=@data.json"
# Upload with deferred mode (faster response)
curl -X POST "http://localhost:8000/api/v1/data/?stamp_id=YOUR_STAMP_ID&deferred=true" \
-F "file=@data.json"
# Upload with stamp validation
curl -X POST "http://localhost:8000/api/v1/data/?stamp_id=YOUR_STAMP_ID&validate_stamp=true" \
-F "file=@data.json"
# Upload with timing information
curl -X POST "http://localhost:8000/api/v1/data/?stamp_id=YOUR_STAMP_ID&include_timing=true" \
-F "file=@data.json"
# Upload with maximum redundancy (paranoid mode)
curl -X POST "http://localhost:8000/api/v1/data/?stamp_id=YOUR_STAMP_ID&redundancy=4" \
-F "file=@data.json"
# Upload with no redundancy (storage-efficient)
curl -X POST "http://localhost:8000/api/v1/data/?stamp_id=YOUR_STAMP_ID&redundancy=0" \
-F "file=@data.json"Returns 400 if stamp is full (100% utilized), not usable, or invalid redundancy level. Returns 404 if stamp not found. Returns 413 if file exceeds the upload size limit.
Upload Size Limit Error (413):
{
"code": "FILE_TOO_LARGE",
"message": "Upload exceeds maximum size of 10 MB.",
"max_size_mb": 10
}Performance Timing Response (when include_timing=true):
{
"reference": "abc123...",
"message": "File 'data.json' uploaded successfully",
"timing": {
"stamp_validate_ms": null,
"file_read_ms": 0.12,
"bee_upload_ms": 145.67,
"total_ms": 146.01
}
}Server-Timing Header (always included):
Server-Timing: file-read-ms;dur=0.12, bee-upload-ms;dur=145.67, total-ms;dur=146.01
The W3C Server-Timing header is visible in browser DevTools Network tab for easy performance profiling.
POST /api/v1/data/manifest?stamp_id={id}&validate_stamp={bool}&deferred={bool}&include_timing={bool}&redundancy={level}
Upload multiple files as a TAR archive collection/manifest.
- Performance: 15x faster than individual uploads (50 files in ~500ms vs ~14s)
- Request: Multipart form-data with TAR archive file
- validate_stamp: Optional (default: false) - Pre-validate stamp before upload
- deferred: Optional (default: false) - Use deferred upload mode (see table above)
- include_timing: Optional (default: false) - Include timing breakdown in response
- redundancy: Optional (default: 2) - Erasure coding level (0-4) - see redundancy table above
- Response:
{"reference": "manifest-hash...", "file_count": 50, "message": "Collection uploaded successfully", "timing": null}
Usage Example:
# Create TAR archive with multiple files
tar -cvf files.tar file1.json file2.json file3.json
# Upload as collection (direct mode, default redundancy=2)
curl -X POST "http://localhost:8000/api/v1/data/manifest?stamp_id=YOUR_STAMP_ID" \
-F "file=@files.tar"
# Upload with deferred mode (faster response)
curl -X POST "http://localhost:8000/api/v1/data/manifest?stamp_id=YOUR_STAMP_ID&deferred=true" \
-F "file=@files.tar"
# Upload with pre-validation
curl -X POST "http://localhost:8000/api/v1/data/manifest?stamp_id=YOUR_STAMP_ID&validate_stamp=true" \
-F "file=@files.tar"
# Upload with timing information
curl -X POST "http://localhost:8000/api/v1/data/manifest?stamp_id=YOUR_STAMP_ID&include_timing=true" \
-F "file=@files.tar"
# Upload with maximum redundancy for critical archives
curl -X POST "http://localhost:8000/api/v1/data/manifest?stamp_id=YOUR_STAMP_ID&redundancy=4" \
-F "file=@files.tar"Manifest Timing Response (when include_timing=true):
{
"reference": "manifest-hash...",
"file_count": 50,
"message": "Collection uploaded successfully with 50 files",
"timing": {
"stamp_validate_ms": null,
"file_read_ms": 0.35,
"tar_validate_ms": 1.22,
"tar_count_ms": 0.89,
"bee_upload_ms": 487.15,
"total_ms": 489.92,
"file_count": 50,
"ms_per_file": 9.80,
"files_per_second": 102.06
}
}The ms_per_file and files_per_second metrics are useful for comparing local Bee node performance vs gateway performance.
Accessing individual files after upload:
- Via Bee node:
GET /bzz/{manifest_reference}/{file_path} - Via bee-js: Use
MantarayNode.unmarshal()to extract individual file references
Download raw data from Swarm as a file (triggers browser download).
- Use case: End users downloading files, browser integration
- Response: Raw binary data with user-friendly filename
- Headers:
Content-Disposition: attachment; filename="provenance-abc12345.json"Content-Type: Auto-detected (application/json, image/png, etc.)
- Filenames:
- JSON data →
provenance-{hash}.json - Images →
image-{hash}.png/jpg - PDFs →
document-{hash}.pdf - Text →
text-{hash}.txt - Binary →
data-{hash}.bin
- JSON data →
Download data as JSON with metadata (for API clients).
- Use case: Web apps, mobile apps, API integrations needing metadata
- Response:
{"data": "base64-encoded-content", "content_type": "application/json", "size": 2048, "reference": "abc..."} - Benefits: Get file metadata without triggering download, programmatic access
Get the wallet address and BZZ balance of the connected Bee node.
- Response:
{"walletAddress": "0x...", "bzzBalance": "254399000000000"} - Use case: Identify the Ethereum wallet address and check BZZ token balance
- BZZ Balance: Returned in wei (smallest unit of BZZ token)
- Note: Only available when connected to local Bee nodes, not public gateways
Get the chequebook address and balance information of the connected Bee node.
- Response:
{"chequebookAddress": "0x...", "availableBalance": "1000000000", "totalBalance": "1000000000"} - Use case: Identify the chequebook smart contract address and check available funds
- Balance Fields:
availableBalance: Funds available for creating new postage stamps (in wei)totalBalance: Total funds in the chequebook (in wei)
- Note: Only available when connected to local Bee nodes, not public gateways
Perform a comprehensive health check on a stamp to determine if it can be used for uploads.
Use cases:
- Check if a recently purchased stamp is ready for use (propagation delay)
- Verify a stamp before starting a large batch upload
- Diagnose why uploads are failing
Response:
{
"stamp_id": "abc123...",
"can_upload": true,
"errors": [],
"warnings": [
{
"code": "HIGH_UTILIZATION",
"message": "Stamp is 82% utilized.",
"suggestion": "Monitor usage and consider purchasing additional stamps."
}
],
"status": {
"exists": true,
"local": true,
"usable": true,
"utilizationPercent": 82.5,
"utilizationStatus": "warning",
"batchTTL": 86400,
"expectedExpiration": "2026-01-12-17-30",
"secondsSincePurchase": null,
"estimatedReadyAt": null,
"propagationStatus": "ready"
}
}Error Codes (blocking - can_upload: false):
| Code | Description |
|---|---|
NOT_FOUND |
Stamp doesn't exist on the connected node |
NOT_LOCAL |
Stamp exists but isn't owned by this Bee node |
EXPIRED |
Stamp TTL has reached 0 |
NOT_USABLE |
Stamp not yet usable (e.g., propagation delay) |
FULL |
Stamp is at 100% utilization |
Warning Codes (non-blocking - can_upload: true):
| Code | Description |
|---|---|
LOW_TTL |
Stamp expires in less than 1 hour |
NEARLY_FULL |
Stamp is 95%+ utilized |
HIGH_UTILIZATION |
Stamp is 80%+ utilized |
File uploads are limited to 10 MB by default. This applies to both /api/v1/data/ and /api/v1/data/manifest endpoints, and a file of exactly the limit is accepted — the multipart envelope that wraps it does not count against the limit.
# Configure in .env (value in megabytes)
MAX_UPLOAD_SIZE_MB=10Uploads exceeding the limit receive a 413 response:
{"code": "FILE_TOO_LARGE", "message": "Upload exceeds maximum size of 10 MB.", "max_size_mb": 10}POST /api/v1/stamps/ and PATCH /api/v1/stamps/{id}/extend spend the gateway operator's BZZ on behalf of the caller. Two limits bound that, answering different questions:
# Configure in .env
X402_MAX_STAMP_BZZ=5.0 # Most a single request may cost (0 disables)
STAMP_DAILY_BZZ_PER_CALLER=0.5 # Most one caller may spend per day (-1 disables)The first stops any one request taking a large share of the wallet however it is shaped — batch cost scales with amount x 2^depth, so the accepted depth and duration ranges span orders of magnitude. The second stops the first simply being applied repeatedly.
A request over the per-request ceiling receives 400:
{"code": "STAMP_COST_EXCEEDS_LIMIT", "message": "...", "cost_bzz": 12.5, "limit_bzz": 5.0}A caller who has spent their daily budget receives 429, with the reset time and what remains:
{"code": "DAILY_SPEND_BUDGET_EXHAUSTED", "message": "...", "remaining_bzz": 0.02,
"daily_budget_bzz": 0.5, "resets_at": "2026-09-09T24:00:00Z"}Both are checked before the wallet balance, so the answer does not depend on how much money happens to be left, and charged only after the money is actually spent — a purchase the Swarm node refuses costs the caller nothing.
The budget counts BZZ rather than batches, because these endpoints take a depth and a duration: a count would let a caller stay inside their allowance and still spend arbitrarily by asking for larger batches. It is keyed on the client IP, since the callers here are CLIs, SDKs and MCP clients that send no Origin. An IP is not an identity — it is shared behind NAT and cheap to change — so this bounds casual and accidental spending rather than preventing deliberate spending. A caller who needs more can pay: a settled x402 payment bypasses the daily budget, though not the per-request ceiling.
Counters gateway_stamp_spend_refusals_total and gateway_stamp_spend_bzz_total make refusals and committed spend visible in Prometheus, so a limit set too low shows up on a dashboard rather than in a complaint.
Global per-IP rate limiting protects against abuse. Uses a sliding window algorithm with burst capacity.
# Configure in .env
RATE_LIMIT_ENABLED=true # Master switch (default: true)
RATE_LIMIT_PER_MINUTE=60 # Requests per minute per IP (default: 60)
RATE_LIMIT_BURST=10 # Extra burst capacity (default: 10)Every response includes rate limit headers:
X-RateLimit-Limit: 70
X-RateLimit-Remaining: 65
When the limit is exceeded, the gateway returns 429:
{"error": "Rate limit exceeded", "detail": "Too many requests. Try again in 42 seconds.", "retry_after": 42}x402 startup checks (#370): with X402_ENABLED=true the gateway refuses to start on an unknown X402_NETWORK (only base and base-sepolia), a missing, zero or mis-checksummed X402_PAY_TO_ADDRESS, a mainnet paired with the public x402.org facilitator (test networks only), or inconsistent facilitator credentials (X402_FACILITATOR_CDP_API_KEY_ID/_SECRET or X402_FACILITATOR_BEARER_TOKEN). The pay-to wallet's Base ETH balance is shown on /health for information only; the facilitator pays settlement gas.
Exempt paths (never rate-limited): /, /health, /docs, /redoc, /api/v1/openapi.json, /metrics. With x402 billing on, pre-stamped chunk uploads (POST /api/v1/chunks/) are also exempt, because each chunk is already paid for per byte (prepaid credit or the free daily quota).
With x402 enabled the global limit still applies to every other route. Free-tier writes additionally meet the x402 free-tier limit (X402_FREE_TIER_RATE_LIMIT, default 3/min), and count against both. Callers sharing one IP (NAT, CI runners, a shared backend) share one budget.
All path and query parameters are validated with strict patterns:
| Parameter | Pattern | Example |
|---|---|---|
stamp_id |
64-character hex | a1b2c3... (64 chars) |
reference |
64-128 character hex | d4e5f6... (64-128 chars) |
Invalid inputs receive a 422 response before any processing occurs.
Error messages never expose internal infrastructure details:
- No internal IP addresses or hostnames
- No port numbers or file paths
- No stack traces or library versions
- Generic messages like "The Bee node may be unavailable" instead of connection strings
The Server header is suppressed in all responses to prevent version fingerprinting.
Cause: The stamp ID doesn't exist on the connected Bee node.
Solutions:
- Verify the stamp ID is correct
- Check you're connected to the right Bee node with
GET /api/v1/stamps/ - The stamp may have expired and been removed from the network
Cause: The stamp exists on the network but wasn't purchased through this node.
Solutions:
- Use a stamp with
"local": truefromGET /api/v1/stamps/ - Connect to the Bee node that owns this stamp
- Purchase a new stamp through this node
Cause: After purchasing a stamp, there's a ~2 minute propagation delay before it can be used.
Solutions:
- Wait for the stamp to propagate (~2 minutes after purchase)
- Check stamp status with
GET /api/v1/stamps/{stamp_id}/check— use thepropagationStatusfield - The
propagationStatusfield transitions:"propagating"→"ready"when the stamp becomes usable - Use
estimatedReadyAt(ISO 8601) to know when to retry
Propagation timing fields (available on all stamp responses):
secondsSincePurchase: How long since purchase through this gateway (null for external stamps)estimatedReadyAt: ISO 8601 timestamp when stamp should be readypropagationStatus:"propagating"(waiting),"ready"(usable),"unknown"(not tracked by this gateway)
Example workflow after purchase:
# Purchase stamp
curl -X POST "http://localhost:8000/api/v1/stamps/" \
-H "Content-Type: application/json" \
-d '{"duration_hours": 25, "size": "small"}'
# Response: {"batchID": "abc123...", "message": "..."}
# Check propagation status (repeat until propagationStatus="ready")
curl "http://localhost:8000/api/v1/stamps/abc123.../check"
# Response includes: "propagationStatus": "propagating", "estimatedReadyAt": "2026-03-16T12:02:00+00:00"
# Now upload (once propagationStatus="ready")
curl -X POST "http://localhost:8000/api/v1/data/?stamp_id=abc123..." \
-F "file=@data.json"Cause: The stamp's TTL has reached 0.
Solutions:
- Purchase a new stamp with
POST /api/v1/stamps/ - Extend an existing non-expired stamp with
PATCH /api/v1/stamps/{id}/extend
Cause: The stamp has reached 100% utilization and cannot accept more data.
Solutions:
- Purchase a new stamp with larger capacity (
size: "large") - Use a different stamp that has remaining capacity
When uploads fail due to stamp issues, the API returns structured error responses with actionable suggestions:
{
"detail": {
"code": "NOT_USABLE",
"message": "Stamp is not yet usable for uploads. If this stamp was recently purchased, it may take 30-90 seconds for the network to propagate the stamp.",
"suggestion": "Wait 30-90 seconds after purchase and try again. Check stamp status with GET /api/v1/stamps/{stamp_id}/check to monitor when it becomes usable.",
"stamp_id": "abc123...",
"stamp_status": {
"exists": true,
"local": true,
"usable": false,
"utilizationPercent": 0,
"batchTTL": 86400
}
}
}Use validate_stamp=true to check stamp validity before upload:
curl -X POST "http://localhost:8000/api/v1/data/?stamp_id=abc123...&validate_stamp=true" \
-F "file=@data.json"This adds a small latency overhead but catches common stamp issues early with clear error messages.
This project is licensed under the MIT License - see the LICENSE file for details.
- ✅ Commercial use - Use in commercial applications
- ✅ Distribution - Distribute copies or substantial portions
- ✅ Modification - Modify and create derivative works
- ✅ Private use - Use privately without restrictions
⚠️ Include license - Include MIT license and copyright notice- ❌ No liability - No warranty or liability from authors
The MIT License encourages adoption while maintaining attribution, making it ideal for:
- Research and academic projects
- Commercial integrations
- Open-source ecosystem growth
- Ethereum/Web3 community standards