Skip to content

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

Payment Channel Flow

๐Ÿ’ก Implementation Details

Channel Creation Flow

  1. 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.
  2. Channel Creation: Consumer calls createChannel() with initial deposit and duration
  3. Proxy Deployment: Factory deploys minimal proxy contract pointing to PaymentChannel implementation
  4. Initialization: Channel contract initialized with consumer, provider, token, and pricing details
  5. 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 request
    • createChannel(...): 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 parameters
    • close(...): Settle payments with signature verification
    • claimTimeout(): Emergency fund recovery after expiration
    • deposit(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

Terminal
# 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

Terminal
# 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