🌟 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
mkdir my-blob-app
cd my-blob-app
npm init -y
npm install @blobkit/sdk ethers dotenvStep 2: Configure environment
# 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.orgStep 3: Write and run code
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:
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
// 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. You send data to proxy: Your app sends the blob data to the proxy server
- 2. Proxy creates blob transaction: Server generates KZG commitments and formats Type 3 transaction
- 3. Proxy pays gas: Server wallet pays the gas fees upfront
- 4. You reimburse proxy: Cost is deducted from your escrow deposit
- 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).
// 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. Deposit funds: You deposit ETH to the escrow contract
- 2. Submit blob: You request blob storage through proxy
- 3. Proxy executes: Proxy sends blob transaction and pays gas
- 4. Proxy claims payment: Proxy proves job completion to escrow
- 5. Escrow releases funds: Contract pays proxy from your deposit
// 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
# NPM
npm install @blobkit/sdk ethers
# Yarn
yarn add @blobkit/sdk ethers
# PNPM
pnpm add @blobkit/sdk ethersNote: 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
# 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, errorSecurity: 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.
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 varsWriting Data
// 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
// 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
// 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)
// 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)
// 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
// 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
# 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 address2. Run Proxy Server
# 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 startImportant: 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
// 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
# 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
// 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
// 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 minuteCost 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
// 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
// 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
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
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
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
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
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.
// Always do this first
import { initializeKzg } from '@blobkit/sdk';
await initializeKzg();BLOB_TOO_LARGE
Data exceeds blob capacity (~124KB usable after encoding).
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.
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
// 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
// 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
- • GitHub Issues: Report bugs
- • Discussions: Ask questions
- • Stack Overflow: Tag with
blobkit
⚙️ Advanced Configuration
All Configuration Options
The BlobKit constructor accepts a configuration object with these options:
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:
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:
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
// 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
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
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)
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
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'); // 1500000000000000000nValidation Functions
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
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 IDConstants
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
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()
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()
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()
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()
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()
async readBlobAsString(
blobTxHash: string,
blobIndex?: number = 0
): Promise<string>
// Decodes blob data as UTF-8 string
const text = await blobkit.readBlobAsString(txHash);readBlobAsJSON()
async readBlobAsJSON(
blobTxHash: string,
blobIndex?: number = 0
): Promise<unknown>
// Decodes blob data as JSON
const data = await blobkit.readBlobAsJSON(txHash);Payment & Job Methods
getJobStatus()
async getJobStatus(jobId: string): Promise<JobStatus>
// Returns
{
exists: boolean,
user: string,
amount: bigint,
completed: boolean,
timestamp: number,
blobTxHash: string
}refundIfExpired()
async refundIfExpired(jobId: string): Promise<TransactionResponse>
// Refund expired job (after 5 minutes)
const tx = await blobkit.refundIfExpired(jobId);
await tx.wait();getAddress()
async getAddress(): Promise<string>
// Get current wallet address
const address = await blobkit.getAddress();getBalance()
async getBalance(): Promise<bigint>
// Get ETH balance of current wallet
const balance = await blobkit.getBalance();
console.log(formatEther(balance), 'ETH');Direct Component Access
PaymentManager
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
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
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)
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
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
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
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.
// 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.