BLOBKIT v2.0.0

🌟 Introduction

10-100x Cheaper Ethereum Storage

EIP-4844 introduced blob space to Ethereum. This is a new type of data storage that costs 10-100x less than regular transactions. BlobKit makes it easy to use this new storage layer, even though wallets don't support it yet.

The numbers: Store ~124KB of data for $0.01-$1. Data stays available for 18 days, which is perfect for temporary storage needs.

What Are Blobs?

Blobs are a new type of data attached to Ethereum transactions. They were added in March 2024 to make Ethereum cheaper for Layer 2 rollups.

How Blobs Are Different

  • Size: Each blob is 128KB (131,072 bytes), but usable capacity is ~124KB after encoding overhead
  • Cost: Uses separate gas pricing that's much cheaper than regular gas
  • Storage: Data is guaranteed available for 18 days, then nodes can delete it
  • Commitment: Uses KZG cryptography to prove data availability without storing it forever
  • Access: Can't be accessed from smart contracts, only from consensus layer

Think of blobs like a bulletin board: You can post notices that everyone can read for a few weeks, then they get taken down. Much cheaper than carving messages in stone (permanent storage).

Why BlobKit?

Blob transactions are powerful but complex. They require special cryptography (KZG commitments) and a new transaction format that wallets like MetaMask don't support yet. BlobKit solves these problems.

Without BlobKit

  • ❌ Implement KZG cryptography yourself
  • ❌ Build custom transaction encoding
  • ❌ Can't use MetaMask or other wallets
  • ❌ Handle blob gas pricing manually
  • ❌ Write custom archival logic

With BlobKit

  • ✅ One function to write blobs
  • ✅ Works with any wallet
  • ✅ Automatic KZG handling
  • ✅ Built-in cost estimation
  • ✅ Archive support included

What Can You Build?

Layer 2 Rollups

Rollups post transaction batches to blob space. This is why blob space was created. Arbitrum, Optimism, and others save millions in fees using blobs.

Decentralized Social Media

Store posts, comments, and media that need temporary blockchain guarantees. Perfect for content that needs censorship resistance for a few weeks.

Gaming & NFTs

Store game states, leaderboards, or NFT metadata during reveal events. Much cheaper than permanent storage for temporary data.

Data Availability

Build data availability layers for sidechains or validiums. Guarantee data is available without paying for permanent storage.

🚀 Quickstart

Get your first blob on Ethereum in 5 minutes. We'll use the Sepolia testnet so it's free to try.

Prerequisites

You Need These

  • Node.js v16+ (nodejs.org)
  • A code editor like VS Code
  • An Ethereum RPC URL (free from Alchemy)
  • A test wallet with private key
  • Some Sepolia ETH (free from faucet)

Your First Blob

Step 1: Set up project

BASH
mkdir my-blob-app
cd my-blob-app
npm init -y
npm install @blobkit/sdk ethers dotenv

Step 2: Configure environment

.env
# Create .env file with these values:
RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY
PRIVATE_KEY=0xYOUR_PRIVATE_KEY
CHAIN_ID=11155111

# Use proxy because MetaMask can't send blob transactions
PROXY_URL=https://proxy-sepolia.blobkit.org

Step 3: Write and run code

index.js
require('dotenv').config();
const { BlobKit, initializeKzg } = require('@blobkit/sdk');
const { ethers } = require('ethers');

async function main() {
  // Initialize KZG cryptography (required for blobs)
  await initializeKzg();
  
  // Connect to Ethereum
  const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
  const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
  
  // Create BlobKit with proxy (because wallets can't send blobs)
  const blobkit = new BlobKit({
    rpcUrl: process.env.RPC_URL,
    chainId: parseInt(process.env.CHAIN_ID),
    proxyUrl: process.env.PROXY_URL  // Proxy handles blob transaction
  }, wallet);
  
  // Initialize async components
  await blobkit.initialize();
  
  // Write data to blob space
  const data = { message: 'Hello Blobs!', timestamp: Date.now() };
  const receipt = await blobkit.writeBlob(data);
  
  console.log('Blob stored! TX:', receipt.blobTxHash);
  
  // Read it back
  const retrieved = await blobkit.readBlobAsJSON(receipt.blobTxHash);
  console.log('Retrieved:', retrieved);
}

main().catch(console.error);

Run it:

BASH
node index.js

🔧 How It Works

Understanding how BlobKit works helps you use it effectively. Let's break down the architecture.

Blob Transactions Explained

Blob transactions (Type 3) are special Ethereum transactions that carry blob data. They're different from regular transactions:

Transaction Structure

JAVASCRIPT
// Regular transaction (Type 2)
{
  to: "0x...",
  value: "1000000000000000000",
  data: "0x...",
  gasLimit: 21000,
  maxFeePerGas: "20000000000"
}

// Blob transaction (Type 3) 
{
  to: "0x...",
  value: "1000000000000000000",
  data: "0x...",
  gasLimit: 21000,
  maxFeePerGas: "20000000000",
  
  // New blob fields
  blobVersionedHashes: ["0x01..."],  // KZG commitments
  maxFeePerBlobGas: "1000000000",    // Blob gas price
  blobs: [/* 128KB blob data */],    // Actual data
  kzgCommitments: ["0x..."],         // Cryptographic commitments
  kzgProofs: ["0x..."]                // Cryptographic proofs
}

The blob data isn't stored in blocks. Only the commitment (a cryptographic hash) goes on-chain. The actual blob data is shared separately and nodes keep it for 18 days.

The Wallet Problem

Why Wallets Can't Send Blobs

MetaMask, Rainbow, and other wallets don't support Type 3 transactions yet. They can't:

  • • Generate KZG commitments and proofs
  • • Format blob transactions correctly
  • • Calculate blob gas costs
  • • Display blob data to users

This is a chicken-and-egg problem. Wallets won't add support until there are apps using blobs, but apps can't use blobs without wallet support. BlobKit solves this with a proxy system.

The Proxy Solution

Since wallets can't send blob transactions, BlobKit uses a proxy server that can. Here's how it works:

Proxy Flow

  1. 1. You send data to proxy: Your app sends the blob data to the proxy server
  2. 2. Proxy creates blob transaction: Server generates KZG commitments and formats Type 3 transaction
  3. 3. Proxy pays gas: Server wallet pays the gas fees upfront
  4. 4. You reimburse proxy: Cost is deducted from your escrow deposit
  5. 5. You get receipt: Transaction hash returned for reading the blob

Why this works: The proxy has a regular Ethereum wallet that can send any transaction type. Your wallet just needs to deposit funds to the escrow contract (a regular transaction that any wallet can do).

JAVASCRIPT
// Without proxy (doesn't work with MetaMask)
const tx = await wallet.sendTransaction({
  type: 3,  // MetaMask: "Unknown transaction type"
  blobs: [blobData],
  kzgCommitments: [commitment],
  // ... more blob fields
});

// With proxy (works with any wallet)
const blobkit = await BlobKit.init({
  proxyUrl: 'https://proxy.blobkit.org'
}, wallet);

// Just send data, proxy handles everything
const receipt = await blobkit.writeBlob(data);

Escrow System

The proxy needs payment for gas fees. This happens through an escrow smart contract:

How Escrow Works

  1. 1. Deposit funds: You deposit ETH to the escrow contract
  2. 2. Submit blob: You request blob storage through proxy
  3. 3. Proxy executes: Proxy sends blob transaction and pays gas
  4. 4. Proxy claims payment: Proxy proves job completion to escrow
  5. 5. Escrow releases funds: Contract pays proxy from your deposit
SOLIDITY
// Escrow contract functions
contract BlobKitEscrow {
  // User deposits funds
  function deposit() payable {
    balances[msg.sender] += msg.value;
  }
  
  // User creates job (reserves funds)
  function createJob(jobId, amount) {
    require(balances[msg.sender] >= amount);
    balances[msg.sender] -= amount;
    jobs[jobId] = Job(msg.sender, amount, false);
  }
  
  // Proxy completes job (gets paid)
  function completeJob(jobId, blobTxHash, proof) {
    require(authorizedProxies[msg.sender]);
    require(verifyProof(jobId, blobTxHash, proof));
    
    Job storage job = jobs[jobId];
    require(!job.completed);
    
    job.completed = true;
    payable(msg.sender).transfer(job.amount);
  }
  
  // User can refund if job expires (5 minutes)
  function refundExpiredJob(jobId) {
    Job storage job = jobs[jobId];
    require(block.timestamp > job.timestamp + 5 minutes);
    require(!job.completed);
    
    balances[job.user] += job.amount;
  }
}

Trust model: The escrow contract ensures the proxy can only take payment after proving it submitted your blob. If the proxy fails, you get an automatic refund after 5 minutes.

📦 Installation

Package Installation

BASH
# NPM
npm install @blobkit/sdk ethers

# Yarn
yarn add @blobkit/sdk ethers

# PNPM
pnpm add @blobkit/sdk ethers

Note: ethers.js is a peer dependency. BlobKit uses it for Ethereum interactions but doesn't bundle it to avoid version conflicts with your app.

Environment Setup

.env
# Required
RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY
PRIVATE_KEY=0xYOUR_PRIVATE_KEY
CHAIN_ID=11155111  # Sepolia testnet

# Proxy (required because wallets don't support blobs)
PROXY_URL=https://proxy-sepolia.blobkit.org

# Optional BlobKit-specific variables
BLOBKIT_RPC_URL=...        # Override RPC for BlobKit
BLOBKIT_ARCHIVE_URL=...    # Archive service for old blobs
BLOBKIT_LOG_LEVEL=info     # debug, info, warn, error

Security: Never commit .env files. Add to .gitignore immediately.

💻 Basic Usage

Initialization

Important: Always call initializeKzg() first. This loads the cryptographic parameters needed for blob commitments.

TYPESCRIPT
import { BlobKit, initializeKzg } from '@blobkit/sdk';
import { ethers } from 'ethers';

// Step 1: Initialize KZG (once per app)
await initializeKzg();

// Step 2: Set up provider and wallet
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);

// Step 3: Create BlobKit instance
const blobkit = await BlobKit.init({
  rpcUrl: process.env.RPC_URL,
  chainId: 11155111,  // Sepolia
  proxyUrl: process.env.PROXY_URL  // Required for wallet support
}, wallet);

// Alternative: Use environment variables
import { createFromEnv } from '@blobkit/sdk';
const blobkit = createFromEnv(wallet);  // Reads BLOBKIT_* env vars

Writing Data

TYPESCRIPT
// Write any JSON-serializable data
const receipt = await blobkit.writeBlob({
  type: 'message',
  content: 'Hello World',
  timestamp: Date.now()
});

// Receipt contains:
console.log(receipt.jobId);        // Unique job ID
console.log(receipt.blobTxHash);   // Transaction hash
console.log(receipt.blobHash);     // Blob commitment hash
console.log(receipt.status);       // 'pending' | 'confirmed' | 'failed'

// Write with metadata (useful for indexing)
await blobkit.writeBlob(data, {
  appId: 'my-app',
  version: '1.0.0',
  contentType: 'application/json'
});

Reading Data

TYPESCRIPT
// Read as JSON
const data = await blobkit.readBlobAsJSON(txHash);

// Read as string
const text = await blobkit.readBlobAsString(txHash);

// Read raw bytes
const result = await blobkit.readBlob(txHash);
console.log(result.data);    // Uint8Array
console.log(result.source);  // 'rpc' | 'archive'

// Note: Blobs expire after 18 days
// Configure archive for permanent access:
const blobkit = await BlobKit.init({
  rpcUrl: '...',
  archiveUrl: 'https://api.blobscan.com'
}, wallet);

Cost Management

TYPESCRIPT
// Always estimate costs first
const data = { /* your data */ };
const bytes = new TextEncoder().encode(JSON.stringify(data));

const estimate = await blobkit.estimateCost(bytes);
console.log('Cost:', estimate.totalCostEth, 'ETH');

// Set a maximum cost
const MAX_COST = 0.01;  // 0.01 ETH
if (parseFloat(estimate.totalCostEth) > MAX_COST) {
  throw new Error('Cost too high, try again later');
}

// Proceed with write
await blobkit.writeBlob(data);

🔄 Proxy System

The proxy system is essential because wallets can't send blob transactions. You can use the hosted proxy or deploy your own for full control.

Using the Hosted Proxy

🎉 The Hosted Proxy is FREE!

We provide free hosted proxy servers for both mainnet and testnet. There are no fees, no registration required, and no hidden costs. You only pay for the actual blob gas costs on Ethereum.

Hosted Proxy Details

  • Mainnet Proxy: https://proxy.blobkit.org
  • Mainnet Escrow: 0x2e8e414bc5c6B0b8339853CEDf965B4A28FB4838
  • Sepolia Proxy: https://proxy-sepolia.blobkit.org
  • Sepolia Escrow: 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD77
  • Proxy Fee: FREE (0%) - Hosted proxy has no fees!
  • Rate Limit: 10 requests per minute (default)
  • Max Blob Size: ~124KB usable
  • Job Timeout: 5 minutes (300 seconds)
TYPESCRIPT
// Step 1: Configure BlobKit with proxy
const blobkit = await BlobKit.init({
  rpcUrl: 'YOUR_RPC_URL',
  chainId: 11155111,  // Sepolia
  proxyUrl: 'https://proxy-sepolia.blobkit.org',
  escrowContract: '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD77'  // Sepolia escrow
}, wallet);

// Step 2: Write blob (deposit happens automatically!)
// BlobKit handles everything internally:
// - Estimates the cost
// - Deposits exact amount to escrow
// - Submits to proxy
// - Returns receipt
const receipt = await blobkit.writeBlob(data);
console.log('Blob written:', receipt.transactionHash);

// Optional: Check if you have any remaining balance in escrow
const balance = await blobkit.getBalance();
if (balance > 0n) {
  console.log('Remaining escrow balance:', ethers.formatEther(balance), 'ETH');
}

// Step 3: Blob is written!
console.log('Success! Transaction:', receipt.blobTxHash);
console.log('Job ID:', receipt.jobId);
console.log('Block number:', receipt.blockNumber);

// You can read it back
const readResult = await blobkit.readBlob(receipt.blobTxHash);
console.log('Read back data:', readResult.data);

Escrow Management

How Much to Deposit?

  • Minimum: 0.01 ETH (covers ~10-100 blobs depending on gas)
  • Recommended: 0.1 ETH for regular usage
  • Calculate: Number of blobs × 0.001 ETH (average cost)
TYPESCRIPT
// Check your escrow balance
const balance = await blobkit.getEscrowBalance();
const address = await blobkit.getAddress();

console.log('Wallet:', address);
console.log('Escrow balance:', ethers.formatEther(balance), 'ETH');

// Estimate if you have enough for your data
const data = { /* your data */ };
const bytes = new TextEncoder().encode(JSON.stringify(data));
const estimate = await blobkit.estimateCost(bytes);

const estimatedCost = ethers.parseEther(estimate.totalCostEth);
// Hosted proxy is FREE - no additional fees!
const proxyFee = 0n;  // 0% for hosted proxy
const totalCost = estimatedCost;  // Just the blob gas cost

console.log('Blob cost:', estimate.totalCostEth, 'ETH');
console.log('Proxy fee:', 'FREE (hosted proxy)');
console.log('Total cost:', ethers.formatEther(totalCost), 'ETH');

if (balance < totalCost) {
  console.log('Insufficient escrow balance!');
  const needed = totalCost - balance;
  console.log('Need to deposit:', ethers.formatEther(needed), 'ETH');
}

How Escrow Works

TYPESCRIPT
// IMPORTANT: Deposits are AUTOMATIC!
// You don't need to manually deposit - it happens during writeBlob

// Write a blob (deposit happens automatically)
const receipt = await blobkit.writeBlob(data);
console.log('Blob written:', receipt.blobTxHash);
console.log('Job ID:', receipt.jobId);

// The writeBlob method automatically:
// 1. Estimates the exact cost
// 2. Deposits that amount to escrow  
// 3. Proxy uses it to pay for blob gas
// 4. Returns receipt when complete

// Check if you have any balance from overpayment
const balance = await blobkit.getBalance();
if (balance > 0n) {
  console.log('Remaining balance:', ethers.formatEther(balance));
}

// If proxy fails, refund after 5 minute timeout
const expiredJobs = ['job1', 'job2'];  // Your expired job IDs
for (const jobId of expiredJobs) {
  try {
    const refundTx = await blobkit.refundIfExpired(jobId);
    await refundTx.wait();
    console.log('Refunded:', jobId);
  } catch (error) {
    console.log('Already refunded or not expired:', jobId);
  }
}

Deploy Your Own Proxy

💰 Set Your Own Fees

When you deploy your own proxy, you can set any fee percentage from 0% to 10%. This allows you to:

  • • Run it free for your users (0% fee)
  • • Monetize your proxy service (1-10% fee)
  • • Cover operational costs
  • • Create a sustainable business model

Running your own proxy gives you full control. You need to deploy two components:

1. Deploy Escrow Contract

BASH
# Clone BlobKit repository
git clone https://github.com/ETHCF/blobkit
cd blobkit/packages/contracts

# Install dependencies
npm install

# Configure deployment
export ESCROW_OWNER=0xYOUR_ADDRESS
export INITIAL_PROXIES=0xYOUR_PROXY_ADDRESS
export JOB_TIMEOUT=300  # 5 minutes in seconds

# Deploy contract
forge script script/Deploy.s.sol \
  --rpc-url YOUR_RPC_URL \
  --private-key YOUR_PRIVATE_KEY \
  --broadcast

# Note the deployed contract address

2. Run Proxy Server

BASH
# Using Docker
docker run -d \
  --name blobkit-proxy \
  -p 3001:3001 \
  -e RPC_URL="YOUR_RPC_URL" \
  -e PRIVATE_KEY="OPERATOR_PRIVATE_KEY" \
  -e ESCROW_CONTRACT="DEPLOYED_CONTRACT_ADDRESS" \
  -e REDIS_URL="redis://localhost:6379" \
  blobkit/proxy-server:latest

# Or run manually
cd packages/proxy-server
npm install
npm run build

# Create .env file
RPC_URL=YOUR_RPC_URL
PRIVATE_KEY=OPERATOR_PRIVATE_KEY  # Wallet that pays gas
ESCROW_CONTRACT=DEPLOYED_ADDRESS
REDIS_URL=redis://localhost:6379
PORT=3001

# Start server
npm run start

Important: The proxy operator wallet needs ETH to pay for blob transactions. Users reimburse these costs through the escrow contract.

Proxy Operations

Managing the Escrow Contract

JAVASCRIPT
// Add authorized proxy
await escrow.addAuthorizedProxy(proxyAddress);

// Set proxy fee (0-10% - only for your own proxy!)
// The hosted proxy runs at 0% fee
await escrow.setProxyFee(proxyAddress, 5);  // Example: 5% fee for your proxy

// Update job timeout (if needed - default is 5 minutes)
await escrow.updateJobTimeout(300);  // 5 minutes

// Pause in emergency
await escrow.pause();

// Check contract state
const isProxyAuthorized = await escrow.authorizedProxies(proxyAddress);
const proxyFee = await escrow.proxyFees(proxyAddress);
const timeout = await escrow.jobTimeout();

Monitoring Proxy Health

BASH
# Check proxy status
curl https://your-proxy.com/api/v1/health

# Response:
{
  "status": "healthy",
  "version": "2.0.0",
  "chainId": 1,
  "escrowContract": "0x...",
  "operatorAddress": "0x...",
  "operatorBalance": "1.5",
  "jobQueueSize": 3
}

# Monitor logs
docker logs -f blobkit-proxy

# Check metrics (Prometheus format)
curl https://your-proxy.com/metrics

🚀 Production Guide

Everything you need to run BlobKit in production with confidence.

Production Checklist

Before Going Live

  • Test on Sepolia testnet first
  • Set up error monitoring (Sentry, DataDog)
  • Implement retry logic with exponential backoff
  • Add escrow balance monitoring
  • Set cost limits per blob
  • Handle proxy downtime gracefully
  • Implement data archival for blobs older than 18 days
  • Set up cost tracking and alerts
  • Use environment variables for all secrets
  • Document blob data format for your team

Security Considerations

  • Private Keys: Never expose in code or logs
  • Escrow Deposits: Only deposit what you need
  • Data Validation: Always validate data before storing
  • Rate Limiting: Implement client-side rate limiting
  • Access Control: Restrict who can trigger blob writes

Monitoring & Debugging

Key Metrics to Track

TYPESCRIPT
// Monitoring setup
class BlobMonitor {
  constructor(blobkit) {
    this.blobkit = blobkit;
    this.metrics = {
      totalBlobs: 0,
      totalCost: 0,
      failures: 0,
      avgCostPerBlob: 0
    };
  }
  
  async trackWrite(data) {
    const startTime = Date.now();
    
    try {
      // Track cost
      const bytes = new TextEncoder().encode(JSON.stringify(data));
      const estimate = await this.blobkit.estimateCost(bytes);
      
      // Write blob
      const receipt = await this.blobkit.writeBlob(data);
      
      // Update metrics
      this.metrics.totalBlobs++;
      this.metrics.totalCost += parseFloat(estimate.totalCostEth);
      this.metrics.avgCostPerBlob = this.metrics.totalCost / this.metrics.totalBlobs;
      
      // Log success
      console.log({
        event: 'blob_write_success',
        jobId: receipt.jobId,
        txHash: receipt.blobTxHash,
        cost: estimate.totalCostEth,
        duration: Date.now() - startTime,
        escrowBalance: await this.blobkit.getEscrowBalance()
      });
      
      return receipt;
      
    } catch (error) {
      this.metrics.failures++;
      
      // Log failure
      console.error({
        event: 'blob_write_failure',
        error: error.message,
        code: error.code,
        duration: Date.now() - startTime
      });
      
      throw error;
    }
  }
  
  getMetrics() {
    return this.metrics;
  }
}

Debug Mode

TYPESCRIPT
// Enable debug logging
const blobkit = await BlobKit.init({
  rpcUrl: process.env.RPC_URL,
  chainId: 1,
  proxyUrl: 'https://proxy.blobkit.org',
  logLevel: 'debug'  // Shows all internal operations
}, wallet);

// Add request interceptor for debugging
blobkit.on('request', (req) => {
  console.log('Request:', {
    method: req.method,
    url: req.url,
    data: req.data
  });
});

blobkit.on('response', (res) => {
  console.log('Response:', {
    status: res.status,
    data: res.data
  });
});

// Track escrow balance changes
setInterval(async () => {
  const balance = await blobkit.getEscrowBalance();
  console.log('Escrow balance check:', ethers.formatEther(balance), 'ETH');
}, 60000);  // Check every minute

Cost Optimization

Cost Saving Strategies

  • 1. Batch small items: Combine multiple small pieces into one blob
  • 2. Compress data: Use gzip to reduce size by 50-90%
  • 3. Time submissions: Submit during low gas periods (weekends, nights)
  • 4. Set max costs: Skip high fee periods automatically
  • 5. Use testnet: Develop and test on Sepolia first
TYPESCRIPT
// Cost optimization implementation
class OptimizedBlobWriter {
  constructor(blobkit, options = {}) {
    this.blobkit = blobkit;
    this.maxCostEth = options.maxCostEth || 0.01;
    this.batchSize = options.batchSize || 50000;  // 50KB batches
    this.batch = [];
    this.batchBytes = 0;
  }
  
  // Add item to batch
  async add(item) {
    const itemBytes = new TextEncoder().encode(JSON.stringify(item));
    
    // If item alone is too big, compress it
    if (itemBytes.length > 100000) {
      return this.writeCompressed(item);
    }
    
    // If batch would exceed size, flush first
    if (this.batchBytes + itemBytes.length > this.batchSize) {
      await this.flush();
    }
    
    this.batch.push(item);
    this.batchBytes += itemBytes.length;
  }
  
  // Write batch to blob
  async flush() {
    if (this.batch.length === 0) return;
    
    const data = { 
      items: this.batch,
      count: this.batch.length,
      timestamp: Date.now()
    };
    
    // Check cost
    const bytes = new TextEncoder().encode(JSON.stringify(data));
    const estimate = await this.blobkit.estimateCost(bytes);
    
    if (parseFloat(estimate.totalCostEth) > this.maxCostEth) {
      console.log('Cost too high, waiting...');
      // Store locally and retry later
      await this.saveForLater(data);
      return;
    }
    
    // Write blob
    const receipt = await this.blobkit.writeBlob(data);
    console.log(`Flushed ${this.batch.length} items in 1 blob`);
    
    // Reset batch
    this.batch = [];
    this.batchBytes = 0;
    
    return receipt;
  }
  
  // Compress large items
  async writeCompressed(item) {
    const compressed = await gzip(JSON.stringify(item));
    return this.blobkit.writeBlob({
      compressed: true,
      data: compressed.toString('base64')
    });
  }
}

Gas Price Monitoring

TYPESCRIPT
// Monitor blob gas prices
async function getBlobGasPrice(provider) {
  const block = await provider.getBlock('latest');
  return block.blobGasPrice;  // In wei
}

// Wait for low gas
async function waitForLowGas(provider, maxGwei = 10) {
  while (true) {
    const gasPrice = await getBlobGasPrice(provider);
    const gwei = Number(gasPrice) / 1e9;
    
    if (gwei <= maxGwei) {
      console.log(`Gas price OK: ${gwei} gwei`);
      return;
    }
    
    console.log(`Gas too high: ${gwei} gwei, waiting...`);
    await new Promise(r => setTimeout(r, 60000));  // Check every minute
  }
}

// Use it
await waitForLowGas(provider, 10);  // Wait for <10 gwei
await blobkit.writeBlob(data);

📝 Examples

Complete Application

blob-app.ts
import { BlobKit, initializeKzg, BlobKitError, BlobKitErrorCode } from '@blobkit/sdk';
import { ethers } from 'ethers';
import * as dotenv from 'dotenv';

dotenv.config();

class BlobStorage {
  private blobkit: BlobKit;
  
  constructor(blobkit: BlobKit) {
    this.blobkit = blobkit;
  }
  
  async store(data: any, maxCost = 0.01): Promise<string> {
    // Check size
    const bytes = new TextEncoder().encode(JSON.stringify(data));
    if (bytes.length > 128000) {
      throw new Error('Data too large for single blob');
    }
    
    // Check cost
    const estimate = await this.blobkit.estimateCost(bytes);
    if (parseFloat(estimate.totalCostEth) > maxCost) {
      throw new Error(`Cost too high: ${estimate.totalCostEth} ETH`);
    }
    
    // Store with retries
    let lastError;
    for (let i = 0; i < 3; i++) {
      try {
        const receipt = await this.blobkit.writeBlob(data);
        
        // Wait for confirmation if using proxy
        if (receipt.status === 'pending') {
          let status = receipt.status;
          let attempts = 0;
          
          while (status === 'pending' && attempts < 30) {
            await new Promise(r => setTimeout(r, 2000));
            status = await this.blobkit.getJobStatus(receipt.jobId);
            attempts++;
          }
          
          if (status !== 'confirmed') {
            throw new Error('Job failed: ' + status);
          }
        }
        
        return receipt.blobTxHash!;
        
      } catch (error) {
        lastError = error;
        console.error(`Attempt ${i + 1} failed:`, error.message);
        await new Promise(r => setTimeout(r, 2000 * (i + 1)));
      }
    }
    
    throw lastError;
  }
  
  async retrieve(txHash: string): Promise<any> {
    try {
      return await this.blobkit.readBlobAsJSON(txHash);
    } catch (error) {
      if (error.code === BlobKitErrorCode.BLOB_NOT_FOUND) {
        throw new Error('Blob expired or not found');
      }
      throw error;
    }
  }
}

async function main() {
  // Initialize
  await initializeKzg();
  
  const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
  const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
  
  const blobkit = await BlobKit.init({
    rpcUrl: process.env.RPC_URL!,
    chainId: parseInt(process.env.CHAIN_ID!),
    proxyUrl: process.env.PROXY_URL
  }, wallet);
  
  const storage = new BlobStorage(blobkit);
  
  // Store data
  const txHash = await storage.store({
    message: 'Hello from BlobKit!',
    timestamp: Date.now()
  });
  
  console.log('Stored at:', txHash);
  
  // Retrieve data
  const data = await storage.retrieve(txHash);
  console.log('Retrieved:', data);
}

main().catch(console.error);

Error Handling

TYPESCRIPT
import { BlobKitError, BlobKitErrorCode } from '@blobkit/sdk';

try {
  await blobkit.writeBlob(data);
} catch (error) {
  if (error instanceof BlobKitError) {
    switch (error.code) {
      case BlobKitErrorCode.KZG_ERROR:
        // KZG operation failed
        console.error('KZG error - may need to reinitialize');
        await initializeKzg();
        break;
        
      case BlobKitErrorCode.BLOB_TOO_LARGE:
        // Data exceeds blob capacity (~124KB usable)
        console.error('Data too large, need to split or compress');
        break;
        
      case BlobKitErrorCode.INSUFFICIENT_BALANCE:
        // Not enough ETH
        console.error('Insufficient balance in escrow');
        break;
        
      case BlobKitErrorCode.BLOB_NOT_FOUND:
        // Blob expired (>18 days)
        console.error('Blob has expired');
        break;
        
      case BlobKitErrorCode.PROXY_UNAVAILABLE:
        // Proxy server is down
        console.error('Proxy server unavailable');
        break;
        
      default:
        console.error('Unknown error:', error.message);
    }
  } else {
    // Non-BlobKit error
    console.error('Unexpected error:', error);
  }
}

📖 API Reference

Core Methods

TYPESCRIPT
class BlobKit {
  // Initialize BlobKit
  static async init(config: BlobKitConfig, signer?: Signer): Promise<BlobKit>
  
  // Write blob
  async writeBlob(
    data: Uint8Array | string | object,
    meta?: Partial<BlobMeta>,
    jobId?: string,
    maxRetries?: number
  ): Promise<BlobReceipt>
  
  // Read blob
  async readBlob(txHash: string, index?: number): Promise<BlobReadResult>
  async readBlobAsString(txHash: string): Promise<string>
  async readBlobAsJSON(txHash: string): Promise<any>
  
  // Cost estimation
  async estimateCost(data: Uint8Array): Promise<CostEstimate>
  
  // Proxy & escrow operations  
  async getJobStatus(jobId: string): Promise<JobStatus>
  async refundIfExpired(jobId: string): Promise<TransactionResponse>
  async getBalance(): Promise<bigint>
  async getAddress(): Promise<string>
}

Configuration

TYPESCRIPT
interface BlobKitConfig {
  rpcUrl: string           // Ethereum RPC endpoint
  chainId?: number         // Network ID (1 = mainnet, 11155111 = sepolia)
  proxyUrl?: string        // Proxy server URL (required for wallet support)
  archiveUrl?: string      // Archive service for old blobs
  escrowContract?: string  // Custom escrow contract address
  logLevel?: 'debug' | 'info' | 'warn' | 'error'
}

Types

TYPESCRIPT
interface BlobReceipt {
  jobId: string
  blobHash?: string
  blobTxHash?: string
  status: 'pending' | 'confirmed' | 'failed'
  error?: string
}

interface CostEstimate {
  blobGasPrice: string
  baseFee: string
  totalCostWei: string
  totalCostEth: string
  totalCostUsd?: string
}

❓ Troubleshooting

Error Index

Quick reference for all error codes. Click any error to jump to its documentation.

Common Issues & Solutions

KZG_ERROR

KZG operation failed. Ensure KZG is initialized.

TYPESCRIPT
// Always do this first
import { initializeKzg } from '@blobkit/sdk';
await initializeKzg();

BLOB_TOO_LARGE

Data exceeds blob capacity (~124KB usable after encoding).

TYPESCRIPT
const bytes = new TextEncoder().encode(JSON.stringify(data));
// Actual usable capacity is ~124KB due to encoding overhead
if (bytes.length > 126972) {
  // Split into multiple blobs or compress
}

BLOB_NOT_FOUND

Blobs expire after 18 days. Use an archive service for permanent storage.

TYPESCRIPT
const blobkit = await BlobKit.init({
  rpcUrl: '...',
  archiveUrl: 'https://api.blobscan.com'  // Archive for old blobs
}, wallet);

Common Production Issues

Proxy returns 429 Too Many Requests

TYPESCRIPT
// You're hitting rate limits (100 req/min)
// Solution: Add rate limiting
const queue = [];
let processing = false;

async function rateLimitedWrite(data) {
  queue.push(data);
  if (!processing) {
    processing = true;
    while (queue.length > 0) {
      const item = queue.shift();
      await blobkit.writeBlob(item);
      await new Promise(r => setTimeout(r, 600));  // 100 req/min = 600ms between
    }
    processing = false;
  }
}

Escrow Balance Runs Out Mid-Operation

TYPESCRIPT
// Implement auto-deposit on failure
// Note: Deposits are automatic!
// writeBlob handles the entire flow:
async function writeBlob(data) {
  // This automatically:
  // 1. Estimates cost
  // 2. Deposits to escrow
  // 3. Submits to proxy
  // 4. Returns receipt
  const receipt = await blobkit.writeBlob(data);
  console.log('Success!', receipt.blobTxHash);
  return receipt;
}

Get Help

⚙️ Advanced Configuration

All Configuration Options

The BlobKit constructor accepts a configuration object with these options:

TYPESCRIPT
import { BlobKit } from '@blobkit/sdk';

const blobkit = new BlobKit({
  // Core configuration
  rpcUrl: string,                    // Required: Ethereum RPC endpoint
  chainId?: number,                  // Chain ID (default: 1)
  archiveUrl?: string,               // Archive service URL (default: rpcUrl)
  defaultCodec?: string,             // Default data encoding (default: 'application/json')
  
  // Proxy configuration  
  proxyUrl?: string,                 // Proxy server URL (default: '')
  escrowContract?: string,           // Custom escrow address (default: auto-detected)
  maxProxyFeePercent?: number,       // Max acceptable proxy fee (default: 5)
  callbackUrl?: string,              // Webhook for async notifications (default: '')
  
  // Logging
  logLevel?: 'debug' | 'info' | 'silent',  // Log verbosity (default: 'info')
  
  // KZG setup
  kzgSetup?: {
    trustedSetupData?: Uint8Array,   // Pre-loaded setup data
    trustedSetupUrl?: string,         // URL to load setup (browser)
    trustedSetupPath?: string,        // File path to setup (Node.js)
    expectedHash?: string             // Hash for integrity check
  },
  
  // Metrics hooks
  metricsHooks?: {
    onBlobWrite?: (size: number, duration: number, success: boolean) => void,
    onBlobRead?: (size: number, duration: number, success: boolean, source: string) => void,
    onProxyRequest?: (url: string, duration: number, success: boolean) => void,
    onKzgOperation?: (operation: string, duration: number, success: boolean) => void,
    onError?: (error: Error, context: string) => void
  }
}, signer);

Environment-Based Setup

Initialize BlobKit from environment variables:

TYPESCRIPT
import { createFromEnv } from '@blobkit/sdk';
import { ethers } from 'ethers';

// Set environment variables
process.env.BLOBKIT_RPC_URL = 'https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY';
process.env.BLOBKIT_ARCHIVE_URL = 'https://api.blobscan.com';
process.env.BLOBKIT_CHAIN_ID = '1';
process.env.BLOBKIT_PROXY_URL = 'https://proxy.blobkit.org';
process.env.BLOBKIT_LOG_LEVEL = 'debug';
process.env.BLOBKIT_KZG_TRUSTED_SETUP_PATH = './trusted_setup.txt';

// Create BlobKit instance
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY);
const blobkit = createFromEnv(wallet);

Environment Variables

  • BLOBKIT_RPC_URL: Ethereum RPC endpoint (default: 'http://localhost:8545')
  • BLOBKIT_ARCHIVE_URL: Archive service URL (default: 'https://api.blobscan.com')
  • BLOBKIT_CHAIN_ID: Chain ID number (default: 31337)
  • BLOBKIT_PROXY_URL: Proxy server URL
  • BLOBKIT_LOG_LEVEL: Log level (default: 'info')
  • BLOBKIT_KZG_TRUSTED_SETUP_PATH: Path to KZG trusted setup file

📊 Metrics and Monitoring

Metrics Hooks

Track performance and errors with custom metrics hooks:

TYPESCRIPT
const blobkit = new BlobKit({
  rpcUrl: '...',
  metricsHooks: {
    // Track blob writes
    onBlobWrite: (size, duration, success) => {
      console.log(`Blob write: ${size} bytes in ${duration}ms, success: ${success}`);
      // Send to your metrics service
      metrics.track('blob.write', { size, duration, success });
    },
    
    // Track blob reads
    onBlobRead: (size, duration, success, source) => {
      console.log(`Blob read: ${size} bytes from ${source} in ${duration}ms`);
      metrics.track('blob.read', { size, duration, success, source });
    },
    
    // Track proxy requests
    onProxyRequest: (url, duration, success) => {
      console.log(`Proxy request to ${url}: ${duration}ms, success: ${success}`);
      metrics.track('proxy.request', { url, duration, success });
    },
    
    // Track KZG operations
    onKzgOperation: (operation, duration, success) => {
      console.log(`KZG ${operation}: ${duration}ms, success: ${success}`);
      metrics.track('kzg.operation', { operation, duration, success });
    },
    
    // Track errors
    onError: (error, context) => {
      console.error(`Error in ${context}:`, error);
      errorReporter.report(error, { context });
    }
  }
});

Compression Settings

TYPESCRIPT
// Custom archive URL
const blobkit = new BlobKit({
  rpcUrl: '...',
  archiveUrl: 'https://archive.example.com'  // Use different archive service
});

// Custom default codec
const blobkit = new BlobKit({
  rpcUrl: '...',
  defaultCodec: 'text/plain'  // Default to plain text encoding
});

🔄 Codec System

Built-in Codecs

BlobKit includes three built-in codecs for data encoding:

JsonCodec

Serializes JavaScript objects to JSON

TYPESCRIPT
import { JsonCodec } from '@blobkit/sdk';

const codec = new JsonCodec();
const encoded = codec.encode({ hello: 'world' });  // Uint8Array
const decoded = codec.decode(encoded);              // { hello: 'world' }

TextCodec

Handles UTF-8 text strings

TYPESCRIPT
import { TextCodec } from '@blobkit/sdk';

const codec = new TextCodec();
const encoded = codec.encode('Hello, world!');  // Uint8Array
const decoded = codec.decode(encoded);          // 'Hello, world!'

RawCodec

Direct binary data (no transformation)

TYPESCRIPT
import { RawCodec } from '@blobkit/sdk';

const codec = new RawCodec();
const data = new Uint8Array([1, 2, 3, 4]);
const encoded = codec.encode(data);     // Returns same Uint8Array
const decoded = codec.decode(encoded);  // Returns same Uint8Array

🔧 Utility Functions

Data Conversion

TYPESCRIPT
import { 
  bytesToHex, 
  hexToBytes,
  formatEther,
  parseEther
} from '@blobkit/sdk';

// Convert bytes to hex string
const bytes = new Uint8Array([1, 2, 3, 4]);
const hex = bytesToHex(bytes);  // '0x01020304'

// Convert hex string to bytes
const restored = hexToBytes(hex);  // Uint8Array([1, 2, 3, 4])

// Format wei to ETH string
const wei = BigInt('1000000000000000000');
const eth = formatEther(wei);  // '1'

// Parse ETH string to wei
const weiValue = parseEther('1.5');  // 1500000000000000000n

Validation Functions

TYPESCRIPT
import { 
  isValidAddress,
  validateBlobSize
} from '@blobkit/sdk';

// Validate Ethereum address
const valid = isValidAddress('0x742d35Cc6634C0532925a3b844Bc9e7595f2bD77');  // true
const invalid = isValidAddress('not-an-address');  // false

// Validate blob size (throws if too large)
const data = new Uint8Array(100000);  // 100KB
try {
  validateBlobSize(data);  // OK
} catch (error) {
  console.error('Blob too large');
}

Job Management

TYPESCRIPT
import { 
  generateJobId,
  calculatePayloadHash
} from '@blobkit/sdk';

// Calculate hash of payload
const data = new Uint8Array([1, 2, 3, 4]);
const hash = calculatePayloadHash(data);  // '0x...' keccak256 hash

// Generate unique job ID
const userAddress = '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD77';
const jobId = generateJobId(userAddress, hash, 0);  // Unique job ID

Constants

TYPESCRIPT
import { 
  FIELD_ELEMENTS_PER_BLOB,
  BYTES_PER_FIELD_ELEMENT,
  BLOB_SIZE
} from '@blobkit/sdk';

console.log(FIELD_ELEMENTS_PER_BLOB);  // 4096
console.log(BYTES_PER_FIELD_ELEMENT);  // 31
console.log(BLOB_SIZE);                 // 131072

📖 Complete API Reference

BlobKit Class

Constructor

TYPESCRIPT
const blobkit = new BlobKit(config: BlobKitConfig, signer?: Signer)

// Initialize async components (KZG, proxy)
await blobkit.initialize()

Note: There is no static init() method. Use the constructor directly.

Write Methods

writeBlob()

TYPESCRIPT
async writeBlob(
  data: Uint8Array | string | object,
  meta?: Partial<BlobMeta>,
  jobId?: string,
  maxRetries?: number = 3
): Promise<BlobReceipt>

// Example
const receipt = await blobkit.writeBlob(
  { message: 'Hello, blobs!' },
  { 
    appId: 'my-app',
    codec: 'application/json'
  }
);

Writes data to a blob. Automatically handles escrow deposits when using proxy.

estimateCost()

TYPESCRIPT
async estimateCost(payload: Uint8Array): Promise<CostEstimate>

// Returns
{
  blobFee: string,      // ETH for blob storage
  gasFee: string,       // ETH for transaction
  proxyFee: string,     // ETH for proxy (if used)
  totalETH: string      // Total ETH required
}

generateJobId()

TYPESCRIPT
generateJobId(
  userAddress: string,
  payloadHash: string,
  nonce: number
): string

// Generate deterministic job ID
const jobId = blobkit.generateJobId(
  await blobkit.getAddress(),
  calculatePayloadHash(data),
  0
);

Read Methods

readBlob()

TYPESCRIPT
async readBlob(
  blobTxHash: string,
  blobIndex?: number = 0
): Promise<BlobReadResult>

// Returns
{
  data: Uint8Array,
  commitment: string,
  proof: string,
  versionedHash: string,
  blockNumber: number,
  source: 'rpc' | 'archive' | 'fallback'
}

readBlobAsString()

TYPESCRIPT
async readBlobAsString(
  blobTxHash: string,
  blobIndex?: number = 0
): Promise<string>

// Decodes blob data as UTF-8 string
const text = await blobkit.readBlobAsString(txHash);

readBlobAsJSON()

TYPESCRIPT
async readBlobAsJSON(
  blobTxHash: string,
  blobIndex?: number = 0
): Promise<unknown>

// Decodes blob data as JSON
const data = await blobkit.readBlobAsJSON(txHash);

Payment & Job Methods

getJobStatus()

TYPESCRIPT
async getJobStatus(jobId: string): Promise<JobStatus>

// Returns
{
  exists: boolean,
  user: string,
  amount: bigint,
  completed: boolean,
  timestamp: number,
  blobTxHash: string
}

refundIfExpired()

TYPESCRIPT
async refundIfExpired(jobId: string): Promise<TransactionResponse>

// Refund expired job (after 5 minutes)
const tx = await blobkit.refundIfExpired(jobId);
await tx.wait();

getAddress()

TYPESCRIPT
async getAddress(): Promise<string>

// Get current wallet address
const address = await blobkit.getAddress();

getBalance()

TYPESCRIPT
async getBalance(): Promise<bigint>

// Get ETH balance of current wallet
const balance = await blobkit.getBalance();
console.log(formatEther(balance), 'ETH');

Direct Component Access

PaymentManager

TYPESCRIPT
import { PaymentManager } from '@blobkit/sdk';

const paymentManager = new PaymentManager(
  rpcUrl: string,
  escrowContract: string,
  signer: Signer
);

// Direct escrow operations
await paymentManager.depositForBlob(jobId, amount);
await paymentManager.getBalance(address);
await paymentManager.getJobStatus(jobId);
await paymentManager.refundIfExpired(jobId);

ProxyClient

TYPESCRIPT
import { ProxyClient } from '@blobkit/sdk';

const proxyClient = new ProxyClient({
  proxyUrl: string,
  escrowContract?: string,
  logLevel?: 'debug' | 'info' | 'silent'
});

// Submit blob via proxy
const result = await proxyClient.submitBlob({
  jobId: string,
  paymentTxHash: string,
  payload: Uint8Array,
  signature: Uint8Array,
  meta: BlobMeta
});

// Check proxy health
const health = await proxyClient.getHealth();

BlobReader

TYPESCRIPT
import { BlobReader } from '@blobkit/sdk';

const reader = new BlobReader({
  rpcUrl: string,
  archiveUrl?: string,
  logLevel?: 'debug' | 'info' | 'silent'
});

// Read blob data
const result = await reader.readBlob(txHash, blobIndex);

// Static decode methods
const text = BlobReader.decodeToString(data);
const json = BlobReader.decodeToJSON(data);

BlobSubmitter (Node.js only)

TYPESCRIPT
import { BlobSubmitter } from '@blobkit/sdk';

const submitter = new BlobSubmitter({
  rpcUrl: string,
  chainId: number,
  escrowAddress?: string
});

// Submit blob directly (requires KZG)
const result = await submitter.submitBlob(
  signer: Signer,
  payload: Uint8Array,
  kzg: KzgLibrary
);

// Estimate costs
const costs = await submitter.estimateCost(payloadSize);

KZG Functions

KZG Setup

TYPESCRIPT
import { 
  initializeKzg,
  loadTrustedSetupFromURL,
  loadTrustedSetupFromFile
} from '@blobkit/sdk';

// Initialize KZG (required before blob operations)
await initializeKzg();

// Or with custom setup
await initializeKzg({
  trustedSetupPath: './ceremony.txt',
  expectedHash: '0x...'
});

// Load setup manually
const setupData = await loadTrustedSetupFromURL(url);
const setupFile = await loadTrustedSetupFromFile(path);

Blob Encoding

TYPESCRIPT
import { 
  encodeBlob,
  decodeBlob
} from '@blobkit/sdk';

// Encode data to blob format (131072 bytes)
const blob = encodeBlob(data);

// Decode blob back to original data
const decoded = decodeBlob(blob);

KZG Operations

TYPESCRIPT
import { 
  blobToKzgCommitment,
  computeKzgProof,
  commitmentToVersionedHash
} from '@blobkit/sdk';

// Generate commitment (48 bytes)
const commitment = blobToKzgCommitment(blob);

// Generate proof (48 bytes)
const proof = computeKzgProof(blob, commitment);

// Get versioned hash
const versionedHash = await commitmentToVersionedHash(commitment);

⚠️ Current Limitations

No Batch Operations

The SDK currently only supports writing one blob at a time. While EIP-4844 allows up to 6 blobs per transaction, batch operations are not yet implemented.

TYPESCRIPT
// Current: One blob at a time
await blobkit.writeBlob(data1);
await blobkit.writeBlob(data2);

// Future: Batch operations (not yet available)
// await blobkit.writeBatch([data1, data2, data3]);

Maximum Data Size

Single blob: ~124KB usable (after encoding overhead)
Per transaction: 6 blobs max (~744KB total) - not yet supported
Actual blob size: 131,072 bytes (128KB)
Encoding overhead: 4-byte header + field element padding

Wallet Support

Most wallets (MetaMask, WalletConnect, etc.) do not support EIP-4844 blob transactions. This is why the proxy system is required for browser environments.

Data Retention

Blob data is only guaranteed to be available for 18 days. After that, nodes may delete it. Use an archive service for permanent storage.

Smart Contract Access

Blob data cannot be accessed from within smart contracts. It's only available to external applications.

❓ Frequently Asked Questions