Payment Channels
Payment channels enable high-frequency micropayments between API consumers and providers through off-chain payment commitments backed by on-chain escrow. This method is ideal for frequent API calls, allowing you to deposit tokens once into a smart contract and make multiple API requests with cryptographic signatures, eliminating gas fees for individual transactions.
How Payment Channels Work
Users deposit tokens into a smart contract that acts as escrow between the consumer and API provider. Each API request includes a cryptographic signature that authorizes a small payment from this deposit. The provider can later settle multiple payments in a single on-chain transaction, making this extremely cost-effective for high-frequency usage.
Best Use Cases
- High-frequency API access: Make thousands of API calls without individual gas fees
- Micropayment workflows: Pay precise amounts per API request (e.g., $0.001 per call)
- Trusted recurring payments: Establish ongoing payment relationships with API providers
- Batch settlement: Settle multiple payments in a single on-chain transaction
How it works & Architecture
๐ก Implementation Details
Channel Creation Flow
- Provider Registration: API provider registers their price per request in the `ChannelFactory. Optionally they can publish this on to a discovery service for easier client access.
- Channel Creation: Consumer calls
createChannel()
with initial deposit and duration - Proxy Deployment: Factory deploys minimal proxy contract pointing to
PaymentChannel
implementation - Initialization: Channel contract initialized with consumer, provider, token, and pricing details
- Escrow Lock: Initial deposit transferred to channel contract as escrow
X-Payment Header Payload
Payment channels use the channel
scheme with the x402 v1 format:
{
"x402Version": 1,
"network": "base",
"scheme": "channel",
"payload": {
"signature": "0x1234...",
"message": "0xabcd...",
"paymentChannel": {
"channelId": "1234567890",
"address": "0x...",
"sender": "0x...",
"recipient": "0x...",
"balance": "95.5",
"nonce": 42,
"expiration": 1672531200
},
"timestamp": 1672531200
}
}
Key components:
- x402Version: Protocol version (1)
- network: Blockchain network (e.g., "base")
- scheme: Payment method ("channel")
- signature: Consumer's cryptographic authorization
- message: Hash of the signed data
- paymentChannel: Complete channel state information
- timestamp: When payment was created (5-minute validity window)
Verification Process
The middleware performs comprehensive verification in this order:
1. Header Validation
- Parse and validate X-Payment JSON structure
- Verify x402Version is 1 and scheme matches "channel"
- Check timestamp is within acceptable window (5 minutes)
2. Message Reconstruction & Signature Verification
- Reconstruct the signed message from channelId, balance, nonce, and request body
- Verify the message hash matches the submitted hash
- Recover signer address from signature and confirm it matches the channel sender
3. Channel State Validation
For existing channels:
- Verify nonce is greater than last processed nonce
- Calculate payment amount and validate against channel balance
- Ensure channel hasn't expired
For new channels:
- Query channel contract on-chain to validate existence and parameters
- Ensure nonce starts at 0 for first payment
4. State Update
- Update local channel state with new balance and nonce
- Store latest signature for potential on-chain settlement
State Management
Payment channels maintain state in two locations:
Server-Side State (In-Memory)
pub struct ChannelState {
channels: HashMap<U256, PaymentChannel>,
latest_signatures: HashMap<U256, PrimitiveSignature>,
}
Critical Limitations:
- State lost on server restart or crash
- No persistent storage or recovery mechanism
- All pending channel balances reset to on-chain values
Client-Side State (In-Memory)
- Channel configuration and current balance
- Nonce tracking for replay protection
- Private key for signature generation
Recovery Mechanisms:
- Consumers can claim expired channel funds directly
- Providers can close channels and withdraw earned funds
- Unused credits refunded when channels are properly closed
Settlement Process
Payment channels support multiple settlement scenarios:
1. Provider-Initiated Closure
Providers can close channels and withdraw earned funds using the latest signature:
- Submit the most recent signature with final channel state
- Contract validates the signature and calculates earned amount
- Earned funds transferred to provider, remaining balance refunded to consumer
- Channel permanently closed
2. Timeout Claims
Consumers can recover funds from expired channels:
- Available after channel expiration timestamp passes
- Only works if provider hasn't closed the channel first
- Emergency recovery mechanism for unresponsive providers
Smart Contract Architecture
ChannelFactory Contract
- Purpose: Creates and manages payment channels using minimal proxy pattern
- Key Functions:
register(uint256 price)
: Provider sets price per requestcreateChannel(...)
: Deploy new payment channel proxy- Gas-optimized proxy deployment reduces channel creation costs
PaymentChannel Contract
- Purpose: Individual escrow contract for consumer-provider payments
- States: Uninitialized โ Open โ Closed
- Key Functions:
init(...)
: Initialize channel with parametersclose(...)
: Settle payments with signature verificationclaimTimeout()
: Emergency fund recovery after expirationdeposit(uint256)
: Add funds to existing channel
Security Features
- Nonce-based replay protection: Prevents signature reuse
- Timestamp validation: Limits signature validity window
- State transition guards: Prevents unauthorized state changes
- Signature verification: Cryptographic proof of payment authorization
Network Integration
Payment channels currently support:
- Base Sepolia Testnet: Primary testing environment
- Token Support: Any ERC-20 token (USDC recommended)
- Factory Address:
0x5acfbe1f9B0183Ef7F2F8d8993d76f24B862092d
v0.6.0+ Unified Approach (Recommended)
For API Consumers
import { withPaymentInterceptor, ClientInterceptor } from "pipegate-sdk";
// Create channel (same process as before)
const pipeGate = new ClientInterceptor();
const channel = await pipeGate.createPaymentChannel({
recipient: "0x...",
duration: 2592000,
tokenAddress: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
amount: "100",
});
// Single unified interceptor with automatic state management
const client = withPaymentInterceptor(
axios.create({ baseURL: "https://api.example.com" }),
PRIVATE_KEY,
{ channel: channel }
);
// Automatic payment and state updates
const response = await client.get("/endpoint");
For API Providers
use pipegate::middleware::{PaymentsLayer, PaymentsState, Scheme, SchemeConfig};
// Configure channel payments
let channel_config = SchemeConfig::new(
Scheme::PaymentChannels,
"https://base-sepolia-rpc.publicnode.com".to_string(), // Base Sepolia testnet
token_address, // USDC or other ERC-20 token address
recipient_address, // Provider's address to receive channel payments
"0.001".to_string(), // Price per API request in tokens
).await;
// Single middleware for all schemes
let app = Router::new()
.route("/api", get(handler))
.layer(PaymentsLayer::new(
PaymentsState::new(),
MiddlewareConfig::new(vec![channel_config])
));
Legacy Implementation
For API Users
Create Payment Channel
import { ClientInterceptor } from "pipegate-sdk";
const pipeGate = new ClientInterceptor();
// Update these params with what you get from the API provider
const channelParams = {
recipient: "0x...", // API provider's address
duration: 2592000, // 30 days
tokenAddress: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // USDC
amount: "100", // 100 USDC
};
const channel = await pipeGate.createPaymentChannel(channelParams);
await pipeGate.addNewChannel(channel.channelId, channel);
or If you want to use the cast CLI tool, refer to the steps here
Setup API Client
const api = axios.create({
baseURL: "https://api.example.com",
});
api.interceptors.request.use(
pipeGate.createPaymentChannelRequestInterceptor(channelId).request
);
api.interceptors.response.use(
pipeGate.createPaymentChannelResponseInterceptor().response
);
Monitor Channel State
const channelState = pipeGate.getChannelState(channelId);
console.log("Balance:", channelState?.balance);
console.log("Expiration:", channelState?.expiration);
For API Providers
Configure Middleware
use pipegate::middleware::payment_channel::{channel::ChannelState, types::PaymentChannelConfig};
use pipegate::utils::{Address, Url, U256};
let rpc_url: Url = "https://base-rpc.publicnode.com".parse().unwrap();
let state = ChannelState::new();
let config = PaymentChannelConfig {
recipient: Address::from_str("YOUR_ADDRESS").unwrap(),
token_address: Address::from_str("USDC_ADDRESS").unwrap(),
amount: U256::from(1000), // 0.001 USDC in this case
rpc_url: rpc_url.to_string(),
};
Attach Middleware layer
use pipegate::middleware::payment_channel::{PaymentChannelMiddlewareLayer};
let app = Router::new()
.route("/", get(root))
.layer(PaymentChannelMiddlewareLayer::new(state, config));
Register Price in Channel Factory
# Set your price per request (1000 = 0.001 USDC)
cast send $FACTORY_ADDRESS "register(uint256)" 1000 \
--rpc-url $RPC_URL \
--private-key $PRIVATE_KEY
Handle Channel Closure
// Close channel and withdraw funds
let tx_hash = close_channel(
rpc_url,
private_key.as_str(),
&payment_channel,
&signature,
raw_body,
);
or use CLI to close the channel
# Close the channel to withdraw 1 USDC with a nonce of 1000, along with the signature received during the API calls
# Factory Address: 0x5acfbe1f9B0183Ef7F2F8d8993d76f24B862092d
cast send $FACTORY_ADDRESS "close(uint256 channelBalance,uint256 nonce,bytes calldata rawBody,bytes calldata signature)" 1000 1000 0x0 $SIGNATURE \
--rpc-url $RPC_URL \
--private-key $PRIVATE_KEY
Example for channel closure can be found here
Limitations & State Management
Server-Side Limitations
- State Loss on Restart: If the server restarts or goes down, all payment channel state is lost
- Transaction History: All pending transactions in the payment channel are instantly dropped when state is lost
- Recovery: No automatic recovery mechanism for lost state
Client-Side Limitations
- Interceptor Loss: If the client interceptor is dropped or application restarts, the payment channel cannot be reused temporarily
- Manual Recovery: Users need to wait for channel closure to recover unused funds
- Refund Process: Unused credits are refunded when the recipient closes the channel
Fund Recovery Options
For Recipients (API Providers)
Use the new close_and_withdraw_from_state
function for automatic withdrawal:
use pipegate::middleware::payment_channel::channel::close_channel_from_state;
// Automatically close channel and withdraw funds from current state
let tx_hash = close_channel_from_state(
&channel_state,
rpc_url,
private_key.as_str(),
channel_id,
raw_body,
);
For Users (API Consumers)
- Active Channel: Wait for recipient to close the channel to receive refund of unused credits
- Expired Channel: Claim funds directly if the channel has expired and hasn't been closed by the recipient
# Claim expired channel funds (call directly on the channel contract)
cast send $CHANNEL_ADDRESS "claimTimeout()" \
--rpc-url $RPC_URL \
--private-key $PRIVATE_KEY
Best Practices
- Always monitor channel balance
- Set appropriate channel duration
- Handle channel expiration gracefully
- Implement proper error handling
- Plan for state loss: Consider implementing state persistence mechanisms
- Monitor server uptime: Ensure reliable server operation to maintain channel state
- Educate users: Inform API consumers about potential state loss scenarios