

This article is a copy of the upload-system md file currently being used by Net Library to insure that all file uploads are executed accurately.
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 on Farcaster, on the Base App here, or via a web browswer here
If you're not familiar with Net Protocol, go here
⚠️⚠️ 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!
Overview
The Relay System
Upload Entry Points
Platform-Specific Behavior
Platform Detection
Connector Isolation
EIP-6963 Discovery
WalletGuard
Wallet Types & Transaction Handling
Smart Wallet vs EOA
Transaction Flow
Funding Flow
Smart Wallet Detection
Upload Protections
Concurrent Upload Prevention
Cancellable Operations
Payment Recovery
Content Retrieval & Encoding
Storage Types
Session Management
Archival Pattern
Key Files Reference
Dead Code Status
UX Considerations
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.
User funds USDC once → Backend wallet pays gas → One-click uploads
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) |
User funds Upload Wallet with USDC (any amount, presets: $0.10, $0.25, $5.00)
x402 protocol converts USDC → ETH automatically at market rate
Backend wallet (deterministically derived from user's operatorAddress) stores ETH
Session signature (EIP-712) authorizes the backend to act on user's behalf for 1 hour
Backend submits transactions using stored ETH for gas
Content stored onchain via Net Protocol storage contracts
Backend Wallet Address = deterministic_derive(operatorAddress)
Same wallet address → Same backend wallet → Same balance
Different wallet addresses → Different backend wallets → Separate balances
const RELAY_CONFIG = {
apiUrl: "https://www.netprotocol.app",
chainId: 8453, // Base
usdcAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
x402FacilitatorAddress: "0x6462F8f301Bd08c206ee611aA00015bA41acc1D0",
verifyMaxRetries: 5,
verifyRetryDelayMs: 2000,
};
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.
🔴 MANDATORY CHECK LIST - When editing upload logic, verify ALL of these:
Category | User Action | Handler/Hook | File Location |
|---|---|---|---|
Standard Uploads | |||
Single file upload |
|
| |
Batch/multi-file |
|
| |
Drag & drop | Same as single/batch |
| |
Text editor save |
|
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)
└── Uses uploadFile() from use-relay-upload-v2.ts
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).
All handlers are in upload-tab.tsx which imports:
import { useRelayUploadV2 as useRelayUpload } from "@/hooks/use-relay-upload-v2";
// 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 | Connector Used | Connection Method | Notes |
|---|---|---|---|
Warpcast (Farcaster) |
| Frame SDK | Only Farcaster connector enabled |
Coinbase Wallet app |
| 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.
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
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
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).
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();
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)
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({ ... });
}
// 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
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 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;
}
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:
Check actual USDC balance to confirm it's not a real funds issue
If undeployed Smart Wallet: suggest smaller amount or wallet activation
If deployed but still failing: suggest retry (temporary network issue)
Both upload hooks include these safety mechanisms:
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;
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;
};
useEffect(() => {
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []);
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)
When reading content from Net Protocol storage, data can be returned in different encodings:
// 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;
}
File | Purpose |
|---|---|
| Raw file serving (PDF, audio, video, text) |
| Content analysis and type detection |
| CORS proxy for multimedia |
| Discovery index fetching |
CRITICAL: Two different addresses are used:
Field | Purpose | Example |
|---|---|---|
| Backend wallet that wrote to blockchain |
|
| 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 files created via the "Write Something" editor support markdown formatting:
Editor Flow:
User writes content in rich text editor (text-editor-upload.tsx)
Links inserted via toolbar become <a> tags in HTML
On save, htmlToTextWithLinks() converts HTML to markdown: [text](url)
Content stored as .txt file on Net Protocol
Reader Flow:
Content fetched via /api/media-proxy → /api/book-file
Base64/hex decoding applied (see above)
multimedia-viewer.tsx renders content with react-markdown
Links become clickable, headers render with styling
Supported Markdown:
Syntax | Renders As |
|---|---|
| Clickable link (green, opens in external browser via safeOpenUrl) |
| Green H1 |
| Bold text |
| Italic text |
| Bulleted list |
| Numbered list |
| Inline code (green on dark background) |
| Blockquote (left border) |
Files are stored differently based on size:
Size | Storage Type | Contract | Method |
|---|---|---|---|
< 20KB | Regular |
| Single transaction |
20-80KB | Chunked |
| Multiple chunk transactions |
> 80KB | XML with chunks |
| XML metadata + chunk transactions |
STORAGE_CONTRACT_ADDRESS = "0x..." // Regular storage
CHUNKED_STORAGE_ADDRESS = "0x..." // Chunked storage
// 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;
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
}
Check cache - Memory refs store valid sessions
If expired/missing - Request new signature from user
User signs EIP-712 message - "Authorize relay for 1 hour"
Cache session - Reuse for subsequent uploads
After 1 hour - Session expires, new signature needed
// 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
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.
For any feature that needs permanent onchain storage (grids, social receipts, future features):
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 };
}
Data | Location | Permanent? |
|---|---|---|
Actual content (PNG, file) | Net Protocol (onchain) | Yes |
Content reference (contentKey) | KV database | Yes |
Metadata (title, creator, etc.) | KV database | Yes |
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.
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
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
src/features/app/components/upload-wallet-balance.tsx
Shows current balance
Info modal explaining relay system
Multi-platform wallet explanation
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
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()
src/lib/net-protocol.ts
Contract addresses
ABIs
File preparation utilities
Storage key generation
src/features/app/components/grid-archive-modal.tsx
src/hooks/use-grid-archive.ts
src/app/api/grids/archive/route.ts
The following dead code files have been deleted (~4,300 lines total):
File | Lines | Status |
|---|---|---|
| ~2,234 | Deleted |
| ~796 | Deleted |
| ~1,285 | Deleted |
| ~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.
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.
Shown in Upload tab header
Balance displayed in ETH with USD equivalent
Warning when below $0.10 minimum
Amount | Use Case |
|---|---|
$0.10 | Testing, a few uploads |
$0.25 | ~10-25 small uploads |
$5.00 | ~200+ uploads (recommended for regular use) |
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 | User Message | Action |
|---|---|---|
| "Please fund your Upload Wallet" | Show funding modal |
Session expired | "Awaiting wallet signature..." | Request new signature |
Upload failed | Specific error message | Allow retry |
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 }
All uploads use relay - No exceptions, no fallbacks
Two hooks, one pattern - useRelayUploadV2 for single files, useRelayBatchUploadV2 for batches
Backend pays gas - User funds USDC, backend handles ETH
Sessions last 1 hour - Cached in memory, re-sign when expired, cleared on wallet change
Platform = Wallet = Balance - Different apps may have different balances
Archival pattern is standard - Upload via relay, store contentKey in KV
Smart Wallet aware - Detects undeployed wallets, handles simulation failures gracefully
Protected operations - Concurrent upload prevention, cancellation support, cleanup on unmount
Shared utilities - relay-upload-utils.ts ensures consistent behavior across both hooks
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
Connector isolation - Each platform uses ONLY its designated connector (prevents simulation failures)
EIP-6963 for web - Web mode uses wallet discovery, not explicit connectors (prevents Coinbase hijacking)
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 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!
This article is a copy of the upload-system md file currently being used by Net Library to insure that all file uploads are executed accurately.
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 on Farcaster, on the Base App here, or via a web browswer here
If you're not familiar with Net Protocol, go here
⚠️⚠️ 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!
Overview
The Relay System
Upload Entry Points
Platform-Specific Behavior
Platform Detection
Connector Isolation
EIP-6963 Discovery
WalletGuard
Wallet Types & Transaction Handling
Smart Wallet vs EOA
Transaction Flow
Funding Flow
Smart Wallet Detection
Upload Protections
Concurrent Upload Prevention
Cancellable Operations
Payment Recovery
Content Retrieval & Encoding
Storage Types
Session Management
Archival Pattern
Key Files Reference
Dead Code Status
UX Considerations
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.
User funds USDC once → Backend wallet pays gas → One-click uploads
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) |
User funds Upload Wallet with USDC (any amount, presets: $0.10, $0.25, $5.00)
x402 protocol converts USDC → ETH automatically at market rate
Backend wallet (deterministically derived from user's operatorAddress) stores ETH
Session signature (EIP-712) authorizes the backend to act on user's behalf for 1 hour
Backend submits transactions using stored ETH for gas
Content stored onchain via Net Protocol storage contracts
Backend Wallet Address = deterministic_derive(operatorAddress)
Same wallet address → Same backend wallet → Same balance
Different wallet addresses → Different backend wallets → Separate balances
const RELAY_CONFIG = {
apiUrl: "https://www.netprotocol.app",
chainId: 8453, // Base
usdcAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
x402FacilitatorAddress: "0x6462F8f301Bd08c206ee611aA00015bA41acc1D0",
verifyMaxRetries: 5,
verifyRetryDelayMs: 2000,
};
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.
🔴 MANDATORY CHECK LIST - When editing upload logic, verify ALL of these:
Category | User Action | Handler/Hook | File Location |
|---|---|---|---|
Standard Uploads | |||
Single file upload |
|
| |
Batch/multi-file |
|
| |
Drag & drop | Same as single/batch |
| |
Text editor save |
|
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)
└── Uses uploadFile() from use-relay-upload-v2.ts
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).
All handlers are in upload-tab.tsx which imports:
import { useRelayUploadV2 as useRelayUpload } from "@/hooks/use-relay-upload-v2";
// 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 | Connector Used | Connection Method | Notes |
|---|---|---|---|
Warpcast (Farcaster) |
| Frame SDK | Only Farcaster connector enabled |
Coinbase Wallet app |
| 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.
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
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
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).
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();
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)
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({ ... });
}
// 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
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 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;
}
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:
Check actual USDC balance to confirm it's not a real funds issue
If undeployed Smart Wallet: suggest smaller amount or wallet activation
If deployed but still failing: suggest retry (temporary network issue)
Both upload hooks include these safety mechanisms:
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;
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;
};
useEffect(() => {
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []);
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)
When reading content from Net Protocol storage, data can be returned in different encodings:
// 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;
}
File | Purpose |
|---|---|
| Raw file serving (PDF, audio, video, text) |
| Content analysis and type detection |
| CORS proxy for multimedia |
| Discovery index fetching |
CRITICAL: Two different addresses are used:
Field | Purpose | Example |
|---|---|---|
| Backend wallet that wrote to blockchain |
|
| 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 files created via the "Write Something" editor support markdown formatting:
Editor Flow:
User writes content in rich text editor (text-editor-upload.tsx)
Links inserted via toolbar become <a> tags in HTML
On save, htmlToTextWithLinks() converts HTML to markdown: [text](url)
Content stored as .txt file on Net Protocol
Reader Flow:
Content fetched via /api/media-proxy → /api/book-file
Base64/hex decoding applied (see above)
multimedia-viewer.tsx renders content with react-markdown
Links become clickable, headers render with styling
Supported Markdown:
Syntax | Renders As |
|---|---|
| Clickable link (green, opens in external browser via safeOpenUrl) |
| Green H1 |
| Bold text |
| Italic text |
| Bulleted list |
| Numbered list |
| Inline code (green on dark background) |
| Blockquote (left border) |
Files are stored differently based on size:
Size | Storage Type | Contract | Method |
|---|---|---|---|
< 20KB | Regular |
| Single transaction |
20-80KB | Chunked |
| Multiple chunk transactions |
> 80KB | XML with chunks |
| XML metadata + chunk transactions |
STORAGE_CONTRACT_ADDRESS = "0x..." // Regular storage
CHUNKED_STORAGE_ADDRESS = "0x..." // Chunked storage
// 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;
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
}
Check cache - Memory refs store valid sessions
If expired/missing - Request new signature from user
User signs EIP-712 message - "Authorize relay for 1 hour"
Cache session - Reuse for subsequent uploads
After 1 hour - Session expires, new signature needed
// 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
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.
For any feature that needs permanent onchain storage (grids, social receipts, future features):
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 };
}
Data | Location | Permanent? |
|---|---|---|
Actual content (PNG, file) | Net Protocol (onchain) | Yes |
Content reference (contentKey) | KV database | Yes |
Metadata (title, creator, etc.) | KV database | Yes |
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.
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
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
src/features/app/components/upload-wallet-balance.tsx
Shows current balance
Info modal explaining relay system
Multi-platform wallet explanation
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
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()
src/lib/net-protocol.ts
Contract addresses
ABIs
File preparation utilities
Storage key generation
src/features/app/components/grid-archive-modal.tsx
src/hooks/use-grid-archive.ts
src/app/api/grids/archive/route.ts
The following dead code files have been deleted (~4,300 lines total):
File | Lines | Status |
|---|---|---|
| ~2,234 | Deleted |
| ~796 | Deleted |
| ~1,285 | Deleted |
| ~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.
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.
Shown in Upload tab header
Balance displayed in ETH with USD equivalent
Warning when below $0.10 minimum
Amount | Use Case |
|---|---|
$0.10 | Testing, a few uploads |
$0.25 | ~10-25 small uploads |
$5.00 | ~200+ uploads (recommended for regular use) |
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 | User Message | Action |
|---|---|---|
| "Please fund your Upload Wallet" | Show funding modal |
Session expired | "Awaiting wallet signature..." | Request new signature |
Upload failed | Specific error message | Allow retry |
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 }
All uploads use relay - No exceptions, no fallbacks
Two hooks, one pattern - useRelayUploadV2 for single files, useRelayBatchUploadV2 for batches
Backend pays gas - User funds USDC, backend handles ETH
Sessions last 1 hour - Cached in memory, re-sign when expired, cleared on wallet change
Platform = Wallet = Balance - Different apps may have different balances
Archival pattern is standard - Upload via relay, store contentKey in KV
Smart Wallet aware - Detects undeployed wallets, handles simulation failures gracefully
Protected operations - Concurrent upload prevention, cancellation support, cleanup on unmount
Shared utilities - relay-upload-utils.ts ensures consistent behavior across both hooks
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
Connector isolation - Each platform uses ONLY its designated connector (prevents simulation failures)
EIP-6963 for web - Web mode uses wallet discovery, not explicit connectors (prevents Coinbase hijacking)
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 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!
upload-tab.tsxImage editor save |
|
|
Retry failed file |
|
|
Archives (ALSO UPLOADS!) |
Grid archive |
|
|
Social receipt archive |
|
|
Import (No Relay) |
CDN link import |
|
|
WalletGuard protection - Enforces user's wallet preference, disconnects unwanted connections
Payment recovery - localStorage + blockchain scan recovery for unverified USDC payments
upload-tab.tsxImage editor save |
|
|
Retry failed file |
|
|
Archives (ALSO UPLOADS!) |
Grid archive |
|
|
Social receipt archive |
|
|
Import (No Relay) |
CDN link import |
|
|
WalletGuard protection - Enforces user's wallet preference, disconnects unwanted connections
Payment recovery - localStorage + blockchain scan recovery for unverified USDC payments

HAMCASTER FIELD GUIDE
SUMMARY, STRATEGY AND SOCIAL IMPACT

Grok has money.
We'll unpack the most compelling three word sentence of our generation by defining each word: GROK, HAS, and MONEY.

Every Day Clankers
A Guide To The Screen Art of Every Day Clankers

HAMCASTER FIELD GUIDE
SUMMARY, STRATEGY AND SOCIAL IMPACT

Grok has money.
We'll unpack the most compelling three word sentence of our generation by defining each word: GROK, HAS, and MONEY.

Every Day Clankers
A Guide To The Screen Art of Every Day Clankers
<100 subscribers
<100 subscribers
Share Dialog
Share Dialog
1 comment
Net Library's blogpost details a Net Protocol–driven Upload System Architecture in which a backend wallet funds gas via a relay for one-click, onchain uploads. It covers relay flow, platform behavior, wallet detection, session management, protections, archival patterns, and UX. @geaux.eth