Skip to content

feat: implement fixed-price x402 payments for chunk requests without rate limiter token credits #527

@djwhitt

Description

@djwhitt

Summary

Change chunk GET request payment handling to use a fixed-price per-request model that bypasses rate limiting, rather than the current model which adds paid tokens to rate limiter buckets. This maintains the free tier rate limiting while providing a simpler, more predictable paid access model for chunk endpoints.

Current Behavior

When x402 payments are enabled for chunk GET requests (/chunk/:offset):

  1. Rate limiter uses CHUNK_GET_BASE64_SIZE_BYTES (default: ~360 KiB) to predict token consumption
  2. When rate limit exceeded, gateway returns 402 Payment Required
  3. Client makes payment and includes payment header in retry request
  4. Payment is verified and settled
  5. Paid tokens are added to the client's IP bucket (with X_402_RATE_LIMIT_CAPACITY_MULTIPLIER, default 10x)
  6. These tokens are consumed for current and future requests
  7. Tokens are adjusted post-response based on actual size

Code reference: src/routes/chunk/handlers.ts:68-126

Proposed Behavior

Introduce a new fixed-price payment bypass model for chunks:

  1. Rate limiter continues to enforce free tier limits (unchanged)
  2. Client can proactively include payment header OR receive 402 when rate limited
  3. If valid payment header present:
    • Skip rate limit checks entirely (neither consume nor add tokens)
    • Charge fixed price per chunk request (new config: CHUNK_PAYMENT_FIXED_PRICE_USDC)
    • Verify and settle payment as normal
    • Serve chunk data
  4. If no payment header:
    • Apply normal rate limiting using CHUNK_GET_BASE64_SIZE_BYTES for token calculation
    • Return 429/402 when limits exceeded

Rationale

Why this change?

  1. Simpler mental model: Users pay fixed price per chunk vs. accumulating bucket credits
  2. Predictable costs: Every chunk request costs exactly the same amount
  3. No token accounting complexity: Eliminates prediction → adjustment → bucket credit flow
  4. Better for high-frequency chunk access: Users who need many chunks can pay per-request without managing bucket state
  5. Preserves free tier: Rate limiting still protects against abuse for unpaid access
  6. Aligns with chunk characteristics: Chunks have relatively uniform size, making fixed pricing natural

Why not the current model for chunks?

  • Chunks are infrastructure primitives, not user content
  • Bucket credit model adds complexity when users just want "pay to access this chunk"
  • Size variance is lower than general data endpoints, so fixed pricing is fairer
  • Per-request payment is simpler for automated/programmatic access patterns

Technical Implementation Approach

1. New Configuration Variables

# Separate chunk payment pricing from rate limiter sizing
CHUNK_PAYMENT_FIXED_PRICE_USDC=0.001        # Fixed price per chunk request
CHUNK_PAYMENT_BYPASS_RATE_LIMIT=true        # Payment bypasses rate limits (default true)

# Existing variable - still used for rate limiter token calculation only
CHUNK_GET_BASE64_SIZE_BYTES=368640          # For free tier rate limiting

2. Payment Processing Flow

GET /chunk/:offset with X-Payment header
  ↓
Check if CHUNK_PAYMENT_BYPASS_RATE_LIMIT enabled
  ↓
Verify payment (fixed amount, not size-based)
  ↓
Settle payment immediately
  ↓
Skip rate limiter entirely
  ↓
Fetch and serve chunk
  ↓
NO token adjustment, NO bucket credits

3. Code Changes Required

A. New payment processor method (or parameter):

// New method or flag for fixed-price non-bucket payment
interface PaymentProcessor {
  // Existing
  verifyAndSettlePayment(...): Promise<PaymentResult>;
  
  // New - fixed price, no bucket integration
  verifyAndSettleFixedPricePayment(
    fixedPriceUSDC: number,
    ...
  ): Promise<PaymentResult>;
}

B. Update chunk handler (src/routes/chunk/handlers.ts):

// Before rate limit check
if (paymentProcessor && CHUNK_PAYMENT_BYPASS_RATE_LIMIT) {
  const paymentHeader = extractPaymentHeader(request);
  
  if (paymentHeader) {
    // Verify and settle at fixed price
    const paymentResult = await paymentProcessor.verifyAndSettleFixedPricePayment(
      CHUNK_PAYMENT_FIXED_PRICE_USDC,
      paymentHeader,
      request,
      response
    );
    
    if (paymentResult.verified && paymentResult.settled) {
      // Skip rate limiter entirely - proceed to fetch chunk
      // NO token consumption, NO bucket credits
    } else {
      // Payment failed - return 402
      return;
    }
  }
}

// If no payment or bypass disabled, proceed with normal rate limiting
if (rateLimiter !== undefined) {
  // Existing rate limit logic for free tier
}

C. Update payment response generation:

  • When returning 402 for chunks, include fixed price in payment requirement
  • Don't calculate based on perBytePrice, use fixed CHUNK_PAYMENT_FIXED_PRICE_USDC

D. Documentation updates:

  • Update docs/x402-and-rate-limiting.md to explain dual model
  • Update docs/envs.md with new config variables
  • Add examples showing chunk-specific payment patterns

4. Backward Compatibility

Option A: Feature flag (recommended for initial rollout)

# Default to false initially, operators opt-in
CHUNK_PAYMENT_FIXED_PRICE_ENABLED=false

Option B: Automatic detection

# If CHUNK_PAYMENT_FIXED_PRICE_USDC is set, use fixed-price model
# Otherwise, use existing token-bucket model

Open Questions

  1. Should HEAD requests also support fixed-price payment bypass?

    • Current: HEAD uses 0 bytes for rate limiting
    • Proposal: HEAD requests are free (no payment), but also respect rate limits
  2. Should we expose payment bypass availability in /ar-io/info?

    {
      "x402": {
        "dataEgress": { /* existing */ },
        "chunkRequests": {
          "fixedPriceEnabled": true,
          "fixedPriceUSDC": "0.001000",
          "bypassesRateLimits": true
        }
      }
    }
  3. Metrics: How to track fixed-price chunk payments separately?

    • New metric: x402_chunk_fixed_price_payments_total
    • Distinguish from bucket-credit payments in existing metrics
  4. Should POST /chunk remain unmetered?

    • Current: Not rate limited
    • Keep as-is since it's for data ingestion, not egress

Testing Considerations

  1. Unit tests: Payment verification without rate limiter integration
  2. Integration tests:
    • Payment bypasses rate limits
    • No tokens added to buckets
    • Fixed price regardless of response size
  3. E2E tests:
    • Free tier: rate limited as normal
    • Paid tier: unlimited access with per-request payment
    • Mixed: some requests paid, some free

Related Code References

  • Chunk handler: src/routes/chunk/handlers.ts:68-126
  • Rate limiter integration: src/handlers/data-handler-utils.ts:76-408
  • Chunk endpoint documentation: docs/x402-and-rate-limiting.md:76-91
  • Configuration: src/config.ts:441-443

Priority & Scope

Priority: Medium - enhances but doesn't fix broken functionality

Scope:

  • New payment flow for chunks (no bucket integration)
  • Configuration additions
  • Documentation updates
  • Metrics updates

Out of scope (defer to future work):

  • Applying fixed-price model to other endpoints
  • Retroactive bucket credit refunds
  • Payment plan/subscription models

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions