# Blockchain Storage Considerations: Building on Net Protocol

*Net Library is a social decentralized onchain media library powered by Net Protocol and built with Neynar's App Studio on Base.*

By [GEAUX Think](https://paragraph.com/@geauxthink) · 2026-02-09

blockchain, storage, farcaster, neynar, base, onchain, crypto, web3

---

> This article contains a copy of the upload-system md file currently being used by Net Library to insure that all file uploads are executed accurately. This is the second version of this article.

Everything detailed below will make much more sense within the context of a working knowledge of Net Library - a mini app that I'm currently developing, and Net Protocol, a free onchain public good developed by Aspyn Palatnick (@AspynPalatnick on X; @aspyn on Farcaster).

If you're not familiar with Net Library, check it out [here](https://farcaster.xyz/geaux.eth/0x74cc65a8) on Farcaster, on the Base App [here](https://base.app/app/miniapp-generator-fid-282520-251210015136529.neynar.app), or via a web browswer [here](https://miniapp-generator-fid-282520-251210015136529.neynar.app/)

If you're not familiar with Net Protocol, go [here](https://www.netprotocol.app/)

⚠⚠ If you connect via a web browser or The Base App, make sure to link that wallet to your Farcaster account. This allows Net Library to automatically detect your Net Library Membership across all three apps.  
  
⚠⚠ The Net Library Upload System Architecture outlined below is subject to change as needed. I'm currently vibe coding this app so if bugs appear or there's issues related to uploads I might have to tweak/change this file.

This article is meant for education purposes only. I hope this helps anyone who's interested in building cool stuff on top of Net Protocol!

* * *

Upload System Architecture
==========================

> **Last Updated:** v1.42.6 - Emoji font support for Satori rendering, improved receipt layout with smart image grids. All uploads MUST use the relay system. There is no fallback to direct wallet uploads.

* * *

Table of Contents
-----------------

1.  Overview
    
2.  The Relay System
    
3.  Upload Entry Points
    
4.  Platform-Specific Behavior
    
    *   Platform Detection
        
    *   Connector Isolation
        
    *   EIP-6963 Discovery
        
    *   WalletGuard
        
5.  Wallet Types & Transaction Handling
    
    *   Smart Wallet vs EOA
        
    *   Transaction Flow
        
    *   Funding Flow
        
    *   Smart Wallet Detection
        
6.  Upload Protections
    
    *   Concurrent Upload Prevention
        
    *   Cancellable Operations
        
    *   Payment Recovery
        
7.  Content Retrieval & Encoding
    
8.  Storage Types
    
9.  Session Management
    
10.  Archival Pattern
    
11.  Key Files Reference
    
12.  Dead Code Status
    
13.  UX Considerations
    

* * *

Overview
--------

The upload system uses **Net Protocol's x402 relay** for ALL onchain storage. Users fund an "Upload Wallet" with USDC, which is converted to ETH and held in a backend wallet. This backend wallet pays gas fees automatically, eliminating the need for users to sign multiple transactions per upload.

### Key Principle

    User funds USDC once → Backend wallet pays gas → One-click uploads
    

### Signature Requirements

Scenario

Signatures Required

Wallet funded + session cached

**0** (fully automatic)

Wallet funded + session expired

**1** (new session signature)

Wallet needs funding

**2** (USDC transfer + session)

* * *

The Relay System
----------------

### How It Works

1.  **User funds Upload Wallet** with USDC (any amount, presets: $0.10, $0.25, $5.00)
    
2.  **x402 protocol converts** USDC → ETH automatically at market rate
    
3.  **Backend wallet** (deterministically derived from user's `operatorAddress`) stores ETH
    
4.  **Session signature** (EIP-712) authorizes the backend to act on user's behalf for 1 hour
    
5.  **Backend submits transactions** using stored ETH for gas
    
6.  **Content stored onchain** via Net Protocol storage contracts
    

### Backend Wallet Derivation

    Backend Wallet Address = deterministic_derive(operatorAddress)
    

*   Same wallet address → Same backend wallet → Same balance
    
*   Different wallet addresses → Different backend wallets → Separate balances
    

### Configuration (from `use-relay-upload-v2.ts`)

    const RELAY_CONFIG = {
      apiUrl: "https://www.netprotocol.app",
      chainId: 8453, // Base
      usdcAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      x402FacilitatorAddress: "0x6462F8f301Bd08c206ee611aA00015bA41acc1D0",
      verifyMaxRetries: 5,
      verifyRetryDelayMs: 2000,
    };
    

* * *

Upload Entry Points
-------------------

All uploads flow through `useRelayUploadV2` hook → `uploadFile()` function.

> **⚠️ CRITICAL: "Archives" ARE Uploads**
> 
> Grid Archives and Social Archives are **uploads**. They store content onchain via the relay system. When modifying upload logic, ALWAYS check ALL paths below, including archives.

### Complete Upload Path Inventory

**🔴 MANDATORY CHECK LIST** - When editing upload logic, verify ALL of these:

Category

User Action

Handler/Hook

File Location

**Standard Uploads**

  

  

  

  

Single file upload

`handleUpload()` → `uploadFile()`

`upload-tab.tsx`

  

Batch/multi-file

`handleMultiFileUpload()` → `uploadFile()` loop

`upload-tab.tsx`

  

Drag & drop

Same as single/batch

`upload-tab.tsx`

  

Text editor save

`handleTextEditorUpload()` → `uploadFile()`

`upload-tab.tsx`

  

Image editor save

`handleCreatedImageUpload()` → `uploadFile()`

`upload-tab.tsx`

  

Retry failed file

`retryFailedFile()` → `uploadFile()`

`upload-tab.tsx`

**Archives (ALSO UPLOADS!)**

  

  

  

  

Grid archive

`handleArchive()` → `uploadFile()`

`grid-archive-modal.tsx`, `use-grid-archive.ts`

  

Social receipt archive

`handleReceiptUpload()` → `uploadFile()`

`upload-tab.tsx`, `social-receipt-archive.tsx`

**Import (No Relay)**

  

  

  

  

CDN link import

`handleCdnLinkUpload()`

`upload-tab.tsx` (metadata only, no onchain storage)

### Key Files by Upload Type

    Standard Uploads:
    ├── src/features/app/components/upload-tab.tsx (main upload UI)
    ├── src/hooks/use-relay-upload-v2.ts (single file hook)
    └── src/hooks/use-relay-batch-upload-v2.ts (batch hook, cost estimation)
    
    Grid Archives:
    ├── src/features/app/components/grid-archive-modal.tsx (archive UI)
    ├── src/hooks/use-grid-archive.ts (archive logic)
    └── src/app/api/grids/archive/route.ts (API endpoint)
    
    Social Archives:
    ├── src/features/app/components/social-receipt-archive.tsx (archive UI)
    ├── src/app/api/social/receipt/route.tsx (Satori receipt image generation)
    ├── src/app/api/social/fetch/route.ts (Twitter/Farcaster content fetching)
    ├── src/lib/emoji-font.ts (v1.42.6 - Emoji font loader for Satori)
    └── Uses uploadFile() from use-relay-upload-v2.ts
    
    ⚠️ **Satori Image Rendering (v1.42.4+)**: Receipt images are generated via Satori.
       - Images MUST have explicit width/height on both container and img element
       - **v1.42.6**: Emoji font support via emoji-font.ts (Noto Color Emoji + Twemoji fallback)
       - Receipt layout: URL cleanup (trailing t.co stripped), improved opacity (timestamp 0.8, footer 0.7)
    

### Attribution Fields

All upload paths must correctly pass these fields to `createDiscoveryEntry()`:

*   `operator` - Backend wallet address (for CDN URL generation)
    
*   `uploaderAddress` - User's wallet address (for attribution/credit)
    

**Note:** The batch upload in `upload-tab.tsx` uses `useRelayUploadV2.uploadFile()` in a sequential loop, NOT `useRelayBatchUploadV2.uploadBatch()`. The batch hook is used by `multi-file-upload.tsx` only for cost estimation (`estimateBatchCost`).

### Code Location

All handlers are in `upload-tab.tsx` which imports:

    import { useRelayUploadV2 as useRelayUpload } from "@/hooks/use-relay-upload-v2";
    

* * *

Platform-Specific Behavior
--------------------------

### Platform Detection

    // From src/lib/app-mode.ts
    
    function detectFarcasterFrame(): boolean {
      // Checks (priority order):
      // 1. iframe context (window.parent !== window)
      // 2. URL params: fc_action, fid, frame
      // 3. User agent: "warpcast" or "farcaster"
    }
    
    function detectCoinbaseWallet(): boolean {
      // Checks:
      // 1. User agent: "coinbasewallet" or "coinbase"
      // 2. Injected provider: window.ethereum.isCoinbaseWallet or isCoinbaseBrowser
    }
    
    function detectAppMode(): "farcaster" | "base-app" | "web" | "detecting" {
      if (typeof window === "undefined") return "detecting"; // SSR
      if (detectFarcasterFrame()) return "farcaster";
      if (detectCoinbaseWallet()) return "base-app";
      return "web";
    }
    

### Platform → Wallet Connector Mapping

Platform

Connector Used

Connection Method

Notes

Warpcast (Farcaster)

`farcasterMiniApp()`

Frame SDK

Only Farcaster connector enabled

Coinbase Wallet app

`coinbaseWallet({ preference: "all", version: "4" })`

Injected provider

Only Coinbase connector enabled

Web browser

None (EIP-6963 discovery)

EIP-6963

Wallets self-announce via standard

**CRITICAL: Connector Isolation**

Each platform uses ONLY its designated connector. Mixing connectors caused transaction simulation issues:

*   Farcaster mode: `[farcasterMiniApp()]` only
    
*   Base App mode: `[coinbaseWallet()]` only
    
*   Web mode: `[]` (empty - relies on EIP-6963 multiInjectedProviderDiscovery)
    

This is configured in `neynar-wagmi-provider.tsx`.

### Web Mode: EIP-6963 Wallet Discovery

In web browser mode, the app uses NO explicit connectors. Instead, it relies entirely on **EIP-6963** (`multiInjectedProviderDiscovery`) to detect installed wallet extensions.

**Why?** The Coinbase Wallet SDK was hijacking connections meant for MetaMask/Rainbow when added as an explicit connector. By using EIP-6963 only, each wallet properly announces itself and the user can choose.

    // From neynar-wagmi-provider.tsx - Web mode connector config
    if (appMode === "web") {
      // Disable Coinbase auto-injection safeguards
      disableCoinbaseAutoInjection();
    
      // Return empty array - ALL wallets discovered via EIP-6963
      return [];
    }
    
    // wagmi config
    createConfig({
      // ...
      multiInjectedProviderDiscovery: appMode === "web", // Only enabled in web mode
    });
    

**Discovered Wallets (via EIP-6963):**

*   MetaMask (`io.metamask`)
    
*   Rainbow (`me.rainbow`)
    
*   Coinbase Wallet (`com.coinbase.wallet`)
    
*   Any other EIP-6963 compliant wallet
    

### WalletGuard: Enforcing Wallet Preference

The `WalletGuard` component prevents unwanted wallet connections by enforcing user preferences:

    // From neynar-wagmi-provider.tsx
    function WalletGuard({ children }) {
      const { address, connector, isConnected } = useAccount();
      const { disconnect } = useDisconnect();
    
      // If connected wallet doesn't match saved preference, disconnect it
      if (isConnected && savedPreference && connector.id !== savedPreference) {
        disconnect();
      }
    
      // Also verify address matches if we have a saved address
      if (isConnected && savedAddress && address !== savedAddress) {
        disconnect();
      }
    }
    

**Preference Storage:**

*   `net-library-wallet-preference` - Stores connector ID (e.g., "io.metamask")
    
*   `net-library-wallet-address` - Stores connected address (lowercase)
    
*   Cleared on explicit disconnect
    
*   Prevents Coinbase from hijacking MetaMask connections
    

### Critical UX Note

**Each platform may connect a DIFFERENT wallet**, resulting in:

*   Different `operatorAddress` values
    
*   Different backend wallets
    
*   **Separate Upload Wallet balances**
    

However, the **library and uploads are shared** across platforms (content is tied to the user, not the wallet).

* * *

Wallet Types & Transaction Handling
-----------------------------------

### Smart Wallets vs EOA

The relay hook detects wallet type and handles transactions accordingly:

    // Smart Wallet (EIP-5792 compatible, e.g., Coinbase Smart Wallet)
    const { sendCallsAsync } = useSendCalls();
    
    // EOA (Externally Owned Account, traditional wallet)
    const { writeContractAsync } = useWriteContract();
    

### Transaction Flow

    Smart Wallet (Farcaster/Base App):
    1. Try sendCallsAsync() (EIP-5792 batched transactions)
    2. Poll getCallsStatus() for tx hash (Smart Wallets don't return hash immediately)
    3. If sendCallsAsync fails → Check if user rejection or simulation failure
    4. If simulation failure → Fall back to writeContractAsync()
    
    EOA (Web browser):
    1. Use writeContractAsync() directly
    2. Returns tx hash immediately (no polling needed)
    

### Detailed Fallback Logic (from `fundBackendWallet`)

    try {
      // Try Smart Wallet first
      const result = await sendCallsAsync({ calls: [...], chainId: 8453 });
      // Poll for tx hash...
    } catch (smartWalletError) {
      // Check for user rejection (don't fallback)
      if (errMsg.includes("user rejected")) throw;
    
      // Check for simulation failure
      if (isSimulationFailure(errMsg)) {
        // Verify actual USDC balance
        const balance = await checkUserUsdcBalance();
        if (balance < amount) throw new Error("Insufficient USDC");
    
        // If balance is sufficient, it's a simulation problem
        if (!walletDeployed) {
          throw new Error("Smart Wallet needs activation");
        }
      }
    
      // Fall back to EOA method
      actualTxHash = await writeContractAsync({ ... });
    }
    

### Funding Flow (USDC → Backend Wallet)

    // 1. Get x402 payment info from /api/relay/fund (returns payTo address)
    // 2. Check user's USDC balance on Base chain
    // 3. Send USDC directly to payTo address:
    //    - Smart Wallet: sendCallsAsync() → poll getCallsStatus() for tx hash
    //    - EOA: writeContractAsync() → get tx hash directly
    // 4. Save tx hash to localStorage for recovery (in case verify fails)
    // 5. Wait 3 seconds for tx to propagate
    // 6. Verify payment with retries (5 attempts, 2s delay each)
    // 7. x402 converts USDC → ETH at market rate
    // 8. ETH deposited to user's backend wallet
    // 9. Backend wallet ready to pay gas
    

### Balance-Based Verification Fallback

If tx hash polling times out (60 attempts, 1s each), the system falls back to balance verification:

    // Fallback: Check if backend wallet balance increased
    let balanceAttempts = 0;
    while (balanceAttempts < 30) {
      const newBalance = await checkBalance();
      if (newBalance > originalBalance + 0.00001) {
        return true; // Funding succeeded
      }
      await sleep(1000);
      balanceAttempts++;
    }
    

### Smart Wallet Detection

Smart Wallets (ERC-4337) may not be deployed until their first transaction. This causes simulation failures that look like "insufficient funds" errors.

    // From relay-upload-utils.ts
    async function isSmartWalletDeployed(address: Address): Promise<boolean> {
      const code = await basePublicClient.getCode({ address });
      return code !== undefined && code !== "0x" && code.length > 2;
    }
    

### Simulation Failure Detection

Some errors look like "insufficient funds" but are actually Smart Wallet simulation failures:

    function isSimulationFailure(errorMsg: string): boolean {
      const lowerMsg = errorMsg.toLowerCase();
      return (
        lowerMsg.includes("error generating transaction") ||
        lowerMsg.includes("simulation failed") ||
        lowerMsg.includes("execution reverted") ||
        lowerMsg.includes("aa21") || // First tx with undeployed account
        lowerMsg.includes("aa25") || // Invalid account signature
        lowerMsg.includes("useroperation")
      );
    }
    

When a simulation failure is detected, the hooks:

1.  Check actual USDC balance to confirm it's not a real funds issue
    
2.  If undeployed Smart Wallet: suggest smaller amount or wallet activation
    
3.  If deployed but still failing: suggest retry (temporary network issue)
    

* * *

Upload Protections
------------------

Both upload hooks include these safety mechanisms:

### Concurrent Upload Prevention

    const uploadInProgressRef = useRef<boolean>(false);
    
    // At start of upload
    if (uploadInProgressRef.current) {
      return { success: false, error: "Upload already in progress" };
    }
    uploadInProgressRef.current = true;
    
    // In finally block
    uploadInProgressRef.current = false;
    

### Cancellable Operations

    const abortControllerRef = useRef<AbortController | null>(null);
    
    // At start of upload
    abortControllerRef.current = new AbortController();
    const signal = abortControllerRef.current.signal;
    
    // In loops
    if (signal.aborted) {
      throw new Error("Upload cancelled");
    }
    
    // Cancel function exposed to UI
    const cancelUpload = () => {
      abortControllerRef.current?.abort();
      uploadInProgressRef.current = false;
    };
    

### Cleanup on Unmount

    useEffect(() => {
      return () => {
        if (abortControllerRef.current) {
          abortControllerRef.current.abort();
        }
      };
    }, []);
    

### Payment Recovery

If a USDC payment is sent but verification fails (network error, user closes browser), the system can recover:

**1\. Fast Recovery (localStorage)** Pending payments are stored in localStorage for quick recovery:

    // Stored in: net-relay-pending-{address}
    [{ txHash: "0x...", timestamp: 1234567890, amount: 0.10 }, ...]
    
    // Recovery function
    const recoverPendingPayments = async () => {
      const pending = localStorage.getItem(`net-relay-pending-${address}`);
      for (const payment of pending) {
        // Skip payments older than 1 hour
        if (Date.now() - payment.timestamp > 3600000) continue;
    
        const result = await verifyPaymentWithRetries(payment.txHash, 3, 1500);
        if (result.success) {
          // Remove from pending, update balance
        }
      }
    };
    

**2\. Blockchain Scan Recovery (fallback)** If localStorage recovery fails, scan blockchain for unverified USDC transfers:

    // Calls /api/relay/recover which:
    // 1. Scans Base chain for USDC transfers to facilitator
    // 2. Attempts to verify each unprocessed payment
    // 3. Returns { recovered: number, total: number }
    

**Exposed Functions:**

*   `recoverPendingPayments()` - Fast localStorage recovery
    
*   `recoverUnverifiedPayments()` - Full recovery (localStorage + blockchain scan)
    

* * *

Content Retrieval & Encoding
----------------------------

When reading content from Net Protocol storage, data can be returned in different encodings:

### Encoding Detection Pattern

    // Used in book-file, book-content, and net-protocol.ts
    if (dataStr.startsWith("0x")) {
      // Hex encoded - decode from hex
      decoded = hexToBuffer(dataStr);
    } else if (/^[A-Za-z0-9+/=]+$/.test(dataStr) && dataStr.length > 20) {
      // Base64 encoded - decode from base64
      decoded = Buffer.from(dataStr, "base64");
    } else {
      // Plain text - use as-is
      decoded = dataStr;
    }
    

### Key Files for Content Retrieval

File

Purpose

`src/app/api/book-file/route.ts`

Raw file serving (PDF, audio, video, text)

`src/app/api/book-content/route.ts`

Content analysis and type detection

`src/app/api/media-proxy/route.ts`

CORS proxy for multimedia

`src/lib/net-protocol.ts`

Discovery index fetching

### Operator vs UploaderAddress

**CRITICAL**: Two different addresses are used:

Field

Purpose

Example

`operator`

Backend wallet that wrote to blockchain

`0x54D2B6AE54Bb947De4D270736e8Bd493D64560fa`

`uploaderAddress`

User's wallet for attribution/credit

User's connected wallet

*   **CDN URLs** use `operator` (backend wallet)
    
*   **Attribution/tipping** uses `uploaderAddress` (user's wallet)
    
*   **Ownership checks** look at `uploaderAddress` first, then `operator`
    

### Text Content & Markdown Rendering (v1.41.0)

Text files created via the "Write Something" editor support markdown formatting:

**Editor Flow:**

1.  User writes content in rich text editor (`text-editor-upload.tsx`)
    
2.  Links inserted via toolbar become `<a>` tags in HTML
    
3.  On save, `htmlToTextWithLinks()` converts HTML to markdown: `[text](url)`
    
4.  Content stored as `.txt` file on Net Protocol
    

**Reader Flow:**

1.  Content fetched via `/api/media-proxy` → `/api/book-file`
    
2.  Base64/hex decoding applied (see above)
    
3.  `multimedia-viewer.tsx` renders content with `react-markdown`
    
4.  Links become clickable, headers render with styling
    

**Supported Markdown:**

Syntax

Renders As

`[text](url)`

Clickable link (green, opens in external browser via safeOpenUrl)

`# Header`

Green H1

`**bold**`

Bold text

`*italic*`

Italic text

`- item`

Bulleted list

`1. item`

Numbered list

`` `code` ``

Inline code (green on dark background)

`> quote`

Blockquote (left border)

* * *

Storage Types
-------------

Files are stored differently based on size:

Size

Storage Type

Contract

Method

< 20KB

Regular

`STORAGE_CONTRACT_ADDRESS`

Single transaction

20-80KB

Chunked

`CHUNKED_STORAGE_ADDRESS`

Multiple chunk transactions

\> 80KB

XML with chunks

`CHUNKED_STORAGE_ADDRESS`

XML metadata + chunk transactions

### Contract Addresses (Base)

    STORAGE_CONTRACT_ADDRESS = "0x..." // Regular storage
    CHUNKED_STORAGE_ADDRESS = "0x..."  // Chunked storage
    

### Cost Estimation

    // Each transaction costs ~0.000005 ETH on Base (~$0.015 at typical ETH prices)
    const estimatedTransactions = file.size >= 80000
      ? Math.ceil(file.size / 20000) + 2  // Chunks + metadata
      : 2;                                  // Regular: store + index
    
    const estimatedCostEth = estimatedTransactions * 0.000005;
    

* * *

Session Management
------------------

### EIP-712 Signed Sessions

Sessions authorize the backend wallet to submit transactions on the user's behalf.

    // Session structure
    interface RelaySession {
      operatorAddress: Address;    // User's connected wallet
      chainId: number;             // 8453 (Base)
      expiresAt: number;           // Unix timestamp (1 hour from creation)
      signature: string;           // EIP-712 signature
    }
    

### Session Lifecycle

1.  **Check cache** - Memory refs store valid sessions
    
2.  **If expired/missing** - Request new signature from user
    
3.  **User signs EIP-712 message** - "Authorize relay for 1 hour"
    
4.  **Cache session** - Reuse for subsequent uploads
    
5.  **After 1 hour** - Session expires, new signature needed
    

### Caching Strategy

    // In-memory refs (survives re-renders, cleared on page refresh)
    const sessionTokenRef = useRef<string | null>(null);
    const sessionExpiresAtRef = useRef<number>(0);
    const sessionAddressRef = useRef<string | null>(null); // Tracks which wallet the session belongs to
    

### Session Invalidation on Wallet Change

**CRITICAL:** Sessions are now automatically cleared when the wallet address changes:

    useEffect(() => {
      if (address !== sessionAddressRef.current) {
        // Clear session when wallet changes
        sessionTokenRef.current = null;
        sessionExpiresAtRef.current = 0;
        sessionAddressRef.current = address || null;
    
        // Also abort any in-progress operations
        if (abortControllerRef.current) {
          abortControllerRef.current.abort();
        }
        uploadInProgressRef.current = false;
      }
    }, [address]);
    

This prevents using a session signed by Wallet A when Wallet B is now connected.

* * *

Archival Pattern
----------------

For any feature that needs permanent onchain storage (grids, social receipts, future features):

### Standard Archival Flow

    async function archiveContent(content: Blob, metadata: object) {
      // 1. Convert to File
      const file = new File([content], `${name}-${Date.now()}.png`, { type: "image/png" });
    
      // 2. Upload via relay
      const result = await uploadFile(file, {
        title: metadata.title,
        author: userAddress,
        categories: ["archive-type"],
        uploaderFid: fid,
      });
    
      // 3. Handle funding requirement
      if (result.error === "NEEDS_FUNDING") {
        // Show funding UI, return early
        return { needsFunding: true };
      }
    
      // 4. Store reference with contentKey
      await saveArchiveRecord({
        ...metadata,
        contentKey: result.contentKey,  // Links to onchain content
      });
    
      return { success: true, contentKey: result.contentKey };
    }
    

### What Gets Stored Where

Data

Location

Permanent?

Actual content (PNG, file)

Net Protocol (onchain)

Yes

Content reference (contentKey)

KV database

Yes

Metadata (title, creator, etc.)

KV database

Yes

* * *

Key Files Reference
-------------------

### Shared Utilities

    src/lib/relay-upload-utils.ts
    

*   `isSmartWalletDeployed()` - Check if Smart Wallet contract is deployed
    
*   `isSimulationFailure()` - Detect false "insufficient funds" errors from simulation
    
*   `analyzeUploadError()` - Categorize errors for better user feedback
    
*   `ERC20_ABI`, `USDC_ADDRESS`, `USDC_DECIMALS` - Shared constants
    
*   `basePublicClient` - Shared Base chain client
    

**CRITICAL**: Both upload hooks import from this file to ensure consistent behavior across single and batch uploads.

### Primary Hooks (THE upload paths)

**Single File Uploads:**

    src/hooks/use-relay-upload-v2.ts
    

*   `uploadFile()` - Main upload function
    
*   `fundWallet()` - USDC funding with Smart Wallet handling
    
*   `ensureSession()` - Session management
    
*   `getRelayBalance()` / `checkBalance()` - Balance checking
    
*   `cancelUpload()` - Abort in-progress upload
    
*   `isUploading` - Check if upload is in progress
    

**Batch File Uploads:**

    src/hooks/use-relay-batch-upload-v2.ts
    

*   `uploadBatch()` - Batch upload function (for true EIP-5792 batched transactions)
    
*   `estimateBatchCost()` - Cost estimation for multiple files (**currently the main usage**)
    
*   `fundBackendWallet()` - USDC funding with Smart Wallet handling
    
*   `cancelBatchUpload()` - Abort in-progress batch upload
    
*   `isUploading` - Check if batch upload is in progress
    
*   Shares session management pattern with single upload
    

**Current Usage:**

*   `upload-tab.tsx` uses `uploadFile()` in a loop for multi-file uploads (simpler, sequential)
    
*   `multi-file-upload.tsx` uses `estimateBatchCost()` only for showing cost preview
    
*   `uploadBatch()` is available for future use cases needing true transaction batching
    

### Upload UI

    src/features/app/components/upload-tab.tsx (~4,900 lines)
    

*   All upload handlers (single, batch loop, text, image, receipt, CDN)
    
*   UI for single/batch/drag-drop/text/image uploads
    
*   Funding modal integration
    
*   Batch uploads use sequential `uploadFile()` calls (not `uploadBatch()`)
    

    src/features/app/components/multi-file-upload.tsx
    

*   Batch file selection and preview UI
    
*   Uses `use-relay-batch-upload-v2.estimateBatchCost()` for cost estimation display
    

### Upload Wallet Balance Display

    src/features/app/components/upload-wallet-balance.tsx
    

*   Shows current balance
    
*   Info modal explaining relay system
    
*   Multi-platform wallet explanation
    

### Platform Detection

    src/lib/app-mode.ts
    

*   `detectFarcasterFrame()`
    
*   `detectCoinbaseWallet()`
    
*   `detectAppMode()`
    
*   `detectDesktopBrowser()`
    
*   `safeOpenUrl()` - Opens URLs with Farcaster SDK fallback
    
*   `safeComposeCast()` - Compose cast with web fallback
    
*   `triggerHaptic()` - Haptic feedback wrapper
    

### Wallet Provider Configuration

    src/neynar-web-sdk/src/blockchain/app/neynar-wagmi-provider.tsx
    

*   Platform-specific connector selection (Farcaster/Base App/Web)
    
*   EIP-6963 discovery for web mode
    
*   `WalletGuard` component for preference enforcement
    
*   `disableCoinbaseAutoInjection()` safeguard
    
*   Custom storage for reconnection filtering
    
*   `getWalletPreference()` / `setWalletPreference()` / `clearWalletPreference()`
    

### Net Protocol Utilities

    src/lib/net-protocol.ts
    

*   Contract addresses
    
*   ABIs
    
*   File preparation utilities
    
*   Storage key generation
    

### Data URL Utilities

    src/lib/data-url-utils.ts
    

*   `dataUrlToBlob()` - Safe manual base64 decode (no fetch)
    
*   `safeDataUrlToBlob()` - Decode with fetch fallback
    
*   `dataUrlToFile()` - Convert data URL directly to File
    
*   **Required** for Safari/iOS/Base App compatibility (avoids "Load failed" errors)
    

### Grid Archival (Example of archival pattern)

    src/features/app/components/grid-archive-modal.tsx
    src/hooks/use-grid-archive.ts
    src/app/api/grids/archive/route.ts
    

* * *

Dead Code Status
----------------

### ✅ Cleanup Completed

The following dead code files have been **deleted** (~4,300 lines total):

File

Lines

Status

`src/hooks/use-net-upload.ts`

~2,234

✅ Deleted

`src/hooks/use-batch-upload.ts`

~796

✅ Deleted

`src/hooks/use-relay-batch-upload.ts`

~1,285

✅ Deleted

`src/hooks/use-relay-upload.ts`

~1,500

✅ Deleted

These were workarounds before Net Protocol's relay system was available. Users had to sign 10-15+ transactions per upload. The relay v2 system eliminated this need.

### Active Hooks (THE ONLY UPLOAD HOOKS)

*   `use-relay-upload-v2.ts` - Single file uploads (used by upload-tab.tsx)
    
*   `use-relay-batch-upload-v2.ts` - Batch file uploads (used by multi-file-upload.tsx)
    

**DO NOT create new upload hooks.** All uploads MUST use these two hooks.

* * *

UX Considerations
-----------------

### Upload Wallet Balance Visibility

*   Shown in Upload tab header
    
*   Balance displayed in ETH with USD equivalent
    
*   Warning when below $0.10 minimum
    

### Funding Recommendations

Amount

Use Case

$0.10

Testing, a few uploads

$0.25

~10-25 small uploads

$5.00

~200+ uploads (recommended for regular use)

### Multi-Platform Wallet Confusion

Users may not realize different apps connect different wallets. The info bubble in Upload Wallet explains:

*   Each platform connects its own wallet
    
*   Different wallets = separate balances
    
*   Library is shared, only funding is wallet-specific
    

### Error Handling

Error

User Message

Action

`NEEDS_FUNDING`

"Please fund your Upload Wallet"

Show funding modal

Session expired

"Awaiting wallet signature..."

Request new signature

Upload failed

Specific error message

Allow retry

### Safari/iOS/Base App: Data URL Fetch Bug

**⚠️ IMPORTANT:** Safari/iOS/WKWebView (used by Base App) can throw `"Load failed"` when `fetch()` is called on data: URLs due to WebKit security policies.

**Problematic Pattern:**

    // ❌ DON'T DO THIS - fails on Safari/iOS/Base App
    const response = await fetch(dataUrl);  // "Load failed" error!
    const blob = await response.blob();
    

**Safe Pattern:**

    // ✅ USE THIS - works everywhere
    import { dataUrlToBlob, safeDataUrlToBlob } from "@/lib/data-url-utils";
    
    // Option 1: Direct decode (throws on invalid data URL)
    const blob = dataUrlToBlob(dataUrl);
    
    // Option 2: Safe decode with fetch fallback
    const blob = await safeDataUrlToBlob(dataUrl);
    

**Manual decode (if not using utility):**

    const [header, base64Data] = dataUrl.split(",");
    const mimeMatch = header.match(/data:([^;]+)/);
    const mimeType = mimeMatch ? mimeMatch[1] : "image/png";
    const binaryString = atob(base64Data);
    const bytes = new Uint8Array(binaryString.length);
    for (let i = 0; i < binaryString.length; i++) {
      bytes[i] = binaryString.charCodeAt(i);
    }
    const blob = new Blob([bytes], { type: mimeType });
    

**Affected Code Paths (all fixed):**

*   `handleReceiptUpload()` - Social receipt archive uploads
    
*   `handleImageUpload()` - Created image uploads from editor
    
*   Image editor export/download
    

* * *

Quick Reference: Upload Flow
----------------------------

    User clicks "Upload"
            ↓
        uploadFile(file, metadata)
            ↓
        checkBalance() → Sufficient?
            ↓ No              ↓ Yes
        Return NEEDS_FUNDING  ensureSession()
            ↓                     ↓
        Show funding UI       Session valid?
                                  ↓ No           ↓ Yes
                              Sign EIP-712     Use cached
                                  ↓                ↓
                              ←←←←←←←←←←←←←←←←←←←←←
                                        ↓
                              prepareFileForUpload()
                                        ↓
                              Submit to relay backend
                                        ↓
                              Backend pays gas, stores onchain
                                        ↓
                              Return { success, cdnUrl, contentKey }
    

* * *

Summary
-------

1.  **All uploads use relay** - No exceptions, no fallbacks
    
2.  **Two hooks, one pattern** - `useRelayUploadV2` for single files, `useRelayBatchUploadV2` for batches
    
3.  **Backend pays gas** - User funds USDC, backend handles ETH
    
4.  **Sessions last 1 hour** - Cached in memory, re-sign when expired, cleared on wallet change
    
5.  **Platform = Wallet = Balance** - Different apps may have different balances
    
6.  **Archival pattern is standard** - Upload via relay, store contentKey in KV
    
7.  **Smart Wallet aware** - Detects undeployed wallets, handles simulation failures gracefully
    
8.  **Protected operations** - Concurrent upload prevention, cancellation support, cleanup on unmount
    
9.  **Shared utilities** - `relay-upload-utils.ts` ensures consistent behavior across both hooks
    
10.  **Archives ARE uploads** - Grid Archives and Social Archives use the same relay upload system. When editing upload logic, ALWAYS check: single uploads, batch uploads, grid archives, social archives
    
11.  **Connector isolation** - Each platform uses ONLY its designated connector (prevents simulation failures)
    
12.  **EIP-6963 for web** - Web mode uses wallet discovery, not explicit connectors (prevents Coinbase hijacking)
    
13.  **WalletGuard protection** - Enforces user's wallet preference, disconnects unwanted connections
    
14.  **Payment recovery** - localStorage + blockchain scan recovery for unverified USDC payments
    
15.  **Data URL safety** - Never use `fetch()` on data: URLs; use `@/lib/data-url-utils` to avoid Safari/iOS "Load failed" errors
    

* * *

Thanks for reading, sharing and engaging with this article!

I'm really excited about the future of onchain storage and communication - and how Net Protocol makes all of that happen.

Check out [Net Library](https://miniapp-generator-fid-282520-251210015136529.neynar.app/) on Farcaster, The Base App, or simply via your favorite browser.  
(If you connect via a web browser or The Base App, make sure to link that wallet to your Farcaster account. This allows Net Library to automatically detect your Net Library Membership across all three apps)

Have questions? Want to learn more? Like interviewing nerdy dads?  
You can reach me:  
@geaux\_eth on X (twitter)  
@geaux.eth on Farcaster

God Bless!

---

*Originally published on [GEAUX Think](https://paragraph.com/@geauxthink/blockchain-storage-considerations-building-on-net-protocol)*
