From 4dd4a53ca9552417beccf28b68d180ab5ddba33c Mon Sep 17 00:00:00 2001 From: Willy Ogorzaly Date: Fri, 17 Oct 2025 14:15:42 -0500 Subject: [PATCH 001/155] Expand CSP media and connect sources --- next.config.mjs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/next.config.mjs b/next.config.mjs index e15589017..a2147ad33 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -12,7 +12,7 @@ const cspHeader = ` default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://challenges.cloudflare.com https://www.youtube.com https://www.youtube.com/iframe_api https://auth.privy.nounspace.com https://cdn.segment.com; style-src 'self' 'unsafe-inline' https://i.ytimg.com https://mint.highlight.xyz; - media-src 'self' blob: data: https://stream.warpcast.com https://stream.farcaster.xyz https://res.cloudinary.com/; + media-src 'self' blob: data: https://stream.warpcast.com https://stream.farcaster.xyz https://res.cloudinary.com/ https://*.cloudflarestream.com https://*.b-cdn.net; img-src 'self' blob: data: https:; font-src 'self' https: data: blob: https://fonts.googleapis.com https://fonts.gstatic.com; object-src 'none'; @@ -48,7 +48,10 @@ const cspHeader = ` https://api.coingecko.com https://stream.warpcast.com https://stream.farcaster.xyz - https://res.cloudinary.com/; + https://res.cloudinary.com/ + https://*.cloudflarestream.com + https://*.b-cdn.net + https://cca-lite.coinbase.com; upgrade-insecure-requests; `; From 3c060b3e39c1247d2282618a4ed27bda16929a77 Mon Sep 17 00:00:00 2001 From: Willy Ogorzaly Date: Wed, 15 Oct 2025 15:30:31 -0500 Subject: [PATCH 002/155] Lower frame embed z-index under mobile nav --- src/fidgets/framesV2/components/FrameRenderer.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fidgets/framesV2/components/FrameRenderer.tsx b/src/fidgets/framesV2/components/FrameRenderer.tsx index 75856f054..e1d6776c3 100644 --- a/src/fidgets/framesV2/components/FrameRenderer.tsx +++ b/src/fidgets/framesV2/components/FrameRenderer.tsx @@ -187,7 +187,7 @@ export default function FrameRenderer({ display: "flex", alignItems: "center", justifyContent: "center", - zIndex: 10, + zIndex: 0, }} role="status" aria-live="polite" @@ -313,7 +313,7 @@ export default function FrameRenderer({ position: "relative", width: "100%", height: "100%", - zIndex: 1, + zIndex: -1, }} > Date: Wed, 15 Oct 2025 15:47:37 -0500 Subject: [PATCH 003/155] Ensure frame image stays above card overlays --- src/fidgets/framesV2/components/FrameRenderer.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/fidgets/framesV2/components/FrameRenderer.tsx b/src/fidgets/framesV2/components/FrameRenderer.tsx index e1d6776c3..3d19339ac 100644 --- a/src/fidgets/framesV2/components/FrameRenderer.tsx +++ b/src/fidgets/framesV2/components/FrameRenderer.tsx @@ -304,6 +304,7 @@ export default function FrameRenderer({ background: "#fff", position: "relative", overflow: "hidden", + zIndex: 0, }} > {/* Frame image (responsive, fills container) */} @@ -313,7 +314,7 @@ export default function FrameRenderer({ position: "relative", width: "100%", height: "100%", - zIndex: -1, + zIndex: 0, }} > Date: Wed, 22 Oct 2025 16:28:47 -0500 Subject: [PATCH 004/155] documentation update --- docs/ARCHITECTURE/AUTHENTICATION.md | 254 +++++++ docs/ARCHITECTURE/OVERVIEW.md | 321 +++++++++ docs/ARCHITECTURE/STATE_MANAGEMENT.md | 394 +++++++++++ docs/CHANGELOG-PR1279-en.md | 261 ------- docs/CONTRIBUTING.MD | 11 +- docs/{ => DEVELOPMENT}/AGENTS.md | 0 docs/DEVELOPMENT/CODING_STANDARDS.md | 609 ++++++++++++++++ docs/DEVELOPMENT/COMPONENT_ARCHITECTURE.md | 573 +++++++++++++++ docs/DEVELOPMENT/DEBUGGING.md | 657 +++++++++++++++++ .../DEVELOPMENT_GUIDE.md} | 0 .../DEVELOPMENT_NOTES.md} | 0 docs/DEVELOPMENT/TESTING.md | 663 ++++++++++++++++++ docs/DOCUMENTATION_OVERVIEW.md | 124 ++++ docs/GETTING_STARTED.md | 94 +++ docs/INTEGRATIONS/FARCASTER.md | 558 +++++++++++++++ docs/INTEGRATIONS/SUPABASE.md | 605 ++++++++++++++++ docs/MIGRATION_SUMMARY.md | 124 ++++ docs/README.md | 66 ++ .../DISCOVERY}/MINI_APP_DISCOVERY_SYSTEM.md | 0 docs/SYSTEMS/FIDGETS/OVERVIEW.md | 509 ++++++++++++++ .../SPACES/LAYOUT_MIGRATION_GUIDE.md} | 0 .../SPACES/MULTIPLE_LAYOUTS_OVERVIEW.md} | 0 docs/SYSTEMS/SPACES/OVERVIEW.md | 378 ++++++++++ .../SPACES}/PUBLIC_SPACES_PATTERN.md | 0 .../SPACES}/SPACE_ARCHITECTURE.md | 0 docs/SYSTEMS/THEMES/OVERVIEW.md | 647 +++++++++++++++++ docs/TAB_MANAGEMENT_SYSTEM.md | 590 ---------------- docs/TAG_SYSTEM.md | 261 ------- docs/immutable-homebase-feed-en.md | 410 ----------- docs/mobile-drag-drop-system-en.md | 335 --------- docs/multiple-layouts-system-en.md | 234 ------- 31 files changed, 6582 insertions(+), 2096 deletions(-) create mode 100644 docs/ARCHITECTURE/AUTHENTICATION.md create mode 100644 docs/ARCHITECTURE/OVERVIEW.md create mode 100644 docs/ARCHITECTURE/STATE_MANAGEMENT.md delete mode 100644 docs/CHANGELOG-PR1279-en.md rename docs/{ => DEVELOPMENT}/AGENTS.md (100%) create mode 100644 docs/DEVELOPMENT/CODING_STANDARDS.md create mode 100644 docs/DEVELOPMENT/COMPONENT_ARCHITECTURE.md create mode 100644 docs/DEVELOPMENT/DEBUGGING.md rename docs/{instructions.md => DEVELOPMENT/DEVELOPMENT_GUIDE.md} (100%) rename docs/{notes.md => DEVELOPMENT/DEVELOPMENT_NOTES.md} (100%) create mode 100644 docs/DEVELOPMENT/TESTING.md create mode 100644 docs/DOCUMENTATION_OVERVIEW.md create mode 100644 docs/GETTING_STARTED.md create mode 100644 docs/INTEGRATIONS/FARCASTER.md create mode 100644 docs/INTEGRATIONS/SUPABASE.md create mode 100644 docs/MIGRATION_SUMMARY.md create mode 100644 docs/README.md rename docs/{ => SYSTEMS/DISCOVERY}/MINI_APP_DISCOVERY_SYSTEM.md (100%) create mode 100644 docs/SYSTEMS/FIDGETS/OVERVIEW.md rename docs/{layout-migration-guide-en.md => SYSTEMS/SPACES/LAYOUT_MIGRATION_GUIDE.md} (100%) rename docs/{README-en.md => SYSTEMS/SPACES/MULTIPLE_LAYOUTS_OVERVIEW.md} (100%) create mode 100644 docs/SYSTEMS/SPACES/OVERVIEW.md rename docs/{ => SYSTEMS/SPACES}/PUBLIC_SPACES_PATTERN.md (100%) rename docs/{ => SYSTEMS/SPACES}/SPACE_ARCHITECTURE.md (100%) create mode 100644 docs/SYSTEMS/THEMES/OVERVIEW.md delete mode 100644 docs/TAB_MANAGEMENT_SYSTEM.md delete mode 100644 docs/TAG_SYSTEM.md delete mode 100644 docs/immutable-homebase-feed-en.md delete mode 100644 docs/mobile-drag-drop-system-en.md delete mode 100644 docs/multiple-layouts-system-en.md diff --git a/docs/ARCHITECTURE/AUTHENTICATION.md b/docs/ARCHITECTURE/AUTHENTICATION.md new file mode 100644 index 000000000..070d025de --- /dev/null +++ b/docs/ARCHITECTURE/AUTHENTICATION.md @@ -0,0 +1,254 @@ +# Authentication System + +The Nounspace authentication system combines Privy for wallet-based authentication with Farcaster for social identity, creating a seamless Web3 social experience. + +## Architecture Overview + +The authentication system consists of several key components: + +- **Privy Integration** - Primary authentication provider +- **Farcaster Integration** - Social identity and protocol access +- **Identity Management** - Multi-identity support with cryptographic keys +- **Authenticator System** - Modular authentication methods + +## Core Components + +### 1. Privy Integration + +Privy handles the primary authentication flow: + +```typescript +// Privy store manages user state +interface PrivyState { + privyUser?: PrivyUser | null; +} + +interface PrivyActions { + setPrivyUser: (user: PrivyUser | null) => void; +} +``` + +**Key Features:** +- Wallet connection (MetaMask, WalletConnect, etc.) +- Social login options +- User session management +- Multi-wallet support + +### 2. Farcaster Integration + +Farcaster provides social identity and protocol access: + +```typescript +// Farcaster store manages FID linking +type FarcasterActions = { + getFidsForCurrentIdentity: () => Promise; + registerFidForCurrentIdentity: ( + fid: number, + signingKey: string, + signMessage: (messageHash: Uint8Array) => Promise, + ) => Promise; + setFidsForCurrentIdentity: (fids: number[]) => void; + addFidToCurrentIdentity: (fid: number) => void; +}; +``` + +**Key Features:** +- FID (Farcaster ID) linking to identities +- Cryptographic signature verification +- Social graph access +- Cast and interaction capabilities + +### 3. Identity Management + +The system supports multiple identities per user: + +```typescript +export type AccountStore = IdentityStore & + AuthenticatorStore & + PreKeyStore & + FarcasterStore & + PrivyStore & { + reset: () => void; + hasNogs: boolean; + setHasNogs: (v: boolean) => void; + }; +``` + +**Identity Features:** +- Multiple space identities +- Cryptographic key management +- Identity switching +- Secure key storage + +### 4. Authenticator System + +Modular authentication methods for different services: + +```typescript +// Authenticator manager handles method calls +const authenticatorManager = useMemo(() => ({ + callMethod: async ( + { requestingFidgetId, authenticatorId, methodName, isLookup = false }, + ...args + ): Promise => { + // Handle authenticator method calls + }, + getInitializedAuthenticators: () => { + // Return available authenticators + } +})); +``` + +## Authentication Flow + +### 1. Initial Setup + +1. **User connects wallet** via Privy +2. **Identity creation** with cryptographic keys +3. **Farcaster linking** (optional) +4. **Authenticator setup** for services + +### 2. Farcaster Integration + +```typescript +// Register FID for current identity +const registerFidForCurrentIdentity = async ( + fid: number, + signingKey: string, + signMessage: (messageHash: Uint8Array) => Promise, +) => { + const request: Omit = { + fid, + identityPublicKey: get().account.currentSpaceIdentityPublicKey!, + timestamp: moment().toISOString(), + signingPublicKey: signingKey, + }; + + const signedRequest: FidLinkToIdentityRequest = { + ...request, + signature: bytesToHex(await signMessage(hashObject(request))), + }; + + // Submit to backend + const { data } = await axiosBackend.post( + "/api/fid-link", + signedRequest, + ); +}; +``` + +### 3. Identity Management + +```typescript +// Identity store manages multiple identities +interface IdentityStore { + spaceIdentities: SpaceIdentity[]; + currentSpaceIdentityPublicKey: string | null; + getCurrentIdentity: () => SpaceIdentity | null; + getCurrentIdentityIndex: () => number; + setCurrentSpaceIdentityPublicKey: (key: string | null) => void; +} +``` + +## Security Considerations + +### 1. Cryptographic Security + +- **Key Generation**: Secure random key generation +- **Key Storage**: Encrypted local storage +- **Signature Verification**: Cryptographic signature validation +- **Key Rotation**: Support for key updates + +### 2. Privacy Protection + +- **Local Storage**: Sensitive data stored locally +- **Encryption**: Keys encrypted with user wallet +- **No Central Storage**: No centralized key storage + +### 3. Access Control + +- **Permission System**: Fidget-level permissions +- **Method Authorization**: Authenticator method access control +- **Identity Verification**: Cryptographic identity verification + +## API Integration + +### FID Linking + +```typescript +// Link Farcaster FID to identity +POST /api/fid-link +{ + "fid": number, + "identityPublicKey": string, + "timestamp": string, + "signingPublicKey": string, + "signature": string +} +``` + +### Identity Management + +```typescript +// Get FIDs for identity +GET /api/fid-link?identityPublicKey={key} +``` + +## Development Patterns + +### 1. Adding New Authenticators + +```typescript +// Create authenticator in src/authenticators/ +export const newAuthenticator = (config: AuthenticatorConfig) => ({ + methods: { + methodName: async (params) => { + // Implementation + } + } +}); +``` + +### 2. Using Authenticators in Fidgets + +```typescript +// Access authenticator in fidget +const { callMethod } = useAuthenticatorManager(); + +const result = await callMethod({ + requestingFidgetId: 'my-fidget', + authenticatorId: 'farcaster:signers', + methodName: 'getUserInfo' +}); +``` + +### 3. Permission Management + +```typescript +// Check permissions before calling methods +const hasPermission = authenticatorConfig[authenticatorId] + .permissions[requestingFidgetId]?.includes(methodName); +``` + +## Troubleshooting + +### Common Issues + +1. **Authentication Failures**: Check Privy configuration +2. **FID Linking Issues**: Verify Farcaster integration +3. **Permission Denied**: Check authenticator permissions +4. **Key Management**: Ensure proper key storage + +### Debug Tools + +- Use React DevTools to inspect store state +- Check browser console for authentication errors +- Verify environment variables are set correctly +- Test with different wallet providers + +## Future Considerations + +- Enhanced permission system +- Multi-chain identity support +- Advanced key management +- Social recovery mechanisms diff --git a/docs/ARCHITECTURE/OVERVIEW.md b/docs/ARCHITECTURE/OVERVIEW.md new file mode 100644 index 000000000..bafd47fce --- /dev/null +++ b/docs/ARCHITECTURE/OVERVIEW.md @@ -0,0 +1,321 @@ +# Architecture Overview + +Nounspace is built on a modern, modular architecture that combines Next.js App Router with Zustand state management, creating a highly customizable Farcaster client experience. + +## High-Level Architecture + +```mermaid +graph TB + A[User Interface] --> B[Next.js App Router] + B --> C[Space System] + B --> D[Fidget System] + B --> E[Theme System] + B --> F[Authentication] + + C --> G[Public Spaces] + C --> H[Private Spaces] + C --> I[Layout System] + + D --> J[UI Fidgets] + D --> K[Farcaster Fidgets] + D --> L[Community Fidgets] + + E --> M[Visual Customization] + E --> N[Mobile Themes] + E --> O[Code Injection] + + F --> P[Privy Integration] + F --> Q[Farcaster Integration] + F --> R[Identity Management] + + G --> S[Supabase Storage] + H --> T[Encrypted Storage] + I --> U[Multiple Layouts] + + J --> V[Component Library] + K --> W[Protocol Integration] + L --> X[Community Features] + + M --> Y[CSS Variables] + N --> Z[Mobile Optimization] + O --> AA[Custom HTML/CSS] + + P --> BB[Wallet Connection] + Q --> CC[Social Identity] + R --> DD[Cryptographic Keys] +``` + +## Core Principles + +### 1. Modularity +- **Component-based architecture** with atomic design principles +- **Fidget system** for extensible mini-applications +- **Theme system** for visual customization +- **Space system** for organizational structure + +### 2. State Management +- **Zustand stores** for efficient state management +- **Store composition** for complex state relationships +- **Optimistic updates** for better user experience +- **Persistence** with selective storage strategies + +### 3. Authentication +- **Privy integration** for wallet-based authentication +- **Farcaster integration** for social identity +- **Multi-identity support** with cryptographic keys +- **Authenticator system** for service access + +### 4. Customization +- **Theme system** for visual personalization +- **Layout system** for multiple layout support +- **Fidget system** for functional customization +- **Mobile optimization** for responsive design + +## System Components + +### 1. Frontend Layer +- **Next.js App Router** - Modern React framework +- **TypeScript** - Type-safe development +- **Tailwind CSS** - Utility-first styling +- **Framer Motion** - Animation library + +### 2. State Management Layer +- **Zustand** - Lightweight state management +- **Store composition** - Modular store architecture +- **Persistence** - Local storage integration +- **Optimistic updates** - Immediate UI feedback + +### 3. Authentication Layer +- **Privy** - Wallet authentication +- **Farcaster** - Social protocol integration +- **Identity management** - Multi-identity support +- **Authenticator system** - Service access control + +### 4. Data Layer +- **Supabase** - Database and storage +- **Encrypted storage** - Private data protection +- **Public storage** - Shared content +- **API integration** - External service access + +### 5. Customization Layer +- **Theme system** - Visual customization +- **Layout system** - Multiple layout support +- **Fidget system** - Mini-application framework +- **Mobile system** - Responsive design + +## Data Flow + +### 1. User Interaction +```typescript +// User interacts with UI +const handleUserAction = (action: UserAction) => { + // Update local state optimistically + updateLocalState(action); + + // Sync with server + syncWithServer(action); +}; +``` + +### 2. State Updates +```typescript +// State updates flow through stores +const updateState = (newState: State) => { + // Update store + set((draft) => { + Object.assign(draft, newState); + }, "updateState"); + + // Persist changes + persistState(newState); +}; +``` + +### 3. Server Synchronization +```typescript +// Sync with server +const syncWithServer = async (changes: Changes) => { + try { + await api.updateServer(changes); + } catch (error) { + // Rollback optimistic updates + rollbackChanges(changes); + } +}; +``` + +## Security Architecture + +### 1. Authentication Flow +```typescript +// Authentication flow +const authenticateUser = async () => { + // 1. Connect wallet via Privy + const wallet = await privy.connect(); + + // 2. Create identity + const identity = await createIdentity(); + + // 3. Link Farcaster (optional) + if (userWantsFarcaster) { + await linkFarcaster(identity); + } + + // 4. Initialize stores + await initializeStores(identity); +}; +``` + +### 2. Data Encryption +```typescript +// Encrypt sensitive data +const encryptData = async (data: any, key: string) => { + const encrypted = await encrypt(data, key); + return encrypted; +}; + +// Decrypt data +const decryptData = async (encryptedData: any, key: string) => { + const decrypted = await decrypt(encryptedData, key); + return decrypted; +}; +``` + +### 3. Access Control +```typescript +// Check permissions +const checkPermission = (action: string, resource: string): boolean => { + const permissions = getCurrentUserPermissions(); + return permissions[resource]?.includes(action) || false; +}; +``` + +## Performance Architecture + +### 1. Lazy Loading +```typescript +// Lazy load components +const LazyComponent = lazy(() => import('./Component')); + +// Use with Suspense +const App = () => ( + }> + + +); +``` + +### 2. State Optimization +```typescript +// Optimize state updates +const useOptimizedState = (selector: (state: State) => any) => { + return useStore(selector, shallow); +}; +``` + +### 3. Caching Strategy +```typescript +// Cache expensive computations +const useMemoizedValue = (value: any) => { + return useMemo(() => computeExpensiveValue(value), [value]); +}; +``` + +## Development Architecture + +### 1. Component Structure +``` +src/ +├── app/ # Next.js App Router +│ ├── (spaces)/ # Space routes +│ ├── api/ # API routes +│ └── explore/ # Discovery routes +├── common/ # Shared components +│ ├── components/ # UI components +│ ├── data/ # State management +│ ├── lib/ # Utilities +│ └── providers/ # Context providers +├── fidgets/ # Mini-applications +├── constants/ # Application constants +└── styles/ # Global styles +``` + +### 2. State Management Structure +``` +src/common/data/stores/ +├── app/ # Main app store +│ ├── accounts/ # Authentication & identity +│ ├── homebase/ # Private spaces (homebase) +│ ├── space/ # Public spaces +│ ├── currentSpace/ # Current space context +│ ├── checkpoints/ # State snapshots +│ ├── chat/ # Chat functionality +│ └── setup/ # Onboarding flow +├── createStore.ts # Store utilities +└── types.ts # Type definitions +``` + +### 3. Component Architecture +``` +src/common/components/ +├── atoms/ # Basic components +├── molecules/ # Composite components +├── organisms/ # Complex components +└── templates/ # Page templates +``` + +## Integration Points + +### 1. External Services +- **Privy** - Authentication +- **Farcaster** - Social protocol +- **Supabase** - Database and storage +- **Neynar** - Farcaster API +- **Alchemy** - Blockchain data + +### 2. Internal Systems +- **Space System** - Content organization +- **Fidget System** - Mini-applications +- **Theme System** - Visual customization +- **Layout System** - Multiple layouts +- **Mobile System** - Responsive design + +## Scalability Considerations + +### 1. Store Architecture +- **Modular stores** for independent scaling +- **Store composition** for complex relationships +- **Selective persistence** for performance +- **Optimistic updates** for responsiveness + +### 2. Component Architecture +- **Atomic design** for reusability +- **Lazy loading** for performance +- **Memoization** for optimization +- **Error boundaries** for reliability + +### 3. Data Architecture +- **Encrypted storage** for privacy +- **Public storage** for sharing +- **Caching strategies** for performance +- **Sync mechanisms** for consistency + +## Future Architecture + +### 1. Planned Enhancements +- **Enhanced permission system** for fine-grained access control +- **Advanced theme system** with animation support +- **Fidget marketplace** for community fidgets +- **Collaboration features** for shared spaces + +### 2. Technical Improvements +- **Performance monitoring** for optimization +- **Advanced caching** for better UX +- **Real-time updates** for collaboration +- **Mobile optimization** for better performance + +### 3. Integration Opportunities +- **Additional protocols** for broader compatibility +- **Enhanced authentication** for better security +- **Advanced customization** for power users +- **Community features** for social interaction diff --git a/docs/ARCHITECTURE/STATE_MANAGEMENT.md b/docs/ARCHITECTURE/STATE_MANAGEMENT.md new file mode 100644 index 000000000..365213fcb --- /dev/null +++ b/docs/ARCHITECTURE/STATE_MANAGEMENT.md @@ -0,0 +1,394 @@ +# State Management Architecture + +Nounspace uses a sophisticated Zustand-based state management system with store composition, persistence, and optimistic updates. + +## Architecture Overview + +The state management system is built on several key principles: + +- **Store Composition** - Multiple specialized stores combined into a single app store +- **Persistence** - Selective persistence with merge strategies +- **Optimistic Updates** - Immediate UI updates with server synchronization +- **Type Safety** - Full TypeScript support with strict typing + +## Core Store Architecture + +### 1. Store Composition + +The main app store combines multiple specialized stores: + +```typescript +export type AppStore = { + account: AccountStore; // Authentication & identity + setup: SetupStore; // Onboarding flow + homebase: HomeBaseStore; // Private spaces + space: SpaceStore; // Public spaces + currentSpace: CurrentSpaceStore; // Current space context + checkpoints: CheckpointStore; // State snapshots + chat: ChatStore; // Chat functionality + logout: () => void; + getIsAccountReady: () => boolean; + getIsInitializing: () => boolean; + clearLocalSpaces: () => void; +}; +``` + +### 2. Store Creation System + +The `createStore` utility provides a standardized way to create stores: + +```typescript +export function createStore( + store: any, + persistArgs: PersistOptions, +) { + return create()(devtools(persist(mutative(store), persistArgs))); +} +``` + +**Key Features:** +- **Mutative Integration** - Immutable updates with performance +- **DevTools Support** - Redux DevTools integration +- **Persistence** - Configurable persistence strategies +- **Type Safety** - Full TypeScript support + +### 3. Store Bindings + +The `createStoreBindings` system provides React context integration: + +```typescript +export function createStoreBindings( + storeName: string, + createStoreFunc: () => StoreApi, +) { + const storeContext = createContext | null>(null); + + const provider: React.FC = ({ children }) => { + const storeRef = useRef>(); + if (!storeRef.current) { + storeRef.current = createStoreFunc(); + } + return React.createElement(storeContext.Provider, { value: storeRef.current }, children); + }; + + function useTStore(fn: (state: T) => S): S { + const context = useContext(storeContext); + if (!context) { + throw new Error(`use${storeName} must be use within ${storeName}Provider`); + } + return useStore(context, fn); + } + + return { provider, context: storeContext, useStore: useTStore }; +} +``` + +## Store Types + +### 1. Account Store + +Manages authentication and identity: + +```typescript +export type AccountStore = IdentityStore & + AuthenticatorStore & + PreKeyStore & + FarcasterStore & + PrivyStore & { + reset: () => void; + hasNogs: boolean; + setHasNogs: (v: boolean) => void; + }; +``` + +**Features:** +- Multi-identity support +- Authenticator management +- Farcaster integration +- Cryptographic key management + +### 2. Homebase Store + +Manages private spaces: + +```typescript +export type HomeBaseStore = { + spaces: Record; + addSpace: (space: SpaceData) => void; + updateSpace: (id: string, updates: Partial) => void; + removeSpace: (id: string) => void; + getSpace: (id: string) => SpaceData | null; + getAllSpaces: () => SpaceData[]; +}; +``` + +**Features:** +- Local space management +- Optimistic updates +- Persistence +- Space synchronization + +### 3. Space Store + +Manages public spaces: + +```typescript +export type SpaceStore = { + spaces: Record>; + addSpace: (space: Omit) => void; + updateSpace: (id: string, updates: Partial>) => void; + removeSpace: (id: string) => void; + getSpace: (id: string) => Omit | null; +}; +``` + +**Features:** +- Public space management +- Server synchronization +- Read-only operations +- Space discovery + +### 4. Current Space Store + +Manages current space context: + +```typescript +export type CurrentSpaceStore = { + currentSpaceId: string | null; + currentTabName: string | null; + setCurrentSpace: (spaceId: string | null) => void; + setCurrentTab: (tabName: string | null) => void; + getCurrentSpace: () => SpaceData | null; + getCurrentTab: () => TabData | null; +}; +``` + +**Features:** +- Current space tracking +- Tab management +- Context switching +- State synchronization + +## Persistence Strategy + +### 1. Selective Persistence + +Only specific parts of the store are persisted: + +```typescript +partialize: (state: AppStore) => ({ + account: partializedAccountStore(state), + homebase: partializedHomebaseStore(state), + space: partializedSpaceStore(state), + checkpoints: partializedCheckpointStore(state), + chat: partializedChatStore(state), +}), +``` + +### 2. Merge Strategy + +Persisted state is merged with current state: + +```typescript +merge: (persistedState, currentState: AppStore) => { + return merge(currentState, persistedState); +}, +``` + +### 3. Storage Configuration + +```typescript +const LOCAL_STORAGE_LOCATION = "nounspace-app-store"; + +export function createAppStore() { + return createStore(makeStoreFunc, { + name: LOCAL_STORAGE_LOCATION, + storage: createJSONStorage(() => localStorage), + // ... persistence configuration + }); +} +``` + +## Optimistic Updates + +### 1. Immediate UI Updates + +Stores support optimistic updates for better UX: + +```typescript +// Example: Adding a space optimistically +const addSpace = (space: SpaceData) => { + set((draft) => { + draft.homebase.spaces[space.id] = space; + }, "addSpace"); + + // Sync with server in background + syncSpaceWithServer(space); +}; +``` + +### 2. Error Handling + +Failed optimistic updates are rolled back: + +```typescript +const updateSpace = async (id: string, updates: Partial) => { + const originalSpace = get().homebase.spaces[id]; + + // Apply optimistic update + set((draft) => { + draft.homebase.spaces[id] = { ...originalSpace, ...updates }; + }, "updateSpace"); + + try { + await syncSpaceWithServer(id, updates); + } catch (error) { + // Rollback on error + set((draft) => { + draft.homebase.spaces[id] = originalSpace; + }, "rollbackUpdateSpace"); + } +}; +``` + +## Development Patterns + +### 1. Creating New Stores + +```typescript +// Define store type +export type MyStore = { + data: MyData[]; + addData: (item: MyData) => void; + updateData: (id: string, updates: Partial) => void; + removeData: (id: string) => void; +}; + +// Create store function +export const createMyStoreFunc: MatativeConfig = (set, get) => ({ + data: [], + addData: (item) => { + set((draft) => { + draft.data.push(item); + }, "addData"); + }, + updateData: (id, updates) => { + set((draft) => { + const index = draft.data.findIndex(item => item.id === id); + if (index !== -1) { + Object.assign(draft.data[index], updates); + } + }, "updateData"); + }, + removeData: (id) => { + set((draft) => { + draft.data = draft.data.filter(item => item.id !== id); + }, "removeData"); + }, +}); +``` + +### 2. Using Stores in Components + +```typescript +// Use app store +const { homebase, addSpace } = useAppStore((state) => ({ + homebase: state.homebase, + addSpace: state.homebase.addSpace, +})); + +// Use specific store +const { spaces } = useHomebaseStore((state) => ({ + spaces: state.spaces, +})); +``` + +### 3. Store Composition + +```typescript +// Combine multiple stores +export const createCombinedStoreFunc: MatativeConfig = (set, get) => ({ + ...createStoreAFunc(set, get), + ...createStoreBFunc(set, get), + // Combined actions + resetAll: () => { + get().storeA.reset(); + get().storeB.reset(); + }, +}); +``` + +## Performance Considerations + +### 1. Selective Subscriptions + +```typescript +// Good: Subscribe to specific state +const spaces = useAppStore((state) => state.homebase.spaces); + +// Avoid: Subscribe to entire store +const store = useAppStore(); +``` + +### 2. Memoization + +```typescript +// Memoize expensive computations +const expensiveValue = useMemo(() => { + return computeExpensiveValue(store.data); +}, [store.data]); +``` + +### 3. Store Splitting + +```typescript +// Split large stores into smaller ones +export type LargeStore = { + sectionA: SectionAStore; + sectionB: SectionBStore; + sectionC: SectionCStore; +}; +``` + +## Testing + +### 1. Store Testing + +```typescript +// Test store actions +const store = createTestStore(); +store.getState().addData(testData); +expect(store.getState().data).toContain(testData); +``` + +### 2. Integration Testing + +```typescript +// Test store interactions +const appStore = createAppStore(); +appStore.getState().homebase.addSpace(space); +appStore.getState().currentSpace.setCurrentSpace(space.id); +``` + +## Troubleshooting + +### Common Issues + +1. **Store Not Updating**: Check if component is subscribed to store +2. **Persistence Issues**: Verify partialize function includes required state +3. **Type Errors**: Ensure store types match implementation +4. **Performance Issues**: Check for unnecessary re-renders + +### Debug Tools + +- Use Redux DevTools to inspect store state +- Check browser console for store errors +- Verify store subscriptions in React DevTools +- Test store actions in isolation + +## Future Considerations + +- Enhanced persistence strategies +- Advanced optimistic update patterns +- Store middleware system +- Performance monitoring diff --git a/docs/CHANGELOG-PR1279-en.md b/docs/CHANGELOG-PR1279-en.md deleted file mode 100644 index 62a9fa6de..000000000 --- a/docs/CHANGELOG-PR1279-en.md +++ /dev/null @@ -1,261 +0,0 @@ -# Changelog - Multiple Layouts System - -## Implementation Overview - -**Features:** Mobile drag-and-drop system and homebase feed fix -**Changes:** +869 −1,232 lines - -## 🎯 Main Objectives Achieved - -### 1. Mobile Drag-and-Drop System Fix ✅ - -**Problem:** Dragging fidgets in mobile editor didn't update order in preview. - -**Root Cause:** `Space.tsx` used `Object.keys()` without sorting, ignoring `mobileOrder`. - -**Solution:** Implemented explicit sorting by `mobileOrder` before rendering. - -**Result:** Fully functional drag-and-drop system on mobile. - -### 2. Immutable Contextual Feed ✅ - -**Problem:** Feed appeared in all homebase tabs incorrectly. - -**Root Cause:** `isHomebasePath` too broad (`startsWith('/homebase')`). - -**Solution:** Changed to exact verification (`pathname === '/homebase'`). - -**Result:** Feed appears only in the correct homebase tab. - -### 3. Separation of Concerns (SoC) ✅ - -**Problem:** Components mixed business logic with presentation. - -**Solution:** Implemented clean architecture: -- `Space.tsx`: Controller and data management -- `MobileViewSimplified`: Only rendering -- `ThemeSettingsEditor`: State management - -**Result:** More maintainable and extensible code. - -## 📋 Modified Files - -### Core Components (3 files) - -#### `src/app/(spaces)/Space.tsx` - MAIN FILE -**Main changes:** -- ✅ Implemented sorting by `mobileOrder` (CRITICAL FIX) -- ✅ Removed `showFeedOnMobile` prop -- ✅ Applied Separation of Concerns -- ✅ Improved cleanup system -- ✅ Implemented contextual feed logic - -#### `src/app/(spaces)/MobileViewSimplified.tsx` - NEW FILE -**Replacement:** -- ❌ Removed: `MobileView.tsx` (300+ lines) -- ✅ Added: `MobileViewSimplified.tsx` (~95 lines) - -**Benefits:** -- Less code and complexity -- Reuses existing layout system -- Easier to maintain -- Fewer potential bugs - -#### `src/app/(spaces)/SpacePage.tsx` -**Changes:** -- ✅ Removed `showFeedOnMobile` forwarding - -### State Management (2 files) - -#### `src/common/lib/theme/ThemeSettingsEditor.tsx` -**Main changes:** -- ✅ Implemented automatic feed creation for `/homebase` -- ✅ Removed `NogsGateButton` -- ✅ Updated mini apps and ordering management - -#### `src/common/utils/layoutUtils.ts` -**Changes:** -- ✅ Added support for explicit mobile order -- ✅ Implemented feed injection logic -- ✅ Improved `processTabFidgetIds` - -### Layout System (2 files) - -#### `src/fidgets/layout/tabFullScreen/index.tsx` -**Changes:** -- ✅ Added `isHomebasePath` prop -- ✅ Updated tab ordering (feed first in homebase) -- ✅ Simplified CSS and container logic - -#### `src/fidgets/layout/tabFullScreen/components/TabNavigation.tsx` -**Changes:** -- ✅ Allowed rendering with single tab in homebase -- ✅ Special handling for feed name and icon - -### Mobile Interface (2 files) - -#### `src/common/components/organisms/MobileSettings.tsx` -**Changes:** -- ✅ Changed `DraggableMiniApp` key to `"miniapp-" + miniApp.id` -- ✅ Improved React reconciliation - -#### `src/common/components/organisms/MobileNavbar.tsx` -**Changes:** -- ✅ Added fallback for 'feed' label - -### Utilities (1 file) - -#### `src/common/lib/hooks/useProcessedFidgetIds.ts` -**Changes:** -- ✅ Minor formatting, no logic changes - -## 🔧 Technical Details of Changes - -### 1. Critical Drag-and-Drop Fix - -**Before:** -```typescript -// Space.tsx - PROBLEM -const fidgetIds = Object.keys(config.fidgetInstanceDatums); -// Random order! 😱 -``` - -**After:** -```typescript -// Space.tsx - SOLUTION -const fidgetIds = Object.keys(config.fidgetInstanceDatums || {}).sort((a, b) => { - const aOrder = config.fidgetInstanceDatums[a]?.config?.settings?.mobileOrder || 0; - const bOrder = config.fidgetInstanceDatums[b]?.config?.settings?.mobileOrder || 0; - return aOrder - bOrder; -}); -// Deterministic order! ✅ -``` - -### 2. Contextual Feed Implementation - -**Precise context detection:** -```typescript -// Before: too broad -const isHomebasePath = pathname.startsWith('/homebase'); - -// After: exact -const isHomebaseFeedTab = pathname === '/homebase'; -``` - -**Automatic feed creation:** -```typescript -useEffect(() => { - if (isHomebaseFeedTab && !fidgetInstanceDatums['feed']) { - const feedFidget = { - id: 'feed', - fidgetType: 'feed', - config: { - editable: false, // Immutable - settings: { - customMobileDisplayName: 'Feed', - mobileIconName: 'FaBars', - showOnMobile: true, - mobileOrder: 0 - } - } - }; - saveFidgetInstanceDatums({ - ...fidgetInstanceDatums, - feed: feedFidget - }); - } -}, [isHomebaseFeedTab, fidgetInstanceDatums]); -``` - -### 3. Separation of Concerns - -**Implemented architecture:** -```text -Space.tsx (Controller) -├── Determines which layout to use -├── Manages data migration -├── Applies correct ordering -└── Passes processed data - -MobileViewSimplified (Presenter) -├── Receives ready data -├── Only renders -└── No business logic - -ThemeSettingsEditor (State Manager) -├── Manages fidget state -├── Processes drag callbacks -└── Persists changes -``` - -## 📊 PR Metrics - -### Lines of Code -- **Added:** 869 lines -- **Removed:** 1,232 lines -- **Net result:** -363 lines (code reduction!) - -### Files -- **Modified:** 9 files -- **New:** 1 file (`MobileViewSimplified.tsx`) -- **Removed:** 1 file (`MobileView.tsx`) - -### Complexity -- **Reduced:** `MobileView` 300+ lines → `MobileViewSimplified` ~95 lines -- **Simplified:** Centralized drag-and-drop logic -- **Optimized:** Fewer re-renders and better performance - -## 🐛 Fixed Bugs - -| Bug | Location | Status | -|-----|----------|---------| -| **Drag with no effect** | `Space.tsx` | ✅ Fixed | -| **Feed in wrong place** | `ThemeSettingsEditor.tsx` | ✅ Fixed | -| **Cut off background** | `Space.tsx` | ✅ Fixed | - -## 🔄 Implemented Flow - -### New Drag-and-Drop Flow -```text -1. 👤 User drags fidget in sidebar -2. 🔄 MobileSettings updates local order -3. 📤 Callback to ThemeSettingsEditor -4. 💾 Saves fidgetInstanceDatums with mobileOrder -5. ⭐ Space.tsx sorts by mobileOrder (KEY!) -6. 📱 MobileViewSimplified re-renders -7. ✅ TabNavigation shows new order -``` - -## 🎯 Validation and Testing - -### Tested Scenarios -- ✅ Functional drag-and-drop -- ✅ Order persistence -- ✅ Feed only in homebase -- ✅ Automatic migration -- ✅ Optimized performance - -## 🚀 Deploy - -- **Vercel:** ✅ Successful deploy -- **Checks:** ✅ 4/4 checks passing -- **Preview:** [Available for testing](https://nounspace-f9bd4o5j5-nounspace.vercel.app/) - -## 💡 Lessons Learned - -### Resolved Technical Debt -- **Unnecessary complexity:** `MobileView` replaced with simpler solution -- **Scattered logic:** Centralized in correct location -- **Performance:** Optimized with memoization and SoC - -### Applied Best Practices -- **Separation of Concerns:** Applied consistently -- **Clean code:** Significant line reduction -- **Documentation:** Complete guides for maintenance -- **Testing:** Edge case coverage - ---- - -**Implementation:** Multiple layouts system -**Documentation:** Complete -**Features:** Implemented and tested diff --git a/docs/CONTRIBUTING.MD b/docs/CONTRIBUTING.MD index 8ca40d1b4..148931e11 100644 --- a/docs/CONTRIBUTING.MD +++ b/docs/CONTRIBUTING.MD @@ -2,8 +2,8 @@ To add any code to the `nounspace/Nounspace.ts` repo, you will need to open a PR. -## Typescript -Nounspace is written fully in typescript. At time of writing this (5/7/24), we are still in the process of fixing type errors that exist from the original codebase. +## TypeScript +Nounspace is written fully in TypeScript with strict type checking enabled. ## Steps to open a PR Nounspace expects its contributors to follow the "Fork and Pull" method to open PRs. Below is a TL;DR for this process @@ -21,6 +21,7 @@ For more details on the "Fork and Pull" method, check out [Github's docs](https: ## PR Expectations - All commits follow [conventional commits](https://www.conventionalcommits.org/) -- PR's titles begin with either "[FIDGET]" or "[CLIENT]" to show if the changes made are a Fidget submission or a change to the client code base -- PR's bodies outline the changes made, and the rationale for them. -- All PR's contain no new type errors and are fully valid typescript code +- PR titles begin with either "[FIDGET]" or "[CLIENT]" to show if the changes made are a Fidget submission or a change to the client codebase +- PR bodies outline the changes made and the rationale for them +- All PRs contain no new type errors and are fully valid TypeScript code +- Run `npm run lint` and `npm run check-types` before submitting diff --git a/docs/AGENTS.md b/docs/DEVELOPMENT/AGENTS.md similarity index 100% rename from docs/AGENTS.md rename to docs/DEVELOPMENT/AGENTS.md diff --git a/docs/DEVELOPMENT/CODING_STANDARDS.md b/docs/DEVELOPMENT/CODING_STANDARDS.md new file mode 100644 index 000000000..70ac3e5bb --- /dev/null +++ b/docs/DEVELOPMENT/CODING_STANDARDS.md @@ -0,0 +1,609 @@ +# Coding Standards + +This document outlines the coding standards and best practices for the Nounspace codebase to ensure consistency, maintainability, and code quality. + +## TypeScript Standards + +### 1. Type Safety +- **Strict TypeScript** - Use strict mode with all type checking enabled +- **Explicit Types** - Define types for all function parameters and return values +- **Interface Definitions** - Use interfaces for object shapes and component props +- **Generic Types** - Use generics for reusable type-safe code + +```typescript +// Good: Explicit types +interface UserProps { + id: string; + name: string; + email: string; + isActive: boolean; +} + +const User: React.FC = ({ id, name, email, isActive }) => { + return ( +
+

{name}

+

{email}

+ {isActive ? 'Active' : 'Inactive'} +
+ ); +}; + +// Good: Generic types +interface ApiResponse { + data: T; + status: number; + message: string; +} + +const fetchUser = async (id: string): Promise> => { + const response = await api.get(`/users/${id}`); + return response.data; +}; +``` + +### 2. Type Definitions +- **Centralized Types** - Define types in dedicated files +- **Reusable Types** - Create shared types for common patterns +- **Type Guards** - Use type guards for runtime type checking +- **Discriminated Unions** - Use discriminated unions for complex state + +```typescript +// types/user.ts +export interface User { + id: string; + name: string; + email: string; + role: UserRole; +} + +export type UserRole = 'admin' | 'user' | 'guest'; + +export interface UserState { + user: User | null; + loading: boolean; + error: string | null; +} + +// Type guard +export const isUser = (value: any): value is User => { + return value && typeof value.id === 'string' && typeof value.name === 'string'; +}; +``` + +## React Standards + +### 1. Component Structure +- **Functional Components** - Use functional components with hooks +- **Component Props** - Define clear prop interfaces +- **Default Props** - Use default parameters instead of defaultProps +- **Component Composition** - Prefer composition over inheritance + +```typescript +// Good: Functional component with clear props +interface ButtonProps { + variant?: 'primary' | 'secondary' | 'danger'; + size?: 'sm' | 'md' | 'lg'; + disabled?: boolean; + onClick?: () => void; + children: React.ReactNode; +} + +export const Button: React.FC = ({ + variant = 'primary', + size = 'md', + disabled = false, + onClick, + children, +}) => { + return ( + + ); +}; +``` + +### 2. Hooks Usage +- **Custom Hooks** - Extract reusable logic into custom hooks +- **Hook Dependencies** - Always include all dependencies in useEffect +- **Hook Rules** - Follow the rules of hooks consistently +- **Hook Optimization** - Use useMemo and useCallback appropriately + +```typescript +// Good: Custom hook with proper dependencies +export const useUser = (userId: string) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchUser = async () => { + try { + setLoading(true); + const userData = await api.getUser(userId); + setUser(userData); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + setLoading(false); + } + }; + + if (userId) { + fetchUser(); + } + }, [userId]); // Include all dependencies + + return { user, loading, error }; +}; +``` + +### 3. State Management +- **Local State** - Use useState for component-local state +- **Global State** - Use Zustand for global state management +- **State Updates** - Use functional updates for state that depends on previous state +- **State Normalization** - Keep state normalized and flat + +```typescript +// Good: Functional state updates +const [count, setCount] = useState(0); + +const increment = () => { + setCount(prevCount => prevCount + 1); +}; + +// Good: Normalized state +interface AppState { + users: Record; + spaces: Record; + currentUserId: string | null; +} +``` + +## Code Organization + +### 1. File Structure +- **Atomic Design** - Organize components by atomic design principles +- **Feature-based** - Group related functionality together +- **Barrel Exports** - Use index files for clean imports +- **Co-location** - Keep related files close together + +``` +src/ +├── components/ +│ ├── atoms/ +│ │ ├── Button/ +│ │ │ ├── Button.tsx +│ │ │ ├── Button.test.tsx +│ │ │ └── index.ts +│ │ └── index.ts +│ ├── molecules/ +│ └── organisms/ +├── hooks/ +├── utils/ +├── types/ +└── constants/ +``` + +### 2. Import/Export Standards +- **Named Exports** - Use named exports for better tree shaking +- **Barrel Exports** - Use index files for clean imports +- **Import Order** - Organize imports in a consistent order +- **Absolute Imports** - Use absolute imports for better refactoring + +```typescript +// Good: Import order +// 1. React and external libraries +import React, { useState, useEffect } from 'react'; +import { useRouter } from 'next/router'; +import { Button } from '@mui/material'; + +// 2. Internal imports (absolute paths) +import { useUser } from '@/hooks/useUser'; +import { User } from '@/types/user'; +import { api } from '@/utils/api'; + +// 3. Relative imports +import './Button.css'; +``` + +### 3. Naming Conventions +- **PascalCase** - Use PascalCase for components and types +- **camelCase** - Use camelCase for variables and functions +- **kebab-case** - Use kebab-case for file names +- **Descriptive Names** - Use descriptive names that explain intent + +```typescript +// Good: Naming conventions +// Components +export const UserProfile: React.FC = () => {}; + +// Hooks +export const useUserProfile = () => {}; + +// Types +interface UserProfileProps { + userId: string; + showAvatar?: boolean; +} + +// Files: user-profile.tsx, use-user-profile.ts +``` + +## Error Handling + +### 1. Error Boundaries +- **Error Boundaries** - Use error boundaries to catch component errors +- **Fallback UI** - Provide meaningful fallback UI for errors +- **Error Logging** - Log errors for debugging and monitoring +- **User-friendly Messages** - Show user-friendly error messages + +```typescript +// Error boundary +export class ErrorBoundary extends React.Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('Error caught by boundary:', error, errorInfo); + // Log to error reporting service + } + + render() { + if (this.state.hasError) { + return ( +
+

Something went wrong

+

Please refresh the page or try again later.

+
+ ); + } + + return this.props.children; + } +} +``` + +### 2. Async Error Handling +- **Try-Catch** - Use try-catch for async operations +- **Error States** - Handle error states in components +- **Retry Logic** - Implement retry logic for failed operations +- **Loading States** - Show loading states during async operations + +```typescript +// Good: Async error handling +export const useAsyncOperation = () => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const execute = async (operation: () => Promise) => { + try { + setLoading(true); + setError(null); + const result = await operation(); + setData(result); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + setLoading(false); + } + }; + + return { data, loading, error, execute }; +}; +``` + +## Performance Standards + +### 1. Optimization Techniques +- **Memoization** - Use React.memo, useMemo, and useCallback appropriately +- **Lazy Loading** - Implement lazy loading for heavy components +- **Code Splitting** - Split code into smaller chunks +- **Bundle Analysis** - Regularly analyze bundle size + +```typescript +// Good: Memoization +export const ExpensiveComponent = React.memo(({ + data, + onUpdate, +}) => { + const processedData = useMemo(() => { + return data.map(item => processItem(item)); + }, [data]); + + const handleUpdate = useCallback((id: string, updates: any) => { + onUpdate(id, updates); + }, [onUpdate]); + + return ( +
+ {processedData.map(item => ( + + ))} +
+ ); +}); +``` + +### 2. Bundle Optimization +- **Tree Shaking** - Use named exports for better tree shaking +- **Dynamic Imports** - Use dynamic imports for code splitting +- **Bundle Analysis** - Use tools like webpack-bundle-analyzer +- **Dependency Management** - Keep dependencies up to date + +```typescript +// Good: Dynamic imports +const LazyComponent = lazy(() => import('./HeavyComponent')); + +export const App = () => ( + }> + + +); +``` + +## Testing Standards + +### 1. Test Structure +- **AAA Pattern** - Arrange, Act, Assert pattern for tests +- **Test Isolation** - Each test should be independent +- **Descriptive Names** - Use descriptive test names +- **Test Coverage** - Aim for high test coverage + +```typescript +// Good: Test structure +describe('UserProfile', () => { + it('should display user information when user is loaded', () => { + // Arrange + const mockUser: User = { + id: '1', + name: 'John Doe', + email: 'john@example.com', + role: 'user' + }; + + // Act + render(); + + // Assert + expect(screen.getByText('John Doe')).toBeInTheDocument(); + expect(screen.getByText('john@example.com')).toBeInTheDocument(); + }); +}); +``` + +### 2. Test Types +- **Unit Tests** - Test individual components and functions +- **Integration Tests** - Test component interactions +- **E2E Tests** - Test complete user workflows +- **Accessibility Tests** - Test accessibility compliance + +```typescript +// Good: Integration test +describe('User Management', () => { + it('should allow user to update profile', async () => { + render( + + + + ); + + const nameInput = screen.getByLabelText('Name'); + const saveButton = screen.getByText('Save'); + + fireEvent.change(nameInput, { target: { value: 'New Name' } }); + fireEvent.click(saveButton); + + await waitFor(() => { + expect(screen.getByText('Profile updated')).toBeInTheDocument(); + }); + }); +}); +``` + +## Documentation Standards + +### 1. Code Comments +- **JSDoc Comments** - Use JSDoc for function documentation +- **Inline Comments** - Use inline comments for complex logic +- **TODO Comments** - Use TODO comments for future improvements +- **Deprecation Comments** - Mark deprecated code clearly + +```typescript +/** + * Fetches user data from the API + * @param userId - The unique identifier for the user + * @param options - Optional configuration for the request + * @returns Promise that resolves to user data + * @throws {Error} When the user is not found or API request fails + */ +export const fetchUser = async ( + userId: string, + options?: FetchOptions +): Promise => { + // TODO: Add caching mechanism + try { + const response = await api.get(`/users/${userId}`, options); + return response.data; + } catch (error) { + // Log error for debugging + console.error('Failed to fetch user:', error); + throw new Error('User not found'); + } +}; +``` + +### 2. README Files +- **Component README** - Document complex components +- **API Documentation** - Document API endpoints and usage +- **Setup Instructions** - Provide clear setup instructions +- **Examples** - Include usage examples + +```markdown +# UserProfile Component + +A component for displaying and editing user profile information. + +## Usage + +```typescript +import { UserProfile } from '@/components/UserProfile'; + + +``` + +## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| user | User | - | User data to display | +| onUpdate | (user: User) => void | - | Callback when user is updated | +| showAvatar | boolean | true | Whether to show user avatar | + +## Examples + +See the examples directory for more usage examples. +``` + +## Security Standards + +### 1. Input Validation +- **Sanitization** - Sanitize all user inputs +- **Validation** - Validate data on both client and server +- **Type Safety** - Use TypeScript for compile-time type checking +- **Error Handling** - Handle validation errors gracefully + +```typescript +// Good: Input validation +export const validateUserInput = (input: any): UserInput => { + const schema = z.object({ + name: z.string().min(1).max(100), + email: z.string().email(), + age: z.number().min(0).max(120), + }); + + return schema.parse(input); +}; +``` + +### 2. Security Best Practices +- **HTTPS Only** - Use HTTPS for all communications +- **Input Sanitization** - Sanitize all user inputs +- **XSS Prevention** - Prevent cross-site scripting attacks +- **CSRF Protection** - Implement CSRF protection + +```typescript +// Good: XSS prevention +export const sanitizeHtml = (html: string): string => { + return DOMPurify.sanitize(html, { + ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'], + ALLOWED_ATTR: ['href', 'title'], + }); +}; +``` + +## Accessibility Standards + +### 1. ARIA Attributes +- **Semantic HTML** - Use semantic HTML elements +- **ARIA Labels** - Provide ARIA labels for screen readers +- **Keyboard Navigation** - Ensure keyboard accessibility +- **Focus Management** - Manage focus appropriately + +```typescript +// Good: Accessibility +export const AccessibleButton: React.FC = ({ + children, + onClick, + disabled, + ariaLabel, +}) => { + return ( + + ); +}; +``` + +### 2. Testing Accessibility +- **Screen Reader Testing** - Test with screen readers +- **Keyboard Testing** - Test keyboard navigation +- **Color Contrast** - Ensure proper color contrast +- **Focus Indicators** - Provide clear focus indicators + +```typescript +// Good: Accessibility testing +describe('Accessibility', () => { + it('should be accessible to screen readers', () => { + render(); + + expect(screen.getByRole('main')).toBeInTheDocument(); + expect(screen.getByLabelText('User name')).toBeInTheDocument(); + }); + + it('should support keyboard navigation', () => { + render(); + + const nameInput = screen.getByLabelText('User name'); + nameInput.focus(); + expect(nameInput).toHaveFocus(); + }); +}); +``` + +## Code Review Standards + +### 1. Review Checklist +- **Functionality** - Does the code work as intended? +- **Performance** - Are there any performance issues? +- **Security** - Are there any security vulnerabilities? +- **Accessibility** - Is the code accessible? +- **Testing** - Are there adequate tests? +- **Documentation** - Is the code well documented? + +### 2. Review Process +- **Self Review** - Review your own code before submitting +- **Peer Review** - Get at least one peer review +- **Automated Checks** - Ensure all automated checks pass +- **Documentation** - Update documentation as needed + +## Continuous Improvement + +### 1. Code Quality Metrics +- **Test Coverage** - Maintain high test coverage +- **Code Complexity** - Keep code complexity low +- **Performance Metrics** - Monitor performance metrics +- **Security Scans** - Regular security scans + +### 2. Learning and Development +- **Code Reviews** - Learn from code reviews +- **Best Practices** - Stay updated with best practices +- **Tools and Techniques** - Learn new tools and techniques +- **Community** - Participate in the development community diff --git a/docs/DEVELOPMENT/COMPONENT_ARCHITECTURE.md b/docs/DEVELOPMENT/COMPONENT_ARCHITECTURE.md new file mode 100644 index 000000000..e739bea3f --- /dev/null +++ b/docs/DEVELOPMENT/COMPONENT_ARCHITECTURE.md @@ -0,0 +1,573 @@ +# Component Architecture + +Nounspace follows atomic design principles with a modular component architecture that promotes reusability, maintainability, and scalability. + +## Atomic Design Structure + +### 1. Atoms +Basic building blocks that cannot be broken down further: + +```typescript +// Button atom +export const Button: React.FC = ({ + variant = "primary", + size = "medium", + children, + onClick, + disabled = false, + ...props +}) => { + const baseClasses = "inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none"; + + const variantClasses = { + primary: "bg-primary text-primary-foreground hover:bg-primary/90", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: "border border-input hover:bg-accent hover:text-accent-foreground", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }; + + const sizeClasses = { + sm: "h-9 px-3 text-sm", + md: "h-10 px-4 py-2", + lg: "h-11 px-8", + icon: "h-10 w-10", + }; + + return ( + + ); +}; +``` + +### 2. Molecules +Simple combinations of atoms that form functional units: + +```typescript +// SearchInput molecule +export const SearchInput: React.FC = ({ + placeholder = "Search...", + value, + onChange, + onSearch, + className, +}) => { + const [inputValue, setInputValue] = useState(value || ""); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSearch?.(inputValue); + }; + + return ( +
+ { + setInputValue(e.target.value); + onChange?.(e.target.value); + }} + className="pr-10" + /> + +
+ ); +}; +``` + +### 3. Organisms +Complex components that form distinct sections: + +```typescript +// Navigation organism +export const Navigation: React.FC = ({ + currentSpace, + spaces, + onSpaceChange, + onThemeToggle, + theme, +}) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + + ); +}; +``` + +### 4. Templates +Page-level components that define layout structure: + +```typescript +// Space template +export const SpaceTemplate: React.FC = ({ + space, + theme, + children, +}) => { + return ( +
+ +
+ + {children} + +
+
+
+ ); +}; +``` + +## Component Patterns + +### 1. Compound Components +Components that work together as a cohesive unit: + +```typescript +// Tabs compound component +export const Tabs = ({ children, defaultValue, value, onValueChange }) => { + const [activeTab, setActiveTab] = useState(defaultValue); + + const handleTabChange = (tabValue: string) => { + setActiveTab(tabValue); + onValueChange?.(tabValue); + }; + + return ( + + {children} + + ); +}; + +export const TabsList = ({ children, className }) => ( +
+ {children} +
+); + +export const TabsTrigger = ({ value, children, className }) => { + const { activeTab, onTabChange } = useContext(TabsContext); + + return ( + + ); +}; + +export const TabsContent = ({ value, children, className }) => { + const { activeTab } = useContext(TabsContext); + + if (activeTab !== value) return null; + + return ( +
+ {children} +
+ ); +}; +``` + +### 2. Render Props +Components that accept functions as children: + +```typescript +// DataProvider with render props +export const DataProvider: React.FC = ({ + children, + data, + loading, + error, +}) => { + if (loading) return ; + if (error) return ; + + return ( + + {typeof children === 'function' ? children(data) : children} + + ); +}; + +// Usage + + {(data) => ( +
+

Welcome, {data.name}!

+

Your spaces: {data.spaces.length}

+
+ )} +
+``` + +### 3. Higher-Order Components +Components that enhance other components: + +```typescript +// withTheme HOC +export const withTheme =

( + Component: React.ComponentType

+) => { + return (props: P) => { + const theme = useTheme(); + + return ( +

+ +
+ ); + }; +}; + +// Usage +const ThemedButton = withTheme(Button); +``` + +### 4. Custom Hooks +Reusable logic that can be shared between components: + +```typescript +// useLocalStorage hook +export const useLocalStorage = (key: string, initialValue: T) => { + const [storedValue, setStoredValue] = useState(() => { + try { + const item = window.localStorage.getItem(key); + return item ? JSON.parse(item) : initialValue; + } catch (error) { + console.error(`Error reading localStorage key "${key}":`, error); + return initialValue; + } + }); + + const setValue = (value: T | ((val: T) => T)) => { + try { + const valueToStore = value instanceof Function ? value(storedValue) : value; + setStoredValue(valueToStore); + window.localStorage.setItem(key, JSON.stringify(valueToStore)); + } catch (error) { + console.error(`Error setting localStorage key "${key}":`, error); + } + }; + + return [storedValue, setValue] as const; +}; + +// Usage +const [theme, setTheme] = useLocalStorage('theme', 'light'); +``` + +## Component Composition + +### 1. Composition over Inheritance +```typescript +// Flexible component composition +export const Card = ({ children, className, ...props }) => ( +
+ {children} +
+); + +export const CardHeader = ({ children, className, ...props }) => ( +
+ {children} +
+); + +export const CardTitle = ({ children, className, ...props }) => ( +

+ {children} +

+); + +// Usage + + + Space Settings + + + + + +``` + +### 2. Slot-based Composition +```typescript +// Slot-based component +export const Modal = ({ children, isOpen, onClose }) => { + if (!isOpen) return null; + + return ( +
+
+
+ {children} +
+
+ ); +}; + +// Usage with slots + + + Confirm Action + + +

Are you sure you want to delete this space?

+
+ + + + +
+``` + +## State Management Patterns + +### 1. Local State +```typescript +// Component with local state +export const Counter = () => { + const [count, setCount] = useState(0); + + return ( +
+

Count: {count}

+ +
+ ); +}; +``` + +### 2. Lifted State +```typescript +// State lifted to parent +export const ParentComponent = () => { + const [count, setCount] = useState(0); + + return ( +
+ setCount(count + 1)} /> + +
+ ); +}; +``` + +### 3. Global State +```typescript +// Global state with Zustand +export const useCounterStore = create((set) => ({ + count: 0, + increment: () => set((state) => ({ count: state.count + 1 })), + decrement: () => set((state) => ({ count: state.count - 1 })), +})); + +// Usage in component +export const Counter = () => { + const { count, increment } = useCounterStore(); + + return ( +
+

Count: {count}

+ +
+ ); +}; +``` + +## Performance Optimization + +### 1. Memoization +```typescript +// Memoized component +export const ExpensiveComponent = React.memo(({ data, onUpdate }) => { + const processedData = useMemo(() => { + return data.map(item => ({ + ...item, + processed: expensiveCalculation(item) + })); + }, [data]); + + return ( +
+ {processedData.map(item => ( + + ))} +
+ ); +}); +``` + +### 2. Lazy Loading +```typescript +// Lazy loaded component +const LazyComponent = lazy(() => import('./HeavyComponent')); + +export const App = () => ( + }> + + +); +``` + +### 3. Virtual Scrolling +```typescript +// Virtual scrolling for large lists +export const VirtualList = ({ items, itemHeight, containerHeight }) => { + const [scrollTop, setScrollTop] = useState(0); + + const visibleStart = Math.floor(scrollTop / itemHeight); + const visibleEnd = Math.min( + visibleStart + Math.ceil(containerHeight / itemHeight), + items.length + ); + + const visibleItems = items.slice(visibleStart, visibleEnd); + + return ( +
setScrollTop(e.target.scrollTop)} + > +
+ {visibleItems.map((item, index) => ( +
+ {item.content} +
+ ))} +
+
+ ); +}; +``` + +## Testing Patterns + +### 1. Component Testing +```typescript +// Component test +describe('Button', () => { + it('renders with correct text', () => { + render(); + expect(screen.getByText('Click me')).toBeInTheDocument(); + }); + + it('calls onClick when clicked', () => { + const handleClick = jest.fn(); + render(); + fireEvent.click(screen.getByText('Click me')); + expect(handleClick).toHaveBeenCalledTimes(1); + }); +}); +``` + +### 2. Hook Testing +```typescript +// Hook test +describe('useLocalStorage', () => { + it('returns initial value when no stored value', () => { + const { result } = renderHook(() => useLocalStorage('test', 'initial')); + expect(result.current[0]).toBe('initial'); + }); + + it('updates stored value', () => { + const { result } = renderHook(() => useLocalStorage('test', 'initial')); + act(() => { + result.current[1]('updated'); + }); + expect(result.current[0]).toBe('updated'); + }); +}); +``` + +## Best Practices + +### 1. Component Design +- **Single Responsibility** - Each component should have one clear purpose +- **Composition over Inheritance** - Use composition to build complex components +- **Props Interface** - Define clear prop interfaces with TypeScript +- **Default Props** - Provide sensible defaults for optional props + +### 2. State Management +- **Local State First** - Use local state when possible +- **Lift State Up** - Move shared state to common parent +- **Global State** - Use global state for truly global data +- **State Normalization** - Keep state normalized and flat + +### 3. Performance +- **Memoization** - Use React.memo, useMemo, and useCallback appropriately +- **Lazy Loading** - Load components and data on demand +- **Virtual Scrolling** - Use virtual scrolling for large lists +- **Bundle Splitting** - Split code into smaller chunks + +### 4. Testing +- **Unit Tests** - Test individual components in isolation +- **Integration Tests** - Test component interactions +- **Accessibility Tests** - Ensure components are accessible +- **Visual Tests** - Test visual appearance and behavior diff --git a/docs/DEVELOPMENT/DEBUGGING.md b/docs/DEVELOPMENT/DEBUGGING.md new file mode 100644 index 000000000..d1c2db3e8 --- /dev/null +++ b/docs/DEVELOPMENT/DEBUGGING.md @@ -0,0 +1,657 @@ +# Debugging Guide + +This guide covers debugging strategies, tools, and techniques for the Nounspace codebase to help developers identify and resolve issues effectively. + +## Debugging Philosophy + +### 1. Debugging Principles +- **Reproduce First** - Always reproduce the issue before debugging +- **Isolate the Problem** - Narrow down the scope of the issue +- **Use the Right Tools** - Choose appropriate debugging tools for the situation +- **Document Findings** - Keep track of what you discover during debugging + +### 2. Debugging Process +1. **Reproduce** - Reproduce the issue consistently +2. **Isolate** - Isolate the problematic code +3. **Analyze** - Analyze the root cause +4. **Fix** - Implement the fix +5. **Verify** - Verify the fix works +6. **Test** - Test to ensure no regressions + +## Browser Debugging + +### 1. Chrome DevTools +```typescript +// Console debugging +console.log('Debug info:', { user, data, state }); +console.table(data); // Display data in table format +console.group('User Actions'); // Group related logs +console.log('Action 1'); +console.log('Action 2'); +console.groupEnd(); + +// Performance debugging +console.time('API Call'); +await fetchUserData(); +console.timeEnd('API Call'); + +// Memory debugging +console.memory; // Check memory usage +``` + +### 2. React DevTools +```typescript +// Component debugging +import { useDebugValue } from 'react'; + +const useUser = (userId: string) => { + const [user, setUser] = useState(null); + + // Debug value for React DevTools + useDebugValue(user, user => user ? `User: ${user.name}` : 'No user'); + + return { user, setUser }; +}; + +// Profiler debugging +import { Profiler } from 'react'; + +const onRenderCallback = (id, phase, actualDuration) => { + console.log('Component render:', { id, phase, actualDuration }); +}; + + + + +``` + +### 3. Network Debugging +```typescript +// API debugging +const fetchUser = async (userId: string) => { + try { + console.log('Fetching user:', userId); + const response = await fetch(`/api/users/${userId}`); + console.log('Response status:', response.status); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + console.log('User data:', data); + return data; + } catch (error) { + console.error('Fetch error:', error); + throw error; + } +}; +``` + +## State Debugging + +### 1. Zustand Store Debugging +```typescript +// Store debugging +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; + +const useUserStore = create( + devtools( + (set, get) => ({ + user: null, + loading: false, + error: null, + + setUser: (user) => { + console.log('Setting user:', user); + set({ user }, false, 'setUser'); + }, + + setLoading: (loading) => { + console.log('Setting loading:', loading); + set({ loading }, false, 'setLoading'); + }, + + setError: (error) => { + console.log('Setting error:', error); + set({ error }, false, 'setError'); + }, + }), + { + name: 'user-store', // Unique name for DevTools + } + ) +); + +// Debug store state +const debugStore = () => { + const state = useUserStore.getState(); + console.log('Current store state:', state); +}; +``` + +### 2. State Change Tracking +```typescript +// Track state changes +const useUserStore = create((set, get) => ({ + user: null, + setUser: (user) => { + const prevState = get(); + console.log('Previous state:', prevState); + + set({ user }); + + const newState = get(); + console.log('New state:', newState); + }, +})); + +// Subscribe to state changes +const unsubscribe = useUserStore.subscribe( + (state) => console.log('State changed:', state), + (state) => state.user // Only log when user changes +); +``` + +## Component Debugging + +### 1. Component Lifecycle Debugging +```typescript +// Component debugging +import { useEffect, useRef } from 'react'; + +const UserProfile = ({ userId }) => { + const renderCount = useRef(0); + renderCount.current++; + + console.log(`UserProfile rendered ${renderCount.current} times`); + + useEffect(() => { + console.log('UserProfile mounted'); + return () => console.log('UserProfile unmounted'); + }, []); + + useEffect(() => { + console.log('UserProfile userId changed:', userId); + }, [userId]); + + return
User Profile
; +}; +``` + +### 2. Props Debugging +```typescript +// Props debugging +const UserProfile = (props) => { + console.log('UserProfile props:', props); + + // Debug specific prop changes + const prevProps = useRef(); + useEffect(() => { + if (prevProps.current) { + const changedProps = Object.keys(props).filter( + key => prevProps.current[key] !== props[key] + ); + if (changedProps.length > 0) { + console.log('Changed props:', changedProps); + } + } + prevProps.current = props; + }); + + return
User Profile
; +}; +``` + +## Error Debugging + +### 1. Error Boundaries +```typescript +// Error boundary for debugging +class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false, error: null, errorInfo: null }; + } + + static getDerivedStateFromError(error) { + return { hasError: true, error }; + } + + componentDidCatch(error, errorInfo) { + console.error('Error caught by boundary:', error); + console.error('Error info:', errorInfo); + + // Log to error reporting service + this.logErrorToService(error, errorInfo); + + this.setState({ errorInfo }); + } + + logErrorToService = (error, errorInfo) => { + // Send error to logging service + console.log('Logging error to service:', { error, errorInfo }); + }; + + render() { + if (this.state.hasError) { + return ( +
+

Something went wrong

+
+ Error details +
{this.state.error && this.state.error.toString()}
+
{this.state.errorInfo.componentStack}
+
+
+ ); + } + + return this.props.children; + } +} +``` + +### 2. Error Handling +```typescript +// Error handling with debugging +const fetchUser = async (userId: string) => { + try { + const response = await api.get(`/users/${userId}`); + return response.data; + } catch (error) { + console.error('Fetch user error:', { + userId, + error: error.message, + stack: error.stack, + timestamp: new Date().toISOString() + }); + + // Re-throw with additional context + throw new Error(`Failed to fetch user ${userId}: ${error.message}`); + } +}; +``` + +## Performance Debugging + +### 1. Performance Monitoring +```typescript +// Performance monitoring +import { Profiler } from 'react'; + +const PerformanceProfiler = ({ children, id }) => { + const onRenderCallback = (id, phase, actualDuration, baseDuration, startTime, commitTime) => { + console.log('Performance metrics:', { + id, + phase, + actualDuration, + baseDuration, + startTime, + commitTime + }); + + // Log slow renders + if (actualDuration > 100) { + console.warn(`Slow render detected: ${id} took ${actualDuration}ms`); + } + }; + + return ( + + {children} + + ); +}; +``` + +### 2. Memory Debugging +```typescript +// Memory debugging +const debugMemory = () => { + if (performance.memory) { + console.log('Memory usage:', { + used: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024) + ' MB', + total: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024) + ' MB', + limit: Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024) + ' MB' + }); + } +}; + +// Monitor memory leaks +const useMemoryMonitor = () => { + useEffect(() => { + const interval = setInterval(debugMemory, 5000); + return () => clearInterval(interval); + }, []); +}; +``` + +## Network Debugging + +### 1. API Debugging +```typescript +// API debugging +const apiClient = axios.create({ + baseURL: '/api', + timeout: 10000 +}); + +// Request interceptor +apiClient.interceptors.request.use( + (config) => { + console.log('API Request:', { + url: config.url, + method: config.method, + data: config.data, + headers: config.headers + }); + return config; + }, + (error) => { + console.error('API Request Error:', error); + return Promise.reject(error); + } +); + +// Response interceptor +apiClient.interceptors.response.use( + (response) => { + console.log('API Response:', { + url: response.config.url, + status: response.status, + data: response.data + }); + return response; + }, + (error) => { + console.error('API Response Error:', { + url: error.config?.url, + status: error.response?.status, + data: error.response?.data, + message: error.message + }); + return Promise.reject(error); + } +); +``` + +### 2. WebSocket Debugging +```typescript +// WebSocket debugging +const useWebSocket = (url: string) => { + const [socket, setSocket] = useState(null); + const [connectionState, setConnectionState] = useState('Connecting'); + + useEffect(() => { + const ws = new WebSocket(url); + + ws.onopen = () => { + console.log('WebSocket connected:', url); + setConnectionState('Connected'); + }; + + ws.onmessage = (event) => { + console.log('WebSocket message received:', event.data); + }; + + ws.onclose = () => { + console.log('WebSocket disconnected'); + setConnectionState('Disconnected'); + }; + + ws.onerror = (error) => { + console.error('WebSocket error:', error); + setConnectionState('Error'); + }; + + setSocket(ws); + + return () => { + ws.close(); + }; + }, [url]); + + return { socket, connectionState }; +}; +``` + +## Mobile Debugging + +### 1. Mobile-Specific Debugging +```typescript +// Mobile debugging +const useMobileDebug = () => { + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + const checkMobile = () => { + const mobile = window.innerWidth < 768; + setIsMobile(mobile); + console.log('Mobile detection:', { isMobile: mobile, width: window.innerWidth }); + }; + + checkMobile(); + window.addEventListener('resize', checkMobile); + + return () => window.removeEventListener('resize', checkMobile); + }, []); + + return { isMobile }; +}; +``` + +### 2. Touch Event Debugging +```typescript +// Touch event debugging +const useTouchDebug = () => { + useEffect(() => { + const handleTouchStart = (e: TouchEvent) => { + console.log('Touch start:', { + touches: e.touches.length, + target: e.target, + clientX: e.touches[0]?.clientX, + clientY: e.touches[0]?.clientY + }); + }; + + const handleTouchMove = (e: TouchEvent) => { + console.log('Touch move:', { + touches: e.touches.length, + clientX: e.touches[0]?.clientX, + clientY: e.touches[0]?.clientY + }); + }; + + const handleTouchEnd = (e: TouchEvent) => { + console.log('Touch end:', { + touches: e.touches.length, + changedTouches: e.changedTouches.length + }); + }; + + document.addEventListener('touchstart', handleTouchStart); + document.addEventListener('touchmove', handleTouchMove); + document.addEventListener('touchend', handleTouchEnd); + + return () => { + document.removeEventListener('touchstart', handleTouchStart); + document.removeEventListener('touchmove', handleTouchMove); + document.removeEventListener('touchend', handleTouchEnd); + }; + }, []); +}; +``` + +## Debugging Tools + +### 1. Custom Debug Hooks +```typescript +// Custom debug hook +const useDebug = (value: any, label: string) => { + useEffect(() => { + console.log(`${label}:`, value); + }, [value, label]); +}; + +// Usage +const UserProfile = ({ user }) => { + useDebug(user, 'User Profile User'); + useDebug(user?.name, 'User Name'); + + return
{user?.name}
; +}; +``` + +### 2. Debug Utilities +```typescript +// Debug utilities +export const debug = { + log: (message: string, data?: any) => { + if (process.env.NODE_ENV === 'development') { + console.log(`[DEBUG] ${message}`, data); + } + }, + + warn: (message: string, data?: any) => { + if (process.env.NODE_ENV === 'development') { + console.warn(`[DEBUG] ${message}`, data); + } + }, + + error: (message: string, data?: any) => { + if (process.env.NODE_ENV === 'development') { + console.error(`[DEBUG] ${message}`, data); + } + }, + + group: (label: string, fn: () => void) => { + if (process.env.NODE_ENV === 'development') { + console.group(label); + fn(); + console.groupEnd(); + } + } +}; +``` + +## Debugging Best Practices + +### 1. Debugging Strategy +- **Start Simple** - Begin with basic console.log statements +- **Use Breakpoints** - Set breakpoints in critical code paths +- **Isolate Issues** - Narrow down the problem scope +- **Document Findings** - Keep track of what you discover + +### 2. Debugging Tools +- **Browser DevTools** - Use Chrome DevTools for debugging +- **React DevTools** - Use React DevTools for component debugging +- **Network Tab** - Monitor network requests and responses +- **Performance Tab** - Analyze performance bottlenecks + +### 3. Debugging Techniques +- **Console Debugging** - Use console.log, console.error, etc. +- **Breakpoint Debugging** - Set breakpoints in code +- **Step-through Debugging** - Step through code line by line +- **Variable Inspection** - Inspect variable values and types + +### 4. Debugging Maintenance +- **Remove Debug Code** - Clean up debug code before committing +- **Use Environment Variables** - Control debug output with env vars +- **Document Debug Process** - Keep track of debugging steps +- **Share Debug Findings** - Share debugging insights with team + +## Common Debugging Scenarios + +### 1. State Issues +```typescript +// Debug state issues +const useUser = (userId: string) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + console.log('User effect triggered:', { userId, user, loading, error }); + + const fetchUser = async () => { + setLoading(true); + setError(null); + + try { + const userData = await api.getUser(userId); + console.log('User fetched:', userData); + setUser(userData); + } catch (err) { + console.error('User fetch error:', err); + setError(err.message); + } finally { + setLoading(false); + } + }; + + if (userId) { + fetchUser(); + } + }, [userId]); + + return { user, loading, error }; +}; +``` + +### 2. Component Issues +```typescript +// Debug component issues +const UserProfile = ({ userId }) => { + console.log('UserProfile render:', { userId }); + + const { user, loading, error } = useUser(userId); + + console.log('UserProfile state:', { user, loading, error }); + + if (loading) { + console.log('UserProfile loading'); + return
Loading...
; + } + + if (error) { + console.log('UserProfile error:', error); + return
Error: {error}
; + } + + if (!user) { + console.log('UserProfile no user'); + return
No user found
; + } + + console.log('UserProfile rendering user:', user); + return
{user.name}
; +}; +``` + +### 3. Performance Issues +```typescript +// Debug performance issues +const ExpensiveComponent = ({ data }) => { + console.log('ExpensiveComponent render:', { dataLength: data?.length }); + + const processedData = useMemo(() => { + console.log('Processing data:', data); + return data.map(item => ({ + ...item, + processed: expensiveCalculation(item) + })); + }, [data]); + + console.log('Processed data:', processedData); + + return ( +
+ {processedData.map(item => ( + + ))} +
+ ); +}; +``` diff --git a/docs/instructions.md b/docs/DEVELOPMENT/DEVELOPMENT_GUIDE.md similarity index 100% rename from docs/instructions.md rename to docs/DEVELOPMENT/DEVELOPMENT_GUIDE.md diff --git a/docs/notes.md b/docs/DEVELOPMENT/DEVELOPMENT_NOTES.md similarity index 100% rename from docs/notes.md rename to docs/DEVELOPMENT/DEVELOPMENT_NOTES.md diff --git a/docs/DEVELOPMENT/TESTING.md b/docs/DEVELOPMENT/TESTING.md new file mode 100644 index 000000000..c08b339eb --- /dev/null +++ b/docs/DEVELOPMENT/TESTING.md @@ -0,0 +1,663 @@ +# Testing Guide + +This guide covers testing strategies, patterns, and best practices for the Nounspace codebase to ensure code quality and reliability. + +## Testing Philosophy + +### 1. Testing Pyramid +- **Unit Tests** - Test individual components and functions in isolation +- **Integration Tests** - Test component interactions and data flow +- **E2E Tests** - Test complete user workflows and scenarios +- **Visual Tests** - Test visual appearance and behavior + +### 2. Testing Principles +- **Test Behavior** - Test what the code does, not how it does it +- **Test Isolation** - Each test should be independent and isolated +- **Test Clarity** - Tests should be clear and easy to understand +- **Test Coverage** - Aim for high test coverage with meaningful tests + +## Testing Setup + +### 1. Testing Framework +```typescript +// vitest.config.ts +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + setupFiles: ['./tests/setup.ts'], + globals: true, + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +}); +``` + +### 2. Test Setup +```typescript +// tests/setup.ts +import '@testing-library/jest-dom'; +import { cleanup } from '@testing-library/react'; +import { afterEach } from 'vitest'; + +// Clean up after each test +afterEach(() => { + cleanup(); +}); + +// Mock global objects +global.ResizeObserver = vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), +})); + +// Mock IntersectionObserver +global.IntersectionObserver = vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), +})); +``` + +## Unit Testing + +### 1. Component Testing +```typescript +// Button.test.tsx +import { render, screen, fireEvent } from '@testing-library/react'; +import { Button } from '@/components/Button'; + +describe('Button', () => { + it('renders with correct text', () => { + render(); + expect(screen.getByText('Click me')).toBeInTheDocument(); + }); + + it('calls onClick when clicked', () => { + const handleClick = vi.fn(); + render(); + + fireEvent.click(screen.getByText('Click me')); + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + it('is disabled when disabled prop is true', () => { + render(); + expect(screen.getByRole('button')).toBeDisabled(); + }); + + it('applies correct variant classes', () => { + render(); + expect(screen.getByRole('button')).toHaveClass('btn-primary'); + }); +}); +``` + +### 2. Hook Testing +```typescript +// useCounter.test.ts +import { renderHook, act } from '@testing-library/react'; +import { useCounter } from '@/hooks/useCounter'; + +describe('useCounter', () => { + it('should initialize with default value', () => { + const { result } = renderHook(() => useCounter()); + expect(result.current.count).toBe(0); + }); + + it('should initialize with custom value', () => { + const { result } = renderHook(() => useCounter(5)); + expect(result.current.count).toBe(5); + }); + + it('should increment count', () => { + const { result } = renderHook(() => useCounter()); + + act(() => { + result.current.increment(); + }); + + expect(result.current.count).toBe(1); + }); + + it('should decrement count', () => { + const { result } = renderHook(() => useCounter(5)); + + act(() => { + result.current.decrement(); + }); + + expect(result.current.count).toBe(4); + }); + + it('should reset count', () => { + const { result } = renderHook(() => useCounter(5)); + + act(() => { + result.current.reset(); + }); + + expect(result.current.count).toBe(0); + }); +}); +``` + +### 3. Utility Function Testing +```typescript +// utils.test.ts +import { formatDate, validateEmail, debounce } from '@/utils'; + +describe('formatDate', () => { + it('should format date correctly', () => { + const date = new Date('2023-12-25'); + expect(formatDate(date)).toBe('Dec 25, 2023'); + }); + + it('should handle invalid date', () => { + expect(formatDate(new Date('invalid'))).toBe('Invalid Date'); + }); +}); + +describe('validateEmail', () => { + it('should validate correct email', () => { + expect(validateEmail('test@example.com')).toBe(true); + }); + + it('should reject invalid email', () => { + expect(validateEmail('invalid-email')).toBe(false); + }); +}); + +describe('debounce', () => { + it('should debounce function calls', async () => { + const mockFn = vi.fn(); + const debouncedFn = debounce(mockFn, 100); + + debouncedFn(); + debouncedFn(); + debouncedFn(); + + expect(mockFn).not.toHaveBeenCalled(); + + await new Promise(resolve => setTimeout(resolve, 150)); + expect(mockFn).toHaveBeenCalledTimes(1); + }); +}); +``` + +## Integration Testing + +### 1. Component Integration +```typescript +// UserProfile.test.tsx +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { UserProfile } from '@/components/UserProfile'; +import { UserProvider } from '@/contexts/UserContext'; + +const mockUser = { + id: '1', + name: 'John Doe', + email: 'john@example.com', + role: 'user' +}; + +describe('UserProfile Integration', () => { + it('should display user information', () => { + render( + + + + ); + + expect(screen.getByText('John Doe')).toBeInTheDocument(); + expect(screen.getByText('john@example.com')).toBeInTheDocument(); + }); + + it('should allow editing user information', async () => { + const mockUpdate = vi.fn(); + + render( + + + + ); + + const editButton = screen.getByText('Edit'); + fireEvent.click(editButton); + + const nameInput = screen.getByLabelText('Name'); + fireEvent.change(nameInput, { target: { value: 'Jane Doe' } }); + + const saveButton = screen.getByText('Save'); + fireEvent.click(saveButton); + + await waitFor(() => { + expect(mockUpdate).toHaveBeenCalledWith({ + ...mockUser, + name: 'Jane Doe' + }); + }); + }); +}); +``` + +### 2. Store Integration +```typescript +// userStore.test.ts +import { renderHook, act } from '@testing-library/react'; +import { create } from 'zustand'; +import { userStore } from '@/stores/userStore'; + +describe('UserStore Integration', () => { + it('should manage user state', () => { + const { result } = renderHook(() => userStore()); + + act(() => { + result.current.setUser(mockUser); + }); + + expect(result.current.user).toEqual(mockUser); + expect(result.current.isAuthenticated).toBe(true); + }); + + it('should handle user logout', () => { + const { result } = renderHook(() => userStore()); + + act(() => { + result.current.setUser(mockUser); + }); + + expect(result.current.isAuthenticated).toBe(true); + + act(() => { + result.current.logout(); + }); + + expect(result.current.user).toBeNull(); + expect(result.current.isAuthenticated).toBe(false); + }); +}); +``` + +## End-to-End Testing + +### 1. E2E Test Setup +```typescript +// e2e/user-flow.spec.ts +import { test, expect } from '@playwright/test'; + +test.describe('User Authentication Flow', () => { + test('should allow user to login and access dashboard', async ({ page }) => { + // Navigate to login page + await page.goto('/login'); + + // Fill login form + await page.fill('[data-testid="email-input"]', 'test@example.com'); + await page.fill('[data-testid="password-input"]', 'password123'); + + // Submit form + await page.click('[data-testid="login-button"]'); + + // Wait for redirect to dashboard + await page.waitForURL('/dashboard'); + + // Verify dashboard content + expect(await page.textContent('[data-testid="welcome-message"]')).toContain('Welcome'); + }); +}); +``` + +### 2. E2E Test Scenarios +```typescript +// e2e/space-creation.spec.ts +import { test, expect } from '@playwright/test'; + +test.describe('Space Creation Flow', () => { + test('should create a new space', async ({ page }) => { + // Login first + await page.goto('/login'); + await page.fill('[data-testid="email-input"]', 'test@example.com'); + await page.fill('[data-testid="password-input"]', 'password123'); + await page.click('[data-testid="login-button"]'); + + // Navigate to spaces + await page.goto('/spaces'); + + // Click create space button + await page.click('[data-testid="create-space-button"]'); + + // Fill space form + await page.fill('[data-testid="space-name-input"]', 'My New Space'); + await page.fill('[data-testid="space-description-input"]', 'A test space'); + + // Submit form + await page.click('[data-testid="create-space-submit"]'); + + // Verify space was created + await page.waitForSelector('[data-testid="space-card"]'); + expect(await page.textContent('[data-testid="space-name"]')).toBe('My New Space'); + }); +}); +``` + +## Visual Testing + +### 1. Visual Regression Testing +```typescript +// visual/button.visual.test.ts +import { test, expect } from '@playwright/test'; + +test.describe('Button Visual Tests', () => { + test('should render primary button correctly', async ({ page }) => { + await page.goto('/components/button'); + + const button = page.locator('[data-testid="primary-button"]'); + await expect(button).toHaveScreenshot('primary-button.png'); + }); + + test('should render secondary button correctly', async ({ page }) => { + await page.goto('/components/button'); + + const button = page.locator('[data-testid="secondary-button"]'); + await expect(button).toHaveScreenshot('secondary-button.png'); + }); +}); +``` + +### 2. Responsive Testing +```typescript +// visual/responsive.visual.test.ts +import { test, expect } from '@playwright/test'; + +test.describe('Responsive Design Tests', () => { + test('should render correctly on mobile', async ({ page }) => { + await page.setViewportSize({ width: 375, height: 667 }); + await page.goto('/dashboard'); + + await expect(page).toHaveScreenshot('dashboard-mobile.png'); + }); + + test('should render correctly on tablet', async ({ page }) => { + await page.setViewportSize({ width: 768, height: 1024 }); + await page.goto('/dashboard'); + + await expect(page).toHaveScreenshot('dashboard-tablet.png'); + }); + + test('should render correctly on desktop', async ({ page }) => { + await page.setViewportSize({ width: 1920, height: 1080 }); + await page.goto('/dashboard'); + + await expect(page).toHaveScreenshot('dashboard-desktop.png'); + }); +}); +``` + +## Accessibility Testing + +### 1. Accessibility Test Setup +```typescript +// a11y/accessibility.test.ts +import { test, expect } from '@playwright/test'; +import AxeBuilder from '@axe-core/playwright'; + +test.describe('Accessibility Tests', () => { + test('should not have accessibility violations', async ({ page }) => { + await page.goto('/dashboard'); + + const accessibilityScanResults = await new AxeBuilder({ page }).analyze(); + expect(accessibilityScanResults.violations).toEqual([]); + }); + + test('should be keyboard navigable', async ({ page }) => { + await page.goto('/dashboard'); + + // Test tab navigation + await page.keyboard.press('Tab'); + const focusedElement = page.locator(':focus'); + await expect(focusedElement).toBeVisible(); + + // Test arrow key navigation + await page.keyboard.press('ArrowDown'); + await page.keyboard.press('ArrowUp'); + }); +}); +``` + +### 2. Screen Reader Testing +```typescript +// a11y/screen-reader.test.ts +import { test, expect } from '@playwright/test'; + +test.describe('Screen Reader Tests', () => { + test('should have proper ARIA labels', async ({ page }) => { + await page.goto('/dashboard'); + + // Check for ARIA labels + const elementsWithAriaLabels = page.locator('[aria-label]'); + await expect(elementsWithAriaLabels).toHaveCount(5); + + // Check for ARIA roles + const elementsWithRoles = page.locator('[role]'); + await expect(elementsWithRoles).toHaveCount(3); + }); + + test('should have proper heading structure', async ({ page }) => { + await page.goto('/dashboard'); + + // Check heading hierarchy + const h1 = page.locator('h1'); + const h2 = page.locator('h2'); + const h3 = page.locator('h3'); + + await expect(h1).toHaveCount(1); + await expect(h2).toHaveCount(2); + await expect(h3).toHaveCount(3); + }); +}); +``` + +## Performance Testing + +### 1. Performance Metrics +```typescript +// performance/performance.test.ts +import { test, expect } from '@playwright/test'; + +test.describe('Performance Tests', () => { + test('should load page within acceptable time', async ({ page }) => { + const startTime = Date.now(); + await page.goto('/dashboard'); + const loadTime = Date.now() - startTime; + + expect(loadTime).toBeLessThan(3000); // 3 seconds + }); + + test('should have good Core Web Vitals', async ({ page }) => { + await page.goto('/dashboard'); + + const metrics = await page.evaluate(() => { + return new Promise((resolve) => { + new PerformanceObserver((list) => { + const entries = list.getEntries(); + resolve(entries); + }).observe({ entryTypes: ['largest-contentful-paint', 'first-input', 'cumulative-layout-shift'] }); + }); + }); + + expect(metrics).toBeDefined(); + }); +}); +``` + +### 2. Bundle Size Testing +```typescript +// performance/bundle-size.test.ts +import { test, expect } from '@playwright/test'; + +test.describe('Bundle Size Tests', () => { + test('should have acceptable bundle size', async ({ page }) => { + await page.goto('/dashboard'); + + const bundleSize = await page.evaluate(() => { + return performance.getEntriesByType('resource') + .filter(entry => entry.name.includes('.js')) + .reduce((total, entry) => total + entry.transferSize, 0); + }); + + expect(bundleSize).toBeLessThan(500000); // 500KB + }); +}); +``` + +## Test Data Management + +### 1. Test Fixtures +```typescript +// fixtures/user.fixtures.ts +export const mockUser = { + id: '1', + name: 'John Doe', + email: 'john@example.com', + role: 'user', + createdAt: '2023-01-01T00:00:00Z', + updatedAt: '2023-01-01T00:00:00Z' +}; + +export const mockUsers = [ + mockUser, + { + id: '2', + name: 'Jane Doe', + email: 'jane@example.com', + role: 'admin', + createdAt: '2023-01-02T00:00:00Z', + updatedAt: '2023-01-02T00:00:00Z' + } +]; +``` + +### 2. Test Utilities +```typescript +// utils/test-utils.ts +import { render, RenderOptions } from '@testing-library/react'; +import { ReactElement } from 'react'; +import { UserProvider } from '@/contexts/UserContext'; + +const AllTheProviders = ({ children }: { children: React.ReactNode }) => { + return ( + + {children} + + ); +}; + +const customRender = ( + ui: ReactElement, + options?: Omit +) => render(ui, { wrapper: AllTheProviders, ...options }); + +export * from '@testing-library/react'; +export { customRender as render }; +``` + +## Test Automation + +### 1. CI/CD Integration +```yaml +# .github/workflows/test.yml +name: Tests + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run unit tests + run: npm run test:unit + + - name: Run integration tests + run: npm run test:integration + + - name: Run E2E tests + run: npm run test:e2e + + - name: Run accessibility tests + run: npm run test:a11y +``` + +### 2. Test Reporting +```typescript +// vitest.config.ts +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + exclude: [ + 'node_modules/', + 'tests/', + '**/*.d.ts', + '**/*.config.*' + ] + }, + reporters: ['verbose', 'junit'], + outputFile: { + junit: './test-results/junit.xml' + } + } +}); +``` + +## Best Practices + +### 1. Test Organization +- **Group related tests** in describe blocks +- **Use descriptive test names** that explain what is being tested +- **Keep tests focused** on a single behavior +- **Use consistent naming** conventions + +### 2. Test Maintenance +- **Update tests** when code changes +- **Remove obsolete tests** that are no longer relevant +- **Refactor tests** to keep them maintainable +- **Monitor test performance** and optimize slow tests + +### 3. Test Quality +- **Write meaningful tests** that catch real bugs +- **Avoid testing implementation details** focus on behavior +- **Use appropriate test types** for different scenarios +- **Maintain good test coverage** without sacrificing quality + +### 4. Debugging Tests +- **Use debugging tools** like browser DevTools for E2E tests +- **Add logging** to understand test failures +- **Use test utilities** to simplify test setup +- **Document test scenarios** for complex tests diff --git a/docs/DOCUMENTATION_OVERVIEW.md b/docs/DOCUMENTATION_OVERVIEW.md new file mode 100644 index 000000000..765229aec --- /dev/null +++ b/docs/DOCUMENTATION_OVERVIEW.md @@ -0,0 +1,124 @@ +# Documentation Overview + +This document provides an overview of the Nounspace documentation structure and organization. + +## Documentation Structure + +``` +docs/ +├── README.md # Main documentation hub +├── GETTING_STARTED.md # Setup and quick start guide +├── CONTRIBUTING.md # Contributing guidelines +│ +├── ARCHITECTURE/ # System architecture documentation +│ ├── OVERVIEW.md # High-level architecture overview +│ ├── AUTHENTICATION.md # Authentication system (Privy + Farcaster) +│ └── STATE_MANAGEMENT.md # Zustand store architecture +│ +├── SYSTEMS/ # Core system documentation +│ ├── SPACES/ # Space system +│ │ ├── OVERVIEW.md # Space architecture and patterns +│ │ ├── SPACE_ARCHITECTURE.md # Detailed space architecture +│ │ ├── PUBLIC_SPACES_PATTERN.md # Public space patterns +│ │ ├── MULTIPLE_LAYOUTS_OVERVIEW.md # Multiple layouts system +│ │ └── LAYOUT_MIGRATION_GUIDE.md # Layout migration guide +│ ├── FIDGETS/ # Fidget system +│ │ └── OVERVIEW.md # Fidget architecture +│ ├── THEMES/ # Theme system +│ │ └── OVERVIEW.md # Theme architecture +│ └── DISCOVERY/ # Discovery system +│ └── MINI_APP_DISCOVERY_SYSTEM.md # Mini-app discovery system +│ +├── INTEGRATIONS/ # External integrations +│ ├── FARCASTER.md # Farcaster protocol integration +│ └── SUPABASE.md # Supabase integration +│ +├── DEVELOPMENT/ # Development guides +│ ├── AGENTS.md # AI agent instructions +│ ├── DEVELOPMENT_GUIDE.md # Comprehensive development guide +│ ├── DEVELOPMENT_NOTES.md # Development notes and findings +│ ├── COMPONENT_ARCHITECTURE.md # Atomic design system +│ ├── CODING_STANDARDS.md # Code style and standards +│ ├── TESTING.md # Testing strategies +│ └── DEBUGGING.md # Debugging guide +│ +└── REFERENCE/ # Reference documentation + └── (placeholder directories) +``` + +## Available Documentation + +### Core Documentation +- **README.md** - Main documentation hub with navigation +- **GETTING_STARTED.md** - Setup and installation guide +- **CONTRIBUTING.md** - Contributing guidelines + +### Architecture +- **ARCHITECTURE/OVERVIEW.md** - High-level architecture with diagrams +- **ARCHITECTURE/AUTHENTICATION.md** - Complete authentication system documentation +- **ARCHITECTURE/STATE_MANAGEMENT.md** - Zustand store architecture and patterns + +### Systems +- **SYSTEMS/SPACES/OVERVIEW.md** - Space architecture, public/private patterns, lifecycle +- **SYSTEMS/SPACES/SPACE_ARCHITECTURE.md** - Detailed space architecture +- **SYSTEMS/SPACES/PUBLIC_SPACES_PATTERN.md** - Public space patterns +- **SYSTEMS/SPACES/MULTIPLE_LAYOUTS_OVERVIEW.md** - Multiple layouts system +- **SYSTEMS/SPACES/LAYOUT_MIGRATION_GUIDE.md** - Layout migration guide +- **SYSTEMS/FIDGETS/OVERVIEW.md** - Fidget system, types, development patterns +- **SYSTEMS/THEMES/OVERVIEW.md** - Theme system, customization, CSS variables +- **SYSTEMS/DISCOVERY/MINI_APP_DISCOVERY_SYSTEM.md** - Mini-app discovery system + +### Integrations +- **INTEGRATIONS/FARCASTER.md** - Farcaster protocol integration, FID management, social features +- **INTEGRATIONS/SUPABASE.md** - Database, storage, authentication, real-time features + +### Development +- **DEVELOPMENT/AGENTS.md** - AI agent instructions and guidelines +- **DEVELOPMENT/DEVELOPMENT_GUIDE.md** - Comprehensive development guide +- **DEVELOPMENT/DEVELOPMENT_NOTES.md** - Development notes and findings +- **DEVELOPMENT/COMPONENT_ARCHITECTURE.md** - Atomic design, patterns, best practices +- **DEVELOPMENT/CODING_STANDARDS.md** - TypeScript, React, testing, security standards +- **DEVELOPMENT/TESTING.md** - Unit, integration, E2E, accessibility testing +- **DEVELOPMENT/DEBUGGING.md** - Debugging tools, techniques, common issues + +## Key Features + +### 1. Organized Structure +- Logical hierarchy by topic and concern +- Clear separation between architecture, systems, and development guides +- Easy navigation with comprehensive README + +### 2. Accurate Documentation +- All documentation reflects actual codebase implementation +- Real code examples from the codebase +- Best practices and patterns + +### 3. Comprehensive Coverage +- Architecture documentation with diagrams +- System-specific guides with code examples +- Integration guides for external services +- Development guides for contributors + +### 4. Practical Examples +- Real code examples from the codebase +- Common issues and troubleshooting +- Testing strategies + +## Getting Started + +### For Developers +1. **Start with README.md** - The main hub provides navigation to all documentation +2. **Use GETTING_STARTED.md** - Quick setup and installation guide +3. **Reference Architecture Docs** - Understand the system before making changes +4. **Follow Coding Standards** - Ensure consistency with project standards + +### For Contributors +1. **Read CONTRIBUTING.MD** - Guidelines for contributions +2. **Review Coding Standards** - Follow TypeScript and React best practices +3. **Write Tests** - Follow testing guide for comprehensive coverage +4. **Document Changes** - Update relevant documentation with code changes + +### For Users +1. **Explore Systems Docs** - Learn about spaces, fidgets, and themes +2. **Check Integration Guides** - Understand external service integrations +3. **Use Troubleshooting Sections** - Find solutions to common issues diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md new file mode 100644 index 000000000..7bcac2e11 --- /dev/null +++ b/docs/GETTING_STARTED.md @@ -0,0 +1,94 @@ +# Getting Started + +This guide will help you set up the Nounspace development environment and understand the basic concepts. + +## Prerequisites + +- Node.js v22.11.0 or later +- npm or yarn package manager +- Git + +## Installation + +1. **Clone the repository** + ```bash + git clone https://github.com/nounspace/nounspace.ts.git + cd nounspace.ts + ``` + +2. **Install dependencies** + ```bash + npm install + ``` + +3. **Set up environment variables** + ```bash + cp .env.example .env.local + ``` + + Configure the following required environment variables: + - `NEXT_PUBLIC_PRIVY_APP_ID` - Privy application ID + - `NEXT_PUBLIC_NEYNAR_API_KEY` - Neynar API key + - `NEXT_PUBLIC_SUPABASE_URL` - Supabase project URL + - `NEXT_PUBLIC_SUPABASE_ANON_KEY` - Supabase anonymous key + +4. **Start the development server** + ```bash + npm run dev + ``` + +## Project Structure + +``` +src/ +├── app/ # Next.js App Router +│ ├── (spaces)/ # Space-related routes +│ ├── api/ # API routes +│ ├── explore/ # Discovery pages +│ ├── frames/ # Frame-related routes +│ ├── home/ # Home page +│ ├── notifications/ # Notifications +│ ├── privacy/ # Privacy page +│ ├── pwa/ # PWA configuration +│ └── terms/ # Terms page +├── authenticators/ # Authentication system +├── common/ # Shared code +│ ├── components/ # UI components (atomic design) +│ ├── data/ # State management +│ ├── fidgets/ # Core fidget functionality +│ ├── lib/ # Utilities and helpers +│ └── providers/ # React context providers +├── constants/ # Application constants +├── contracts/ # Blockchain contract interfaces +├── fidgets/ # Mini-applications +├── pages/ # Legacy Next.js pages +└── styles/ # Global styles +``` + +## Key Concepts + +### Spaces +Spaces are customizable hubs that users can personalize with themes, tabs, and fidgets. + +### Fidgets +Mini-applications that can be added to spaces to provide specific functionality. + +### Themes +Visual customization system that allows users to personalize their spaces. + +### Authentication +The app uses Privy for authentication with Farcaster integration for social features. + +## Development Workflow + +1. **Make changes** to the codebase +2. **Run linting** with `npm run lint` +3. **Check types** with `npm run check-types` +4. **Test changes** with `npm run test` +5. **Create a PR** following the [Contributing](CONTRIBUTING.md) guidelines + +## Next Steps + +- Read the [Architecture Overview](ARCHITECTURE/OVERVIEW.md) to understand the system +- Check out [Fidget Development Guide](SYSTEMS/FIDGETS/DEVELOPMENT_GUIDE.md) to create fidgets +- Review [Component Architecture](DEVELOPMENT/COMPONENT_ARCHITECTURE.md) for UI development diff --git a/docs/INTEGRATIONS/FARCASTER.md b/docs/INTEGRATIONS/FARCASTER.md new file mode 100644 index 000000000..bf1103342 --- /dev/null +++ b/docs/INTEGRATIONS/FARCASTER.md @@ -0,0 +1,558 @@ +# Farcaster Integration + +Nounspace integrates deeply with the Farcaster protocol to provide social features and identity management. + +## Overview + +Farcaster integration enables: +- **Social Identity** - Farcaster ID (FID) linking and management +- **Social Features** - Casts, feeds, and social interactions +- **Protocol Access** - Direct access to Farcaster protocol features +- **Identity Verification** - Cryptographic identity verification + +## Core Components + +### 1. FID Management +```typescript +// FID linking and management +export type FarcasterStore = { + getFidsForCurrentIdentity: () => Promise; + registerFidForCurrentIdentity: ( + fid: number, + signingKey: string, + signMessage: (messageHash: Uint8Array) => Promise, + ) => Promise; + setFidsForCurrentIdentity: (fids: number[]) => void; + addFidToCurrentIdentity: (fid: number) => void; +}; +``` + +### 2. Identity Linking +```typescript +// Link Farcaster FID to identity +const registerFidForCurrentIdentity = async ( + fid: number, + signingKey: string, + signMessage: (messageHash: Uint8Array) => Promise, +) => { + const request: Omit = { + fid, + identityPublicKey: get().account.currentSpaceIdentityPublicKey!, + timestamp: moment().toISOString(), + signingPublicKey: signingKey, + }; + + const signedRequest: FidLinkToIdentityRequest = { + ...request, + signature: bytesToHex(await signMessage(hashObject(request))), + }; + + const { data } = await axiosBackend.post( + "/api/fid-link", + signedRequest, + ); + + if (!isUndefined(data.value)) { + get().account.addFidToCurrentIdentity(data.value!.fid); + analytics.track(AnalyticsEvent.LINK_FID, { fid }); + } +}; +``` + +## Farcaster Fidgets + +### 1. Cast Fidget +```typescript +// Cast display and interaction +export const Cast: FidgetModule = { + Component: ({ config, properties, theme }) => { + const [cast, setCast] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchCast = async () => { + try { + const castData = await api.getCast(config.castHash); + setCast(castData); + } catch (error) { + console.error('Failed to fetch cast:', error); + } finally { + setLoading(false); + } + }; + + if (config.castHash) { + fetchCast(); + } + }, [config.castHash]); + + if (loading) return
Loading cast...
; + if (!cast) return
Cast not found
; + + return ( +
+
+ {cast.author.display_name} +
+

{cast.author.display_name}

+

@{cast.author.username}

+
+
+
+

{cast.text}

+
+
+ + + +
+
+ ); + }, + properties: { + fidgetName: "Cast", + description: "Display and interact with Farcaster casts", + fields: [ + { + fieldName: "castHash", + type: "string", + default: "", + label: "Cast Hash" + } + ], + category: "farcaster", + tags: ["farcaster", "social"], + version: "1.0.0" + } +}; +``` + +### 2. Feed Fidget +```typescript +// Feed display and management +export const Feed: FidgetModule = { + Component: ({ config, properties, theme }) => { + const [feed, setFeed] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchFeed = async () => { + try { + const feedData = await api.getFeed(config.feedType); + setFeed(feedData); + } catch (error) { + console.error('Failed to fetch feed:', error); + } finally { + setLoading(false); + } + }; + + fetchFeed(); + }, [config.feedType]); + + if (loading) return
Loading feed...
; + + return ( +
+

Feed

+
+ {feed.map(cast => ( + + ))} +
+
+ ); + }, + properties: { + fidgetName: "Feed", + description: "Display Farcaster feed", + fields: [ + { + fieldName: "feedType", + type: "select", + default: "following", + options: ["following", "trending", "recent"], + label: "Feed Type" + } + ], + category: "farcaster", + tags: ["farcaster", "social", "feed"], + version: "1.0.0" + } +}; +``` + +### 3. Frame Fidget +```typescript +// Frame display and interaction +export const Frame: FidgetModule = { + Component: ({ config, properties, theme }) => { + const [frame, setFrame] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchFrame = async () => { + try { + const frameData = await api.getFrame(config.frameUrl); + setFrame(frameData); + } catch (error) { + console.error('Failed to fetch frame:', error); + } finally { + setLoading(false); + } + }; + + if (config.frameUrl) { + fetchFrame(); + } + }, [config.frameUrl]); + + if (loading) return
Loading frame...
; + if (!frame) return
Frame not found
; + + return ( +
+ `, + fidgetBorderColor: "var(--user-theme-fidget-border-color)", + fidgetBorderWidth: "var(--user-theme-fidget-border-width)", + fidgetShadow: "var(--user-theme-fidget-shadow)", + isScrollable: false, + showOnMobile: true, + url: "" } - } - }, - "gallery": { - id: "gallery", - fidgetType: "gallery", - config: { - data: {}, - editable: true, - settings: { - showAnalyticsCharts: true, - showDataVisualizations: true, - maxImages: 8 - } - } - }, - "feed": { - id: "feed", - fidgetType: "feed", - config: { - data: {}, - editable: true, - settings: { - showAnalyticsDiscussions: true, - showMarketInsights: true, - maxCasts: 12 - } - } - }, - "iframe": { - id: "iframe", + }, fidgetType: "iframe", - config: { - data: {}, - editable: true, - settings: { - showExternalAnalytics: true, - showTradingTools: true, - url: "https://analytics.clanker.world" - } - } + id: "iframe:1c3fcd3d-7c7d-4c3f-920c-8ab48460eb4e" } }, - fidgetTrayContents: [ - "Market", - "Portfolio", - "Rss", - "gallery", - "feed", - "iframe", - "Video", - "Chat" - ], + fidgetTrayContents: [], isEditable: false, - timestamp: new Date().toISOString() - } - }, - layout: { - defaultLayoutFidget: "grid", - gridSpacing: 16, - theme: { - background: "linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)", - fidgetBackground: "rgba(255, 255, 255, 0.1)", - font: "Inter, system-ui, sans-serif", - fontColor: "#ffffff" + timestamp: "2025-11-06T20:37:03.565Z" } } }; diff --git a/src/config/clanker/clanker.navigation.ts b/src/config/clanker/clanker.navigation.ts index 28436e526..b8ebda109 100644 --- a/src/config/clanker/clanker.navigation.ts +++ b/src/config/clanker/clanker.navigation.ts @@ -4,6 +4,7 @@ export const clankerNavigation: NavigationConfig = { items: [ { id: 'home', label: 'Home', href: '/home', icon: 'home' }, { id: 'notifications', label: 'Notifications', href: '/notifications', icon: 'notifications', requiresAuth: true }, + { id: 'clanker-token', label: '$CLANKER', href: '/t/base/0x1bc0c42215582d5a085795f4badbac3ff36d1bcb/Profile', icon: 'robot' }, ] }; diff --git a/src/config/clanker/initialSpaces/index.ts b/src/config/clanker/initialSpaces/index.ts index 6603d7508..74ff4e240 100644 --- a/src/config/clanker/initialSpaces/index.ts +++ b/src/config/clanker/initialSpaces/index.ts @@ -1,5 +1,6 @@ +// Export the initial space creators from nouns config export { default as createInitialProfileSpaceConfigForFid } from './initialProfileSpace'; export { default as createInitialChannelSpaceConfig } from './initialChannelSpace'; export { default as createInitialTokenSpaceConfigForAddress } from './initialTokenSpace'; -export { default as createInitialProposalSpaceConfigForProposalId } from './initialProposalSpace'; +export { default as createInitalProposalSpaceConfigForProposalId } from './initialProposalSpace'; export { default as INITIAL_HOMEBASE_CONFIG } from './initialHomebase'; diff --git a/src/config/clanker/initialSpaces/initialChannelSpace.ts b/src/config/clanker/initialSpaces/initialChannelSpace.ts index 45d144afc..3980bab28 100644 --- a/src/config/clanker/initialSpaces/initialChannelSpace.ts +++ b/src/config/clanker/initialSpaces/initialChannelSpace.ts @@ -1,75 +1,55 @@ import { SpaceConfig } from "@/app/(spaces)/Space"; +import { FilterType, FeedType } from "@neynar/nodejs-sdk/build/api"; import { cloneDeep } from "lodash"; import { getLayoutConfig } from "@/common/utils/layoutFormatUtils"; import { INITIAL_SPACE_CONFIG_EMPTY } from "../../initialSpaceConfig"; -import { FilterType, FeedType } from "@neynar/nodejs-sdk/build/api"; + +const INITIAL_CHANNEL_SPACE_CONFIG = cloneDeep(INITIAL_SPACE_CONFIG_EMPTY); +INITIAL_CHANNEL_SPACE_CONFIG.tabNames = ["Channel"]; const createInitialChannelSpaceConfig = ( - channelId: string, + channelId: string, ): Omit => { - const config = cloneDeep(INITIAL_SPACE_CONFIG_EMPTY); - - // Apply Clanker-specific theme defaults while preserving required UserTheme shape - config.theme = { - id: "clanker-channel-theme", - name: "Clanker Channel Theme", - properties: { - font: "Inter, system-ui, sans-serif", - fontColor: "#ffffff", - headingsFont: "Inter, system-ui, sans-serif", - headingsFontColor: "#00ff88", - background: - "linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)", - backgroundHTML: "", - musicURL: "", - fidgetBackground: "#00ff88", - fidgetBorderWidth: "1px", - fidgetBorderColor: "#00ff88", - fidgetShadow: "0 4px 20px rgba(0, 255, 136, 0.2)", - fidgetBorderRadius: "12px", - gridSpacing: "16", - }, - }; - - // Minimal initial fidget: a channel feed similar to nouns' channel space - config.fidgetInstanceDatums = { - "feed:channel": { - config: { - editable: false, - settings: { - feedType: FeedType.Filter, - filterType: FilterType.ChannelId, - channel: channelId, - }, - data: {}, - }, - fidgetType: "feed", - id: "feed:channel", - }, - }; - - const layoutItems = [ - { - w: 6, - h: 8, - x: 0, - y: 0, - i: "feed:channel", - minW: 4, - maxW: 20, - minH: 6, - maxH: 12, - moved: false, - static: false, - }, - ]; - - const layoutConfig = getLayoutConfig(config.layoutDetails); - layoutConfig.layout = layoutItems; - - config.tabNames = ["Channel"]; - - return config; + const config = cloneDeep(INITIAL_CHANNEL_SPACE_CONFIG); + + config.fidgetInstanceDatums = { + "feed:channel": { + config: { + editable: false, + settings: { + feedType: FeedType.Filter, + filterType: FilterType.ChannelId, + channel: channelId, + }, + data: {}, + }, + fidgetType: "feed", + id: "feed:channel", + }, + }; + + const layoutItems = [ + { + w: 6, + h: 8, + x: 0, + y: 0, + i: "feed:channel", + minW: 4, + maxW: 20, + minH: 6, + maxH: 12, + moved: false, + static: false, + }, + ]; + + const layoutConfig = getLayoutConfig(config.layoutDetails); + layoutConfig.layout = layoutItems; + + config.tabNames = ["Channel"]; + + return config; }; export default createInitialChannelSpaceConfig; diff --git a/src/config/clanker/initialSpaces/initialHomebase.ts b/src/config/clanker/initialSpaces/initialHomebase.ts index c9841dafd..091cfcd61 100644 --- a/src/config/clanker/initialSpaces/initialHomebase.ts +++ b/src/config/clanker/initialSpaces/initialHomebase.ts @@ -1,157 +1,106 @@ import { SpaceConfig } from "@/app/(spaces)/Space"; -import { cloneDeep } from "lodash"; -import { getLayoutConfig } from "@/common/utils/layoutFormatUtils"; -import { INITIAL_SPACE_CONFIG_EMPTY } from "../../initialSpaceConfig"; - -const INITIAL_HOMEBASE_CONFIG: SpaceConfig = (() => { - const config = cloneDeep(INITIAL_SPACE_CONFIG_EMPTY); - - config.theme = { - id: "clanker-homebase-theme", - name: "Clanker Homebase Theme", - properties: { - font: "Inter, system-ui, sans-serif", - fontColor: "#ffffff", - headingsFont: "Inter, system-ui, sans-serif", - headingsFontColor: "#a8e6cf", - background: - "linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)", - backgroundHTML: "", - musicURL: "", - fidgetBackground: "#a8e6cf", - fidgetBorderWidth: "1px", - fidgetBorderColor: "#a8e6cf", - fidgetShadow: "0 10px 40px rgba(168, 230, 207, 0.2)", - fidgetBorderRadius: "20px", - gridSpacing: "24", - }, - }; - - config.fidgetInstanceDatums = { - "market-data": { - id: "market-data", - fidgetType: "Market", - config: { - data: {}, - editable: true, - settings: { - showEcosystemOverview: true, - showMarketCap: true, - showVolume: true, - }, - }, - }, - portfolio: { - id: "portfolio", - fidgetType: "Portfolio", - config: { - data: {}, - editable: true, - settings: { - showEcosystemMetrics: true, - showPerformance: true, - showTrends: true, - }, - }, - }, - rss: { - id: "rss", - fidgetType: "Rss", - config: { - data: {}, - editable: true, - settings: { - showEcosystemNews: true, - showTokenNews: true, - maxItems: 8, - }, - }, - }, - gallery: { - id: "gallery", - fidgetType: "gallery", - config: { - data: {}, - editable: true, - settings: { - showEcosystemImages: true, - showDataVisualizations: true, - maxImages: 10, - }, - }, - }, - feed: { - id: "feed", - fidgetType: "feed", - config: { - data: {}, - editable: true, - settings: { - showEcosystemDiscussions: true, - showTrendingTopics: true, - maxCasts: 12, - }, - }, - }, - links: { - id: "links", - fidgetType: "links", - config: { - data: {}, - editable: true, - settings: { - showEcosystemLinks: true, - showExternalTools: true, - }, - }, - }, - text: { - id: "text", - fidgetType: "text", - config: { - data: {}, - editable: true, - settings: { - showEcosystemDescription: true, - showStats: true, - }, - }, - }, - iframe: { - id: "iframe", - fidgetType: "iframe", - config: { - data: {}, - editable: true, - settings: { - showEcosystemAnalytics: true, - url: "https://analytics.clanker.world", - }, - }, - }, - }; - - const layoutItems = [ - { w: 6, h: 4, x: 0, y: 0, i: "market-data" }, - { w: 6, h: 4, x: 6, y: 0, i: "portfolio" }, - { w: 4, h: 3, x: 0, y: 4, i: "rss" }, - { w: 4, h: 3, x: 4, y: 4, i: "gallery" }, - { w: 4, h: 3, x: 8, y: 4, i: "feed" }, - { w: 8, h: 4, x: 0, y: 7, i: "links" }, - { w: 4, h: 4, x: 8, y: 7, i: "text" }, - { w: 12, h: 3, x: 0, y: 11, i: "iframe" }, - ]; - - const layoutConfig = getLayoutConfig(config.layoutDetails); - layoutConfig.layout = layoutItems; - - config.tabNames = ["Homebase"]; - - // Ensure required SpaceConfig field present and return full SpaceConfig - return { - ...config, - isEditable: false, - }; -})(); - -export default INITIAL_HOMEBASE_CONFIG; +import DEFAULT_THEME from "@/common/lib/theme/defaultTheme"; + +const tutorialText = ` +### 🖌️ Click the paintbrush in the bottom-left corner to open Customization Mode + +### Add Fidgets +1. Click the blue **+** button. +2. Drag a Fidget to an open spot on the grid. +3. Click Save + +(after saving, scroll down here for more instructions) + +![Add Fidget2](https://space.mypinata.cloud/ipfs/bafkreiczpd2bzyoboj6uxr65kta5cmg3bziveq2nz5egx4fuxr2nmkthru) + +### Customize Fidgets +1. From customization mode, click any Fidget on the grid to open its settings. +2. Click 'Style' to customize a fidget's look. Any Fidget styles set to "Theme" inherit their look from the Tab's theme. + +![EditFidget](https://space.mypinata.cloud/ipfs/bafybeihcjkbcljxr4ttgt6xcxmsc4m4qvw62hkolifmd3t5pdc7kvj5nji) + +### Arrange Fidgets +- **Move:** Drag from the center +![move fidget](https://space.mypinata.cloud/ipfs/QmYWvdpdiyKwjVAqjhcFTBkiTUnc8rF4p2EGg3C4sTRsr6) +- **Resize:** Drag from an edge or corner +![Resize Fidget](https://space.mypinata.cloud/ipfs/bafybeifmssfizx5xjmmqyc6wwqxgs2xdhhbdtnvldtj276mfdul3eacmwu) +- **Stash in Fidget Tray:** Click a fidget then click ⇱ to save it for later. +![image](https://space.mypinata.cloud/ipfs/bafkreigy7ymnuwertvr6bn4bmwfe3vu3vvw4r6ekkzv6prs4zwlibsjtya) +- **Delete:** Click a fidget then click X it to delete it forever. +![image](https://space.mypinata.cloud/ipfs/bafkreieucvjlovm7wq5ftcvvvcv52sujnqqjwji5epoigb2ycumhmgrtfy) + +### Customize Theme +- **Templates:** Select a pre-made Theme. Then, customize it further to make it your own. +- **Style:** Set a background color for the Tab, or set the default styles for all Fidgets on the Tab. +- **Fonts:** Set the default header and body fonts for Fidgets on the Tab. +- **Code:** Add HTML/CSS to fully customize the Tab's background, or generate a custom background with a prompt. + +![Edit Theme2](https://space.mypinata.cloud/ipfs/bafybeietizt4vgyaiv62ytn25ztanusjmbcw6iwm3v2gedcpipr6koxh3e) + +### Customize Music +Add a soundtrack to each Tab. Search for or paste the link to any song or playlist on YouTube, or select a music NFT. + +![customize music](https://space.mypinata.cloud/ipfs/bafkreigtslrp3kjj42gp25bxd5nubs47qx22ivj3pkm3tc7j3fbqt3b5hy) + +### Homebase vs. Space +**Your Space** is your public profile that everyone can see. +**Your Homebase** is a private dashboard that only you can see. + +You can use the same tricks and Fidgets to customize them both. Use your **Homebase** to access the content, communities, and functionality you love, and use your **Space** to share the content and functionality you love with your friends. + +### Questions or feedback? + +Tag [@nounspacetom](https://nounspace.com/s/nounspacetom) in a cast or join our [Discord](https://discord.gg/H8EYnEmj6q). + +### Happy customizing! +`; +const onboardingFidgetID = "text:onboarding"; +const onboardingFidgetConfig = { + config: { + editable: true, + settings: { + title: "", + text: tutorialText, + urlColor: "blue", + fontFamily: "Londrina Solid", + fontColor: "#073b4c", + headingsFontFamily: "Londrina Solid", + headingsFontColor: "#2563ea", + backgroundColor: "#06d6a0", + borderColor: "#ffd166", + }, + data: {}, + }, + fidgetType: "text", + id: onboardingFidgetID, +}; + +const layoutID = ""; +const INITIAL_HOMEBASE_CONFIG: SpaceConfig = { + layoutID, + layoutDetails: { + layoutConfig: { + layout: [ + // Existing layouts can go here, e.g., feed, profile, etc. + { + w: 6, + h: 7, + x: 0, + y: 0, + i: onboardingFidgetID, + moved: false, + static: false, + }, + ], + }, + layoutFidget: "grid", + }, + theme: DEFAULT_THEME, + fidgetInstanceDatums: { + [onboardingFidgetID]: onboardingFidgetConfig, + }, + isEditable: false, + fidgetTrayContents: [], +}; + +export default INITIAL_HOMEBASE_CONFIG; \ No newline at end of file diff --git a/src/config/clanker/initialSpaces/initialProfileSpace.ts b/src/config/clanker/initialSpaces/initialProfileSpace.ts index 7453842dc..2c172fa01 100644 --- a/src/config/clanker/initialSpaces/initialProfileSpace.ts +++ b/src/config/clanker/initialSpaces/initialProfileSpace.ts @@ -1,127 +1,83 @@ import { SpaceConfig } from "@/app/(spaces)/Space"; +import { FeedType, FilterType } from "@neynar/nodejs-sdk/build/api"; import { cloneDeep } from "lodash"; import { getLayoutConfig } from "@/common/utils/layoutFormatUtils"; import { INITIAL_SPACE_CONFIG_EMPTY } from "../../initialSpaceConfig"; +// Set default tabNames for profile spaces +const INITIAL_PROFILE_SPACE_CONFIG = cloneDeep(INITIAL_SPACE_CONFIG_EMPTY); +INITIAL_PROFILE_SPACE_CONFIG.tabNames = ["Profile"]; + const createInitialProfileSpaceConfigForFid = ( - fid: number, + fid: number, + username?: string, ): Omit => { - const config = cloneDeep(INITIAL_SPACE_CONFIG_EMPTY); - - config.theme = { - id: "clanker-profile-theme", - name: "Clanker Profile Theme", - properties: { - font: "Inter, system-ui, sans-serif", - fontColor: "#ffffff", - headingsFont: "Inter, system-ui, sans-serif", - headingsFontColor: "#00d4ff", - background: "linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)", - backgroundHTML: "", - musicURL: "", - fidgetBackground: "#00d4ff", - fidgetBorderWidth: "1px", - fidgetBorderColor: "#00d4ff", - fidgetShadow: "0 4px 20px rgba(0, 212, 255, 0.2)", - fidgetBorderRadius: "12px", - gridSpacing: "16", - }, - }; - - config.fidgetInstanceDatums = { - profile: { - id: "profile", - fidgetType: "profile", - config: { - data: { fid }, - editable: true, - settings: { - showAvatar: true, - showUsername: true, - showStats: true, - }, - }, - }, - portfolio: { - id: "portfolio", - fidgetType: "Portfolio", - config: { - data: {}, - editable: true, - settings: { - showBalance: true, - showValue: true, - showChange: true, - }, - }, - }, - feed: { - id: "feed", - fidgetType: "feed", - config: { - data: {}, - editable: true, - settings: { - showUserCasts: true, - showTokenDiscussions: true, - maxCasts: 20, - }, - }, - }, - "market-data": { - id: "market-data", - fidgetType: "Market", - config: { - data: {}, - editable: true, - settings: { - showUserTokens: true, - showPriceChanges: true, - }, - }, - }, - gallery: { - id: "gallery", - fidgetType: "gallery", - config: { - data: {}, - editable: true, - settings: { - showUserImages: true, - showTokenImages: true, - maxImages: 15, - }, - }, - }, - links: { - id: "links", - fidgetType: "links", - config: { - data: {}, - editable: true, - settings: { - showUserLinks: true, - showTokenLinks: true, - }, - }, - }, - }; - - const layoutItems = [ - { w: 6, h: 4, x: 0, y: 0, i: "profile" }, - { w: 6, h: 4, x: 6, y: 0, i: "portfolio" }, - { w: 8, h: 4, x: 0, y: 4, i: "feed" }, - { w: 4, h: 4, x: 8, y: 4, i: "market-data" }, - { w: 6, h: 3, x: 0, y: 8, i: "gallery" }, - { w: 6, h: 3, x: 6, y: 8, i: "links" }, - ]; - - const layoutConfig = getLayoutConfig(config.layoutDetails); - layoutConfig.layout = layoutItems; - - config.tabNames = ["Profile"]; + const config = cloneDeep(INITIAL_PROFILE_SPACE_CONFIG); + config.fidgetInstanceDatums = { + "feed:profile": { + config: { + editable: false, + settings: { + feedType: FeedType.Filter, + users: fid, + filterType: FilterType.Fids, + }, + data: {}, + }, + fidgetType: "feed", + id: "feed:profile", + }, + "Portfolio:cd627e89-d661-4255-8c4c-2242a950e93e": { + config: { + editable: false, + settings: { + trackType: "farcaster", + farcasterUsername: username ?? "", + walletAddresses: "", + }, + data: {}, + }, + fidgetType: "Portfolio", + id: "Portfolio:cd627e89-d661-4255-8c4c-2242a950e93e", + }, + }; + const layoutItems = [ + { + w: 6, + h: 8, + x: 0, + y: 0, + i: "feed:profile", + minW: 4, + maxW: 36, + minH: 6, + maxH: 36, + moved: false, + static: false, + }, + { + w: 6, + h: 8, + x: 7, + y: 0, + i: "Portfolio:cd627e89-d661-4255-8c4c-2242a950e93e", + minW: 3, + maxW: 36, + minH: 3, + maxH: 36, + moved: false, + static: false, + } + ]; - return config; + // Set the layout configuration + const layoutConfig = getLayoutConfig(config.layoutDetails); + layoutConfig.layout = layoutItems; + + // Set default tab names + config.tabNames = ["Profile"]; + + return config; }; export default createInitialProfileSpaceConfigForFid; diff --git a/src/config/clanker/initialSpaces/initialProposalSpace.ts b/src/config/clanker/initialSpaces/initialProposalSpace.ts index 7813b3d8a..e555345a0 100644 --- a/src/config/clanker/initialSpaces/initialProposalSpace.ts +++ b/src/config/clanker/initialSpaces/initialProposalSpace.ts @@ -1,113 +1,219 @@ import { SpaceConfig } from "@/app/(spaces)/Space"; import { cloneDeep } from "lodash"; -import { getLayoutConfig } from "@/common/utils/layoutFormatUtils"; import { INITIAL_SPACE_CONFIG_EMPTY } from "../../initialSpaceConfig"; +import { Address } from "viem"; -const createInitialProposalSpaceConfigForProposalId = ( - proposalId: string, +export const createInitalProposalSpaceConfigForProposalId = ( + proposalId: string, + proposerAddress: Address ): Omit => { - const config = cloneDeep(INITIAL_SPACE_CONFIG_EMPTY); + const config = cloneDeep(INITIAL_SPACE_CONFIG_EMPTY); - config.theme = { - id: "clanker-proposal-theme", - name: "Clanker Proposal Theme", - properties: { - font: "Inter, system-ui, sans-serif", - fontColor: "#ffffff", - headingsFont: "Inter, system-ui, sans-serif", - headingsFontColor: "#ff6b6b", - background: - "linear-gradient(135deg, #ff6b6b 0%, #4ecdc4 50%, #45b7d1 100%)", - backgroundHTML: "", - musicURL: "", - fidgetBackground: "#ff6b6b", - fidgetBorderWidth: "2px", - fidgetBorderColor: "#ff6b6b", - fidgetShadow: "0 6px 24px rgba(255, 107, 107, 0.3)", - fidgetBorderRadius: "16px", - gridSpacing: "20", - }, - }; + config.fidgetInstanceDatums = { + "iframe:2f0a1c7b-da0c-474c-ad30-59915d0096b1": { + config: { + editable: true, + data: {}, + settings: { + url: `https://www.nouns.camp/proposals/${proposalId}?tab=description`, + showOnMobile: true, + customMobileDisplayName: "Proposal", + background: "var(--user-theme-fidget-background)", + fidgetBorderWidth: "var(--user-theme-fidget-border-width)", + fidgetBorderColor: "var(--user-theme-fidget-border-color)", + fidgetShadow: "var(--user-theme-fidget-shadow)", + }, + }, + fidgetType: "iframe", + id: "iframe:2f0a1c7b-da0c-474c-ad30-59915d0096b1", + }, + "iframe:10e88b10-b999-4ddc-a577-bd0eeb6bc76d": { + config: { + editable: true, + data: {}, + settings: { + url: "https://euphonious-kulfi-5e5a30.netlify.app/?id=" + proposalId, + showOnMobile: true, + customMobileDisplayName: "TLDR", + background: "var(--user-theme-fidget-background)", + fidgetBorderWidth: "var(--user-theme-fidget-border-width)", + fidgetBorderColor: "var(--user-theme-fidget-border-color)", + fidgetShadow: "var(--user-theme-fidget-shadow)", + }, + }, + fidgetType: "iframe", + id: "iframe:10e88b10-b999-4ddc-a577-bd0eeb6bc76d", + }, + "iframe:1afc071b-ce6b-4527-9419-f2e057a9fb0a": { + config: { + editable: true, + data: {}, + settings: { + url: `https://www.nouns.camp/proposals/${proposalId}?tab=activity`, + showOnMobile: true, + customMobileDisplayName: "Activity", + background: "var(--user-theme-fidget-background)", + fidgetBorderWidth: "var(--user-theme-fidget-border-width)", + fidgetBorderColor: "var(--user-theme-fidget-border-color)", + fidgetShadow: "var(--user-theme-fidget-shadow)", + }, + }, + fidgetType: "iframe", + id: "iframe:1afc071b-ce6b-4527-9419-f2e057a9fb0a", + }, + "iframe:ffb3cd56-3203-4b94-b842-adab9a7eabc9": { + config: { + editable: true, + data: {}, + settings: { + url: `https://chat-fidget.vercel.app/?room=prop%20${proposalId}%20chat&owner=${proposerAddress}`, + showOnMobile: true, + customMobileDisplayName: "Chat", + background: "var(--user-theme-fidget-background)", + fidgetBorderWidth: "var(--user-theme-fidget-border-width)", + fidgetBorderColor: "var(--user-theme-fidget-border-color)", + fidgetShadow: "var(--user-theme-fidget-shadow)", + }, + }, + fidgetType: "iframe", + id: "iframe:ffb3cd56-3203-4b94-b842-adab9a7eabc9", + }, + }; - config.fidgetInstanceDatums = { - text: { - id: "text", - fidgetType: "text", - config: { - data: { proposalId }, - editable: true, - settings: { - showProposalText: true, - showTitle: true, - showDescription: true, - }, - }, - }, - SnapShot: { - id: "SnapShot", - fidgetType: "SnapShot", - config: { - data: { proposalId }, - editable: true, - settings: { - showVoting: true, - showResults: true, - showVoteCount: true, - }, - }, - }, - feed: { - id: "feed", - fidgetType: "feed", - config: { - data: { proposalId }, - editable: true, - settings: { - showProposalDiscussions: true, - maxCasts: 15, - }, - }, - }, - links: { - id: "links", - fidgetType: "links", - config: { - data: { proposalId }, - editable: true, - settings: { - showProposalLinks: true, - showExternalVoting: true, - }, - }, - }, - gallery: { - id: "gallery", - fidgetType: "gallery", - config: { - data: { proposalId }, - editable: true, - settings: { - showProposalImages: true, - maxImages: 8, - }, - }, - }, - }; + config.layoutDetails = { + layoutConfig: { + layout: [ + { + w: 4, + h: 10, + x: 0, + y: 0, + i: "iframe:2f0a1c7b-da0c-474c-ad30-59915d0096b1", + minW: 2, + maxW: 36, + minH: 2, + maxH: 36, + moved: false, + static: false, + resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], + isBounded: false, + }, + { + w: 4, + h: 6, + x: 4, + y: 0, + i: "iframe:10e88b10-b999-4ddc-a577-bd0eeb6bc76d", + minW: 2, + maxW: 36, + minH: 2, + maxH: 36, + moved: false, + static: false, + resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], + }, + { + w: 4, + h: 10, + x: 8, + y: 0, + i: "iframe:1afc071b-ce6b-4527-9419-f2e057a9fb0a", + minW: 2, + maxW: 36, + minH: 2, + maxH: 36, + moved: false, + static: false, + resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], + isBounded: false, + }, + { + w: 4, + h: 4, + x: 4, + y: 6, + i: "iframe:ffb3cd56-3203-4b94-b842-adab9a7eabc9", + minW: 2, + maxW: 36, + minH: 2, + maxH: 36, + moved: false, + static: false, + resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], + isBounded: false, + }, + ], + }, + layoutFidget: "default-layout-fidget", // Assign a valid string value + }; - const layoutItems = [ - { w: 8, h: 4, x: 0, y: 0, i: "text" }, - { w: 4, h: 4, x: 8, y: 0, i: "SnapShot" }, - { w: 6, h: 4, x: 0, y: 4, i: "feed" }, - { w: 6, h: 4, x: 6, y: 4, i: "links" }, - { w: 12, h: 3, x: 0, y: 8, i: "gallery" }, - ]; + config.theme = { + id: "Homebase-Tab 10-Theme", + name: "Homebase-Tab 10-Theme", + properties: { + background: "#ffffff", + backgroundHTML: ` + + + + Nouns DAO Animated Background + + + +`, + fidgetBackground: "#ffffff", + fidgetBorderColor: "#eeeeee", + fidgetBorderWidth: "1px", + fidgetShadow: "none", + font: "Inter", + fontColor: "#000000", + headingsFont: "Londrina Solid", + headingsFontColor: "#000000", + musicURL: "https://www.youtube.com/watch?v=dMXlZ4y7OK4&t=1804", + fidgetBorderRadius: "12px", + gridSpacing: "16", + }, + }; - const layoutConfig = getLayoutConfig(config.layoutDetails); - layoutConfig.layout = layoutItems; - - config.tabNames = ["Proposal"]; - - return config; + return config; }; -export default createInitialProposalSpaceConfigForProposalId; +export default createInitalProposalSpaceConfigForProposalId; diff --git a/src/config/clanker/initialSpaces/initialTokenSpace.ts b/src/config/clanker/initialSpaces/initialTokenSpace.ts index 34586955b..c5073ffc7 100644 --- a/src/config/clanker/initialSpaces/initialTokenSpace.ts +++ b/src/config/clanker/initialSpaces/initialTokenSpace.ts @@ -1,151 +1,445 @@ import { SpaceConfig } from "@/app/(spaces)/Space"; import { cloneDeep } from "lodash"; -import { getLayoutConfig } from "@/common/utils/layoutFormatUtils"; import { INITIAL_SPACE_CONFIG_EMPTY } from "../../initialSpaceConfig"; +import { getNetworkWithId } from "@/common/lib/utils/networks"; +import { EtherScanChainName } from "../../../constants/etherscanChainIds"; +import { getGeckoUrl } from "@/common/lib/utils/links"; +import { Address } from "viem"; +import { getLayoutConfig } from "@/common/utils/layoutFormatUtils"; -const createInitialTokenSpaceConfigForAddress = ( - tokenAddress: string, +export const createInitialTokenSpaceConfigForAddress = ( + address: string, + castHash: string | null, + casterFid: string | null, + symbol: string, + isClankerToken: boolean, + network: EtherScanChainName = "base", ): Omit => { - const config = cloneDeep(INITIAL_SPACE_CONFIG_EMPTY); + const config = cloneDeep(INITIAL_SPACE_CONFIG_EMPTY); + + config.fidgetInstanceDatums = { + "Swap:f9e0259a-4524-4b37-a261-9f3be26d4af1": { + config: { + data: {}, + editable: true, + settings: { + defaultBuyToken: address, + defaultSellToken: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + fromChain: getNetworkWithId(network), + toChain: getNetworkWithId(network), + size: 0.8, + }, + }, + fidgetType: "Swap", + id: "Swap:f9e0259a-4524-4b37-a261-9f3be26d4af1", + }, + ...(isClankerToken && + castHash && + casterFid && + castHash !== "clank.fun deployment" && { + "cast:9c63b80e-bd46-4c8e-9e4e-c6facc41bf71": { + config: { + data: {}, + editable: true, + settings: { + background: "var(--user-theme-fidget-background)", + castHash: castHash, + casterFid: casterFid, + fidgetBorderColor: "var(--user-theme-fidget-border-color)", + fidgetBorderWidth: "var(--user-theme-fidget-border-width)", + fidgetShadow: "var(--user-theme-fidget-shadow)", + }, + }, + fidgetType: "cast", + id: "cast:9c63b80e-bd46-4c8e-9e4e-c6facc41bf71", + }, + }), + "feed:3de67742-56f2-402c-b751-7e769cdcfc56": { + config: { + data: {}, + editable: true, + settings: { + Xhandle: "thenounspace", + background: "var(--user-theme-fidget-background)", + feedType: "filter", + keyword: `$${symbol}`, + fidgetBorderColor: "var(--user-theme-fidget-border-color)", + fidgetBorderWidth: "var(--user-theme-fidget-border-width)", + fidgetShadow: "var(--user-theme-fidget-shadow)", + filterType: "keyword", + fontColor: "var(--user-theme-font-color)", + fontFamily: "var(--user-theme-font)", + }, + }, + fidgetType: "feed", + id: "feed:3de67742-56f2-402c-b751-7e769cdcfc56", + }, + "Market:733222fa-38f8-4343-9fa2-6646bb47dde0": { + config: { + data: {}, + editable: true, + settings: { + background: "var(--user-theme-fidget-background)", + fidgetBorderColor: "var(--user-theme-fidget-border-color)", + fidgetBorderWidth: "var(--user-theme-fidget-border-width)", + fidgetShadow: "var(--user-theme-fidget-shadow)", + chain: getNetworkWithId(network), + token: address, + dataSource: "geckoterminal", + theme: "light", + size: 1, + }, + }, + + fidgetType: "Market", + id: "Market:733222fa-38f8-4343-9fa2-6646bb47dde0", + }, + "links:5b4c8b73-416d-4842-9dc5-12fc186d8f57": { + config: { + data: {}, + editable: true, + settings: { + DescriptionColor: "black", + HeaderColor: "black", + background: "rgba(255, 255, 255, 0.5)", + css: "", + fidgetBorderColor: "var(--user-theme-fidget-border-color)", + fidgetBorderWidth: "var(--user-theme-fidget-border-width)", + fidgetShadow: "var(--user-theme-fidget-shadow)", + headingsFontFamily: "'__Inter_d65c78', '__Inter_Fallback_d65c78'", + itemBackground: "#e0eeff", + links: [ + ...(isClankerToken + ? [ + { + avatar: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAMAAABF0y+mAAAAM1BMVEX////q5PfUxe7o4fbMu+uPadTGs+jKueqJYNHEseiJYtKOZ9PJt+rUx+7MuuvPwOvm3vVnLuiEAAAAXUlEQVR4Ac3QAxaAQAAE0LXR/S+bXdNDWuOvSSGBMsYhCikVRG2MxejcFZoHcXoDQCF9gBiMURC1cfYzpDFSiEnKAHF6w4TuiMscs0bt+69JQyW8VyvkOVeH6p/QAF54BSckEkJ8AAAAAElFTkSuQmCC", + description: "", + text: "Clanker.world", + url: `https://www.clanker.world/clanker/${address}`, + }, + ] + : []), + { + avatar: + "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBwgHBgkIBwgKCgkLDRYPDQwMDRsUFRAWIB0iIiAdHx8kKDQsJCYxJx8fLT0tMTU3Ojo6Iys/RD84QzQ5OjcBCgoKDQwNGg8PGjclHyU3Nzc3Nzc3Ljc3Nzc3Nzg3Nzc3NzctKzc3Nzc3Nzc3Kzc3Nzc3Nzc3ODc3Nzc3Nys3N//AABEIABwAHAMBIgACEQEDEQH/xAAaAAABBQEAAAAAAAAAAAAAAAAHAgQFBggD/8QAKRAAAgEDAgQFBQAAAAAAAAAAAQIDAAURBDEGEiFREyJBccEHFKHR8P/EABgBAAIDAAAAAAAAAAAAAAAAAAIDAQQF/8QAHhEAAQQDAAMAAAAAAAAAAAAAAgABAxEEEiETQVH/2gAMAwEAAhEDEQA/ABZGperdZOBdRcYQ87SpI4ykMcfM2O5HxTbgPQQ6ozayXDNAwCIdgd8n4o4/TZJ5LNNrdVpmgaedhEHGHMa4AJ925j7YrekIYofI7WnuVMs/3zhi4WdpDPEJIY3MbTRMHVG6eV8E8jdR5WwagmBBxR6v3FMV5F5suj0/2ulaR4JplQBpzjDMD+O+3WgHqX8Od05geViMjY0stmBiNqtRfOrvYb3PaNWJYTlT0dDs47H9+lGvhLjKRLa7W9hNC4OI5D1gf+9NjvWeSxpxBrJ4VYRyFQRg4O4qvDliw6SNYoNr46vfGHE6xNJpdFJmRifFlXv6gfJoftJzMSaS7s5yxyaTScnLKcr9Ib+L/9k=", + description: "", + text: "GeckoTerminal", + url: getGeckoUrl(address as Address, network), + }, + { + avatar: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAEGElEQVR4AbWWA5AcXRSFX2ytbe/+DNccx6vYtm3bTjm2jUGyVg/CRmzb6dwXdO12LaYHt+oMu/s797zbQMYWy7JVMzIyYkaPHjstOTlln1SmKBCJpDfFYukNqUxe2D459cCwESOmnz17Nh62rY4sVXfu3LGZOHHiJIlUdsfG1v577Tr12PLUyMaOFUuk9ydPnjqNJEl7k8HQRZWVK1f2a9Y8/GmduvU5gLHC+zRr3uL50uXLB+P0BMFfvnzZqHvPXkegY8Fgvho2smW79ehx8vXr17bGRu4qkyl05oL5kkikV+7fv+9RYecyOQe3uKRyBTZhV+aa9+7d95CloXx179HrRKkzsWLF6t4wwd/NBSQmibW7d+9OP3DgSFuscRMmbIY5KDETK1asGFQCjgckPDzysSU63LNnT2f+9SMqKuZ98W2aNw9//ubNGwduo6lTp46rW6+BRSI+efJkMuJVTEzca/4pOmHSpKnc2sNwMEIgdvaObOs2bTPaJaccxFK0bFXQoKGN0QawRCLJPWBXQ3B5Dbe1cxC09smpaSdh50rFYq4sb9kqV4gB3ARUMBo9evQkoTF36dJ1Ix+SnJy6V4iB6OjYV2CgAUpLS99dTtTf/fwDPvv5B3729PT+CmtnEQNOzq7fVq9e3QnhgqteXmlwX1//L0eOHGkFLp2x4CLlDXfBU+YawMs9d+78idyfIrGk1AFMSEy6iXi1YMGigeYYcHRy+Y6n/8/8qAiyK0qCe3qpBhJENxCvli1b1s9UAz4+fp8XLFo0/A9cSVxvqSq6rkVSqVxnbQPDho2gt27dKvvzPeMyFaYirj9TEuRF1LZd8hFrG4Cunf58Vhbd8FIR1E2InwUTO9CAQYPmW9kAVxcIOgDAzC84SEtORAcOH5bBZdgiBmDC2UKtNrE0uJKgoyDyhxwcS8tE43jqRsfEPjfXwPLly6fjO2op8VeCqIcC/GNxOHy/m5+fXw3hGjhw8HIhBrp3774GGVFKLekGsMMYyJdaS07nNtTr9b4BgcEfStzXE0UM4tWuXbu6QNRnzp8/H1oe2GBgq8P6DgTQ89Lg0P3Ls3rGkXeRWTA8MCjko5u757d//v3//dix45aUFmd54N1wZwVwK1UReYkH5WtUWY9ljiAvUH0koHYbDNXVBNlLWXSdqAAM3V+/cPw4WQOZWziN8zqqsVpLLeFNdzmibmYS11xNhp7SauvAQeJVWnoexHzZOCh3zt/O0FJhRsM2wimiKSRDNFo6WU1Qs2FqVXCgN4KgHJzKO1fIeCIhhc9RiHYCOH8rHMrpA+w/L/POnVrI1FLnUu5woGWg1wLA7yC1DcpLlB+yVGWTZH2NlkmBjtbCwTNg6h/BHHwF2FeY/qdwtcuB/zapdXSnzEt3bJCR9QPwKOxl9MLyXAAAAABJRU5ErkJggg==", + description: "", + text: "BaseScan", + url: `https://basescan.org/address/${address}`, + }, + ], + title: `$${symbol} Links`, + viewMode: "list", + }, + }, + fidgetType: "links", + id: "links:5b4c8b73-416d-4842-9dc5-12fc186d8f57", + }, + "Chat:09528872-6659-460e-bb25-0c200cccb0ec": { + config: { + data: {}, + editable: true, + settings: { + background: "var(--user-theme-fidget-background)", + fidgetBorderColor: "var(--user-theme-fidget-border-color)", + fidgetBorderWidth: "var(--user-theme-fidget-border-width)", + fidgetShadow: "var(--user-theme-fidget-shadow)", + roomName: address, + }, + }, + fidgetType: "Chat", + id: "Chat:09528872-6659-460e-bb25-0c200cccb0ec", + }, + }; + + const newLayout = isClankerToken && + castHash && + casterFid && + castHash !== "clank.fun deployment" + ? [ + { + h: 6, + i: "Chat:09528872-6659-460e-bb25-0c200cccb0ec", + maxH: 36, + maxW: 36, + minH: 2, + minW: 2, + moved: false, + resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], + static: false, + w: 4, + x: 4, + y: 4, + }, + { + h: 8, + i: "feed:3de67742-56f2-402c-b751-7e769cdcfc56", + maxH: 36, + maxW: 36, + minH: 2, + minW: 4, + moved: false, + resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], + static: false, + w: 4, + x: 8, + y: 0, + }, + { + h: 5, + i: "Swap:f9e0259a-4524-4b37-a261-9f3be26d4af1", + maxH: 36, + maxW: 36, + minH: 3, + minW: 2, + moved: false, + resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], + static: false, + w: 4, + x: 0, + y: 5, + }, + { + h: 5, + i: "Market:733222fa-38f8-4343-9fa2-6646bb47dde0", + maxH: 36, + maxW: 36, + minH: 2, + minW: 2, + moved: false, + resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], + static: false, + w: 4, + x: 0, + y: 0, + }, + { + h: 2, + i: "links:5b4c8b73-416d-4842-9dc5-12fc186d8f57", + maxH: 36, + maxW: 36, + minH: 2, + minW: 2, + moved: false, + resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], + static: false, + w: 4, + x: 8, + y: 8, + }, + { + h: 4, + i: "cast:9c63b80e-bd46-4c8e-9e4e-c6facc41bf71", + maxH: 4, + maxW: 12, + minH: 1, + minW: 3, + moved: false, + resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], + static: false, + w: 4, + x: 4, + y: 0, + }, + ] + : [ + { + h: 8, + i: "Chat:09528872-6659-460e-bb25-0c200cccb0ec", + maxH: 36, + maxW: 36, + minH: 2, + minW: 2, + moved: false, + resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], + static: false, + w: 4, + x: 4, + y: 0, + }, + { + h: 2, + i: "links:5b4c8b73-416d-4842-9dc5-12fc186d8f57", + maxH: 36, + maxW: 36, + minH: 2, + minW: 2, + moved: false, + resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], + static: false, + w: 4, + x: 4, + y: 8, + }, + { + h: 10, + i: "feed:3de67742-56f2-402c-b751-7e769cdcfc56", + maxH: 36, + maxW: 36, + minH: 2, + minW: 4, + moved: false, + resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], + static: false, + w: 4, + x: 8, + y: 0, + }, + { + h: 5, + i: "Market:733222fa-38f8-4343-9fa2-6646bb47dde0", + maxH: 36, + maxW: 36, + minH: 2, + minW: 2, + moved: false, + resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], + static: false, + w: 4, + x: 0, + y: 0, + }, + { + h: 5, + i: "Swap:f9e0259a-4524-4b37-a261-9f3be26d4af1", + maxH: 36, + maxW: 36, + minH: 3, + minW: 2, + moved: false, + resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], + static: false, + w: 4, + x: 0, + y: 5, + }, + ]; + + // Set the layout configuration + const layoutConfig = getLayoutConfig(config.layoutDetails); + layoutConfig.layout = newLayout; + + config.theme = { + id: "default", + name: "Default", + properties: { + background: "#ffffff", + backgroundHTML: ` + + + + + + Aurora Borealis (Light Theme) + + + +
+
+ + + `, + fidgetBackground: "#ffffffb0", // equivalent to rgba(255, 255, 255, 0.69) + fidgetBorderColor: "#eeeeee", // equivalent to rgba(238, 238, 238, 1) + fidgetBorderWidth: "0", + fidgetShadow: "none", + font: "Inter", + fontColor: "#000000", + headingsFont: "Inter", + headingsFontColor: "#000000", + musicURL: "https://www.youtube.com/watch?v=dMXlZ4y7OK4&t=1804", + fidgetBorderRadius: "12px", + gridSpacing: "16", + }, + }; - return config; + return config; }; -export default createInitialTokenSpaceConfigForAddress; +export default createInitialTokenSpaceConfigForAddress; \ No newline at end of file diff --git a/src/config/index.ts b/src/config/index.ts index 49adc3192..a6ee9c58e 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -86,7 +86,7 @@ function resolveCommunity(): CommunityConfig { export const createInitialProfileSpaceConfigForFid = (fid: number, username?: string) => { switch (resolveCommunity()) { case 'clanker': - return clankerCreateInitialProfileSpaceConfigForFid(fid); + return clankerCreateInitialProfileSpaceConfigForFid(fid, username); case 'example': return exampleCreateInitialProfileSpaceConfigForFid(fid, username); case 'nouns': diff --git a/src/config/systemConfig.ts b/src/config/systemConfig.ts index cfc4cdcd3..95ea50f6f 100644 --- a/src/config/systemConfig.ts +++ b/src/config/systemConfig.ts @@ -136,7 +136,7 @@ export interface NavigationItem { id: string; label: string; href: string; - icon?: 'home' | 'explore' | 'notifications' | 'search' | 'space' | 'custom'; + icon?: 'home' | 'explore' | 'notifications' | 'search' | 'space' | 'robot' | 'custom'; openInNewTab?: boolean; requiresAuth?: boolean; } diff --git a/tests/clankerInitialSpaces.test.ts b/tests/clankerInitialSpaces.test.ts new file mode 100644 index 000000000..b9a874aa6 --- /dev/null +++ b/tests/clankerInitialSpaces.test.ts @@ -0,0 +1,28 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { createInitialProfileSpaceConfigForFid } from "@/config"; + +const PORTFOLIO_FIDGET_ID = "Portfolio:cd627e89-d661-4255-8c4c-2242a950e93e"; + +describe("clanker initial profile space config", () => { + const originalCommunity = process.env.NEXT_PUBLIC_COMMUNITY; + + beforeAll(() => { + process.env.NEXT_PUBLIC_COMMUNITY = "clanker"; + }); + + afterAll(() => { + process.env.NEXT_PUBLIC_COMMUNITY = originalCommunity; + }); + + it("passes the farcaster username to the Portfolio fidget settings", () => { + const username = "clankerfan"; + + const config = createInitialProfileSpaceConfigForFid(123, username); + const portfolioFidget = config.fidgetInstanceDatums?.[PORTFOLIO_FIDGET_ID]; + + expect(portfolioFidget).toBeDefined(); + expect( + portfolioFidget?.config?.settings?.farcasterUsername, + ).toBe(username); + }); +}); From 1d08bc9278f351adacaf421bdb682838b108fb88 Mon Sep 17 00:00:00 2001 From: Willy Ogorzaly Date: Thu, 13 Nov 2025 15:19:23 -0600 Subject: [PATCH 071/155] Add owner address handling to chat fidget (#1537) --- .../t/[network]/[contractAddress]/utils.ts | 17 +++--- .../organisms/FidgetSettingsEditor.tsx | 13 +++- src/common/fidgets/index.d.ts | 1 + .../initialSpaces/initialProposalSpace.ts | 17 +++--- .../nouns/initialSpaces/initialTokenSpace.ts | 2 + src/fidgets/ui/chat.tsx | 59 ++++++++++++++++++- 6 files changed, 90 insertions(+), 19 deletions(-) diff --git a/src/app/(spaces)/t/[network]/[contractAddress]/utils.ts b/src/app/(spaces)/t/[network]/[contractAddress]/utils.ts index 21b708081..eae8f72ce 100644 --- a/src/app/(spaces)/t/[network]/[contractAddress]/utils.ts +++ b/src/app/(spaces)/t/[network]/[contractAddress]/utils.ts @@ -222,7 +222,12 @@ export const loadTokenSpacePageData = async ( const castHash = tokenData?.clankerData?.cast_hash || ""; const casterFid = String(tokenData?.clankerData?.requestor_fid || ""); const isClankerToken = !!tokenData?.clankerData; - + + const spaceOwnerAddress = + finalOwnerType === 'address' && finalOwnerId + ? finalOwnerId as Address + : "0x0000000000000000000000000000000000000000" as Address; + // Create space config const config = { ...createInitialTokenSpaceConfigForAddress( @@ -231,17 +236,15 @@ export const loadTokenSpacePageData = async ( casterFid, symbol, isClankerToken, - network as EtherScanChainName + network as EtherScanChainName, + spaceOwnerAddress ), timestamp: new Date().toISOString(), }; - + // Convert ownerId to the appropriate type based on ownerIdType const spaceOwnerFid = finalOwnerType === 'fid' ? Number(finalOwnerId) : undefined; - const spaceOwnerAddress = finalOwnerType === 'address' && finalOwnerId ? - finalOwnerId as Address : - "0x0000000000000000000000000000000000000000" as Address; - + return { spaceId: internalData.spaceId, spaceName, diff --git a/src/common/components/organisms/FidgetSettingsEditor.tsx b/src/common/components/organisms/FidgetSettingsEditor.tsx index dcd0e2068..fb4f810a6 100644 --- a/src/common/components/organisms/FidgetSettingsEditor.tsx +++ b/src/common/components/organisms/FidgetSettingsEditor.tsx @@ -68,6 +68,12 @@ export const FidgetSettingsRow: React.FC = ({ }) => { const InputComponent = field.inputSelector; const isValid = !field.validator || field.validator(value); + const errorMessage = + !isValid && field.errorMessage + ? typeof field.errorMessage === "function" + ? field.errorMessage(value) + : field.errorMessage + : undefined; return (
= ({ )}
-
+
= ({ !isValid && "border-red-500" )} /> + {errorMessage && ( +

+ {errorMessage} +

+ )}
); diff --git a/src/common/fidgets/index.d.ts b/src/common/fidgets/index.d.ts index 1b72d7c60..9d1230063 100644 --- a/src/common/fidgets/index.d.ts +++ b/src/common/fidgets/index.d.ts @@ -51,6 +51,7 @@ export type FidgetFieldConfig = { readonly displayName?: string; readonly displayNameHint?: string; readonly validator?: (value) => boolean; + readonly errorMessage?: string | ((value: any) => string); readonly inputSelector: | typeof TextInput | typeof ColorSelector diff --git a/src/config/nouns/initialSpaces/initialProposalSpace.ts b/src/config/nouns/initialSpaces/initialProposalSpace.ts index e555345a0..38a507aa5 100644 --- a/src/config/nouns/initialSpaces/initialProposalSpace.ts +++ b/src/config/nouns/initialSpaces/initialProposalSpace.ts @@ -61,22 +61,23 @@ export const createInitalProposalSpaceConfigForProposalId = ( fidgetType: "iframe", id: "iframe:1afc071b-ce6b-4527-9419-f2e057a9fb0a", }, - "iframe:ffb3cd56-3203-4b94-b842-adab9a7eabc9": { + "Chat:ffb3cd56-3203-4b94-b842-adab9a7eabc9": { config: { editable: true, data: {}, settings: { - url: `https://chat-fidget.vercel.app/?room=prop%20${proposalId}%20chat&owner=${proposerAddress}`, - showOnMobile: true, - customMobileDisplayName: "Chat", background: "var(--user-theme-fidget-background)", - fidgetBorderWidth: "var(--user-theme-fidget-border-width)", + customMobileDisplayName: "Chat", fidgetBorderColor: "var(--user-theme-fidget-border-color)", + fidgetBorderWidth: "var(--user-theme-fidget-border-width)", fidgetShadow: "var(--user-theme-fidget-shadow)", + roomName: `prop ${proposalId} chat`, + roomOwnerAddress: proposerAddress, + showOnMobile: true, }, }, - fidgetType: "iframe", - id: "iframe:ffb3cd56-3203-4b94-b842-adab9a7eabc9", + fidgetType: "Chat", + id: "Chat:ffb3cd56-3203-4b94-b842-adab9a7eabc9", }, }; @@ -132,7 +133,7 @@ export const createInitalProposalSpaceConfigForProposalId = ( h: 4, x: 4, y: 6, - i: "iframe:ffb3cd56-3203-4b94-b842-adab9a7eabc9", + i: "Chat:ffb3cd56-3203-4b94-b842-adab9a7eabc9", minW: 2, maxW: 36, minH: 2, diff --git a/src/config/nouns/initialSpaces/initialTokenSpace.ts b/src/config/nouns/initialSpaces/initialTokenSpace.ts index c5073ffc7..eaa096789 100644 --- a/src/config/nouns/initialSpaces/initialTokenSpace.ts +++ b/src/config/nouns/initialSpaces/initialTokenSpace.ts @@ -14,6 +14,7 @@ export const createInitialTokenSpaceConfigForAddress = ( symbol: string, isClankerToken: boolean, network: EtherScanChainName = "base", + ownerAddress?: Address, ): Omit => { const config = cloneDeep(INITIAL_SPACE_CONFIG_EMPTY); @@ -152,6 +153,7 @@ export const createInitialTokenSpaceConfigForAddress = ( fidgetBorderWidth: "var(--user-theme-fidget-border-width)", fidgetShadow: "var(--user-theme-fidget-shadow)", roomName: address, + roomOwnerAddress: ownerAddress ?? "", }, }, fidgetType: "Chat", diff --git a/src/fidgets/ui/chat.tsx b/src/fidgets/ui/chat.tsx index 5e1ff5a71..59b2ef6f1 100644 --- a/src/fidgets/ui/chat.tsx +++ b/src/fidgets/ui/chat.tsx @@ -8,12 +8,38 @@ import { } from "@/common/fidgets"; import { defaultStyleFields, ErrorWrapper, WithMargin } from "@/fidgets/helpers"; import { BsChatDots, BsChatDotsFill } from "react-icons/bs"; +import { isAddress } from "viem"; export type ChatFidgetSettings = { roomName: string; + roomOwnerAddress?: string; size: number; } & FidgetSettingsStyle; +const isValidEthereumAddress = (value: unknown): boolean => { + if (typeof value !== "string") { + return !value; + } + + const trimmed = value.trim(); + + if (trimmed.length === 0) { + return true; + } + + return isAddress(trimmed); +}; + +const getOwnerAddressParam = (value?: string): string | undefined => { + if (!value) { + return undefined; + } + + const trimmed = value.trim(); + + return isAddress(trimmed) ? trimmed : undefined; +}; + const frameConfig: FidgetProperties = { fidgetName: "Chat", icon: 0x1f4ac, // 💬 @@ -33,6 +59,22 @@ const frameConfig: FidgetProperties = { ), group: "settings", }, + { + fieldName: "roomOwnerAddress", + displayName: "Room Owner Address", + displayNameHint: + "When creating a new room, set the room owner by inputting the Ethereum address of the wallet they'll use to update the room. Room owners can update the room's avatar and token gate settings.", + default: "", + validator: isValidEthereumAddress, + errorMessage: "Owner must be an Ethereum address", + required: false, + inputSelector: (props) => ( + + + + ), + group: "settings", + }, ...defaultStyleFields, ], size: { @@ -46,7 +88,10 @@ const frameConfig: FidgetProperties = { const Chat: React.FC< FidgetArgs & { inEditMode: boolean } > = ({ - settings: { roomName = "0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab" }, + settings: { + roomName = "0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab", + roomOwnerAddress, + }, }) => { // console.log("Room name:", roomName); @@ -59,12 +104,20 @@ const Chat: React.FC< ); } - const url = `https://chat-fidget.vercel.app/?room=${roomName}`; + const ownerAddress = getOwnerAddressParam(roomOwnerAddress); + const chatUrl = new URL("https://chat-fidget.vercel.app/"); + chatUrl.searchParams.set("room", roomName); + + if (ownerAddress) { + chatUrl.searchParams.set("owner", ownerAddress); + } + + const url = chatUrl.toString(); return (
+
+ ); + } if (isLoading) { return ( From 47a9fcf71eacc4f448409bac28c505c187e3960b Mon Sep 17 00:00:00 2001 From: Jesse Paterson Date: Mon, 17 Nov 2025 12:01:53 -0300 Subject: [PATCH 075/155] opengraph fix revert --- .../components/Embeds/OpenGraphEmbed.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/fidgets/farcaster/components/Embeds/OpenGraphEmbed.tsx b/src/fidgets/farcaster/components/Embeds/OpenGraphEmbed.tsx index 2571b779d..dcce92e44 100644 --- a/src/fidgets/farcaster/components/Embeds/OpenGraphEmbed.tsx +++ b/src/fidgets/farcaster/components/Embeds/OpenGraphEmbed.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; import Image from "next/image"; -import { getYouTubeId } from "@/common/lib/utils/youtubeUtils"; +import { getYouTubeId, isYouTubeUrl } from "@/common/lib/utils/youtubeUtils"; interface OpenGraphEmbedProps { url: string; @@ -19,13 +19,8 @@ const OpenGraphEmbed: React.FC = ({ url }) => { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - // Extract YouTube ID once and check if it's a YouTube URL - const youtubeId = getYouTubeId(url); - const isYouTube = youtubeId !== null; - useEffect(() => { - // Skip OpenGraph fetch for YouTube URLs since we render the embed directly - if (isYouTube) { + if (isYouTubeUrl(url)) { setIsLoading(false); return; } @@ -47,7 +42,9 @@ const OpenGraphEmbed: React.FC = ({ url }) => { } }; fetchOGData(); - }, [url, isYouTube]); + }, [url]); + + const youtubeId = getYouTubeId(url); if (youtubeId) { return (
@@ -125,4 +122,4 @@ const OpenGraphEmbed: React.FC = ({ url }) => { ); }; -export default OpenGraphEmbed; +export default OpenGraphEmbed; \ No newline at end of file From d267c094dc57906c9f49424ed3e7a91dd62ee92f Mon Sep 17 00:00:00 2001 From: Jhonattan2121 Date: Sun, 16 Nov 2025 18:31:16 -0300 Subject: [PATCH 076/155] feat(zora): normalize Zora URLs to /coin and improve embed UI --- .../farcaster/components/Embeds/ZoraEmbed.tsx | 155 ++++++++++++++++++ .../farcaster/components/Embeds/index.tsx | 3 + .../farcaster/components/Embeds/zoraUtils.ts | 77 +++++++++ src/fidgets/zora/zoraEmbed.tsx | 2 +- 4 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 src/fidgets/farcaster/components/Embeds/ZoraEmbed.tsx create mode 100644 src/fidgets/farcaster/components/Embeds/zoraUtils.ts diff --git a/src/fidgets/farcaster/components/Embeds/ZoraEmbed.tsx b/src/fidgets/farcaster/components/Embeds/ZoraEmbed.tsx new file mode 100644 index 000000000..13931764b --- /dev/null +++ b/src/fidgets/farcaster/components/Embeds/ZoraEmbed.tsx @@ -0,0 +1,155 @@ +import React, { useEffect, useState } from "react"; +import { parseZoraUrl } from "./zoraUtils"; + +interface ZoraEmbedProps { + url: string; +} + +type OG = { + title?: string | null; + description?: string | null; + image?: string | null; + siteName?: string | null; + url?: string | null; +}; + +const ZoraEmbed: React.FC = ({ url }) => { + const [og, setOg] = useState(null); + const [loading, setLoading] = useState(true); + const parsed = parseZoraUrl(url); + + useEffect(() => { + let cancelled = false; + const fetchOG = async () => { + if (!parsed) { + setLoading(false); + return; + } + + // Only call opengraph for https pages + if (!parsed.pageUrl.startsWith("https://")) { + setLoading(false); + return; + } + + try { + setLoading(true); + const resp = await fetch(`/api/opengraph?url=${encodeURIComponent(parsed.pageUrl)}`); + if (!resp.ok) throw new Error("open graph fetch failed"); + const data = await resp.json(); + if (!cancelled) setOg(data); + } catch (e) { + // ignore + } finally { + if (!cancelled) setLoading(false); + } + }; + fetchOG(); + return () => { + cancelled = true; + }; + }, [url]); + + // Build trade link - MVP: point to the page URL discovered + const tradeUrl = parsed?.pageUrl ?? url; + + if (loading) { + return ( +
+
+
+
+
+
+ ); + } + + if (og && (og.image || og.title)) { + const siteLabel = og.siteName || (() => { + try { return new URL(tradeUrl).hostname; } catch { return "zora.co"; } + })(); + + // Try to mimic Zora's card: subtle gray gradient header with inset preview that looks like a framed post + return ( +
+
+ {/* Header / preview area (gradient) */} + + + {/* Footer: title + actions */} +
+
+
+ {og.title &&
{og.title}
} + {og.description &&
{og.description}
} +
+ + +
+ +
{siteLabel}
+
+
+
+ ); + } + + // Fallback + return ( +
+
+
+
Zora
+ {parsed?.contract && ( +
{parsed.contract}{parsed?.tokenId ? ` • #${parsed.tokenId}` : ''}
+ )} +
+ + +
+
+ ); +}; + +export default ZoraEmbed; diff --git a/src/fidgets/farcaster/components/Embeds/index.tsx b/src/fidgets/farcaster/components/Embeds/index.tsx index f8a5b90e4..01ab664e5 100644 --- a/src/fidgets/farcaster/components/Embeds/index.tsx +++ b/src/fidgets/farcaster/components/Embeds/index.tsx @@ -7,6 +7,7 @@ import ParagraphXyzEmbed from "./ParagraphXyzEmbed"; import VideoEmbed from "./VideoEmbed"; import ImageEmbed from "./ImageEmbed"; import SmartFrameEmbed from "./SmartFrameEmbed"; +import ZoraEmbed from "./ZoraEmbed"; import { isImageUrl, isVideoUrl } from "@/common/lib/utils/urls"; import CreateCastImage from "./createCastImage"; @@ -59,6 +60,8 @@ export const renderEmbedForUrl = ( return tweetId ? : null; } else if (url.startsWith("https://nouns.build")) { return ; + } else if (url.includes("zora.co") || url.startsWith("zoraCoin:") || url.includes("/coin/")) { + return ; } else if (url.includes("paragraph.xyz") || url.includes("pgrph.xyz")) { return ; } else if (!isImageUrl(url)) { diff --git a/src/fidgets/farcaster/components/Embeds/zoraUtils.ts b/src/fidgets/farcaster/components/Embeds/zoraUtils.ts new file mode 100644 index 000000000..570774259 --- /dev/null +++ b/src/fidgets/farcaster/components/Embeds/zoraUtils.ts @@ -0,0 +1,77 @@ +export const parseZoraUrl = (input: string): { pageUrl: string; contract?: string; tokenId?: string } | null => { + try { + // Handle zoraCoin scheme: zoraCoin://[/] + if (input.startsWith("zoraCoin:")) { + const after = input.replace(/^zoraCoin:\/\//, ""); + const parts = after.split("/").filter(Boolean); + const contract = parts[0]; + const tokenId = parts[1]; + // Normalize all zoraCoin scheme URLs to use the /coin/ namespace. + const contractNormalized = contract ? (contract.includes(":") ? contract : contract) : undefined; + const pageUrl = contractNormalized + ? tokenId + ? `https://zora.co/coin/${contractNormalized}/${tokenId}` + : `https://zora.co/coin/${contractNormalized}` + : input; + return { pageUrl, contract: contractNormalized, tokenId }; + } + + const u = new URL(input); + const hostname = u.hostname.toLowerCase(); + + // Accept zora.co host variants + if (hostname.includes("zora.co")) { + const decodedPath = decodeURIComponent(u.pathname || ""); + + // coin pattern + const coinMatch = decodedPath.match(/\/?coin\/((?:[a-zA-Z0-9_-]+:)?0x[a-fA-F0-9]{40})(?:\/(\d+))?/); + if (coinMatch) { + const contract = coinMatch[1]; + const tokenId = coinMatch[2]; + const pageUrl = tokenId ? `https://zora.co/coin/${contract}/${tokenId}` : `https://zora.co/coin/${contract}`; + return { pageUrl, contract, tokenId }; + } + + // tokens pattern (leave as tokens) + const tokensMatch = decodedPath.match(/\/?tokens\/(0x[a-fA-F0-9]{40})(?:\/(\d+))?/); + if (tokensMatch) { + const contract = tokensMatch[1]; + const tokenId = tokensMatch[2]; + const pageUrl = tokenId ? `https://zora.co/tokens/${contract}/${tokenId}` : `https://zora.co/tokens/${contract}`; + return { pageUrl, contract, tokenId }; + } + + // collect patterns + const collectPattern1 = /\/?collect\/(?:([^/]+):)?(0x[a-fA-F0-9]{40})(?:\/(\d+))?/; + const collectPattern2 = /\/?collect\/(0x[a-fA-F0-9]{40})(?:\/(\d+))?/; + const collectMatch = decodedPath.match(collectPattern1) || decodedPath.match(collectPattern2); + if (collectMatch) { + let contract: string | undefined; + let tokenId: string | undefined; + if (collectMatch[2] && collectMatch[2].startsWith("0x")) { + contract = collectMatch[2]; + tokenId = collectMatch[3]; + } else if (collectMatch[1] && collectMatch[1].startsWith("0x")) { + contract = collectMatch[1]; + tokenId = collectMatch[2]; + } + // Normalize to coin/. If the collect path used a namespace (e.g. base:0x...) + const contractNormalized = contract ? (contract.includes(":") ? contract : contract) : undefined; + const pageUrl = contractNormalized + ? tokenId + ? `https://zora.co/coin/${contractNormalized}/${tokenId}` + : `https://zora.co/coin/${contractNormalized}` + : input; + return { pageUrl, contract: contractNormalized, tokenId }; + } + + return { pageUrl: input }; + } + + return null; + } catch (e) { + return null; + } +}; + +export default parseZoraUrl; diff --git a/src/fidgets/zora/zoraEmbed.tsx b/src/fidgets/zora/zoraEmbed.tsx index 62b4dad86..d2c25c49a 100644 --- a/src/fidgets/zora/zoraEmbed.tsx +++ b/src/fidgets/zora/zoraEmbed.tsx @@ -24,7 +24,7 @@ export const ZoraProperties: FidgetProperties = { { fieldName: "text", default: - "https://zora.co/collect/zora:0x460779723619a8e25632bce2e74b6b9ce4915c7b/4", + "https://zora.co/coin/0x460779723619a8e25632bce2e74b6b9ce4915c7b/4", required: true, inputSelector: TextInput, group: "settings", From 4c7fac467aa1fc529e5db87d1685c9012f1c64ca Mon Sep 17 00:00:00 2001 From: Jhonattan2121 Date: Sun, 16 Nov 2025 18:57:29 -0300 Subject: [PATCH 077/155] chore(zora): apply review fixes (router match, parser cleanup, accessibility, styles) --- .../farcaster/components/Embeds/ZoraEmbed.tsx | 13 +++++++------ src/fidgets/farcaster/components/Embeds/index.tsx | 2 +- .../farcaster/components/Embeds/zoraUtils.ts | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/fidgets/farcaster/components/Embeds/ZoraEmbed.tsx b/src/fidgets/farcaster/components/Embeds/ZoraEmbed.tsx index 13931764b..bd20208d1 100644 --- a/src/fidgets/farcaster/components/Embeds/ZoraEmbed.tsx +++ b/src/fidgets/farcaster/components/Embeds/ZoraEmbed.tsx @@ -48,7 +48,7 @@ const ZoraEmbed: React.FC = ({ url }) => { return () => { cancelled = true; }; - }, [url]); + }, [url, parsed?.pageUrl]); // Build trade link - MVP: point to the page URL discovered const tradeUrl = parsed?.pageUrl ?? url; @@ -77,10 +77,10 @@ const ZoraEmbed: React.FC = ({ url }) => {
{/* Inner rounded preview card (lighter) */} - - @@ -143,6 +143,7 @@ const ZoraEmbed: React.FC = ({ url }) => { target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-2 px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-lg" + aria-label={`View ${parsed?.contract ? `contract ${parsed.contract}${parsed?.tokenId ? ` token #${parsed.tokenId}` : ''}` : 'on Zora'}`} > View diff --git a/src/fidgets/farcaster/components/Embeds/index.tsx b/src/fidgets/farcaster/components/Embeds/index.tsx index 01ab664e5..4e671c1e1 100644 --- a/src/fidgets/farcaster/components/Embeds/index.tsx +++ b/src/fidgets/farcaster/components/Embeds/index.tsx @@ -60,7 +60,7 @@ export const renderEmbedForUrl = ( return tweetId ? : null; } else if (url.startsWith("https://nouns.build")) { return ; - } else if (url.includes("zora.co") || url.startsWith("zoraCoin:") || url.includes("/coin/")) { + } else if (url.includes("zora.co") || url.startsWith("zoraCoin:")) { return ; } else if (url.includes("paragraph.xyz") || url.includes("pgrph.xyz")) { return ; diff --git a/src/fidgets/farcaster/components/Embeds/zoraUtils.ts b/src/fidgets/farcaster/components/Embeds/zoraUtils.ts index 570774259..78a9c68cf 100644 --- a/src/fidgets/farcaster/components/Embeds/zoraUtils.ts +++ b/src/fidgets/farcaster/components/Embeds/zoraUtils.ts @@ -7,7 +7,7 @@ export const parseZoraUrl = (input: string): { pageUrl: string; contract?: strin const contract = parts[0]; const tokenId = parts[1]; // Normalize all zoraCoin scheme URLs to use the /coin/ namespace. - const contractNormalized = contract ? (contract.includes(":") ? contract : contract) : undefined; + const contractNormalized = contract ? contract : undefined; const pageUrl = contractNormalized ? tokenId ? `https://zora.co/coin/${contractNormalized}/${tokenId}` @@ -56,7 +56,7 @@ export const parseZoraUrl = (input: string): { pageUrl: string; contract?: strin tokenId = collectMatch[2]; } // Normalize to coin/. If the collect path used a namespace (e.g. base:0x...) - const contractNormalized = contract ? (contract.includes(":") ? contract : contract) : undefined; + const contractNormalized = contract ? contract : undefined; const pageUrl = contractNormalized ? tokenId ? `https://zora.co/coin/${contractNormalized}/${tokenId}` From cf911ddd36fd1ca7e31b513d6a77bbada451ce3d Mon Sep 17 00:00:00 2001 From: Willy Ogorzaly Date: Thu, 6 Nov 2025 14:40:00 -0600 Subject: [PATCH 078/155] fix: avoid unnecessary reloads for iframe embeds --- src/fidgets/ui/IFrame.tsx | 201 ++++++++++++++++++++++++++------------ 1 file changed, 136 insertions(+), 65 deletions(-) diff --git a/src/fidgets/ui/IFrame.tsx b/src/fidgets/ui/IFrame.tsx index 8efb6d88c..2dbf3ae1e 100644 --- a/src/fidgets/ui/IFrame.tsx +++ b/src/fidgets/ui/IFrame.tsx @@ -8,7 +8,7 @@ import { useIsMobile } from "@/common/lib/hooks/useIsMobile"; import { debounce } from "lodash"; import { isValidHttpUrl } from "@/common/lib/utils/url"; import { defaultStyleFields, ErrorWrapper, transformUrl, WithMargin } from "@/fidgets/helpers"; -import React, { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import DOMPurify from "isomorphic-dompurify"; import { BsCloud, BsCloudFill } from "react-icons/bs"; import { useMiniApp } from "@/common/utils/useMiniApp"; @@ -317,6 +317,9 @@ const IFrame: React.FC> = ({ const [iframelyEmbedAttributes, setIframelyEmbedAttributes] = useState(null); + const sanitizedEmbedContainerRef = useRef(null); + const iframelyEmbedContainerRef = useRef(null); + const sanitizedEmbedScript = useMemo(() => { if (!embedScript) return null; const clean = DOMPurify.sanitize(embedScript, { @@ -366,6 +369,34 @@ const IFrame: React.FC> = ({ // Scale value is set from size prop const _scaleValue = size; + useEffect(() => { + if (!sanitizedEmbedContainerRef.current) { + return; + } + + if (!sanitizedEmbedScript) { + sanitizedEmbedContainerRef.current.innerHTML = ""; + return; + } + + sanitizedEmbedContainerRef.current.innerHTML = sanitizedEmbedScript; + }, [sanitizedEmbedScript]); + + useEffect(() => { + if (!iframelyEmbedContainerRef.current) { + return; + } + + const html = embedInfo?.iframelyHtml; + if (!html) { + iframelyEmbedContainerRef.current.innerHTML = ""; + return; + } + + iframelyEmbedContainerRef.current.innerHTML = html; + }, [embedInfo?.iframelyHtml]); + + useEffect(() => { if (sanitizedEmbedScript) return; async function checkEmbedInfo() { @@ -417,24 +448,48 @@ const IFrame: React.FC> = ({ checkEmbedInfo(); }, [sanitizedUrl, isValid, sanitizedEmbedScript]); - if (sanitizedEmbedScript) { - if (isMiniAppEnvironment && sanitizedEmbedAttributes?.src) { - const allowedSrc = resolveAllowedEmbedSrc(sanitizedEmbedAttributes.src); + const sanitizedEmbedSrc = sanitizedEmbedAttributes?.src ?? null; + const resolvedSanitizedMiniAppSrc = useMemo(() => { + if (!isMiniAppEnvironment || !sanitizedEmbedSrc) { + return undefined; + } - if (!allowedSrc) { - return ( - - ); - } + return resolveAllowedEmbedSrc(sanitizedEmbedSrc); + }, [isMiniAppEnvironment, sanitizedEmbedSrc]); + + const sanitizedMiniAppBootstrapDoc = useMemo(() => { + if (!resolvedSanitizedMiniAppSrc) { + return null; + } + return createMiniAppBootstrapSrcDoc(resolvedSanitizedMiniAppSrc); + }, [resolvedSanitizedMiniAppSrc]); + + if (sanitizedEmbedScript) { + if ( + isMiniAppEnvironment && + sanitizedEmbedSrc && + resolvedSanitizedMiniAppSrc === null + ) { + return ( + + ); + } + + if ( + isMiniAppEnvironment && + sanitizedEmbedSrc && + resolvedSanitizedMiniAppSrc && + sanitizedMiniAppBootstrapDoc && + sanitizedEmbedAttributes + ) { const allowFullScreen = "allowfullscreen" in sanitizedEmbedAttributes; const sandboxRules = ensureSandboxRules( sanitizedEmbedAttributes.sandbox, ); - const bootstrapDoc = createMiniAppBootstrapSrcDoc(allowedSrc); const widthAttr = sanitizedEmbedAttributes.width; const heightAttr = sanitizedEmbedAttributes.height; @@ -442,8 +497,8 @@ const IFrame: React.FC> = ({ return (
`, fidgetBorderColor: "var(--user-theme-fidget-border-color)", diff --git a/src/fidgets/snapshot/SnapShot.tsx b/src/fidgets/snapshot/SnapShot.tsx index e40c7446a..2e0d1309c 100644 --- a/src/fidgets/snapshot/SnapShot.tsx +++ b/src/fidgets/snapshot/SnapShot.tsx @@ -133,6 +133,7 @@ export const SnapShot: React.FC> = ({ ens: settings.snapshotEns, skip, first, + apiUrl: settings.subgraphUrl, }); // Helper function to get CSS variable or fallback to setting value diff --git a/src/fidgets/snapshot/components/Badge.tsx b/src/fidgets/snapshot/components/Badge.tsx index dde80c4c4..0e1dce502 100644 --- a/src/fidgets/snapshot/components/Badge.tsx +++ b/src/fidgets/snapshot/components/Badge.tsx @@ -8,17 +8,17 @@ interface BadgeProps { const getStatusBadgeColor = (status: ProposalStatus): string => { switch (status) { case "Pending": - return "bg-yellow-500 w-16"; + return "bg-yellow-500 min-w-16 text-center"; case "Active": - return "bg-blue-400 w-16"; + return "bg-blue-400 min-w-16 text-center"; case "Passed": - return "bg-green-500 w-16"; + return "bg-green-500 min-w-16 text-center"; case "Failed": - return "bg-red-500 w-16"; + return "bg-red-500 min-w-16 text-center"; case "Closed": - return "bg-gray-500 w-16"; + return "bg-gray-500 min-w-16 text-center"; default: - return "bg-gray-500 w-16"; + return "bg-gray-500 min-w-16 text-center"; } }; diff --git a/src/fidgets/snapshot/components/ProposalItem.tsx b/src/fidgets/snapshot/components/ProposalItem.tsx index 6bb8f0d36..13c93a518 100644 --- a/src/fidgets/snapshot/components/ProposalItem.tsx +++ b/src/fidgets/snapshot/components/ProposalItem.tsx @@ -15,6 +15,8 @@ import { import { Action, initialState, reducer, State } from "../utils/stateManagement"; import voteOnProposal, { ProposalType } from "../utils/voteOnProposal"; +import { resolveIpfsUrl } from "@/common/lib/utils/url"; + interface Proposal { id: string; title: string; @@ -64,8 +66,8 @@ const ProposalItem: React.FC = memo( }; return ( - extractImageUrl(proposal.body) || - `https://cdn.stamp.fyi/space/${proposal.space.id.toString()}?s=96&cb=80b2fc0910dab4fa` + resolveIpfsUrl(extractImageUrl(proposal.body) || + `https://cdn.stamp.fyi/space/${proposal.space.id.toString()}?s=96&cb=80b2fc0910dab4fa`) ); }, [proposal.body, proposal.space.id]); diff --git a/src/fidgets/snapshot/components/ProposalPreview.tsx b/src/fidgets/snapshot/components/ProposalPreview.tsx index 041345e55..09e7a90b5 100644 --- a/src/fidgets/snapshot/components/ProposalPreview.tsx +++ b/src/fidgets/snapshot/components/ProposalPreview.tsx @@ -54,8 +54,8 @@ const ProposalPreview: React.FC = memo(({ body }) => { "td", ], protocols: { - href: ["http", "https", "mailto"], - src: ["http", "https"], + href: ["http", "https", "mailto", "ipfs"], + src: ["http", "https", "ipfs"], }, }), [] From 18c58aa47b61f224daab61f0bb1dfad6186dfbda Mon Sep 17 00:00:00 2001 From: Solomon Date: Sat, 6 Dec 2025 09:33:06 -0800 Subject: [PATCH 107/155] bugfix(Next): Patch NextJS against CVE-2025-66478 (#1609) --- .nvmrc | 2 +- package.json | 2 +- yarn.lock | 135 ++++++++++++++++++++++----------------------------- 3 files changed, 60 insertions(+), 79 deletions(-) diff --git a/.nvmrc b/.nvmrc index 05f04686d..d135defb2 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v22.11.0 \ No newline at end of file +22.12.0 \ No newline at end of file diff --git a/package.json b/package.json index 61e91ed09..2e1f25229 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "moment": "^2.30.1", "mutative": "^1.0.3", "neverthrow": "^6.2.2", - "next": "^15.2.2", + "next": "^15.2.6", "next-themes": "^0.3.0", "prop-types": "^15.8.1", "react": "^18.3.1", diff --git a/yarn.lock b/yarn.lock index 000fdaca6..59dc4d905 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2045,10 +2045,10 @@ dependencies: webpack-bundle-analyzer "4.10.1" -"@next/env@15.3.4": - version "15.3.4" - resolved "https://registry.npmjs.org/@next/env/-/env-15.3.4.tgz" - integrity sha512-ZkdYzBseS6UjYzz6ylVKPOK+//zLWvD6Ta+vpoye8cW11AjiQjGYVibF0xuvT4L0iJfAPfZLFidaEzAOywyOAQ== +"@next/env@15.5.7": + version "15.5.7" + resolved "https://registry.yarnpkg.com/@next/env/-/env-15.5.7.tgz#4168db34ae3bc9fd9ad3b951d327f4cfc38d4362" + integrity sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg== "@next/eslint-plugin-next@^14.2.3": version "14.2.30" @@ -2057,45 +2057,45 @@ dependencies: glob "10.3.10" -"@next/swc-darwin-arm64@15.3.4": - version "15.3.4" - resolved "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.4.tgz" - integrity sha512-z0qIYTONmPRbwHWvpyrFXJd5F9YWLCsw3Sjrzj2ZvMYy9NPQMPZ1NjOJh4ojr4oQzcGYwgJKfidzehaNa1BpEg== - -"@next/swc-darwin-x64@15.3.4": - version "15.3.4" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.4.tgz#aa7fd968af7e53aa17d4f234cf7722b3899712cf" - integrity sha512-Z0FYJM8lritw5Wq+vpHYuCIzIlEMjewG2aRkc3Hi2rcbULknYL/xqfpBL23jQnCSrDUGAo/AEv0Z+s2bff9Zkw== - -"@next/swc-linux-arm64-gnu@15.3.4": - version "15.3.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.4.tgz#5da3d6d6055665d0c3a2dab0bc0471064bc9eece" - integrity sha512-l8ZQOCCg7adwmsnFm8m5q9eIPAHdaB2F3cxhufYtVo84pymwKuWfpYTKcUiFcutJdp9xGHC+F1Uq3xnFU1B/7g== - -"@next/swc-linux-arm64-musl@15.3.4": - version "15.3.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.4.tgz#9043ccc397746c94c2452d301e8f95a33aec22e8" - integrity sha512-wFyZ7X470YJQtpKot4xCY3gpdn8lE9nTlldG07/kJYexCUpX1piX+MBfZdvulo+t1yADFVEuzFfVHfklfEx8kw== - -"@next/swc-linux-x64-gnu@15.3.4": - version "15.3.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.4.tgz#49a33f904a51a8c665406ca7e5a748f480bf195d" - integrity sha512-gEbH9rv9o7I12qPyvZNVTyP/PWKqOp8clvnoYZQiX800KkqsaJZuOXkWgMa7ANCCh/oEN2ZQheh3yH8/kWPSEg== - -"@next/swc-linux-x64-musl@15.3.4": - version "15.3.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.4.tgz#8beaff35d8f11961ea80d12a10226581df4c5a74" - integrity sha512-Cf8sr0ufuC/nu/yQ76AnarbSAXcwG/wj+1xFPNbyNo8ltA6kw5d5YqO8kQuwVIxk13SBdtgXrNyom3ZosHAy4A== - -"@next/swc-win32-arm64-msvc@15.3.4": - version "15.3.4" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.4.tgz#149d9a35068ecda317af138814539929c9c269af" - integrity sha512-ay5+qADDN3rwRbRpEhTOreOn1OyJIXS60tg9WMYTWCy3fB6rGoyjLVxc4dR9PYjEdR2iDYsaF5h03NA+XuYPQQ== - -"@next/swc-win32-x64-msvc@15.3.4": - version "15.3.4" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.4.tgz#a652327782d838c2b875eaf216187c51b8409775" - integrity sha512-4kDt31Bc9DGyYs41FTL1/kNpDeHyha2TC0j5sRRoKCyrhNcfZ/nRQkAUlF27mETwm8QyHqIjHJitfcza2Iykfg== +"@next/swc-darwin-arm64@15.5.7": + version "15.5.7" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz#f0c9ccfec2cd87cbd4b241ce4c779a7017aed958" + integrity sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw== + +"@next/swc-darwin-x64@15.5.7": + version "15.5.7" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz#18009e9fcffc5c0687cc9db24182ddeac56280d9" + integrity sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg== + +"@next/swc-linux-arm64-gnu@15.5.7": + version "15.5.7" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz#fe7c7e08264cf522d4e524299f6d3e63d68d579a" + integrity sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA== + +"@next/swc-linux-arm64-musl@15.5.7": + version "15.5.7" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz#94228fe293475ec34a5a54284e1056876f43a3cf" + integrity sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw== + +"@next/swc-linux-x64-gnu@15.5.7": + version "15.5.7" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz#078c71201dfe7fcfb8fa6dc92aae6c94bc011cdc" + integrity sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw== + +"@next/swc-linux-x64-musl@15.5.7": + version "15.5.7" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz#72947f5357f9226292353e0bb775643da3c7a182" + integrity sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA== + +"@next/swc-win32-arm64-msvc@15.5.7": + version "15.5.7" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz#397b912cd51c6a80e32b9c0507ecd82514353941" + integrity sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ== + +"@next/swc-win32-x64-msvc@15.5.7": + version "15.5.7" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz#e02b543d9dc6c1631d4ac239cb1177245dfedfe4" + integrity sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw== "@neynar/nodejs-sdk@^2.23.0": version "2.46.0" @@ -5252,11 +5252,6 @@ "@supabase/realtime-js" "2.11.15" "@supabase/storage-js" "2.7.1" -"@swc/counter@0.1.3": - version "0.1.3" - resolved "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz" - integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== - "@swc/helpers@0.5.15": version "0.5.15" resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz" @@ -7176,13 +7171,6 @@ bufferutil@^4.0.1, bufferutil@^4.0.8: dependencies: node-gyp-build "^4.3.0" -busboy@1.6.0: - version "1.6.0" - resolved "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz" - integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== - dependencies: - streamsearch "^1.1.0" - call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz" @@ -11599,28 +11587,26 @@ next-tick@^1.1.0: resolved "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz" integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== -next@^15.2.2: - version "15.3.4" - resolved "https://registry.npmjs.org/next/-/next-15.3.4.tgz" - integrity sha512-mHKd50C+mCjam/gcnwqL1T1vPx/XQNFlXqFIVdgQdVAFY9iIQtY0IfaVflEYzKiqjeA7B0cYYMaCrmAYFjs4rA== +next@^15.2.6: + version "15.5.7" + resolved "https://registry.yarnpkg.com/next/-/next-15.5.7.tgz#4507700b2bbcaf2c9fb7a9ad25c0dac2ba4a9a75" + integrity sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ== dependencies: - "@next/env" "15.3.4" - "@swc/counter" "0.1.3" + "@next/env" "15.5.7" "@swc/helpers" "0.5.15" - busboy "1.6.0" caniuse-lite "^1.0.30001579" postcss "8.4.31" styled-jsx "5.1.6" optionalDependencies: - "@next/swc-darwin-arm64" "15.3.4" - "@next/swc-darwin-x64" "15.3.4" - "@next/swc-linux-arm64-gnu" "15.3.4" - "@next/swc-linux-arm64-musl" "15.3.4" - "@next/swc-linux-x64-gnu" "15.3.4" - "@next/swc-linux-x64-musl" "15.3.4" - "@next/swc-win32-arm64-msvc" "15.3.4" - "@next/swc-win32-x64-msvc" "15.3.4" - sharp "^0.34.1" + "@next/swc-darwin-arm64" "15.5.7" + "@next/swc-darwin-x64" "15.5.7" + "@next/swc-linux-arm64-gnu" "15.5.7" + "@next/swc-linux-arm64-musl" "15.5.7" + "@next/swc-linux-x64-gnu" "15.5.7" + "@next/swc-linux-x64-musl" "15.5.7" + "@next/swc-win32-arm64-msvc" "15.5.7" + "@next/swc-win32-x64-msvc" "15.5.7" + sharp "^0.34.3" node-addon-api@^2.0.0: version "2.0.2" @@ -13653,7 +13639,7 @@ shallowequal@1.1.0: resolved "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz" integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== -sharp@^0.34.1, sharp@^0.34.5: +sharp@^0.34.3, sharp@^0.34.5: version "0.34.5" resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.34.5.tgz#b6f148e4b8c61f1797bde11a9d1cfebbae2c57b0" integrity sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg== @@ -13937,11 +13923,6 @@ stream-shift@^1.0.2: resolved "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz" integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== -streamsearch@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz" - integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== - strict-uri-encode@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz" From 6ac30a577e900a2d2bcee8355b6fd1d2b3de3ec6 Mon Sep 17 00:00:00 2001 From: Jesse Paterson Date: Sat, 29 Nov 2025 18:34:46 -0600 Subject: [PATCH 108/155] planning --- BRANCH_ANALYSIS.md | 260 --- DIRECTORY_REFACTORING_PLAN.md | 940 --------- EXISTING_UTILITIES_ANALYSIS.md | 208 -- docs/ADMIN_ASSET_UPLOAD_STRATEGY.md | 509 +++++ docs/ASSETS_CONFIG_STORAGE_RELATIONSHIP.md | 386 ++++ docs/ASSET_HANDLING_STRATEGY.md | 337 ++++ docs/ASSET_PERFORMANCE_OPTIMIZATION.md | 329 ++++ docs/BUILD_TIME_CONFIG_SUMMARY.md | 152 ++ docs/COMMUNITY_CONFIG_SYSTEM.md | 796 ++++++++ docs/CONFIG_DATABASE_SCHEMA_DETAILED.md | 1027 ++++++++++ docs/DATABASE_CONFIG_MIGRATION_PLAN.md | 1000 ++++++++++ docs/E2BIG_SOLUTION.md | 99 + docs/INCREMENTAL_IMPLEMENTATION_PLAN.md | 1753 +++++++++++++++++ docs/NAVIGATION_SPACE_REFERENCE_APPROACH.md | 313 +++ ...VIGATION_SPACE_REFERENCE_IMPLEMENTATION.md | 357 ++++ docs/NEXTJS_CONFIG_APPROACHES.md | 347 ++++ docs/QUICK_START_IMPLEMENTATION.md | 655 ++++++ docs/QUICK_START_TESTING.md | 301 +++ docs/SHARED_THEMES_APPROACH.md | 255 +++ docs/SIMPLE_BUILD_TIME_CONFIG.md | 424 ++++ docs/SPACE_REFERENCE_APPROACH.md | 202 ++ docs/UPDATED_IMPLEMENTATION_SUMMARY.md | 147 ++ next.config.mjs | 39 + scripts/analyze-config-size.ts | 83 + scripts/seed-community-configs.ts | 224 +++ src/config/index.ts | 20 + ...0251129172847_create_community_configs.sql | 57 + .../20251129172848_add_navpage_space_type.sql | 15 + supabase/seed.sql | 101 + 29 files changed, 9928 insertions(+), 1408 deletions(-) delete mode 100644 BRANCH_ANALYSIS.md delete mode 100644 DIRECTORY_REFACTORING_PLAN.md delete mode 100644 EXISTING_UTILITIES_ANALYSIS.md create mode 100644 docs/ADMIN_ASSET_UPLOAD_STRATEGY.md create mode 100644 docs/ASSETS_CONFIG_STORAGE_RELATIONSHIP.md create mode 100644 docs/ASSET_HANDLING_STRATEGY.md create mode 100644 docs/ASSET_PERFORMANCE_OPTIMIZATION.md create mode 100644 docs/BUILD_TIME_CONFIG_SUMMARY.md create mode 100644 docs/COMMUNITY_CONFIG_SYSTEM.md create mode 100644 docs/CONFIG_DATABASE_SCHEMA_DETAILED.md create mode 100644 docs/DATABASE_CONFIG_MIGRATION_PLAN.md create mode 100644 docs/E2BIG_SOLUTION.md create mode 100644 docs/INCREMENTAL_IMPLEMENTATION_PLAN.md create mode 100644 docs/NAVIGATION_SPACE_REFERENCE_APPROACH.md create mode 100644 docs/NAVIGATION_SPACE_REFERENCE_IMPLEMENTATION.md create mode 100644 docs/NEXTJS_CONFIG_APPROACHES.md create mode 100644 docs/QUICK_START_IMPLEMENTATION.md create mode 100644 docs/QUICK_START_TESTING.md create mode 100644 docs/SHARED_THEMES_APPROACH.md create mode 100644 docs/SIMPLE_BUILD_TIME_CONFIG.md create mode 100644 docs/SPACE_REFERENCE_APPROACH.md create mode 100644 docs/UPDATED_IMPLEMENTATION_SUMMARY.md create mode 100644 scripts/analyze-config-size.ts create mode 100644 scripts/seed-community-configs.ts create mode 100644 supabase/migrations/20251129172847_create_community_configs.sql create mode 100644 supabase/migrations/20251129172848_add_navpage_space_type.sql diff --git a/BRANCH_ANALYSIS.md b/BRANCH_ANALYSIS.md deleted file mode 100644 index 28e1bbf07..000000000 --- a/BRANCH_ANALYSIS.md +++ /dev/null @@ -1,260 +0,0 @@ -# Branch Analysis: `codex/update-token-directory-api` vs `canary` - -## Summary -- **Total Changes**: 38 files changed, 6,236 insertions(+), 31 deletions(-) -- **Main Feature**: New Token Directory fidget with API endpoint for displaying token/NFT holders, Farcaster channel members, and CSV-uploaded directories - ---- - -## Itemized Changes - -### 1. Core Feature Files - -#### 1.1 New Directory Fidget Component -- **File**: `src/fidgets/token/Directory.tsx` -- **Size**: 2,373 lines (new file) -- **Purpose**: Main React component for displaying directory of token holders, Farcaster channel members, or CSV-uploaded users -- **Key Features**: - - Three data sources: token holders, Farcaster channels, CSV uploads - - Two layout styles: cards and list - - Multiple sort options (token holdings, followers) - - Pagination - - Real-time data fetching with debouncing - - Settings backfill from `lastFetchSettings` - -#### 1.2 New API Endpoint -- **File**: `src/pages/api/token/directory.ts` -- **Size**: 1,105 lines (new file) -- **Purpose**: Server-side API for fetching token holder data -- **Key Features**: - - Supports ERC20 tokens and NFTs - - Multiple networks: Base, Polygon, Ethereum Mainnet - - Integrates with Moralis and Alchemy APIs - - Fetches ENS metadata - - Enriches with Neynar profile data - - Aggregates holders by FID - -### 2. Supporting Utility Modules - -#### 2.1 Data Transformation -- **File**: `src/common/data/api/token/transform.ts` (316 lines) -- **Purpose**: Transforms raw holder data into directory member format -- **Functions**: `transformAndAggregate`, `extractNeynarProfileData`, `extractPrimaryAddress` - -#### 2.2 Type Definitions -- **File**: `src/common/data/api/token/types.ts` (113 lines) -- **Purpose**: Centralized TypeScript types for directory API - -#### 2.3 Utility Functions -- **File**: `src/common/data/api/token/utils.ts` (137 lines) -- **Purpose**: Helper functions (address normalization, balance parsing, social record parsing) - -#### 2.4 ENS Enrichment -- **File**: `src/common/data/api/token/enrichEns.ts` (153 lines) -- **Purpose**: Fetches ENS metadata using Enstate.rs and wagmi - -#### 2.5 Neynar Enrichment -- **File**: `src/common/data/api/token/enrichNeynar.ts` (68 lines) -- **Purpose**: Fetches Farcaster profile data via Neynar API - -#### 2.6 Moralis Integration -- **File**: `src/common/data/api/token/fetchMoralis.ts` (121 lines) -- **Purpose**: Fetches ERC20 token holders from Moralis API - -#### 2.7 Dependency Injection -- **File**: `src/common/data/api/token/dependencies.ts` (39 lines) -- **Purpose**: Provides dependency injection for testing - -### 3. Infrastructure Changes - -#### 3.1 FidgetWrapper Enhancement -- **File**: `src/common/fidgets/FidgetWrapper.tsx` (+99 lines, -31 lines) -- **Changes**: Added settings backfill logic using `lastFetchSettings` from fidget data -- **Purpose**: Automatically populates empty settings from previous fetch configuration - -#### 3.2 New API Endpoints -- **File**: `src/pages/api/farcaster/neynar/bulk-address.ts` (34 lines) -- **Purpose**: Bulk Farcaster user lookup by Ethereum addresses - -### 4. Documentation - -#### 4.1 API Cleanup Opportunities -- **File**: `docs/API_CLEANUP_OPPORTUNITIES.md` (569 lines) -- **Purpose**: Documents potential API simplifications - -#### 4.2 Data Field Patterns -- **File**: `docs/SYSTEMS/FIDGETS/DATA_FIELD_PATTERNS.md` (625 lines) -- **Purpose**: Documents fidget data field patterns - -### 5. Minor Updates -- Updated various components to support new Directory fidget -- Added SVG icons (ens.svg, etherscan.svg, github.svg) -- Updated package.json dependencies -- Added environment variable examples - ---- - -## Areas of Unnecessary Complexity - -### 🔴 Critical Issues - -#### 1. **Massive Single File: Directory.tsx (2,373 lines)** - - **Problem**: One component file contains: - - Component logic (45+ React hooks) - - Data fetching logic (3 different sources) - - CSV parsing logic - - Data transformation logic - - UI rendering (cards + list views) - - Helper functions (duplicated from API) - - **Impact**: Extremely difficult to maintain, test, and understand - - **Recommendation**: Split into: - - `Directory.tsx` (main component, ~200 lines) - - `useDirectoryData.ts` (data fetching hook) - - `useCsvParser.ts` (CSV parsing logic) - - `DirectoryCardView.tsx` (card layout) - - `DirectoryListView.tsx` (list layout) - - `directoryUtils.ts` (shared utilities) - -#### 2. **Duplicate Logic Between Component and API** - - **Problem**: Functions duplicated in both files: - - `parseSocialRecord` (exists in both Directory.tsx and API/utils.ts) - - `extractNeynarPrimaryAddress` (exists in both Directory.tsx and transform.ts) - - `extractNeynarSocialAccounts` (exists in both Directory.tsx and transform.ts) - - `normalizeAddress` (exists in both, though API version is exported) - - **Impact**: Code duplication, maintenance burden, potential inconsistencies - - **Recommendation**: Move all shared utilities to `src/common/data/api/token/utils.ts` and import - -#### 3. **Complex State Management in Component** - - **Problem**: Component uses 45+ React hooks (useState, useEffect, useCallback, useMemo, useRef) - - **Specific Issues**: - - Multiple interdependent useEffects - - Complex dependency arrays - - State synchronization logic scattered throughout - - AbortController management mixed with data fetching - - **Impact**: Difficult to debug, potential race conditions, performance issues - - **Recommendation**: Extract to custom hooks: - - `useDirectoryFetch.ts` (handles all fetching logic) - - `useDirectoryState.ts` (manages local UI state) - - `useDirectoryPagination.ts` (handles pagination) - -#### 4. **API Endpoint Too Large (1,105 lines)** - - **Problem**: Single API handler contains: - - Multiple data source fetchers (Moralis, Alchemy) - - ENS enrichment logic - - Neynar enrichment logic - - Data transformation - - Error handling - - **Impact**: Hard to test individual pieces, difficult to maintain - - **Recommendation**: Already partially modularized, but could further split: - - Keep main handler thin (~100 lines) - - Move fetchers to separate modules (already done) - - Create orchestration layer - -### 🟡 Moderate Issues - -#### 5. **Overly Complex CSV Parsing** - - **Problem**: CSV parsing logic embedded in component (~200 lines) - - **Issues**: - - Header detection logic - - Multiple type handling (address, fid, username) - - Fallback parsing logic - - Inline chunking utilities - - **Recommendation**: Extract to `src/common/data/api/token/csvParser.ts` - -#### 6. **Settings Backfill Logic in FidgetWrapper** - - **Problem**: Generic backfill logic added to FidgetWrapper (~70 lines) - - **Issues**: - - Uses `isEqual` from lodash for deep comparison - - Complex serialization/deserialization logic - - May affect all fidgets, not just Directory - - **Impact**: Could cause unexpected behavior in other fidgets - - **Recommendation**: Move to Directory-specific hook or make opt-in - -#### 7. **Multiple Data Source Handling** - - **Problem**: Three completely different data sources handled in one component - - **Issues**: - - Different fetch functions for each source - - Different data transformation logic - - Different error handling - - **Recommendation**: Create abstraction layer: - - `DirectoryDataSource` interface - - `TokenHoldersSource`, `FarcasterChannelSource`, `CsvSource` implementations - - Unified `fetchDirectoryData` function - -#### 8. **Complex Data Enrichment Pipeline** - - **Problem**: Multiple enrichment steps with conditional logic - - **Issues**: - - ENS enrichment (Enstate.rs + wagmi fallback) - - Neynar enrichment (batched lookups) - - Social account extraction - - Primary address resolution - - **Recommendation**: Create enrichment pipeline: - - `EnrichmentPipeline` class - - Individual enrichment steps as plugins - - Configurable enrichment options - -### 🟢 Minor Issues - -#### 9. **Type Safety Issues** - - **Problem**: Extensive use of `any` types in CSV parsing and Neynar response handling - - **Recommendation**: Add proper types for all API responses - -#### 10. **Hardcoded Constants** - - **Problem**: Magic numbers and strings scattered throughout: - - `PAGE_SIZE = 100` - - `CHANNEL_FETCH_DEBOUNCE_MS = 800` - - `STALE_AFTER_MS = 60 * 60 * 1000` - - Batch sizes (25, 50, 100) - - **Recommendation**: Move to config file or constants module - -#### 11. **Error Handling Inconsistency** - - **Problem**: Different error handling patterns: - - Some functions throw errors - - Some return empty objects/arrays - - Some log and continue - - **Recommendation**: Standardize error handling strategy - -#### 12. **Excessive Console Logging** - - **Problem**: Many `console.log` statements throughout code - - **Recommendation**: Use proper logging library or remove debug logs - ---- - -## Recommendations Summary - -### Immediate Actions (High Priority) -1. **Split Directory.tsx** into smaller, focused modules -2. **Remove duplicate code** between component and API -3. **Extract data fetching** to custom hooks -4. **Create data source abstraction** for the three sources - -### Short-term Improvements (Medium Priority) -5. Extract CSV parsing to separate module -6. Simplify FidgetWrapper backfill logic (make opt-in) -7. Standardize error handling -8. Add proper TypeScript types - -### Long-term Refactoring (Low Priority) -9. Create enrichment pipeline abstraction -10. Move constants to config -11. Implement proper logging -12. Add comprehensive unit tests - ---- - -## Code Metrics - -- **Directory.tsx**: 2,373 lines, ~179 functions/components -- **API endpoint**: 1,105 lines, ~70 functions -- **Total new code**: ~6,200 lines -- **React hooks in Directory**: 45+ (useState, useEffect, useCallback, useMemo, useRef) -- **Duplicate functions**: ~5 major functions duplicated between component and API - ---- - -## Testing Coverage - -- **Test file**: `tests/tokenDirectory.api.test.ts` (359 lines) -- **Coverage**: API endpoint tests present -- **Missing**: Component tests, integration tests, CSV parsing tests - diff --git a/DIRECTORY_REFACTORING_PLAN.md b/DIRECTORY_REFACTORING_PLAN.md deleted file mode 100644 index df4be57f0..000000000 --- a/DIRECTORY_REFACTORING_PLAN.md +++ /dev/null @@ -1,940 +0,0 @@ -# Directory Component Refactoring Plan - -## Overview -This document provides specific recommendations for splitting `Directory.tsx` (2,373 lines) into smaller, maintainable modules and consolidating duplicate functions into shared utilities. - ---- - -## 1. Shared Utilities Consolidation - -### 1.1 Functions to Move to `src/common/data/api/token/utils.ts` - -These functions are **duplicated** between `Directory.tsx` and the API/transform modules: - -#### Already in utils.ts (keep there): -- ✅ `normalizeAddress` - Already exported -- ✅ `parseSocialRecord` - Already exported (matches `parseSocialRecordValue`) - -#### Functions to ADD to utils.ts: - -```typescript -// From Directory.tsx lines 605-646 -export function extractNeynarPrimaryAddress(user: any): string | null { - // Move implementation from Directory.tsx - // This is duplicated in transform.ts as extractPrimaryAddress -} - -// From Directory.tsx lines 648-681 -export function extractNeynarSocialAccounts(user: any): { - xHandle: string | null; - xUrl: string | null; - githubHandle: string | null; - githubUrl: string | null; -} { - // Move implementation from Directory.tsx - // Similar logic exists in transform.ts extractNeynarProfileData -} - -// From Directory.tsx line 683 -export function buildEtherscanUrl(address?: string | null): string | null { - return address ? `https://etherscan.io/address/${address.toLowerCase()}` : null; -} - -// From Directory.tsx line 688 -export const BLOCK_EXPLORER_URLS: Record<"base" | "polygon" | "mainnet", string> = { - mainnet: "https://etherscan.io/address/", - base: "https://basescan.org/address/", - polygon: "https://polygonscan.com/address/", -}; - -export function getBlockExplorerLink( - network: "base" | "polygon" | "mainnet", - address: string -): string { - return `${BLOCK_EXPLORER_URLS[network]}${address}`; -} -``` - -#### Functions to ADD to `src/common/lib/utils/directoryUtils.ts` (new file): - -```typescript -// Component-specific utilities (not API-related) - -// From Directory.tsx line 706 -export function formatTokenBalance(value: string | null | undefined): string { - if (value == null || value === "") return "0"; - const parsed = Number(value); - if (!Number.isFinite(parsed)) { - return value; - } - return parsed.toLocaleString(undefined, { - minimumFractionDigits: 0, - maximumFractionDigits: 2, - }); -} - -// From Directory.tsx line 694 -export function resolveFontFamily( - value: string | undefined, - fallback: string, - fontOptions: typeof FONT_FAMILY_OPTIONS -): string { - // Move implementation -} - -// From Directory.tsx line 955 -export function getLastActivityLabel(timestamp?: string | null): string | null { - // Move implementation using formatDistanceToNow -} - -// From Directory.tsx line 932 -export function getMemberPrimaryLabel(member: DirectoryMemberData): string { - return member.displayName || member.username || member.ensName || member.address; -} - -// From Directory.tsx line 935 -export function getMemberSecondaryLabel(member: DirectoryMemberData): string | null { - // Move implementation -} - -// From Directory.tsx line 947 -export function getMemberAvatarSrc(member: DirectoryMemberData): string | undefined { - // Move implementation -} - -// From Directory.tsx line 952 -export function getMemberAvatarFallback(member: DirectoryMemberData): string | undefined { - // Move implementation -} - -// From Directory.tsx line 724 -export function getFarcasterProfileUrl( - username?: string | null, - fid?: number | null -): string | null { - // Move implementation -} - -// From Directory.tsx line 735 -export function getEnsProfileUrl(ensName?: string | null): string | null { - return ensName ? `https://app.ens.domains/${ensName}` : null; -} -``` - ---- - -## 2. Component Extraction Plan - -### 2.1 New File Structure - -``` -src/fidgets/token/ -├── Directory.tsx # Main component (~200 lines) -├── Directory.module.ts # Fidget module export -├── components/ -│ ├── DirectoryCardView.tsx # Card layout (~150 lines) -│ ├── DirectoryListView.tsx # List layout (~150 lines) -│ ├── BadgeIcons.tsx # Badge component (~200 lines) -│ ├── ProfileLink.tsx # Profile link wrapper (~30 lines) -│ └── PaginationControls.tsx # Pagination component (~50 lines) -├── hooks/ -│ ├── useDirectoryData.ts # Data fetching hook (~300 lines) -│ ├── useDirectoryState.ts # Local state management (~100 lines) -│ ├── useDirectoryPagination.ts # Pagination logic (~50 lines) -│ └── useDirectoryFilters.ts # Filtering/sorting logic (~100 lines) -├── dataSources/ -│ ├── tokenHoldersSource.ts # Token holders fetching (~100 lines) -│ ├── farcasterChannelSource.ts # Channel fetching (~150 lines) -│ └── csvSource.ts # CSV parsing & fetching (~200 lines) -├── constants.ts # Constants and options (~100 lines) -└── types.ts # Type definitions (~100 lines) -``` - ---- - -## 3. Detailed Module Breakdown - -### 3.1 `src/fidgets/token/types.ts` (NEW) - -**Extract from Directory.tsx lines 42-106:** - -```typescript -export type DirectoryNetwork = "base" | "polygon" | "mainnet"; -export type DirectoryAssetType = "token" | "nft"; -export type DirectorySortOption = "tokenHoldings" | "followers"; -export type DirectoryLayoutStyle = "cards" | "list"; -export type DirectoryIncludeOption = "holdersWithFarcasterAccount" | "allHolders"; -export type DirectorySource = "tokenHolders" | "farcasterChannel" | "csv"; -export type DirectoryChannelFilterOption = "members" | "followers" | "all"; -export type CsvTypeOption = "address" | "fid" | "username"; -export type CsvSortOption = "followers" | "csvOrder"; - -export interface DirectoryMemberData { - // ... (lines 55-73) -} - -export interface DirectoryFidgetData extends FidgetData { - // ... (lines 75-81) -} - -export type DirectoryFidgetSettings = FidgetSettings & FidgetSettingsStyle & { - // ... (lines 83-106) -}; -``` - ---- - -### 3.2 `src/fidgets/token/constants.ts` (NEW) - -**Extract from Directory.tsx lines 38-177:** - -```typescript -export const STALE_AFTER_MS = 60 * 60 * 1000; -export const PAGE_SIZE = 100; -export const CHANNEL_FETCH_DEBOUNCE_MS = 800; - -export const NETWORK_OPTIONS = [ - { name: "Base", value: "base" }, - { name: "Polygon", value: "polygon" }, - { name: "Ethereum Mainnet", value: "mainnet" }, -] as const; - -export const SORT_OPTIONS = [ - { name: "Token holdings", value: "tokenHoldings" }, - { name: "Followers", value: "followers" }, -] as const; - -export const LAYOUT_OPTIONS = [ - { name: "Cards", value: "cards" }, - { name: "List", value: "list" }, -] as const; - -export const ASSET_TYPE_OPTIONS = [ - { name: "Token", value: "token" }, - { name: "NFT", value: "nft" }, -] as const; - -export const INCLUDE_OPTIONS = [ - { name: "Holders with Farcaster Account", value: "holdersWithFarcasterAccount" }, - { name: "All holders", value: "allHolders" }, -] as const; - -export const SOURCE_OPTIONS = [ - { name: "Token Holders", value: "tokenHolders" }, - { name: "Farcaster Channel", value: "farcasterChannel" }, - { name: "CSV", value: "csv" }, -] as const; - -export const CHANNEL_FILTER_OPTIONS = [ - { name: "Members", value: "members" }, - { name: "Followers", value: "followers" }, - { name: "All", value: "all" }, -] as const; - -export const CSV_TYPE_OPTIONS = [ - { name: "Address", value: "address" }, - { name: "FID", value: "fid" }, - { name: "Farcaster username", value: "username" }, -] as const; - -export const CSV_SORT_OPTIONS = [ - { name: "Followers", value: "followers" }, - { name: "CSV order", value: "csvOrder" }, -] as const; - -// Badge image paths -export const FARCASTER_BADGE_SRC = "/images/farcaster.jpeg"; -export const ENS_BADGE_SRC = "/images/ens.svg"; -export const X_BADGE_SRC = "/images/twitter.avif"; -export const GITHUB_BADGE_SRC = "/images/github.svg"; -export const ETHERSCAN_BADGE_SRC = "/images/etherscan.svg"; -``` - ---- - -### 3.3 `src/fidgets/token/components/BadgeIcons.tsx` (NEW) - -**Extract from Directory.tsx lines 738-927:** - -```typescript -import React from "react"; -import { mergeClasses } from "@/common/lib/utils/mergeClasses"; -import { - FARCASTER_BADGE_SRC, - ENS_BADGE_SRC, - X_BADGE_SRC, - GITHUB_BADGE_SRC, - ETHERSCAN_BADGE_SRC, -} from "../constants"; -import { getFarcasterProfileUrl, getEnsProfileUrl } from "@/common/lib/utils/directoryUtils"; -import { buildEtherscanUrl } from "@/common/data/api/token/utils"; - -export type BadgeIconsProps = { - username?: string | null; - ensName?: string | null; - ensAvatarUrl?: string | null; - fid?: number | null; - primaryAddress?: string | null; - etherscanUrl?: string | null; - xHandle?: string | null; - xUrl?: string | null; - githubHandle?: string | null; - githubUrl?: string | null; - size?: number; - gapClassName?: string; -}; - -export const BadgeIcons: React.FC = ({ - // ... props -}) => { - // Move entire implementation from Directory.tsx lines 753-927 -}; -``` - ---- - -### 3.4 `src/fidgets/token/components/ProfileLink.tsx` (NEW) - -**Extract from Directory.tsx lines 2285-2310:** - -```typescript -import React from "react"; -import Link from "next/link"; -import { mergeClasses } from "@/common/lib/utils/mergeClasses"; - -export type ProfileLinkProps = { - username?: string | null; - fallbackHref?: string; - className?: string; - children: React.ReactNode; -}; - -export const ProfileLink: React.FC = ({ - username, - fallbackHref, - className, - children, -}) => { - // Move implementation from Directory.tsx lines 2292-2310 -}; -``` - ---- - -### 3.5 `src/fidgets/token/components/PaginationControls.tsx` (NEW) - -**Extract from Directory.tsx lines 2312-2366:** - -```typescript -import React from "react"; -import { PAGE_SIZE } from "../constants"; - -export type PaginationControlsProps = { - currentPage: number; - pageCount: number; - onPrev: () => void; - onNext: () => void; - totalCount: number; -}; - -export const PaginationControls: React.FC = ({ - // ... props -}) => { - // Move implementation from Directory.tsx lines 2327-2366 -}; -``` - ---- - -### 3.6 `src/fidgets/token/components/DirectoryCardView.tsx` (NEW) - -**Extract from Directory.tsx lines 2162-2267:** - -```typescript -import React from "react"; -import { Avatar, AvatarFallback, AvatarImage } from "@/common/components/atoms/avatar"; -import BoringAvatar from "boring-avatars"; -import { DirectoryMemberData, DirectoryFidgetSettings } from "../types"; -import { ProfileLink } from "./ProfileLink"; -import { BadgeIcons } from "./BadgeIcons"; -import { - getMemberPrimaryLabel, - getMemberSecondaryLabel, - getMemberAvatarSrc, - getMemberAvatarFallback, - getLastActivityLabel, - formatTokenBalance, -} from "@/common/lib/utils/directoryUtils"; -import { buildEtherscanUrl } from "@/common/data/api/token/utils"; -import { toFarcasterCdnUrl } from "@/common/lib/utils/farcasterCdn"; - -export type DirectoryCardViewProps = { - members: DirectoryMemberData[]; - settings: DirectoryFidgetSettings; - tokenSymbol?: string | null; - headingTextStyle: React.CSSProperties; - headingFontFamilyStyle: React.CSSProperties; - network: "base" | "polygon" | "mainnet"; - includeFilter: "holdersWithFarcasterAccount" | "allHolders"; -}; - -export const DirectoryCardView: React.FC = ({ - members, - settings, - tokenSymbol, - headingTextStyle, - headingFontFamilyStyle, - network, - includeFilter, -}) => { - // Extract card rendering logic from Directory.tsx lines 2162-2267 - // This is the grid of cards -}; -``` - ---- - -### 3.7 `src/fidgets/token/components/DirectoryListView.tsx` (NEW) - -**Extract from Directory.tsx lines 2072-2161:** - -```typescript -import React from "react"; -import { Avatar, AvatarFallback, AvatarImage } from "@/common/components/atoms/avatar"; -import BoringAvatar from "boring-avatars"; -import { DirectoryMemberData, DirectoryFidgetSettings } from "../types"; -import { ProfileLink } from "./ProfileLink"; -import { BadgeIcons } from "./BadgeIcons"; -import { - getMemberPrimaryLabel, - getMemberSecondaryLabel, - getMemberAvatarSrc, - getMemberAvatarFallback, - getLastActivityLabel, - formatTokenBalance, -} from "@/common/lib/utils/directoryUtils"; -import { buildEtherscanUrl, getBlockExplorerLink } from "@/common/data/api/token/utils"; -import { toFarcasterCdnUrl } from "@/common/lib/utils/farcasterCdn"; - -export type DirectoryListViewProps = { - members: DirectoryMemberData[]; - settings: DirectoryFidgetSettings; - tokenSymbol?: string | null; - headingTextStyle: React.CSSProperties; - network: "base" | "polygon" | "mainnet"; - includeFilter: "holdersWithFarcasterAccount" | "allHolders"; -}; - -export const DirectoryListView: React.FC = ({ - members, - settings, - tokenSymbol, - headingTextStyle, - network, - includeFilter, -}) => { - // Extract list rendering logic from Directory.tsx lines 2072-2161 - // This is the list view -}; -``` - ---- - -### 3.8 `src/fidgets/token/dataSources/csvSource.ts` (NEW) - -**Extract from Directory.tsx lines 1371-1445 (parseCsv) and 1447-1787 (fetchCsvDirectory):** - -```typescript -import { CsvTypeOption, DirectoryMemberData, DirectoryFidgetSettings } from "../types"; -import { extractNeynarPrimaryAddress, extractNeynarSocialAccounts } from "@/common/data/api/token/utils"; -import { buildEtherscanUrl } from "@/common/data/api/token/utils"; - -export function parseCsv(raw: string, type: CsvTypeOption): string[] { - // Move implementation from Directory.tsx lines 1371-1445 -} - -export async function fetchCsvDirectory( - settings: DirectoryFidgetSettings, - controller: AbortController -): Promise { - // Move implementation from Directory.tsx lines 1447-1787 - // This handles: - // - CSV parsing - // - Username/FID/Address lookup - // - Neynar API calls - // - ENS resolution - // - Member data transformation -} -``` - ---- - -### 3.9 `src/fidgets/token/dataSources/farcasterChannelSource.ts` (NEW) - -**Extract from Directory.tsx lines 1263-1369:** - -```typescript -import { DirectoryMemberData, DirectoryFidgetSettings, DirectoryChannelFilterOption } from "../types"; -import { extractNeynarPrimaryAddress, extractNeynarSocialAccounts } from "@/common/data/api/token/utils"; -import { buildEtherscanUrl } from "@/common/data/api/token/utils"; - -export async function fetchFarcasterChannelDirectory( - settings: DirectoryFidgetSettings, - controller: AbortController -): Promise { - // Move implementation from Directory.tsx lines 1263-1369 - // This handles: - // - Channel members/followers fetching - // - User data transformation - // - Sorting by followers -} -``` - ---- - -### 3.10 `src/fidgets/token/dataSources/tokenHoldersSource.ts` (NEW) - -**Extract from Directory.tsx lines 1219-1261:** - -```typescript -import { DirectoryMemberData, DirectoryFidgetSettings } from "../types"; - -export async function fetchTokenHoldersDirectory( - settings: DirectoryFidgetSettings, - controller: AbortController -): Promise<{ - members: DirectoryMemberData[]; - tokenSymbol: string | null; - tokenDecimals: number | null; -}> { - // Move implementation from Directory.tsx lines 1219-1261 - // This handles: - // - API call to /api/token/directory - // - Response parsing - // - Sorting -} -``` - ---- - -### 3.11 `src/fidgets/token/hooks/useDirectoryData.ts` (NEW) - -**Extract from Directory.tsx lines 1073-1200, 1788-1825:** - -```typescript -import { useState, useCallback, useRef, useEffect, useMemo } from "react"; -import { DirectoryFidgetData, DirectoryFidgetSettings } from "../types"; -import { fetchTokenHoldersDirectory } from "../dataSources/tokenHoldersSource"; -import { fetchFarcasterChannelDirectory } from "../dataSources/farcasterChannelSource"; -import { fetchCsvDirectory } from "../dataSources/csvSource"; -import { sortMembers } from "../utils/sorting"; -import { STALE_AFTER_MS } from "../constants"; -import { isEqual } from "lodash"; - -export function useDirectoryData( - settings: DirectoryFidgetSettings, - initialData: DirectoryFidgetData | undefined, - saveData: (data: DirectoryFidgetData) => Promise -) { - const [directoryData, setDirectoryData] = useState(() => ({ - members: initialData?.members ?? [], - lastUpdatedTimestamp: initialData?.lastUpdatedTimestamp ?? null, - tokenSymbol: initialData?.tokenSymbol ?? null, - tokenDecimals: initialData?.tokenDecimals ?? null, - lastFetchSettings: initialData?.lastFetchSettings, - })); - - const [isRefreshing, setIsRefreshing] = useState(false); - const [error, setError] = useState(null); - const abortControllerRef = useRef(null); - - // Sync with prop data - useEffect(() => { - // Move logic from Directory.tsx lines 1087-1113 - }, [initialData]); - - // Cleanup on unmount - useEffect(() => { - return () => { - abortControllerRef.current?.abort(); - }; - }, []); - - const persistDataIfChanged = useCallback( - async (payload: DirectoryFidgetData) => { - // Move logic from Directory.tsx lines 1201-1217 - }, - [directoryData, saveData] - ); - - const fetchDirectory = useCallback(async () => { - // Move logic from Directory.tsx lines 1788-1825 - // Routes to appropriate data source based on settings.source - }, [settings, persistDataIfChanged]); - - const shouldRefresh = useMemo(() => { - // Move logic from Directory.tsx lines 1149-1199 - }, [settings, directoryData]); - - // Auto-refresh effect - useEffect(() => { - // Move logic from Directory.tsx lines 1189-1199 - }, [shouldRefresh, fetchDirectory]); - - return { - directoryData, - isRefreshing, - error, - fetchDirectory, - refresh: fetchDirectory, - }; -} -``` - ---- - -### 3.12 `src/fidgets/token/hooks/useDirectoryState.ts` (NEW) - -**Extract from Directory.tsx lines 1004-1021, 1030-1031, 1145-1147:** - -```typescript -import { useState, useEffect } from "react"; -import { DirectorySortOption, DirectoryLayoutStyle, DirectoryChannelFilterOption, DirectoryFidgetSettings } from "../types"; -import { sanitizeSortOption } from "../utils/sorting"; - -export function useDirectoryState(settings: DirectoryFidgetSettings) { - const [currentSort, setCurrentSort] = useState( - sanitizeSortOption(settings.sortBy) - ); - const [currentLayout, setCurrentLayout] = useState( - settings.layoutStyle - ); - const [currentChannelFilter, setCurrentChannelFilter] = useState( - (settings.channelFilter ?? "members") as DirectoryChannelFilterOption - ); - const [currentPage, setCurrentPage] = useState(1); - const [debouncedChannelName, setDebouncedChannelName] = useState( - (settings.channelName ?? "").trim() - ); - - // Sync with settings changes - useEffect(() => { - setCurrentSort(sanitizeSortOption(settings.sortBy)); - setCurrentLayout(settings.layoutStyle); - setCurrentChannelFilter((settings.channelFilter ?? "members") as DirectoryChannelFilterOption); - setCurrentPage(1); - }, [settings.layoutStyle, settings.sortBy, settings.channelFilter]); - - // Debounce channel name - useEffect(() => { - // Move logic from Directory.tsx lines 1121-1128 - }, [settings.channelName, settings.source]); - - return { - currentSort, - setCurrentSort, - currentLayout, - setCurrentLayout, - currentChannelFilter, - setCurrentChannelFilter, - currentPage, - setCurrentPage, - debouncedChannelName, - }; -} -``` - ---- - -### 3.13 `src/fidgets/token/hooks/useDirectoryFilters.ts` (NEW) - -**Extract from Directory.tsx lines 1893-1943:** - -```typescript -import { useMemo } from "react"; -import { DirectoryMemberData, DirectorySortOption, DirectoryIncludeOption } from "../types"; -import { sortMembers } from "../utils/sorting"; - -export function useDirectoryFilters( - members: DirectoryMemberData[], - sortBy: DirectorySortOption, - includeFilter: DirectoryIncludeOption, - source: "tokenHolders" | "farcasterChannel" | "csv" -) { - const filteredSortedMembers = useMemo(() => { - // Move logic from Directory.tsx lines 1893-1915 - // Filters by includeFilter - // Sorts by sortBy - }, [members, sortBy, includeFilter, source]); - - const emptyStateMessage = useMemo(() => { - // Move logic from Directory.tsx lines 1932-1942 - }, [source, includeFilter]); - - return { - filteredSortedMembers, - emptyStateMessage, - }; -} -``` - ---- - -### 3.14 `src/fidgets/token/hooks/useDirectoryPagination.ts` (NEW) - -**Extract from Directory.tsx lines 1916-1930:** - -```typescript -import { useMemo } from "react"; -import { PAGE_SIZE } from "../constants"; - -export function useDirectoryPagination( - members: DirectoryMemberData[], - currentPage: number -) { - const pageCount = useMemo(() => { - return Math.ceil(members.length / PAGE_SIZE); - }, [members.length]); - - const displayedMembers = useMemo(() => { - const start = (currentPage - 1) * PAGE_SIZE; - const end = start + PAGE_SIZE; - return members.slice(start, end); - }, [members, currentPage]); - - return { - pageCount, - displayedMembers, - }; -} -``` - ---- - -### 3.15 `src/fidgets/token/utils/sorting.ts` (NEW) - -**Extract from Directory.tsx lines 968-995:** - -```typescript -import { DirectorySortOption, DirectoryMemberData } from "../types"; - -export function sanitizeSortOption(value: unknown): DirectorySortOption { - return value === "followers" ? "followers" : "tokenHoldings"; -} - -export function sortMembers( - members: DirectoryMemberData[], - sortBy: DirectorySortOption -): DirectoryMemberData[] { - const entries = [...members]; - - if (sortBy === "followers") { - entries.sort((a, b) => (b.followers ?? -1) - (a.followers ?? -1)); - return entries; - } - - entries.sort((a, b) => { - try { - const aValue = BigInt(a.balanceRaw ?? "0"); - const bValue = BigInt(b.balanceRaw ?? "0"); - if (bValue > aValue) return 1; - if (bValue < aValue) return -1; - return 0; - } catch (error) { - return 0; - } - }); - - return entries; -} -``` - ---- - -### 3.16 `src/fidgets/token/Directory.tsx` (REFACTORED - Main Component) - -**Final structure (~200-250 lines):** - -```typescript -import React, { useMemo } from "react"; -import { FidgetArgs } from "@/common/fidgets"; -import { DirectoryFidgetSettings, DirectoryFidgetData } from "./types"; -import { directoryProperties } from "./Directory.module"; -import { useDirectoryData } from "./hooks/useDirectoryData"; -import { useDirectoryState } from "./hooks/useDirectoryState"; -import { useDirectoryFilters } from "./hooks/useDirectoryFilters"; -import { useDirectoryPagination } from "./hooks/useDirectoryPagination"; -import { DirectoryCardView } from "./components/DirectoryCardView"; -import { DirectoryListView } from "./components/DirectoryListView"; -import { PaginationControls } from "./components/PaginationControls"; -import { resolveFontFamily } from "@/common/lib/utils/directoryUtils"; -import { FONT_FAMILY_OPTIONS } from "@/common/lib/theme/fonts"; - -const Directory: React.FC> = ({ - settings, - data, - saveData, -}) => { - // Data fetching - const { directoryData, isRefreshing, error, refresh } = useDirectoryData( - settings, - data, - saveData - ); - - // Local state - const { - currentSort, - setCurrentSort, - currentLayout, - setCurrentLayout, - currentChannelFilter, - setCurrentChannelFilter, - currentPage, - setCurrentPage, - debouncedChannelName, - } = useDirectoryState(settings); - - // Filtering and sorting - const { filteredSortedMembers, emptyStateMessage } = useDirectoryFilters( - directoryData.members, - currentSort, - settings.include ?? "holdersWithFarcasterAccount", - settings.source ?? "tokenHolders" - ); - - // Pagination - const { pageCount, displayedMembers } = useDirectoryPagination( - filteredSortedMembers, - currentPage - ); - - // Font styles - const primaryFontFamily = useMemo( - () => resolveFontFamily(settings.primaryFontFamily, "var(--user-theme-headings-font)", FONT_FAMILY_OPTIONS), - [settings.primaryFontFamily] - ); - // ... other style computations - - // Render - return ( -
- {/* Header */} - {/* View controls */} - {/* Error display */} - {/* Content: Card or List view */} - {currentLayout === "list" ? ( - - ) : ( - - )} - {/* Pagination */} -
- ); -}; - -export default Directory; -``` - ---- - -## 4. Consolidation Summary - -### Functions to Consolidate: - -| Function | Current Location(s) | Target Location | Priority | -|----------|-------------------|-----------------|----------| -| `parseSocialRecordValue` / `parseSocialRecord` | Directory.tsx, utils.ts | `utils.ts` (keep existing) | 🔴 High | -| `extractNeynarPrimaryAddress` | Directory.tsx, transform.ts | `utils.ts` (new) | 🔴 High | -| `extractNeynarSocialAccounts` | Directory.tsx | `utils.ts` (new) | 🔴 High | -| `normalizeAddress` | Directory.tsx, utils.ts | `utils.ts` (keep existing) | 🔴 High | -| `buildEtherscanUrl` | Directory.tsx | `utils.ts` (new) | 🟡 Medium | -| `getBlockExplorerLink` | Directory.tsx | `utils.ts` (new) | 🟡 Medium | -| `formatTokenBalance` | Directory.tsx | `directoryUtils.ts` (new) | 🟡 Medium | -| `resolveFontFamily` | Directory.tsx | `directoryUtils.ts` (new) | 🟡 Medium | -| `getLastActivityLabel` | Directory.tsx | `directoryUtils.ts` (new) | 🟡 Medium | -| `getMemberPrimaryLabel` | Directory.tsx | `directoryUtils.ts` (new) | 🟡 Medium | -| `getMemberSecondaryLabel` | Directory.tsx | `directoryUtils.ts` (new) | 🟡 Medium | -| `getMemberAvatarSrc` | Directory.tsx | `directoryUtils.ts` (new) | 🟡 Medium | -| `getMemberAvatarFallback` | Directory.tsx | `directoryUtils.ts` (new) | 🟡 Medium | -| `getFarcasterProfileUrl` | Directory.tsx | `directoryUtils.ts` (new) | 🟡 Medium | -| `getEnsProfileUrl` | Directory.tsx | `directoryUtils.ts` (new) | 🟡 Medium | -| `sortMembers` | Directory.tsx | `utils/sorting.ts` (new) | 🟡 Medium | -| `sanitizeSortOption` | Directory.tsx | `utils/sorting.ts` (new) | 🟡 Medium | - ---- - -## 5. Migration Steps - -### Phase 1: Extract Shared Utilities (Week 1) -1. ✅ Move duplicate functions to `utils.ts` -2. ✅ Create `directoryUtils.ts` for component-specific utilities -3. ✅ Update imports in both Directory.tsx and API files - -### Phase 2: Extract Components (Week 2) -1. ✅ Extract `BadgeIcons`, `ProfileLink`, `PaginationControls` -2. ✅ Extract `DirectoryCardView` and `DirectoryListView` -3. ✅ Update Directory.tsx to use new components - -### Phase 3: Extract Data Sources (Week 3) -1. ✅ Extract CSV parsing and fetching -2. ✅ Extract Farcaster channel fetching -3. ✅ Extract token holders fetching -4. ✅ Create unified data source interface - -### Phase 4: Extract Hooks (Week 4) -1. ✅ Extract `useDirectoryData` -2. ✅ Extract `useDirectoryState` -3. ✅ Extract `useDirectoryFilters` -4. ✅ Extract `useDirectoryPagination` - -### Phase 5: Refactor Main Component (Week 5) -1. ✅ Simplify Directory.tsx to orchestration only -2. ✅ Update all imports -3. ✅ Test all functionality -4. ✅ Remove old code - ---- - -## 6. Expected Results - -### Before: -- **Directory.tsx**: 2,373 lines -- **Duplicated functions**: 5+ -- **React hooks**: 45+ -- **Maintainability**: Low - -### After: -- **Directory.tsx**: ~200-250 lines -- **Total modules**: 15+ focused files -- **Duplicated functions**: 0 -- **React hooks**: Organized into custom hooks -- **Maintainability**: High -- **Testability**: High (each module can be tested independently) - ---- - -## 7. Testing Strategy - -After refactoring, each module should have: -- **Unit tests** for utility functions -- **Component tests** for UI components -- **Hook tests** for custom hooks -- **Integration tests** for data sources -- **E2E tests** for full Directory functionality - diff --git a/EXISTING_UTILITIES_ANALYSIS.md b/EXISTING_UTILITIES_ANALYSIS.md deleted file mode 100644 index c4591c439..000000000 --- a/EXISTING_UTILITIES_ANALYSIS.md +++ /dev/null @@ -1,208 +0,0 @@ -# Existing Utilities Analysis - -This document identifies which utilities from the Directory refactoring plan already exist in the codebase and which need to be created. - ---- - -## ✅ Already Existing Utilities - -### 1. **Social Record Parsing** ✅ -- **Location**: `src/common/data/api/token/utils.ts` (lines 64-102) -- **Function**: `parseSocialRecord(value, platform)` -- **Status**: ✅ Already exists and is exported -- **Action**: Use existing function, remove duplicate `parseSocialRecordValue` from Directory.tsx - -### 2. **Address Normalization** ✅ -- **Location**: `src/common/data/api/token/utils.ts` (lines 7-9) -- **Function**: `normalizeAddress(address)` -- **Status**: ✅ Already exists and is exported -- **Action**: Use existing function, remove duplicate from Directory.tsx - -### 3. **Farcaster CDN URL Conversion** ✅ -- **Location**: `src/common/lib/utils/farcasterCdn.ts` -- **Function**: `toFarcasterCdnUrl(url)` -- **Status**: ✅ Already exists and is used in Directory.tsx -- **Action**: Continue using existing function - -### 4. **Block Explorer URLs** ✅ (Partial) -- **Location**: `src/common/components/molecules/AlchemyChainSelector.tsx` (lines 12-26) -- **Data**: `CHAIN_OPTIONS` array contains `scanUrl` for each chain -- **Status**: ✅ Data exists but not as a utility function -- **Action**: Extract to utility function or use existing data structure -- **Note**: Directory uses `"base" | "polygon" | "mainnet"` but CHAIN_OPTIONS uses `AlchemyNetwork` type. Need to map or create adapter. - -### 5. **Ethereum Address Formatting** ✅ -- **Location**: `src/common/lib/utils/ethereum.ts` -- **Function**: `formatEthereumAddress(address)` -- **Status**: ✅ Exists (used in ScanAddress.tsx) -- **Action**: Can be used for address display formatting - -### 6. **Number Formatting** ✅ (Different purpose) -- **Location**: `src/common/lib/utils/formatNumber.ts` -- **Function**: `formatNumber(value)` - formats large numbers (1B, 1M, 1K) -- **Status**: ✅ Exists but different purpose than token balance formatting -- **Action**: Directory's `formatTokenBalance` is more specific (handles decimals, locale), keep separate - ---- - -## ❌ Missing Utilities (Need to Create) - -### 1. **Neynar Primary Address Extraction** ❌ -- **Current**: Duplicated in `Directory.tsx` (lines 605-646) and `transform.ts` (lines 104-164 as `extractPrimaryAddress`) -- **Action**: Consolidate into `src/common/data/api/token/utils.ts` -- **Priority**: 🔴 High - -### 2. **Neynar Social Accounts Extraction** ❌ -- **Current**: Only in `Directory.tsx` (lines 648-681) -- **Similar**: `transform.ts` has `extractNeynarProfileData` (lines 34-99) which includes social accounts -- **Action**: Consolidate into `src/common/data/api/token/utils.ts` -- **Priority**: 🔴 High - -### 3. **Etherscan URL Builder** ❌ -- **Current**: Only in `Directory.tsx` (line 683) -- **Similar**: `ScanAddress.tsx` builds URLs but inline (line 20) -- **Action**: Create utility function in `src/common/data/api/token/utils.ts` or `src/common/lib/utils/links.ts` -- **Priority**: 🟡 Medium - -### 4. **Block Explorer Link Builder** ❌ -- **Current**: Only in `Directory.tsx` (lines 688-692, 929-930) -- **Similar**: `ScanAddress.tsx` builds URLs inline -- **Action**: Create utility function, can use `CHAIN_OPTIONS` from `AlchemyChainSelector.tsx` as reference -- **Priority**: 🟡 Medium - -### 5. **Token Balance Formatting** ❌ -- **Current**: Only in `Directory.tsx` (lines 706-716) -- **Action**: Create in `src/common/lib/utils/directoryUtils.ts` (new file) -- **Priority**: 🟡 Medium - -### 6. **Font Family Resolution** ❌ -- **Current**: Only in `Directory.tsx` (lines 694-704) -- **Action**: Create in `src/common/lib/utils/directoryUtils.ts` (new file) -- **Priority**: 🟡 Medium - -### 7. **Last Activity Label** ❌ -- **Current**: Only in `Directory.tsx` (lines 955-966) -- **Uses**: `formatDistanceToNow` from `date-fns` -- **Action**: Create in `src/common/lib/utils/directoryUtils.ts` (new file) -- **Priority**: 🟡 Medium - -### 8. **Member Label Helpers** ❌ -- **Current**: Only in `Directory.tsx`: - - `getMemberPrimaryLabel` (line 932) - - `getMemberSecondaryLabel` (lines 935-945) - - `getMemberAvatarSrc` (lines 947-950) - - `getMemberAvatarFallback` (lines 952-953) -- **Action**: Create in `src/common/lib/utils/directoryUtils.ts` (new file) -- **Priority**: 🟡 Medium - -### 9. **Profile URL Builders** ❌ -- **Current**: Only in `Directory.tsx`: - - `getFarcasterProfileUrl` (lines 724-733) - - `getEnsProfileUrl` (lines 735-736) -- **Similar**: Profile links use `/s/[handle]` pattern (see `ProfileSpace.tsx`) -- **Action**: Create in `src/common/lib/utils/directoryUtils.ts` (new file) -- **Priority**: 🟡 Medium - ---- - -## 🔄 Components to Extract - -### 1. **BadgeIcons Component** ❌ -- **Current**: Only in `Directory.tsx` (lines 738-927) -- **Similar**: `src/common/components/atoms/badge.tsx` exists but is a generic badge component (different purpose) -- **Action**: Extract to `src/fidgets/token/components/BadgeIcons.tsx` -- **Priority**: 🔴 High - -### 2. **ProfileLink Component** ❌ -- **Current**: Only in `Directory.tsx` (lines 2285-2317) -- **Similar**: Profile links exist throughout codebase (e.g., `/s/[handle]` pattern) -- **Action**: Extract to `src/fidgets/token/components/ProfileLink.tsx` -- **Priority**: 🟡 Medium - -### 3. **PaginationControls Component** ❌ -- **Current**: Only in `Directory.tsx` (lines 2319-2366) -- **Similar**: No existing pagination component found -- **Action**: Extract to `src/fidgets/token/components/PaginationControls.tsx` -- **Priority**: 🟡 Medium - ---- - -## 📋 Updated Refactoring Plan - -### Phase 1: Consolidate Existing Utilities - -1. **Remove duplicates from Directory.tsx**: - - Remove `parseSocialRecordValue` → use `parseSocialRecord` from `utils.ts` - - Remove `normalizeAddress` → use existing from `utils.ts` - -2. **Add missing utilities to `src/common/data/api/token/utils.ts`**: - - `extractNeynarPrimaryAddress` (consolidate from Directory.tsx and transform.ts) - - `extractNeynarSocialAccounts` (consolidate from Directory.tsx) - - `buildEtherscanUrl` (from Directory.tsx) - - `getBlockExplorerLink` (from Directory.tsx, can use CHAIN_OPTIONS pattern) - -3. **Create `src/common/lib/utils/directoryUtils.ts`**: - - `formatTokenBalance` - - `resolveFontFamily` - - `getLastActivityLabel` - - `getMemberPrimaryLabel` - - `getMemberSecondaryLabel` - - `getMemberAvatarSrc` - - `getMemberAvatarFallback` - - `getFarcasterProfileUrl` - - `getEnsProfileUrl` - -### Phase 2: Extract Components - -1. Extract `BadgeIcons` component -2. Extract `ProfileLink` component -3. Extract `PaginationControls` component -4. Extract view components (`DirectoryCardView`, `DirectoryListView`) - -### Phase 3: Extract Hooks and Data Sources - -1. Extract data fetching hooks -2. Extract state management hooks -3. Extract data source modules - ---- - -## 🔍 Key Findings - -### Duplication Status: -- ✅ **2 functions** already exist and can be reused (`parseSocialRecord`, `normalizeAddress`) -- ❌ **15+ functions** need to be created/consolidated -- ❌ **3 components** need to be extracted - -### Existing Patterns to Leverage: -1. **Block Explorer URLs**: `CHAIN_OPTIONS` in `AlchemyChainSelector.tsx` has scan URLs - can be used as reference -2. **Profile Links**: `/s/[handle]` pattern used throughout (see `ProfileSpace.tsx`) -3. **Farcaster URLs**: `warpcast.com` URLs used in multiple places -4. **Address Formatting**: `formatEthereumAddress` exists in `ethereum.ts` - -### Type Compatibility Notes: -- Directory uses `DirectoryNetwork = "base" | "polygon" | "mainnet"` -- `AlchemyChainSelector` uses `AlchemyNetwork` which includes more chains -- May need adapter function or type mapping - ---- - -## 📊 Summary Statistics - -| Category | Existing | Missing | Total | -|----------|----------|---------|-------| -| Utility Functions | 2 | 15+ | 17+ | -| Components | 0 | 3 | 3 | -| Hooks | 0 | 4 | 4 | -| Data Sources | 0 | 3 | 3 | - ---- - -## 🎯 Recommended Action Plan - -1. **Immediate**: Remove duplicate `parseSocialRecordValue` and `normalizeAddress` from Directory.tsx -2. **High Priority**: Consolidate Neynar extraction functions into `utils.ts` -3. **Medium Priority**: Create `directoryUtils.ts` for component-specific utilities -4. **Medium Priority**: Extract BadgeIcons component (largest component to extract) -5. **Lower Priority**: Extract other components and hooks - diff --git a/docs/ADMIN_ASSET_UPLOAD_STRATEGY.md b/docs/ADMIN_ASSET_UPLOAD_STRATEGY.md new file mode 100644 index 000000000..e37049e9c --- /dev/null +++ b/docs/ADMIN_ASSET_UPLOAD_STRATEGY.md @@ -0,0 +1,509 @@ +# Admin Asset Upload Strategy + +## Overview + +Admins need to upload assets through the admin UI (no repo access), and those assets must be available at build time. This requires: + +1. **Asset upload** - Admins upload to Supabase Storage +2. **Asset storage** - Store asset URLs/paths in database +3. **Build-time download** - Download assets during build +4. **Asset resolution** - Make assets available to Next.js + +## Architecture + +``` +Admin UI → Upload to Supabase Storage → Store URLs in DB + ↓ + Build Time: + Download Assets + → Copy to public/ + → Reference in Config +``` + +## Implementation + +### 1. Database Schema + +Add asset storage information to the config: + +```sql +-- In community_configs table, assets_config JSONB column: +{ + "logos": { + "main": { + "storagePath": "community-assets/nouns/logo.svg", + "publicUrl": "https://{supabase-url}/storage/v1/object/public/community-assets/nouns/logo.svg", + "localPath": "/images/nouns/logo.svg" // Where it will be in public/ after build + }, + "icon": { + "storagePath": "community-assets/nouns/noggles.svg", + "publicUrl": "https://...", + "localPath": "/images/nouns/noggles.svg" + }, + // ... etc + } +} +``` + +Or simpler - just store storage paths, generate URLs at build time: + +```json +{ + "assets": { + "logos": { + "main": "community-assets/nouns/logo.svg", + "icon": "community-assets/nouns/noggles.svg", + "favicon": "community-assets/nouns/favicon.ico", + "appleTouch": "community-assets/nouns/apple-touch.png", + "og": "community-assets/nouns/og.png", + "splash": "community-assets/nouns/splash.png" + } + } +} +``` + +### 2. Supabase Storage Setup + +Create a storage bucket for community assets: + +```sql +-- Create storage bucket +INSERT INTO storage.buckets (id, name, public) +VALUES ('community-assets', 'community-assets', true); + +-- Set up RLS policies +CREATE POLICY "Admins can upload assets" +ON storage.objects FOR INSERT +WITH CHECK ( + bucket_id = 'community-assets' AND + EXISTS ( + SELECT 1 FROM community_config_admins cca + WHERE cca.admin_identity_public_key = auth.uid()::text + AND cca.is_active = true + ) +); + +CREATE POLICY "Public can read assets" +ON storage.objects FOR SELECT +USING (bucket_id = 'community-assets'); +``` + +### 3. Build-Time Asset Download + +```javascript +// next.config.mjs + +import { createClient } from '@supabase/supabase-js'; +import { writeFile, mkdir } from 'fs/promises'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { createWriteStream } from 'fs'; +import { pipeline } from 'stream/promises'; +import { Readable } from 'stream'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +async function downloadAsset(supabase, storagePath, localPath) { + try { + const { data, error } = await supabase.storage + .from('community-assets') + .download(storagePath); + + if (error || !data) { + console.warn(`⚠️ Failed to download ${storagePath}:`, error?.message); + return false; + } + + // Ensure directory exists + const fullPath = join(__dirname, 'public', localPath); + const dir = dirname(fullPath); + await mkdir(dir, { recursive: true }); + + // Convert blob to buffer and write + const buffer = Buffer.from(await data.arrayBuffer()); + await writeFile(fullPath, buffer); + + console.log(`✅ Downloaded ${storagePath} → ${localPath}`); + return true; + } catch (error) { + console.error(`❌ Error downloading ${storagePath}:`, error.message); + return false; + } +} + +async function downloadAssets(config, communityId) { + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + + if (!supabaseUrl || !supabaseKey) { + console.warn('⚠️ Missing Supabase credentials, skipping asset download'); + return config; + } + + const supabase = createClient(supabaseUrl, supabaseKey); + const assets = config.assets?.logos || {}; + const downloadedAssets = {}; + + // Download each asset + for (const [key, storagePath] of Object.entries(assets)) { + if (typeof storagePath === 'string' && storagePath.startsWith('community-assets/')) { + // Generate local path + const localPath = `/images/${communityId}/${storagePath.split('/').pop()}`; + + // Download asset + const success = await downloadAsset(supabase, storagePath, localPath); + + if (success) { + downloadedAssets[key] = localPath; + } else { + // Fall back to static asset if download fails + console.warn(`⚠️ Using static fallback for ${key}`); + downloadedAssets[key] = null; // Will be replaced with static in loader + } + } else if (typeof storagePath === 'string') { + // Already a public path, use as-is + downloadedAssets[key] = storagePath; + } + } + + // Update config with downloaded asset paths + return { + ...config, + assets: { + logos: downloadedAssets + } + }; +} + +async function generateConfig() { + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + + if (!supabaseUrl || !supabaseKey) { + console.warn('⚠️ Missing Supabase credentials, using static config'); + return; + } + + const supabase = createClient(supabaseUrl, supabaseKey); + const community = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + + try { + const { data, error } = await supabase + .rpc('get_active_community_config', { p_community_id: community }) + .single(); + + if (error || !data) { + console.warn('⚠️ No config in DB, using static'); + return; + } + + // Download assets from Supabase Storage + const configWithAssets = await downloadAssets(data, community); + + // Set as env var + process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(configWithAssets); + console.log('✅ Loaded config with downloaded assets from database'); + } catch (error) { + console.warn('⚠️ Error loading config:', error.message); + } +} + +await generateConfig(); + +// Continue with Next.js config... +``` + +### 4. Config Loader with Asset Fallback + +```typescript +// src/config/index.ts + +import { SystemConfig } from './systemConfig'; +import { nounsSystemConfig } from './nouns/index'; +import { clankerSystemConfig } from './clanker/index'; +import { exampleSystemConfig } from './example/index'; + +// Import static assets for fallback +import { nounsAssets } from './nouns/nouns.assets'; +import { clankerAssets } from './clanker/clanker.assets'; +import { exampleAssets } from './example/example.assets'; + +const STATIC_CONFIGS: Record = { + nouns: nounsSystemConfig, + clanker: clankerSystemConfig as unknown as SystemConfig, + example: exampleSystemConfig, +}; + +const STATIC_ASSETS: Record = { + nouns: nounsAssets, + clanker: clankerAssets, + example: exampleAssets, +}; + +export const loadSystemConfig = (): SystemConfig => { + const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + const community = communityConfig.toLowerCase(); + + const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; + + if (buildTimeConfig) { + try { + const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; + const staticAssets = STATIC_ASSETS[community]; + + // Merge assets: use downloaded assets, fall back to static + const mergedAssets = { + logos: { + main: dbConfig.assets?.logos?.main || staticAssets?.logos.main, + icon: dbConfig.assets?.logos?.icon || staticAssets?.logos.icon, + favicon: dbConfig.assets?.logos?.favicon || staticAssets?.logos.favicon, + appleTouch: dbConfig.assets?.logos?.appleTouch || staticAssets?.logos.appleTouch, + og: dbConfig.assets?.logos?.og || staticAssets?.logos.og, + splash: dbConfig.assets?.logos?.splash || staticAssets?.logos.splash, + } + }; + + return { + ...dbConfig, + assets: mergedAssets, + }; + } catch (error) { + console.warn('Failed to parse build-time config:', error); + } + } + + return STATIC_CONFIGS[community] || STATIC_CONFIGS.nouns; +}; +``` + +### 5. Admin Upload API + +```typescript +// src/app/api/admin/assets/upload/route.ts + +import { createClient } from '@supabase/supabase-js'; +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(request: NextRequest) { + const formData = await request.formData(); + const file = formData.get('file') as File; + const communityId = formData.get('communityId') as string; + const assetType = formData.get('assetType') as string; // 'main', 'icon', etc. + + // Verify admin permissions (use your auth system) + const adminIdentity = request.headers.get('x-admin-identity'); + if (!adminIdentity) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! + ); + + // Check admin permissions + const { data: admin } = await supabase + .from('community_config_admins') + .select('id') + .eq('community_id', communityId) + .eq('admin_identity_public_key', adminIdentity) + .eq('is_active', true) + .single(); + + if (!admin) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + + // Generate storage path + const fileExt = file.name.split('.').pop(); + const storagePath = `community-assets/${communityId}/${assetType}.${fileExt}`; + + // Upload to Supabase Storage + const { data, error } = await supabase.storage + .from('community-assets') + .upload(storagePath, file, { + upsert: true, + contentType: file.type, + }); + + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } + + // Get public URL + const { data: { publicUrl } } = supabase.storage + .from('community-assets') + .getPublicUrl(storagePath); + + // Update config in database + // (You'll need to update the assets_config JSONB column) + + return NextResponse.json({ + success: true, + storagePath, + publicUrl, + }); +} +``` + +### 6. Admin UI Component + +```typescript +// src/app/admin/config/[communityId]/assets/page.tsx + +'use client'; + +import { useState } from 'react'; +import { useDropzone } from 'react-dropzone'; + +export default function AssetUploadPage({ params }: { params: { communityId: string } }) { + const [uploading, setUploading] = useState(false); + + const onDrop = async (acceptedFiles: File[]) => { + const file = acceptedFiles[0]; + if (!file) return; + + setUploading(true); + + const formData = new FormData(); + formData.append('file', file); + formData.append('communityId', params.communityId); + formData.append('assetType', 'main'); // or from UI selection + + try { + const response = await fetch('/api/admin/assets/upload', { + method: 'POST', + body: formData, + headers: { + 'x-admin-identity': getAdminIdentity(), // Your auth + }, + }); + + const result = await response.json(); + + if (result.success) { + // Update config in DB + await updateConfigAsset(params.communityId, 'main', result.storagePath); + alert('Asset uploaded! Trigger rebuild to see changes.'); + } + } catch (error) { + console.error('Upload failed:', error); + } finally { + setUploading(false); + } + }; + + const { getRootProps, getInputProps } = useDropzone({ + onDrop, + accept: { + 'image/*': ['.svg', '.png', '.jpg', '.jpeg', '.webp'], + }, + }); + + return ( +
+

Upload Assets

+
+ +

Drag & drop assets here, or click to select

+
+ {uploading &&

Uploading...

} +
+ ); +} +``` + +## Asset Path Structure + +### Storage Paths (in Supabase) +``` +community-assets/ + ├── nouns/ + │ ├── logo.svg + │ ├── noggles.svg + │ ├── favicon.ico + │ ├── apple-touch.png + │ ├── og.png + │ └── splash.png + ├── clanker/ + │ └── ... + └── example/ + └── ... +``` + +### Public Paths (after build) +``` +public/ + └── images/ + ├── nouns/ + │ ├── logo.svg + │ ├── noggles.svg + │ └── ... + ├── clanker/ + │ └── ... + └── example/ + └── ... +``` + +## Build Process Flow + +1. **Build starts** → `next.config.mjs` runs +2. **Fetch config** → Get config from database +3. **Download assets** → For each asset in config: + - Download from Supabase Storage + - Save to `public/images/{community}/{filename}` +4. **Update config** → Replace storage paths with public paths +5. **Set env var** → Store config with public paths +6. **Next.js build** → Uses config with public paths +7. **Assets available** → Served from `public/` folder + +## Benefits + +✅ **Admin uploads** - No repo access needed +✅ **Build-time availability** - Assets downloaded before build +✅ **Public paths** - Assets served from `public/` folder +✅ **Fallback safety** - Static assets if download fails +✅ **Version control** - Assets stored in Supabase Storage +✅ **CDN ready** - Can be served via CDN if needed + +## Considerations + +1. **Build time** - Asset downloads add ~1-2 seconds per community +2. **Storage costs** - Supabase Storage usage +3. **Cache invalidation** - May need to clear Next.js cache after asset updates +4. **File size limits** - Set reasonable limits for uploads +5. **File types** - Validate file types (SVG, PNG, etc.) + +## Alternative: CDN URLs + +If you want to skip downloads entirely, you could: + +1. Upload to Supabase Storage +2. Store public URLs directly in config +3. Use URLs directly (no download needed) + +```json +{ + "assets": { + "logos": { + "main": "https://{supabase-url}/storage/v1/object/public/community-assets/nouns/logo.svg" + } + } +} +``` + +**Pros:** Faster builds, no local storage +**Cons:** External dependency, no bundling, CDN costs + +## Recommended Approach + +**Download to public/ folder** because: +- ✅ Assets are part of the build +- ✅ No external dependencies at runtime +- ✅ Can be cached/CDN'd with the app +- ✅ Works offline +- ✅ Predictable paths + +This gives you the best balance of admin flexibility and build reliability. + diff --git a/docs/ASSETS_CONFIG_STORAGE_RELATIONSHIP.md b/docs/ASSETS_CONFIG_STORAGE_RELATIONSHIP.md new file mode 100644 index 000000000..cee4e00b6 --- /dev/null +++ b/docs/ASSETS_CONFIG_STORAGE_RELATIONSHIP.md @@ -0,0 +1,386 @@ +# Assets Config and Storage Relationship + +## Overview + +Yes, `assets_config` is directly tied to assets uploaded to Supabase Storage. The paths stored in `assets_config` reference assets in the `community-assets` storage bucket, which are then downloaded at build time. + +## The Relationship + +### 1. Admin Uploads Asset + +When an admin uploads an asset via the admin UI: + +``` +Admin UI → Upload to Supabase Storage + → Store path in assets_config +``` + +**Example:** +- Admin uploads `logo.svg` for Nouns community +- Asset stored at: `community-assets/nouns/logo.svg` in Supabase Storage +- `assets_config` updated to: `"main": "community-assets/nouns/logo.svg"` + +### 2. Database Storage + +The `assets_config` JSONB column stores the **storage path**: + +```json +{ + "logos": { + "main": "community-assets/nouns/logo.svg", // ← Supabase Storage path + "icon": "community-assets/nouns/noggles.svg", // ← Supabase Storage path + "favicon": "community-assets/nouns/favicon.ico", + "appleTouch": "community-assets/nouns/apple-touch.png", + "og": "community-assets/nouns/og.png", + "splash": "community-assets/nouns/splash.png" + } +} +``` + +### 3. Build-Time Download + +During build (`next.config.mjs`), assets are downloaded: + +```javascript +// next.config.mjs + +async function downloadAssets(config, communityId) { + const assets = config.assets?.logos || {}; + + for (const [key, storagePath] of Object.entries(assets)) { + if (storagePath.startsWith('community-assets/')) { + // Download from Supabase Storage + const { data } = await supabase.storage + .from('community-assets') + .download(storagePath); + + // Save to public/images/{community}/ + const localPath = `/images/${communityId}/${filename}`; + await writeFile(`public${localPath}`, buffer); + + // Update config with public path + assets[key] = localPath; // ← Changed to public path + } + } + + return { ...config, assets: { logos: assets } }; +} +``` + +### 4. Runtime Usage + +After build, the app uses **public paths**: + +```typescript +// src/config/index.ts + +const config = loadSystemConfig(); +const logoSrc = config.assets.logos.main; // "/images/nouns/logo.svg" +``` + +## Complete Flow Diagram + +``` +┌─────────────────┐ +│ Admin Uploads │ +│ logo.svg │ +└────────┬────────┘ + │ + ▼ +┌─────────────────────────┐ +│ Supabase Storage │ +│ community-assets/ │ +│ └─ nouns/ │ +│ └─ logo.svg │ +└────────┬────────────────┘ + │ + ▼ +┌─────────────────────────┐ +│ Database │ +│ assets_config JSONB │ +│ "main": "community- │ +│ assets/nouns/ │ +│ logo.svg" │ +└────────┬────────────────┘ + │ + ▼ +┌─────────────────────────┐ +│ Build Time │ +│ next.config.mjs │ +│ 1. Read storage path │ +│ 2. Download from │ +│ Supabase Storage │ +│ 3. Save to public/ │ +│ 4. Update config to │ +│ public path │ +└────────┬────────────────┘ + │ + ▼ +┌─────────────────────────┐ +│ public/images/ │ +│ └─ nouns/ │ +│ └─ logo.svg │ +└────────┬────────────────┘ + │ + ▼ +┌─────────────────────────┐ +│ Runtime │ +│ App uses: │ +│ "/images/nouns/ │ +│ logo.svg" │ +└─────────────────────────┘ +``` + +## Database Schema + +### Storage Path Format (in database) + +```json +{ + "logos": { + "main": "community-assets/nouns/logo.svg" + } +} +``` + +**Format:** `community-assets/{communityId}/{filename}` + +### Public Path Format (after build) + +```json +{ + "logos": { + "main": "/images/nouns/logo.svg" + } +} +``` + +**Format:** `/images/{communityId}/{filename}` + +## Implementation Details + +### Admin Upload API + +```typescript +// src/app/api/admin/assets/upload/route.ts + +export async function POST(request: NextRequest) { + const file = formData.get('file') as File; + const communityId = formData.get('communityId') as string; + const assetType = formData.get('assetType') as string; // 'main', 'icon', etc. + + // Upload to Supabase Storage + const storagePath = `community-assets/${communityId}/${assetType}.${fileExt}`; + + await supabase.storage + .from('community-assets') + .upload(storagePath, file, { upsert: true }); + + // Update assets_config in database + await supabase + .from('community_configs') + .update({ + assets_config: jsonb_set( + assets_config, + `{logos,${assetType}}`, + `"${storagePath}"`::jsonb + ) + }) + .eq('community_id', communityId); + + return { success: true, storagePath }; +} +``` + +### Build-Time Download + +```javascript +// next.config.mjs + +async function downloadAssets(config, communityId) { + const supabase = createClient(supabaseUrl, supabaseKey); + const assets = config.assets?.logos || {}; + const downloadedAssets = {}; + + for (const [key, storagePath] of Object.entries(assets)) { + if (typeof storagePath === 'string' && storagePath.startsWith('community-assets/')) { + // Download from Supabase Storage + const { data } = await supabase.storage + .from('community-assets') + .download(storagePath); + + if (data) { + // Generate local path + const filename = storagePath.split('/').pop(); + const localPath = `/images/${communityId}/${filename}`; + + // Save to public folder + await writeFile(`public${localPath}`, Buffer.from(await data.arrayBuffer())); + + // Update to public path + downloadedAssets[key] = localPath; + } + } else { + // Already a public path or external URL, use as-is + downloadedAssets[key] = storagePath; + } + } + + // Return config with updated paths + return { + ...config, + assets: { + logos: downloadedAssets + } + }; +} +``` + +## Path Resolution Logic + +### In Database (Storage Paths) + +```json +{ + "logos": { + "main": "community-assets/nouns/logo.svg", // ← Storage path + "icon": "community-assets/nouns/noggles.svg", // ← Storage path + "favicon": "/images/favicon.ico" // ← Public path (static fallback) + } +} +``` + +### After Build (Public Paths) + +```json +{ + "logos": { + "main": "/images/nouns/logo.svg", // ← Downloaded from storage + "icon": "/images/nouns/noggles.svg", // ← Downloaded from storage + "favicon": "/images/favicon.ico" // ← Static (not in storage) + } +} +``` + +## Fallback Strategy + +### Static Assets (Not in Storage) + +Some assets might not be uploaded (e.g., default favicon): + +```json +{ + "logos": { + "main": "community-assets/nouns/logo.svg", // ← From storage + "favicon": "/images/favicon.ico" // ← Static fallback + } +} +``` + +**Build-time logic:** +- If path starts with `community-assets/` → Download from storage +- If path starts with `/` → Use as-is (public path) +- If path starts with `http://` or `https://` → Use as-is (external URL) + +## Storage Bucket Structure + +``` +Supabase Storage: community-assets/ +├── nouns/ +│ ├── logo.svg +│ ├── noggles.svg +│ ├── favicon.ico +│ ├── apple-touch.png +│ ├── og.png +│ └── splash.png +├── clanker/ +│ ├── logo.svg +│ └── ... +└── example/ + └── ... +``` + +## Public Folder Structure (After Build) + +``` +public/ +└── images/ + ├── nouns/ + │ ├── logo.svg ← Downloaded from storage + │ ├── noggles.svg ← Downloaded from storage + │ └── ... + ├── clanker/ + │ └── ... + └── favicon.ico ← Static (not downloaded) +``` + +## Key Points + +1. **`assets_config` stores storage paths** - References assets in Supabase Storage +2. **Build-time transformation** - Storage paths → Public paths +3. **Runtime uses public paths** - App serves from `public/images/` +4. **Fallback support** - Can mix storage paths and static paths +5. **Admin updates storage** - Changes reflected after rebuild + +## Example: Complete Flow + +### Step 1: Admin Uploads Logo + +```typescript +// Admin uploads logo.svg via UI +POST /api/admin/assets/upload +{ + file: File, + communityId: "nouns", + assetType: "main" +} + +// Asset stored at: community-assets/nouns/logo.svg +// Database updated: +{ + "assets_config": { + "logos": { + "main": "community-assets/nouns/logo.svg" // ← Storage path + } + } +} +``` + +### Step 2: Build Time + +```javascript +// next.config.mjs runs +1. Fetch config from DB +2. See "main": "community-assets/nouns/logo.svg" +3. Download from Supabase Storage +4. Save to public/images/nouns/logo.svg +5. Update config: "main": "/images/nouns/logo.svg" +6. Store in env var: NEXT_PUBLIC_BUILD_TIME_CONFIG +``` + +### Step 3: Runtime + +```typescript +// App loads config +const config = loadSystemConfig(); +const logoSrc = config.assets.logos.main; // "/images/nouns/logo.svg" + +// Next.js Image component uses public path + // Serves from public/images/nouns/logo.svg +``` + +## Summary + +**Yes, `assets_config` is directly tied to uploaded assets:** + +1. ✅ **Admin uploads** → Stored in Supabase Storage +2. ✅ **Path stored** → `assets_config` contains storage path +3. ✅ **Build downloads** → Assets copied to `public/images/` +4. ✅ **Path updated** → Config uses public path +5. ✅ **Runtime serves** → App uses public path + +The `assets_config` paths act as the **bridge** between: +- **Storage** (where admins upload) +- **Build** (where assets are downloaded) +- **Runtime** (where assets are served) + diff --git a/docs/ASSET_HANDLING_STRATEGY.md b/docs/ASSET_HANDLING_STRATEGY.md new file mode 100644 index 000000000..a625c0521 --- /dev/null +++ b/docs/ASSET_HANDLING_STRATEGY.md @@ -0,0 +1,337 @@ +# Asset Handling Strategy for Database Configs + +## The Challenge + +Assets in the config can be: +1. **Imported modules** (bundled by Next.js) - `import logo from './assets/logo.svg'` +2. **String paths** (public paths) - `"/images/logo.png"` + +When using environment variables, we need to handle both cases. + +## Recommended Approach: Hybrid Strategy + +**Keep assets in repo, store paths in DB, resolve at build time** + +### Strategy Overview + +1. **Assets stay in version control** - `src/config/{community}/assets/` directory +2. **DB stores relative paths** - Just the paths, not the files +3. **Build-time resolution** - Convert DB paths to actual asset references +4. **Fallback to static** - If DB path doesn't exist, use static asset config + +### Implementation + +#### 1. Database Schema + +Store asset paths as strings in the database: + +```sql +-- In community_configs table, assets_config JSONB column: +{ + "logos": { + "main": "/images/nouns/logo.svg", -- Public path + "icon": "./assets/noggles.svg", -- Relative to config dir + "favicon": "/images/favicon.ico", -- Public path + "appleTouch": "/images/apple-touch-icon.png", + "og": "./assets/og.svg", -- Relative path + "splash": "./assets/splash.svg" -- Relative path + } +} +``` + +#### 2. Build-Time Resolution + +```javascript +// next.config.mjs + +import { createClient } from '@supabase/supabase-js'; +import { existsSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +async function resolveAssets(config, communityId) { + const configDir = join(__dirname, 'src', 'config', communityId); + const publicDir = join(__dirname, 'public'); + + const resolvedAssets = { ...config.assets }; + + // Resolve each asset path + for (const [key, value] of Object.entries(config.assets.logos)) { + if (typeof value === 'string') { + // Check if it's a relative path (starts with ./) + if (value.startsWith('./')) { + const assetPath = join(configDir, value.replace('./', '')); + if (existsSync(assetPath)) { + // Keep relative path - Next.js will bundle it + resolvedAssets.logos[key] = value; + } else { + console.warn(`⚠️ Asset not found: ${assetPath}, using static fallback`); + resolvedAssets.logos[key] = null; // Will use static + } + } else if (value.startsWith('/')) { + // Public path - check if exists + const publicPath = join(publicDir, value); + if (existsSync(publicPath)) { + resolvedAssets.logos[key] = value; + } else { + console.warn(`⚠️ Public asset not found: ${publicPath}`); + resolvedAssets.logos[key] = null; + } + } else { + // Assume it's a valid path + resolvedAssets.logos[key] = value; + } + } + } + + return resolvedAssets; +} + +async function generateConfig() { + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + + if (!supabaseUrl || !supabaseKey) return; + + const supabase = createClient(supabaseUrl, supabaseKey); + const community = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + + try { + const { data, error } = await supabase + .rpc('get_active_community_config', { p_community_id: community }) + .single(); + + if (error || !data) return; + + // Resolve asset paths + const resolvedAssets = await resolveAssets(data, community); + const configWithAssets = { + ...data, + assets: resolvedAssets + }; + + // Set as env var + process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(configWithAssets); + console.log('✅ Loaded config with resolved assets from database'); + } catch (error) { + console.warn('⚠️ Error loading config:', error.message); + } +} + +await generateConfig(); +``` + +#### 3. Config Loader with Asset Fallback + +```typescript +// src/config/index.ts + +import { SystemConfig } from './systemConfig'; +import { nounsSystemConfig } from './nouns/index'; +import { exampleSystemConfig } from './example/index'; +import { clankerSystemConfig } from './clanker/index'; + +// Import static asset configs for fallback +import { nounsAssets } from './nouns/nouns.assets'; +import { clankerAssets } from './clanker/clanker.assets'; +import { exampleAssets } from './example/example.assets'; + +const STATIC_CONFIGS: Record = { + nouns: nounsSystemConfig, + example: exampleSystemConfig, + clanker: clankerSystemConfig as unknown as SystemConfig, +}; + +const STATIC_ASSETS: Record = { + nouns: nounsAssets, + clanker: clankerAssets, + example: exampleAssets, +}; + +export const loadSystemConfig = (): SystemConfig => { + const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + const community = communityConfig.toLowerCase(); + + // Try build-time config from env var + const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; + if (buildTimeConfig) { + try { + const config = JSON.parse(buildTimeConfig) as SystemConfig; + + // Merge assets: use DB assets, fall back to static for missing ones + const staticAssets = STATIC_ASSETS[community]; + if (staticAssets) { + config.assets = { + logos: { + main: config.assets?.logos?.main || staticAssets.logos.main, + icon: config.assets?.logos?.icon || staticAssets.logos.icon, + favicon: config.assets?.logos?.favicon || staticAssets.logos.favicon, + appleTouch: config.assets?.logos?.appleTouch || staticAssets.logos.appleTouch, + og: config.assets?.logos?.og || staticAssets.logos.og, + splash: config.assets?.logos?.splash || staticAssets.logos.splash, + } + }; + } + + return config; + } catch { + // Fall through to static + } + } + + // Fall back to static configs + return STATIC_CONFIGS[community] || STATIC_CONFIGS.nouns; +}; +``` + +--- + +## Alternative Approach: Keep Assets Static + +**Simpler**: Only store non-asset config in DB, keep assets as static imports. + +### Implementation + +```typescript +// src/config/index.ts + +export const loadSystemConfig = (): SystemConfig => { + const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + const community = communityConfig.toLowerCase(); + + // Try build-time config from env var + const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; + + if (buildTimeConfig) { + try { + const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; + const staticConfig = STATIC_CONFIGS[community]; + + // Merge: DB config + static assets + return { + ...dbConfig, + assets: staticConfig.assets, // Always use static assets + }; + } catch { + // Fall through + } + } + + return STATIC_CONFIGS[community] || STATIC_CONFIGS.nouns; +}; +``` + +**Pros:** +- ✅ Simplest approach +- ✅ Assets always bundled by Next.js +- ✅ No path resolution needed +- ✅ Type-safe asset imports + +**Cons:** +- ⚠️ Assets can't be updated via admin UI +- ⚠️ Assets require code deployment + +--- + +## Recommended: Hybrid with Smart Fallback + +**Best of both worlds**: Store asset paths in DB, but fall back to static imports if paths don't resolve. + +### Final Implementation + +```typescript +// src/config/index.ts + +import { SystemConfig } from './systemConfig'; +import { nounsSystemConfig } from './nouns/index'; +// ... other imports + +const STATIC_CONFIGS: Record = { + nouns: nounsSystemConfig, + // ... +}; + +export const loadSystemConfig = (): SystemConfig => { + const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + const community = communityConfig.toLowerCase(); + + const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; + + if (buildTimeConfig) { + try { + const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; + const staticConfig = STATIC_CONFIGS[community]; + + // Smart asset merging: use DB assets if valid, otherwise static + const mergedAssets = { + logos: { + main: dbConfig.assets?.logos?.main || staticConfig.assets.logos.main, + icon: dbConfig.assets?.logos?.icon || staticConfig.assets.logos.icon, + favicon: dbConfig.assets?.logos?.favicon || staticConfig.assets.logos.favicon, + appleTouch: dbConfig.assets?.logos?.appleTouch || staticConfig.assets.logos.appleTouch, + og: dbConfig.assets?.logos?.og || staticConfig.assets.logos.og, + splash: dbConfig.assets?.logos?.splash || staticConfig.assets.logos.splash, + } + }; + + return { + ...dbConfig, + assets: mergedAssets, + }; + } catch { + // Fall through + } + } + + return STATIC_CONFIGS[community] || STATIC_CONFIGS.nouns; +}; +``` + +**Benefits:** +- ✅ Admin can update asset paths in DB +- ✅ Falls back to static assets if DB paths invalid +- ✅ Assets still bundled by Next.js (if relative paths) +- ✅ Public paths work for CDN-hosted assets +- ✅ Type-safe with fallbacks + +--- + +## Database Storage Format + +Store assets as simple string paths in the database: + +```json +{ + "assets": { + "logos": { + "main": "./assets/logo.svg", // Relative - will be bundled + "icon": "./assets/noggles.svg", // Relative - will be bundled + "favicon": "/images/favicon.ico", // Public path + "appleTouch": "/images/apple-touch.png", // Public path + "og": "./assets/og.svg", // Relative - will be bundled + "splash": "./assets/splash.svg" // Relative - will be bundled + } + } +} +``` + +**Path conventions:** +- `./assets/...` - Relative to `src/config/{community}/assets/` (bundled) +- `/images/...` - Public path in `public/images/` (not bundled) +- `https://...` - External URL (if needed) + +--- + +## Summary + +**Recommended**: Hybrid approach with smart fallback +- Store asset paths in DB +- Resolve paths at build time +- Fall back to static assets if DB paths invalid +- Supports both bundled (relative) and public (absolute) paths + +This gives admins flexibility while maintaining the benefits of Next.js asset bundling. + diff --git a/docs/ASSET_PERFORMANCE_OPTIMIZATION.md b/docs/ASSET_PERFORMANCE_OPTIMIZATION.md new file mode 100644 index 000000000..4fbb51319 --- /dev/null +++ b/docs/ASSET_PERFORMANCE_OPTIMIZATION.md @@ -0,0 +1,329 @@ +# Asset Performance Optimization Strategy + +## Current Performance Analysis + +### ✅ What's Already Good + +1. **Next.js Image Component** - You're using `` which provides: + - Automatic image optimization + - Lazy loading + - Responsive images + - WebP/AVIF format conversion (configured in `next.config.mjs`) + +2. **Static File Serving** - Assets in `public/` are: + - Served as static files (very fast) + - Can be cached by CDN + - No server processing needed + +3. **Build-Time Download** - Assets downloaded at build time: + - No runtime fetching + - Part of the static build + - Can be optimized during build + +### ⚠️ Potential Performance Issues + +1. **No Image Optimization** - Raw assets downloaded without optimization +2. **Large File Sizes** - Admin-uploaded assets might be unoptimized +3. **No Format Conversion** - Not leveraging WebP/AVIF automatically +4. **Build Time Impact** - Large assets slow down builds + +## Optimized Solution + +### Enhanced Build-Time Asset Processing + +Add image optimization during the build-time download process: + +```javascript +// next.config.mjs + +import { createClient } from '@supabase/supabase-js'; +import { writeFile, mkdir } from 'fs/promises'; +import { join, dirname, extname } from 'path'; +import { fileURLToPath } from 'url'; +import sharp from 'sharp'; // Already in your dependencies! + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +/** + * Optimize image using Sharp + */ +async function optimizeImage(buffer, format, maxWidth = 2000) { + let image = sharp(buffer); + + // Get metadata + const metadata = await image.metadata(); + + // Resize if too large + if (metadata.width > maxWidth) { + image = image.resize(maxWidth, null, { + withoutEnlargement: true, + fit: 'inside' + }); + } + + // Convert format based on original + switch (format.toLowerCase()) { + case '.png': + case '.jpg': + case '.jpeg': + // Convert to WebP for better compression + return await image.webp({ quality: 85 }).toBuffer(); + case '.svg': + // SVGs are already optimized, but we can validate + return buffer; + default: + return buffer; + } +} + +/** + * Download and optimize asset + */ +async function downloadAndOptimizeAsset(supabase, storagePath, localPath, assetType) { + try { + // Download from Supabase Storage + const { data, error } = await supabase.storage + .from('community-assets') + .download(storagePath); + + if (error || !data) { + console.warn(`⚠️ Failed to download ${storagePath}:`, error?.message); + return false; + } + + const buffer = Buffer.from(await data.arrayBuffer()); + const ext = extname(storagePath); + + // Optimize based on asset type + let optimizedBuffer = buffer; + let optimizedExt = ext; + + // For logos/icons, optimize aggressively + if (['main', 'icon', 'og', 'splash'].includes(assetType)) { + if (['.png', '.jpg', '.jpeg'].includes(ext)) { + optimizedBuffer = await optimizeImage(buffer, ext, 2000); + optimizedExt = '.webp'; // Convert to WebP + localPath = localPath.replace(ext, '.webp'); + } + } + + // For favicon/appleTouch, keep original format but optimize size + if (['favicon', 'appleTouch'].includes(assetType)) { + if (['.png', '.jpg', '.jpeg'].includes(ext)) { + optimizedBuffer = await optimizeImage(buffer, ext, 512); + } + } + + // Ensure directory exists + const fullPath = join(__dirname, 'public', localPath); + const dir = dirname(fullPath); + await mkdir(dir, { recursive: true }); + + // Write optimized asset + await writeFile(fullPath, optimizedBuffer); + + console.log(`✅ Downloaded & optimized ${storagePath} → ${localPath}`); + return { success: true, path: localPath }; + } catch (error) { + console.error(`❌ Error processing ${storagePath}:`, error.message); + return false; + } +} + +async function downloadAssets(config, communityId) { + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + + if (!supabaseUrl || !supabaseKey) { + console.warn('⚠️ Missing Supabase credentials'); + return config; + } + + const supabase = createClient(supabaseUrl, supabaseKey); + const assets = config.assets?.logos || {}; + const downloadedAssets = {}; + + // Download and optimize each asset + for (const [key, storagePath] of Object.entries(assets)) { + if (typeof storagePath === 'string' && storagePath.startsWith('community-assets/')) { + const localPath = `/images/${communityId}/${storagePath.split('/').pop()}`; + const result = await downloadAndOptimizeAsset(supabase, storagePath, localPath, key); + + if (result && result.success) { + downloadedAssets[key] = result.path; + } else { + downloadedAssets[key] = null; // Will use static fallback + } + } else if (typeof storagePath === 'string') { + downloadedAssets[key] = storagePath; + } + } + + return { + ...config, + assets: { + logos: downloadedAssets + } + }; +} +``` + +## Performance Optimizations + +### 1. Image Optimization During Build + +**Benefits:** +- ✅ **Smaller file sizes** - WebP/AVIF conversion reduces size by 25-50% +- ✅ **Faster loads** - Optimized images load faster +- ✅ **Better UX** - Faster page loads, better Core Web Vitals + +**Implementation:** +- Use Sharp (already in dependencies) to optimize images +- Convert PNG/JPG to WebP for better compression +- Resize large images to reasonable dimensions +- Keep SVGs as-is (already optimized) + +### 2. Next.js Image Component Optimization + +You're already using `` which provides: +- ✅ **Automatic optimization** - Next.js optimizes on-demand +- ✅ **Lazy loading** - Images load when needed +- ✅ **Responsive images** - Serves appropriate sizes +- ✅ **Format conversion** - WebP/AVIF when supported + +**Ensure you're using it correctly:** + +```typescript +// ✅ Good - Uses Next.js Image optimization +{`${brand.displayName} + +// ❌ Bad - Regular img tag (no optimization) +Logo +``` + +### 3. CDN Integration + +**For even better performance**, serve assets via CDN: + +```javascript +// Option 1: Use Supabase Storage CDN URLs directly +const publicUrl = supabase.storage + .from('community-assets') + .getPublicUrl(storagePath); + +// Option 2: Use Vercel CDN (if deploying on Vercel) +// Assets in public/ are automatically CDN'd + +// Option 3: Custom CDN (Cloudflare, etc.) +const cdnUrl = `https://cdn.yoursite.com${localPath}`; +``` + +### 4. Caching Strategy + +**Add cache headers** for assets: + +```javascript +// next.config.mjs + +async headers() { + return [ + { + source: '/images/:path*', + headers: [ + { + key: 'Cache-Control', + value: 'public, max-age=31536000, immutable', // 1 year + }, + ], + }, + // ... other headers + ]; +} +``` + +### 5. Lazy Loading for Non-Critical Assets + +**For below-the-fold assets:** + +```typescript +// Use Next.js Image with lazy loading +Splash +``` + +## Performance Comparison + +### Without Optimization +- **Raw PNG logo**: 500KB +- **Load time**: ~2s on 3G +- **Build time**: +5s per large asset + +### With Optimization +- **Optimized WebP logo**: 50KB (90% smaller!) +- **Load time**: ~200ms on 3G +- **Build time**: +2s (optimization overhead) + +## Recommended Approach + +### Hybrid: Optimize + CDN + +1. **Build-time optimization** - Optimize images during download +2. **Public folder** - Store optimized assets in `public/` +3. **CDN serving** - Let Vercel/CDN serve from `public/` +4. **Next.js Image** - Use `` component for additional optimization + +**Why this works:** +- ✅ **Build-time optimization** - Reduces file sizes permanently +- ✅ **CDN caching** - Fast global delivery +- ✅ **Next.js optimization** - Additional on-demand optimization +- ✅ **Best of both worlds** - Pre-optimized + runtime optimization + +## Implementation Priority + +### Phase 1: Basic Optimization (MVP) +- Download assets to `public/` +- Use Next.js `` component +- Add cache headers + +### Phase 2: Build-Time Optimization +- Add Sharp optimization during download +- Convert to WebP for better compression +- Resize large images + +### Phase 3: Advanced Optimization +- Generate multiple sizes (srcset) +- Generate AVIF versions +- Implement CDN integration + +## Performance Metrics to Monitor + +1. **Build time** - Should be < 30s total +2. **Asset sizes** - Logos should be < 100KB +3. **Load times** - First Contentful Paint < 1.8s +4. **Core Web Vitals** - LCP < 2.5s + +## Summary + +**Yes, assets will be performant** with proper optimization: + +✅ **Build-time optimization** - Optimize during download +✅ **Next.js Image component** - Automatic runtime optimization +✅ **CDN serving** - Fast global delivery +✅ **Caching** - Long-term browser/CDN caching +✅ **Format conversion** - WebP/AVIF for smaller sizes + +The key is adding Sharp optimization during the build-time download process. This ensures admin-uploaded assets are optimized before being served, maintaining performance even if admins upload large, unoptimized files. + diff --git a/docs/BUILD_TIME_CONFIG_SUMMARY.md b/docs/BUILD_TIME_CONFIG_SUMMARY.md new file mode 100644 index 000000000..e5c834aea --- /dev/null +++ b/docs/BUILD_TIME_CONFIG_SUMMARY.md @@ -0,0 +1,152 @@ +# Build-Time Configuration System - Quick Summary + +## Overview + +Configurations are stored in the database for admin updates, but fetched at **build time** and generated as static TypeScript files. Runtime has zero database queries. + +## Architecture Flow + +``` +┌─────────────────┐ +│ Admin UI │ +│ (Updates DB) │ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│ Database │ +│ (Stores Config)│ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ ┌──────────────────┐ +│ Build Process │─────▶│ Generate TS Files │ +│ (prebuild hook) │ │ from DB Configs │ +└─────────────────┘ └─────────┬──────────┘ + │ + ▼ + ┌──────────────────┐ + │ Static TS Files │ + │ (in src/config/) │ + └─────────┬──────────┘ + │ + ▼ + ┌──────────────────┐ + │ Runtime App │ + │ (Uses Static Files│ + │ Zero DB Queries) │ + └──────────────────┘ +``` + +## Key Components + +### 1. Database Tables +- `community_configs` - Active configs +- `community_config_history` - Version history +- `community_config_admins` - Admin permissions + +### 2. Build Script +- `scripts/generate-configs.ts` - Fetches from DB, generates TS files +- Runs before every build via `prebuild` hook +- Falls back to static configs if DB unavailable + +### 3. Generated Files +- `src/config/{community}/{community}.{section}.ts` - Generated from DB +- `src/config/{community}/index.ts` - Main export +- Structure matches existing static configs exactly + +### 4. Runtime +- Uses generated static files (same as before) +- Zero database queries +- Fallback to static configs if generation failed + +## Workflow + +### Admin Updates Config +1. Admin edits config in admin UI +2. Changes saved to database +3. Admin clicks "Trigger Rebuild" +4. CI/CD rebuilds application +5. Build process fetches latest configs from DB +6. Generates static TypeScript files +7. Next.js build uses generated files +8. Deploy with new configs + +### Build Process +```bash +npm run build + ↓ +prebuild hook runs + ↓ +scripts/generate-configs.ts + ↓ +Fetches configs from Supabase + ↓ +Generates src/config/{community}/*.ts files + ↓ +next build (uses generated files) +``` + +## Benefits + +✅ **Zero Runtime Overhead** - No database queries in production +✅ **Admin Updates** - Changes made through database UI +✅ **Type Safety** - Generated TypeScript files maintain type safety +✅ **Fast Runtime** - Static file imports are instant +✅ **Safe Fallback** - Static configs always available +✅ **Version History** - Full audit trail in database +✅ **Easy Rollback** - Remove prebuild hook to use static configs + +## Migration Steps + +1. **Create database schema** (migration files) +2. **Seed existing configs** to database +3. **Create generator script** (`scripts/generate-configs.ts`) +4. **Add prebuild hook** to package.json +5. **Test build process** with generated configs +6. **Create admin interface** for updates +7. **Set up rebuild trigger** (CI/CD integration) +8. **Roll out gradually** (one community at a time) + +## Environment Variables + +**Required for Build:** +```bash +NEXT_PUBLIC_SUPABASE_URL=... +NEXT_PUBLIC_SUPABASE_ANON_KEY=... # Or SUPABASE_SERVICE_ROLE_KEY +NEXT_PUBLIC_COMMUNITY=nouns # Which community to build +``` + +**Optional:** +```bash +GENERATE_CONFIGS=nouns,clanker,example # Which configs to generate +``` + +## File Structure + +``` +src/config/ +├── {community}/ # Generated from DB at build time +│ ├── {community}.brand.ts +│ ├── {community}.assets.ts +│ ├── {community}.theme.ts +│ ├── ... +│ └── index.ts +├── index.ts # Main loader (uses generated files) +└── systemConfig.ts # Type definitions + +scripts/ +└── generate-configs.ts # Build-time generator +``` + +## Rollback + +If issues arise: +1. Remove `prebuild` hook from package.json +2. Build uses static configs directly +3. No runtime impact + +## Next Steps + +See `DATABASE_CONFIG_MIGRATION_PLAN.md` for complete implementation details. + diff --git a/docs/COMMUNITY_CONFIG_SYSTEM.md b/docs/COMMUNITY_CONFIG_SYSTEM.md new file mode 100644 index 000000000..9c4c3d41d --- /dev/null +++ b/docs/COMMUNITY_CONFIG_SYSTEM.md @@ -0,0 +1,796 @@ +# Community Configuration System + +## Overview + +The Community Configuration System is a comprehensive whitelabeling solution that allows Nounspace to be customized for different communities (Nouns, Clanker, Example, etc.) through a build-time configuration system. Each community can have its own branding, assets, themes, fidgets, navigation, and initial space configurations. + +## Architecture + +### Core Components + +1. **Configuration Loader** (`src/config/index.ts`) + - Reads `NEXT_PUBLIC_COMMUNITY` environment variable + - Validates and loads the appropriate community configuration + - Provides runtime delegation functions for space creators + +2. **System Config Interface** (`src/config/systemConfig.ts`) + - Defines the `SystemConfig` type structure + - Provides TypeScript interfaces for all configuration sections + +3. **Community Configurations** (`src/config/{community}/`) + - Each community has its own folder with modular config files + - Exported as a single `{community}SystemConfig` object + +4. **Hook for React Components** (`src/common/lib/hooks/useSystemConfig.ts`) + - Provides cached access to system config in React components + - Memoized for performance + +## Configuration Structure + +### SystemConfig Interface + +```typescript +interface SystemConfig { + brand: BrandConfig; // Brand identity (name, tagline, description) + assets: AssetConfig; // Visual assets (logos, icons, favicons) + theme: ThemeConfig; // Theme definitions (default, nounish, etc.) + community: CommunityConfig; // Community integration (URLs, contracts, tokens) + fidgets: FidgetConfig; // Enabled/disabled fidgets + homePage: HomePageConfig; // Home page tabs and layout + explorePage: ExplorePageConfig; // Explore page configuration + navigation?: NavigationConfig; // Navigation items and settings + ui?: UIConfig; // UI colors and styling +} +``` + +### Configuration Modules + +Each community configuration is split into modular files: + +``` +src/config/{community}/ +├── {community}.brand.ts # Brand identity +├── {community}.assets.ts # Visual assets +├── {community}.theme.ts # Theme definitions +├── {community}.community.ts # Community integration +├── {community}.fidgets.ts # Fidget management +├── {community}.home.ts # Home page config +├── {community}.explore.ts # Explore page config +├── {community}.navigation.ts # Navigation config +├── {community}.ui.ts # UI colors +├── index.ts # Main export +└── initialSpaces/ # Initial space templates + ├── initialProfileSpace.ts + ├── initialChannelSpace.ts + ├── initialTokenSpace.ts + ├── initialProposalSpace.ts + └── initialHomebase.ts +``` + +## Configuration Details + +### 1. Brand Config (`brand`) + +Defines the community's brand identity: + +```typescript +interface BrandConfig { + name: string; // Internal name (e.g., "nouns") + displayName: string; // Display name (e.g., "Nouns") + tagline: string; // Tagline + description: string; // Description for SEO/metadata + miniAppTags: string[]; // Tags for mini-app discovery +} +``` + +**Example (Nouns):** +```typescript +export const nounsBrand = { + name: "Nouns", + displayName: "Nouns", + tagline: "A space for Nouns", + description: "The social hub for Nouns", + miniAppTags: ["nouns", "client", "customizable", "social", "link"], +}; +``` + +### 2. Assets Config (`assets`) + +Defines visual assets used throughout the application: + +```typescript +interface AssetConfig { + logos: { + main: string; // Main logo (SVG/PNG path) + icon: string; // Icon logo (SVG/PNG path) + favicon: string; // Favicon path + appleTouch: string; // Apple touch icon path + og: string; // Open Graph image path + splash: string; // Splash screen image path + }; +} +``` + +**Example (Nouns):** +```typescript +export const nounsAssets = { + logos: { + main: logo, // Imported SVG + icon: noggles, // Imported SVG + favicon: "/images/favicon.ico", + appleTouch: "/images/apple-touch-icon.png", + og: og, // Imported SVG + splash: splash, // Imported SVG + }, +}; +``` + +### 3. Theme Config (`theme`) + +Defines available theme templates: + +```typescript +interface ThemeConfig { + default: ThemeProperties; + nounish: ThemeProperties; + gradientAndWave: ThemeProperties; + colorBlobs: ThemeProperties; + floatingShapes: ThemeProperties; + imageParallax: ThemeProperties; + shootingStar: ThemeProperties; + squareGrid: ThemeProperties; + tesseractPattern: ThemeProperties; + retro: ThemeProperties; +} + +interface ThemeProperties { + id: string; + name: string; + properties: { + font: string; + fontColor: string; + headingsFont: string; + headingsFontColor: string; + background: string; + backgroundHTML: string; // Custom HTML/CSS or reference + musicURL: string; + fidgetBackground: string; + fidgetBorderWidth: string; + fidgetBorderColor: string; + fidgetShadow: string; + fidgetBorderRadius: string; + gridSpacing: string; + }; +} +``` + +**Example (Nouns - Default Theme):** +```typescript +default: { + id: "default", + name: "Default", + properties: { + font: "Inter", + fontColor: "#000000", + headingsFont: "Inter", + headingsFontColor: "#000000", + background: "#ffffff", + backgroundHTML: "", + musicURL: "https://www.youtube.com/watch?v=dMXlZ4y7OK4&t=1804", + fidgetBackground: "#ffffff", + fidgetBorderWidth: "1px", + fidgetBorderColor: "#C0C0C0", + fidgetShadow: "none", + fidgetBorderRadius: "12px", + gridSpacing: "16", + }, +} +``` + +### 4. Community Config (`community`) + +Defines community integration details: + +```typescript +interface CommunityConfig { + type: string; // Community type identifier + urls: { + website: string; + discord: string; + twitter: string; + github: string; + forum: string; + }; + social: { + farcaster: string; // Farcaster handle + discord: string; // Discord handle + twitter: string; // Twitter handle + }; + governance: { + proposals: string; // Proposals URL + delegates: string; // Delegates URL + treasury: string; // Treasury URL + }; + tokens: { + erc20Tokens?: CommunityErc20Token[]; + nftTokens?: CommunityNftToken[]; + }; + contracts: { + nouns: Address; // Main NFT contract + auctionHouse: Address; // Auction house contract + space: Address; // Space token contract + nogs: Address; // NOGs contract + [key: string]: Address; // Additional contracts + }; +} +``` + +**Example (Nouns):** +```typescript +export const nounsCommunity = { + type: 'nouns', + urls: { + website: 'https://nouns.com', + discord: 'https://discord.gg/nouns', + twitter: 'https://twitter.com/nounsdao', + github: 'https://github.com/nounsDAO', + forum: 'https://discourse.nouns.wtf', + }, + social: { + farcaster: 'nouns', + discord: 'nouns', + twitter: 'nounsdao', + }, + governance: { + proposals: 'https://nouns.wtf/vote', + delegates: 'https://nouns.wtf/delegates', + treasury: 'https://nouns.wtf/treasury', + }, + tokens: { + erc20Tokens: [ + { + address: '0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab', + symbol: '$SPACE', + decimals: 18, + network: 'base', + }, + ], + nftTokens: [ + { + address: '0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03', + symbol: 'Nouns', + type: 'erc721', + network: 'eth', + }, + ], + }, + contracts: { + nouns: '0x9c8ff314c9bc7f6e59a9d9225fb22946427edc03', + auctionHouse: '0x830bd73e4184cef73443c15111a1df14e495c706', + space: '0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab', + nogs: '0xD094D5D45c06c1581f5f429462eE7cCe72215616', + }, +}; +``` + +### 5. Fidget Config (`fidgets`) + +Controls which fidgets are enabled/disabled: + +```typescript +interface FidgetConfig { + enabled: string[]; // List of enabled fidget IDs + disabled: string[]; // List of disabled fidget IDs +} +``` + +**Example (Nouns):** +```typescript +export const nounsFidgets = { + enabled: [ + 'nounsHome', + 'governance', + 'feed', + 'cast', + 'gallery', + 'text', + 'iframe', + 'links', + 'video', + 'channel', + 'profile', + 'snapshot', + 'swap', + 'rss', + 'market', + 'portfolio', + 'chat', + 'builderScore', + 'framesV2' + ], + disabled: ['example'] +}; +``` + +### 6. Home Page Config (`homePage`) + +Defines the home page structure: + +```typescript +interface HomePageConfig { + defaultTab: string; // Default tab name + tabOrder: string[]; // Tab display order + tabs: { + [key: string]: TabConfig; // Tab configurations + }; + layout: { + defaultLayoutFidget: string; // Default layout (e.g., "grid") + gridSpacing: number; // Grid spacing in pixels + theme: { + background: string; + fidgetBackground: string; + font: string; + fontColor: string; + }; + }; +} +``` + +**Example (Nouns):** +```typescript +export const nounsHomePage = { + defaultTab: "Nouns", + tabOrder: ["Nouns", "Social", "Governance", "Resources", "Funded Works", "Places"], + tabs: { + "Nouns": { + name: "Nouns", + displayName: "Nouns", + layoutID: "88b78f73-37fb-4921-9410-bc298311c0bb", + layoutDetails: { + layoutConfig: { layout: [...] }, + layoutFidget: "grid" + }, + theme: { ... }, + fidgetInstanceDatums: { ... }, + fidgetTrayContents: [], + isEditable: false, + timestamp: "2025-06-20T05:58:44.080Z" + }, + // ... more tabs + }, + layout: { + defaultLayoutFidget: "grid", + gridSpacing: 16, + theme: { + background: "#ffffff", + fidgetBackground: "#ffffff", + font: "Inter", + fontColor: "#000000" + } + } +}; +``` + +### 7. Navigation Config (`navigation`) + +Defines navigation items and settings: + +```typescript +interface NavigationConfig { + items: NavigationItem[]; + logoTooltip?: { + text: string; + href?: string; + }; + showMusicPlayer?: boolean; + showSocials?: boolean; +} + +interface NavigationItem { + id: string; + label: string; + href: string; + icon?: 'home' | 'explore' | 'notifications' | 'search' | 'space' | 'robot' | 'custom'; + openInNewTab?: boolean; + requiresAuth?: boolean; +} +``` + +**Example (Nouns):** +```typescript +export const nounsNavigation: NavigationConfig = { + logoTooltip: { + text: "wtf is nouns?", + href: "https://nouns.wtf", + }, + items: [ + { id: 'home', label: 'Home', href: '/home', icon: 'home' }, + { id: 'explore', label: 'Explore', href: '/explore', icon: 'explore' }, + { id: 'notifications', label: 'Notifications', href: '/notifications', icon: 'notifications', requiresAuth: true }, + { id: 'space-token', label: '$SPACE', href: '/t/base/0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab/Profile', icon: 'space' }, + ], + showMusicPlayer: true, + showSocials: true, +}; +``` + +### 8. UI Config (`ui`) + +Defines UI colors and styling: + +```typescript +interface UIConfig { + primaryColor: string; + primaryHoverColor: string; + primaryActiveColor: string; + castButton: { + backgroundColor: string; + hoverColor: string; + activeColor: string; + }; +} +``` + +## Configuration Loading + +### Build-Time Configuration + +The system uses **build-time configuration** - the community is determined at build time via the `NEXT_PUBLIC_COMMUNITY` environment variable: + +```bash +# Set community at build time +NEXT_PUBLIC_COMMUNITY=nouns npm run build +NEXT_PUBLIC_COMMUNITY=clanker npm run build +NEXT_PUBLIC_COMMUNITY=example npm run build +``` + +### Loading Process + +1. **Environment Variable Reading** + ```typescript + const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + ``` + +2. **Validation** + ```typescript + if (!isValidCommunityConfig(communityConfig)) { + console.warn(`Invalid community configuration: "${communityConfig}"`); + // Falls back to 'nouns' + } + ``` + +3. **Configuration Selection** + ```typescript + switch (communityConfig.toLowerCase()) { + case 'nouns': + return nounsSystemConfig; + case 'example': + return exampleSystemConfig; + case 'clanker': + return clankerSystemConfig; + default: + return nounsSystemConfig; + } + ``` + +### Runtime Delegation + +The system provides runtime delegation functions for space creators that switch based on the active community: + +```typescript +// Profile space creator +export const createInitialProfileSpaceConfigForFid = (fid: number, username?: string, walletAddress?: string) => { + switch (resolveCommunity()) { + case 'clanker': + return clankerCreateInitialProfileSpaceConfigForFid(fid, username, walletAddress); + case 'example': + return exampleCreateInitialProfileSpaceConfigForFid(fid, username); + case 'nouns': + default: + return nounsCreateInitialProfileSpaceConfigForFid(fid, username); + } +}; + +// Similar functions for: +// - createInitialChannelSpaceConfig +// - createInitialTokenSpaceConfigForAddress +// - createInitalProposalSpaceConfigForProposalId +// - createInitialHomebaseConfig +``` + +## Usage in Components + +### Using the Hook + +```typescript +import { useSystemConfig } from '@/common/lib/hooks/useSystemConfig'; + +const MyComponent = () => { + const config = useSystemConfig(); + + return ( +
+

{config.brand.displayName}

+ {config.brand.name} +
+ ); +}; +``` + +### Direct Loading (Server-Side) + +```typescript +import { loadSystemConfig } from '@/config'; + +// In server components or build-time code +const config = loadSystemConfig(); +const metadata: Metadata = { + title: config.brand.displayName, + description: config.brand.description, + openGraph: { + images: [{ url: `${WEBSITE_URL}${config.assets.logos.og}` }], + }, +}; +``` + +### Example: Navigation Component + +```typescript +import { loadSystemConfig } from '@/config'; + +const Navigation = () => { + const { community, navigation, ui } = loadSystemConfig(); + + return ( + + ); +}; +``` + +### Example: Brand Header + +```typescript +import { loadSystemConfig } from '@/config'; + +const BrandHeader = () => { + const { assets, brand, navigation } = loadSystemConfig(); + const logoSrc = assets.logos.icon || assets.logos.main; + + return ( + {`${brand.displayName} + ); +}; +``` + +## Adding a New Community + +### Step 1: Create Community Folder + +```bash +mkdir -p src/config/mycommunity +mkdir -p src/config/mycommunity/initialSpaces +``` + +### Step 2: Create Configuration Files + +Create the following files in `src/config/mycommunity/`: + +- `mycommunity.brand.ts` - Brand identity +- `mycommunity.assets.ts` - Visual assets +- `mycommunity.theme.ts` - Theme definitions +- `mycommunity.community.ts` - Community integration +- `mycommunity.fidgets.ts` - Fidget management +- `mycommunity.home.ts` - Home page config +- `mycommunity.explore.ts` - Explore page config +- `mycommunity.navigation.ts` - Navigation config (optional) +- `mycommunity.ui.ts` - UI colors (optional) +- `index.ts` - Main export + +### Step 3: Create Initial Space Templates + +Create space creator functions in `src/config/mycommunity/initialSpaces/`: + +- `initialProfileSpace.ts` - Profile space creator +- `initialChannelSpace.ts` - Channel space creator +- `initialTokenSpace.ts` - Token space creator +- `initialProposalSpace.ts` - Proposal space creator +- `initialHomebase.ts` - Homebase creator + +### Step 4: Export Configuration + +In `src/config/mycommunity/index.ts`: + +```typescript +import { mycommunityBrand } from './mycommunity.brand'; +import { mycommunityAssets } from './mycommunity.assets'; +// ... import other modules + +export const mycommunitySystemConfig = { + brand: mycommunityBrand, + assets: mycommunityAssets, + theme: mycommunityTheme, + community: mycommunityCommunity, + fidgets: mycommunityFidgets, + homePage: mycommunityHomePage, + explorePage: mycommunityExplorePage, + navigation: mycommunityNavigation, + ui: mycommunityUI, +}; +``` + +### Step 5: Register in Main Config + +In `src/config/index.ts`: + +1. Add to `AVAILABLE_CONFIGURATIONS`: + ```typescript + const AVAILABLE_CONFIGURATIONS = ['nouns', 'example', 'clanker', 'mycommunity'] as const; + ``` + +2. Import the config: + ```typescript + import { mycommunitySystemConfig } from './mycommunity/index'; + ``` + +3. Add to switch statement: + ```typescript + case 'mycommunity': + return mycommunitySystemConfig; + ``` + +4. Import and add space creators: + ```typescript + import { default as mycommunityCreateInitialProfileSpaceConfigForFid } from './mycommunity/initialSpaces/initialProfileSpace'; + // ... import other creators + + // Add to delegation functions + export const createInitialProfileSpaceConfigForFid = (fid: number, username?: string) => { + switch (resolveCommunity()) { + case 'mycommunity': + return mycommunityCreateInitialProfileSpaceConfigForFid(fid, username); + // ... other cases + } + }; + ``` + +### Step 6: Use the Configuration + +```bash +NEXT_PUBLIC_COMMUNITY=mycommunity npm run build +``` + +## Key Features + +### 1. Build-Time Configuration +- Configuration is determined at build time +- No runtime switching between communities +- Optimized bundle size (only one community's config included) + +### 2. Type Safety +- Full TypeScript support +- Type checking for all configuration sections +- Interface validation + +### 3. Modular Structure +- Each configuration section is in its own file +- Easy to maintain and update +- Clear separation of concerns + +### 4. Caching +- Configuration is cached after first load +- `useSystemConfig` hook provides memoized access +- Efficient for React components + +### 5. Fallback Behavior +- Invalid configurations fall back to 'nouns' +- Graceful error handling +- Console warnings for debugging + +### 6. Runtime Delegation +- Space creators delegate to community-specific implementations +- Allows different communities to have different space structures +- Maintains consistent API across communities + +## Best Practices + +1. **Use the Hook in Components** + - Prefer `useSystemConfig()` hook in React components + - Provides caching and memoization + +2. **Direct Loading for Server-Side** + - Use `loadSystemConfig()` in server components or build-time code + - No React hooks available in these contexts + +3. **Type Safety** + - Always use TypeScript interfaces + - Leverage type checking for configuration validation + +4. **Modular Configuration** + - Keep each configuration section in its own file + - Makes it easier to maintain and update + +5. **Consistent Structure** + - Follow the same structure across all communities + - Makes it easier to add new communities + +6. **Documentation** + - Document any community-specific behavior + - Include examples in configuration files + +## Troubleshooting + +### Configuration Not Loading + +1. **Check Environment Variable** + ```bash + echo $NEXT_PUBLIC_COMMUNITY + ``` + +2. **Verify Configuration Exists** + - Check that the community folder exists + - Verify `index.ts` exports `{community}SystemConfig` + +3. **Check Console Warnings** + - Invalid configurations log warnings + - Check browser/server console for errors + +### Type Errors + +1. **Verify Interface Compliance** + - Ensure all required fields are present + - Check TypeScript types match `SystemConfig` interface + +2. **Check Imports** + - Verify all imports are correct + - Ensure types are imported from `systemConfig.ts` + +### Build Issues + +1. **Clear Build Cache** + ```bash + rm -rf .next + npm run build + ``` + +2. **Check Environment Variables** + - Ensure `NEXT_PUBLIC_COMMUNITY` is set correctly + - Verify it's available at build time + +## Examples + +### Nouns Configuration +- **Brand**: Nouns DAO branding +- **Assets**: Noggles logo, Nouns colors +- **Themes**: Nounish theme with animated backgrounds +- **Fidgets**: Full fidget set including Nouns-specific fidgets +- **Navigation**: Home, Explore, Notifications, $SPACE token + +### Clanker Configuration +- **Brand**: Clanker ecosystem branding +- **Assets**: Clanker logo and assets +- **Themes**: Clanker-specific themes +- **Fidgets**: Token-focused fidgets enabled +- **Navigation**: Clanker-specific navigation items + +### Example Configuration +- **Brand**: Template/example branding +- **Assets**: Placeholder assets +- **Themes**: Basic theme set +- **Fidgets**: Core fidgets enabled +- **Navigation**: Basic navigation structure + +## Summary + +The Community Configuration System provides a comprehensive whitelabeling solution that allows Nounspace to be customized for different communities. It uses build-time configuration for optimal performance, provides full type safety, and maintains a modular structure for easy maintenance and extension. + diff --git a/docs/CONFIG_DATABASE_SCHEMA_DETAILED.md b/docs/CONFIG_DATABASE_SCHEMA_DETAILED.md new file mode 100644 index 000000000..b9b14ce1e --- /dev/null +++ b/docs/CONFIG_DATABASE_SCHEMA_DETAILED.md @@ -0,0 +1,1027 @@ +# Detailed Database Config Schema Breakdown + +## Overview + +This document breaks down exactly what information is stored in each JSONB column of the `community_configs` table. Each column corresponds to a section of the `SystemConfig` interface. + +## Database Table Structure + +```sql +CREATE TABLE community_configs ( + id UUID PRIMARY KEY, + community_id VARCHAR(50) UNIQUE, + brand_config JSONB, -- Brand identity + assets_config JSONB, -- Visual assets + theme_config JSONB, -- Theme definitions + community_config JSONB, -- Community integration data + fidgets_config JSONB, -- Fidget enable/disable + home_page_config JSONB, -- Home page structure + explore_page_config JSONB, -- Explore page structure + navigation_config JSONB, -- Navigation items (optional) + ui_config JSONB -- UI colors (optional) +); +``` + +--- + +## 1. `brand_config` (BrandConfig) + +**Purpose:** Brand identity and metadata for the community + +**Structure:** +```json +{ + "name": "Nouns", // Internal identifier (lowercase, no spaces) + "displayName": "Nouns", // Display name shown to users + "tagline": "A space for Nouns", // Short tagline/slogan + "description": "The social hub for Nouns", // Full description (used in meta tags, OG tags) + "miniAppTags": [ // Tags for Farcaster Mini App discovery + "nouns", + "client", + "customizable", + "social", + "link" + ] +} +``` + +**Fields Explained:** +- **`name`**: Internal identifier, used in code/logs (e.g., "nouns", "clanker") +- **`displayName`**: User-facing name shown in UI, titles, headers +- **`tagline`**: Short marketing tagline (1-2 sentences) +- **`description`**: Full description used in: + - HTML `` + - Open Graph tags + - Social media previews + - SEO +- **`miniAppTags`**: Array of strings used for: + - Farcaster Mini App catalog discovery + - Search/filtering in Mini App stores + - Categorization + +**Example:** +```json +{ + "name": "nouns", + "displayName": "Nouns", + "tagline": "A space for Nouns", + "description": "The social hub for Nouns", + "miniAppTags": ["nouns", "client", "customizable", "social", "link"] +} +``` + +--- + +## 2. `assets_config` (AssetConfig) + +**Purpose:** Paths/URLs to visual assets (logos, icons, images) + +**Structure:** +```json +{ + "logos": { + "main": "/images/nouns/logo.svg", // Main logo (large, used in headers) + "icon": "/images/nouns/noggles.svg", // Icon (small, used in nav/favicon) + "favicon": "/images/favicon.ico", // Browser favicon + "appleTouch": "/images/apple-touch-icon.png", // iOS home screen icon + "og": "/images/nouns/og.png", // Open Graph image (social sharing) + "splash": "/images/nouns/splash.png" // Splash screen (Farcaster frames) + } +} +``` + +**Fields Explained:** +- **`main`**: Primary logo, typically: + - SVG or PNG + - Used in main header/navigation + - Larger size (200-400px width) +- **`icon`**: Small icon/logo variant: + - Used in navigation bars + - Mobile headers + - Sometimes same as main, sometimes different (e.g., "noggles" for Nouns) +- **`favicon`**: Browser tab icon: + - `.ico` format + - Multiple sizes (16x16, 32x32) +- **`appleTouch`**: iOS home screen icon: + - PNG format + - 180x180px recommended +- **`og`**: Open Graph image for social sharing: + - PNG/JPG format + - 1200x630px recommended + - Used when sharing links on Twitter, Discord, etc. +- **`splash`**: Splash screen image: + - Used in Farcaster Frame launch screens + - PNG/JPG format + - Full-screen background + +**Path Formats:** + +The `assets_config` paths can be in different formats depending on the stage: + +1. **Storage Paths (in database)**: `"community-assets/nouns/logo.svg"` + - References assets uploaded to Supabase Storage + - Used when admin uploads assets via UI + - Stored directly in `assets_config` JSONB column + +2. **Public Paths (after build)**: `"/images/nouns/logo.svg"` + - References assets in `public/images/` folder + - Generated at build time when assets are downloaded + - Used at runtime by the application + +3. **CDN URLs (optional)**: `"https://cdn.example.com/logo.svg"` + - External CDN URLs (if not using build-time download) + - Can be used directly without download + +**Flow:** +``` +Admin Uploads → Supabase Storage → assets_config stores storage path + ↓ + Build Time: + Download from Storage + → Save to public/images/ + → Update assets_config to public path + ↓ + Runtime: + App uses public path +``` + +**Example:** +```json +{ + "logos": { + "main": "/images/nouns/logo.svg", + "icon": "/images/nouns/noggles.svg", + "favicon": "/images/favicon.ico", + "appleTouch": "/images/apple-touch-icon.png", + "og": "/images/nouns/og.png", + "splash": "/images/nouns/splash.png" + } +} +``` + +--- + +## 3. `theme_config` (ThemeConfig) + +**Purpose:** Visual theme definitions (colors, fonts, backgrounds, styling) + +**Structure:** +```json +{ + "default": { + "id": "default", + "name": "Default", + "properties": { + "font": "Inter", // Primary font family + "fontColor": "#000000", // Primary text color + "headingsFont": "Inter", // Font for headings (h1, h2, etc.) + "headingsFontColor": "#000000", // Heading text color + "background": "#ffffff", // Page background color + "backgroundHTML": "", // HTML/CSS for animated backgrounds (or empty string) + "musicURL": "https://...", // YouTube URL for background music + "fidgetBackground": "#ffffff", // Background color for fidget containers + "fidgetBorderWidth": "1px", // Border width for fidgets + "fidgetBorderColor": "#C0C0C0", // Border color for fidgets + "fidgetShadow": "none", // CSS shadow for fidgets + "fidgetBorderRadius": "12px", // Border radius for fidgets + "gridSpacing": "16" // Grid spacing in pixels + } + }, + "nounish": { /* ... */ }, + "gradientAndWave": { /* ... */ }, + // ... other theme variants +} +``` + +**Fields Explained:** +- **`id`**: Unique identifier for the theme (used in code) +- **`name`**: Display name shown in theme picker +- **`properties.font`**: Primary font family (Google Fonts name or system font) +- **`properties.fontColor`**: Hex color for body text +- **`properties.headingsFont`**: Font family for headings (can differ from body) +- **`properties.headingsFontColor`**: Hex color for headings +- **`properties.background`**: Page background color (hex or rgba) +- **`properties.backgroundHTML`**: + - Empty string `""` for solid color backgrounds + - HTML string for animated backgrounds (e.g., "nounish", "gradientAndWave") + - Contains full HTML/CSS for complex animated backgrounds +- **`properties.musicURL`**: YouTube URL for background music (optional) +- **`properties.fidgetBackground`**: Background color for fidget containers +- **`properties.fidgetBorderWidth`**: CSS border width (e.g., "1px", "2px", "0") +- **`properties.fidgetBorderColor`**: Hex color for fidget borders +- **`properties.fidgetShadow`**: CSS box-shadow value (e.g., "none", "0 5px 15px rgba(0,0,0,0.55)") +- **`properties.fidgetBorderRadius`**: CSS border-radius (e.g., "12px", "0px") +- **`properties.gridSpacing`**: Spacing between grid items in pixels (as string) + +**Theme Variants:** +Each community can define multiple theme variants: +- `default`: Basic theme +- `nounish`: Community-specific theme +- `gradientAndWave`: Animated gradient theme +- `colorBlobs`: Animated color blobs +- `floatingShapes`: Floating shapes animation +- `imageParallax`: Parallax image background +- `shootingStar`: Shooting star animation +- `squareGrid`: Grid pattern background +- `tesseractPattern`: 3D tesseract pattern +- `retro`: Retro/vintage theme + +**Example:** +```json +{ + "default": { + "id": "default", + "name": "Default", + "properties": { + "font": "Inter", + "fontColor": "#000000", + "headingsFont": "Inter", + "headingsFontColor": "#000000", + "background": "#ffffff", + "backgroundHTML": "", + "musicURL": "https://www.youtube.com/watch?v=dMXlZ4y7OK4&t=1804", + "fidgetBackground": "#ffffff", + "fidgetBorderWidth": "1px", + "fidgetBorderColor": "#C0C0C0", + "fidgetShadow": "none", + "fidgetBorderRadius": "12px", + "gridSpacing": "16" + } + }, + "nounish": { + "id": "nounish", + "name": "Nounish", + "properties": { + "font": "Londrina Solid", + "fontColor": "#333333", + "headingsFont": "Work Sans", + "headingsFontColor": "#000000", + "background": "#ffffff", + "backgroundHTML": "nounish", + "musicURL": "https://www.youtube.com/watch?v=dMXlZ4y7OK4&t=1804", + "fidgetBackground": "#FFFAFA", + "fidgetBorderWidth": "2px", + "fidgetBorderColor": "#F05252", + "fidgetShadow": "0 5px 15px rgba(0,0,0,0.55)", + "fidgetBorderRadius": "12px", + "gridSpacing": "16" + } + } +} +``` + +--- + +## 4. `community_config` (CommunityConfig) + +**Purpose:** Community integration data (URLs, contracts, tokens, governance) + +**Structure:** +```json +{ + "type": "nouns", // Community type identifier + "urls": { + "website": "https://nouns.com", + "discord": "https://discord.gg/nouns", + "twitter": "https://twitter.com/nounsdao", + "github": "https://github.com/nounsDAO", + "forum": "https://discourse.nouns.wtf" + }, + "social": { + "farcaster": "nouns", // Farcaster username/handle + "discord": "nouns", // Discord server identifier + "twitter": "nounsdao" // Twitter handle (without @) + }, + "governance": { + "proposals": "https://nouns.wtf/vote", + "delegates": "https://nouns.wtf/delegates", + "treasury": "https://nouns.wtf/treasury" + }, + "tokens": { + "erc20Tokens": [ + { + "address": "0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab", + "symbol": "$SPACE", + "decimals": 18, + "network": "base" + } + ], + "nftTokens": [ + { + "address": "0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03", + "symbol": "Nouns", + "type": "erc721", + "network": "eth" + } + ] + }, + "contracts": { + "nouns": "0x9c8ff314c9bc7f6e59a9d9225fb22946427edc03", + "auctionHouse": "0x830bd73e4184cef73443c15111a1df14e495c706", + "space": "0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab", + "nogs": "0xD094D5D45c06c1581f5f429462eE7cCe72215616" + } +} +``` + +**Fields Explained:** + +### `type` +- Community type identifier (e.g., "nouns", "clanker", "example") +- Used internally for routing/logic + +### `urls` +- **`website`**: Main community website +- **`discord`**: Discord invite link +- **`twitter`**: Twitter profile URL +- **`github`**: GitHub organization URL +- **`forum`**: Forum/Discourse URL + +### `social` +- **`farcaster`**: Farcaster username/handle (without @) +- **`discord`**: Discord server identifier +- **`twitter`**: Twitter handle (without @) + +### `governance` +- **`proposals`**: URL to proposals/voting page +- **`delegates`**: URL to delegates page +- **`treasury`**: URL to treasury page + +### `tokens` +- **`erc20Tokens`**: Array of ERC20 token definitions + - **`address`**: Contract address (checksummed) + - **`symbol`**: Token symbol (e.g., "$SPACE") + - **`decimals`**: Token decimals (usually 18) + - **`network`**: Network identifier ("mainnet", "base", "polygon", "eth") +- **`nftTokens`**: Array of NFT token definitions + - **`address`**: Contract address + - **`symbol`**: Token symbol (e.g., "Nouns") + - **`type`**: Token type ("erc721", "erc1155") + - **`network`**: Network identifier + +### `contracts` +- Key-value pairs of contract names to addresses +- Common contracts: + - **`nouns`**: Main NFT contract + - **`auctionHouse`**: Auction house contract + - **`space`**: Token contract + - **`nogs`**: Additional contract +- Can include any custom contracts as key-value pairs + +**Example:** +```json +{ + "type": "nouns", + "urls": { + "website": "https://nouns.com", + "discord": "https://discord.gg/nouns", + "twitter": "https://twitter.com/nounsdao", + "github": "https://github.com/nounsDAO", + "forum": "https://discourse.nouns.wtf" + }, + "social": { + "farcaster": "nouns", + "discord": "nouns", + "twitter": "nounsdao" + }, + "governance": { + "proposals": "https://nouns.wtf/vote", + "delegates": "https://nouns.wtf/delegates", + "treasury": "https://nouns.wtf/treasury" + }, + "tokens": { + "erc20Tokens": [ + { + "address": "0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab", + "symbol": "$SPACE", + "decimals": 18, + "network": "base" + } + ], + "nftTokens": [ + { + "address": "0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03", + "symbol": "Nouns", + "type": "erc721", + "network": "eth" + } + ] + }, + "contracts": { + "nouns": "0x9c8ff314c9bc7f6e59a9d9225fb22946427edc03", + "auctionHouse": "0x830bd73e4184cef73443c15111a1df14e495c706", + "space": "0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab", + "nogs": "0xD094D5D45c06c1581f5f429462eE7cCe72215616" + } +} +``` + +--- + +## 5. `fidgets_config` (FidgetConfig) + +**Purpose:** Control which fidgets are enabled/disabled for the community + +**Structure:** +```json +{ + "enabled": [ + "nounsHome", + "governance", + "feed", + "cast", + "gallery", + "text", + "iframe", + "links", + "video", + "channel", + "profile", + "snapshot", + "swap", + "rss", + "market", + "portfolio", + "chat", + "builderScore", + "framesV2" + ], + "disabled": [ + "example" + ] +} +``` + +**Fields Explained:** +- **`enabled`**: Array of fidget type IDs that are available for this community + - Controls which fidgets appear in the fidget picker + - Controls which fidgets can be added to spaces + - Empty array means no fidgets enabled +- **`disabled`**: Array of fidget type IDs that are explicitly disabled + - Overrides enabled list + - Useful for temporarily disabling specific fidgets + - Can be empty array `[]` + +**Fidget Types (examples):** +- `nounsHome`: Community-specific home fidget +- `governance`: Governance proposals/ voting +- `feed`: Social feed (Farcaster casts) +- `cast`: Single cast display +- `gallery`: Image gallery +- `text`: Text content +- `iframe`: Embedded iframe +- `links`: Link collection +- `video`: Video player +- `channel`: Channel feed +- `profile`: User profile +- `snapshot`: Snapshot governance +- `swap`: Token swap widget +- `rss`: RSS feed +- `market`: NFT marketplace +- `portfolio`: Token portfolio +- `chat`: Chat widget +- `builderScore`: Builder score display +- `framesV2`: Farcaster frames + +**Example:** +```json +{ + "enabled": [ + "feed", + "cast", + "gallery", + "text", + "iframe", + "links", + "video" + ], + "disabled": [] +} +``` + +--- + +## 6. `home_page_config` (HomePageConfig) + +**Purpose:** Defines the structure, layout, and content of the home page + +**Structure:** +```json +{ + "defaultTab": "Nouns", // Tab shown by default + "tabOrder": [ // Order tabs appear in UI + "Nouns", + "Social", + "Governance", + "Resources", + "Funded Works", + "Places" + ], + "tabs": { + "Nouns": { + "name": "Nouns", // Internal tab identifier + "displayName": "Nouns", // Display name shown to users + "layoutID": "88b78f73-37fb-4921-9410-bc298311c0bb", // Unique layout ID + "layoutDetails": { + "layoutConfig": { + "layout": [ // Grid layout items + { + "w": 12, // Width in grid units + "h": 10, // Height in grid units + "x": 0, // X position + "y": 0, // Y position + "i": "nounsHome:3a8d7f19-...", // Fidget instance ID + "minW": 2, // Minimum width + "maxW": 36, // Maximum width + "minH": 2, // Minimum height + "maxH": 36, // Maximum height + "moved": false, // Has been moved + "static": false, // Is static (can't move) + "resizeHandles": ["s", "w", "e", "n", "sw", "nw", "se", "ne"], + "isBounded": false + } + ] + }, + "layoutFidget": "grid" // Layout type ("grid" or "mobileStack") + }, + "theme": { /* ThemeProperties */ }, // Theme for this tab + "fidgetInstanceDatums": { // Fidget instances on this tab + "nounsHome:3a8d7f19-...": { + "config": { + "data": {}, // Fidget-specific data + "editable": true, // Can user edit? + "settings": { // Fidget settings + "background": "var(--user-theme-fidget-background)", + "fidgetBorderColor": "var(--user-theme-fidget-border-color)", + "showOnMobile": true, + "isScrollable": true + } + }, + "fidgetType": "nounsHome", // Type of fidget + "id": "nounsHome:3a8d7f19-..." // Unique fidget instance ID + } + }, + "fidgetTrayContents": [], // Available fidgets in tray + "isEditable": false, // Can user edit this tab? + "timestamp": "2025-06-20T05:58:44.080Z" // Last modified timestamp + }, + "Social": { /* ... */ }, + "Governance": { /* ... */ } + // ... other tabs + }, + "layout": { + "defaultLayoutFidget": "grid", // Default layout type + "gridSpacing": 16, // Grid spacing in pixels + "theme": { // Default theme + "background": "#ffffff", + "fidgetBackground": "#ffffff", + "font": "Inter", + "fontColor": "#000000" + } + } +} +``` + +**Fields Explained:** + +### Top Level +- **`defaultTab`**: Tab ID shown by default when visiting `/home` +- **`tabOrder`**: Array of tab IDs in display order +- **`tabs`**: Object mapping tab IDs to tab configurations +- **`layout`**: Default layout settings + +### Tab Configuration (`tabs[tabId]`) +- **`name`**: Internal identifier (usually same as key) +- **`displayName`**: User-facing name +- **`layoutID`**: Unique UUID for this layout +- **`layoutDetails`**: Grid layout configuration + - **`layoutConfig.layout`**: Array of grid items (fidget positions) + - **`layoutFidget`**: Layout type ("grid" or "mobileStack") +- **`theme`**: ThemeProperties for this tab +- **`fidgetInstanceDatums`**: Object mapping fidget instance IDs to fidget data + - Each fidget instance has: + - **`config.data`**: Fidget-specific data + - **`config.editable`**: Can user edit this fidget? + - **`config.settings`**: Fidget settings (styling, behavior) + - **`fidgetType`**: Type of fidget (e.g., "feed", "nounsHome") + - **`id`**: Unique instance ID +- **`fidgetTrayContents`**: Array of fidgets available in the tray (usually empty for home page) +- **`isEditable`**: Can user edit this tab? (usually `false` for home page) +- **`timestamp`**: ISO timestamp of last modification + +### Layout Configuration +- **`defaultLayoutFidget`**: Default layout type ("grid" or "mobileStack") +- **`gridSpacing`**: Spacing between grid items in pixels +- **`theme`**: Default theme properties + +**Example:** +```json +{ + "defaultTab": "Nouns", + "tabOrder": ["Nouns", "Social", "Governance"], + "tabs": { + "Nouns": { + "name": "Nouns", + "displayName": "Nouns", + "layoutID": "88b78f73-37fb-4921-9410-bc298311c0bb", + "layoutDetails": { + "layoutConfig": { + "layout": [ + { + "w": 12, + "h": 10, + "x": 0, + "y": 0, + "i": "nounsHome:3a8d7f19-3e77-4c2b-9c7f-1a6f5f5a6f01", + "minW": 2, + "maxW": 36, + "minH": 2, + "maxH": 36, + "moved": false, + "static": false, + "resizeHandles": ["s", "w", "e", "n", "sw", "nw", "se", "ne"], + "isBounded": false + } + ] + }, + "layoutFidget": "grid" + }, + "theme": { + "id": "Homebase-Tab 4 - 1-Theme", + "name": "Homebase-Tab 4 - 1-Theme", + "properties": { + "background": "#ffffff", + "backgroundHTML": "", + "fidgetBackground": "#ffffff", + "fidgetBorderColor": "#eeeeee", + "fidgetBorderRadius": "0px", + "fidgetBorderWidth": "0px", + "fidgetShadow": "none", + "font": "Inter", + "fontColor": "#000000", + "gridSpacing": "0", + "headingsFont": "Inter", + "headingsFontColor": "#000000", + "musicURL": "https://www.youtube.com/watch?v=dMXlZ4y7OK4&t=1804" + } + }, + "fidgetInstanceDatums": { + "nounsHome:3a8d7f19-3e77-4c2b-9c7f-1a6f5f5a6f01": { + "config": { + "data": {}, + "editable": true, + "settings": { + "background": "var(--user-theme-fidget-background)", + "fidgetBorderColor": "var(--user-theme-fidget-border-color)", + "showOnMobile": true, + "isScrollable": true + } + }, + "fidgetType": "nounsHome", + "id": "nounsHome:3a8d7f19-3e77-4c2b-9c7f-1a6f5f5a6f01" + } + }, + "fidgetTrayContents": [], + "isEditable": false, + "timestamp": "2025-06-20T05:58:44.080Z" + } + }, + "layout": { + "defaultLayoutFidget": "grid", + "gridSpacing": 16, + "theme": { + "background": "#ffffff", + "fidgetBackground": "#ffffff", + "font": "Inter", + "fontColor": "#000000" + } + } +} +``` + +--- + +## 7. `explore_page_config` (ExplorePageConfig) + +**Purpose:** Defines the structure, layout, and content of the explore/discovery page + +**Structure:** Same as `home_page_config` (HomePageConfig) + +```json +{ + "defaultTab": "Explore", + "tabOrder": ["Explore", "Channels", "Tokens"], + "tabs": { + "Explore": { /* TabConfig */ }, + "Channels": { /* TabConfig */ }, + "Tokens": { /* TabConfig */ } + }, + "layout": { + "defaultLayoutFidget": "grid", + "gridSpacing": 16, + "theme": { /* ThemeProperties */ } + } +} +``` + +**Fields Explained:** Same as `home_page_config` above + +**Difference from Home Page:** +- Typically has different tabs (Explore, Channels, Tokens vs. Nouns, Social, Governance) +- Usually more discovery-focused content +- May have different default fidgets + +**Example:** +```json +{ + "defaultTab": "Explore", + "tabOrder": ["Explore"], + "tabs": { + "Explore": { + "name": "Explore", + "displayName": "Explore", + "layoutID": "explore-layout-id", + "layoutDetails": { + "layoutConfig": { + "layout": [] + }, + "layoutFidget": "grid" + }, + "theme": { /* ThemeProperties */ }, + "fidgetInstanceDatums": {}, + "fidgetTrayContents": [], + "isEditable": false, + "timestamp": "2025-06-20T05:58:44.080Z" + } + }, + "layout": { + "defaultLayoutFidget": "grid", + "gridSpacing": 16, + "theme": { + "background": "#ffffff", + "fidgetBackground": "#ffffff", + "font": "Inter", + "fontColor": "#000000" + } + } +} +``` + +--- + +## 8. `navigation_config` (NavigationConfig) - Optional + +**Purpose:** Navigation bar items and settings + +**Structure:** +```json +{ + "items": [ + { + "id": "home", + "label": "Home", + "href": "/home", + "icon": "home", + "openInNewTab": false, + "requiresAuth": false + }, + { + "id": "explore", + "label": "Explore", + "href": "/explore", + "icon": "explore", + "openInNewTab": false, + "requiresAuth": false + }, + { + "id": "notifications", + "label": "Notifications", + "href": "/notifications", + "icon": "notifications", + "openInNewTab": false, + "requiresAuth": true + }, + { + "id": "space-token", + "label": "$SPACE", + "href": "/t/base/0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab/Profile", + "icon": "space", + "openInNewTab": false, + "requiresAuth": false + } + ], + "logoTooltip": { + "text": "wtf is nouns?", + "href": "https://nouns.wtf" + }, + "showMusicPlayer": true, + "showSocials": true +} +``` + +**Fields Explained:** + +### `items` (Array of NavigationItem) +- **`id`**: Unique identifier for navigation item +- **`label`**: Display text +- **`href`**: URL/path to navigate to +- **`icon`**: Icon type ("home", "explore", "notifications", "search", "space", "robot", "custom") +- **`openInNewTab`**: Open link in new tab? (boolean) +- **`requiresAuth`**: Require authentication to see this item? (boolean) + +### `logoTooltip` (Optional) +- **`text`**: Tooltip text shown on logo hover +- **`href`**: Optional link when clicking tooltip + +### `showMusicPlayer` (Optional) +- Show music player in navigation? (boolean) + +### `showSocials` (Optional) +- Show social links in navigation? (boolean) + +**Example:** +```json +{ + "items": [ + { + "id": "home", + "label": "Home", + "href": "/home", + "icon": "home" + }, + { + "id": "explore", + "label": "Explore", + "href": "/explore", + "icon": "explore" + } + ], + "logoTooltip": { + "text": "wtf is nouns?", + "href": "https://nouns.wtf" + }, + "showMusicPlayer": true, + "showSocials": true +} +``` + +--- + +## 9. `ui_config` (UIConfig) - Optional + +**Purpose:** UI color scheme and styling + +**Structure:** +```json +{ + "primaryColor": "rgb(37, 99, 235)", // Primary brand color + "primaryHoverColor": "rgb(29, 78, 216)", // Primary color on hover + "primaryActiveColor": "rgb(30, 64, 175)", // Primary color when active + "castButton": { + "backgroundColor": "rgb(37, 99, 235)", // Cast button background + "hoverColor": "rgb(29, 78, 216)", // Cast button hover color + "activeColor": "rgb(30, 64, 175)" // Cast button active color + } +} +``` + +**Fields Explained:** +- **`primaryColor`**: Main brand color (RGB, hex, or CSS color) +- **`primaryHoverColor`**: Color when hovering over primary elements +- **`primaryActiveColor`**: Color when primary elements are active/pressed +- **`castButton`**: Cast button specific colors + - **`backgroundColor`**: Default background + - **`hoverColor`**: Hover state color + - **`activeColor`**: Active/pressed state color + +**Usage:** +- Applied to buttons, links, and interactive elements +- Used for consistent color theming across UI +- Can override theme colors for specific UI elements + +**Example:** +```json +{ + "primaryColor": "rgb(37, 99, 235)", + "primaryHoverColor": "rgb(29, 78, 216)", + "primaryActiveColor": "rgb(30, 64, 175)", + "castButton": { + "backgroundColor": "rgb(37, 99, 235)", + "hoverColor": "rgb(29, 78, 216)", + "activeColor": "rgb(30, 64, 175)" + } +} +``` + +--- + +## Database Schema Summary + +| Column | Type | Required | Purpose | +|--------|------|----------|---------| +| `brand_config` | JSONB | ✅ Yes | Brand identity and metadata | +| `assets_config` | JSONB | ✅ Yes | Visual assets (logos, icons) | +| `theme_config` | JSONB | ✅ Yes | Theme definitions | +| `community_config` | JSONB | ✅ Yes | Community integration data | +| `fidgets_config` | JSONB | ✅ Yes | Enabled/disabled fidgets | +| `home_page_config` | JSONB | ✅ Yes | Home page structure | +| `explore_page_config` | JSONB | ✅ Yes | Explore page structure | +| `navigation_config` | JSONB | ❌ Optional | Navigation items | +| `ui_config` | JSONB | ❌ Optional | UI colors | + +--- + +## Complete Example Config + +Here's a complete example of what a full config would look like in the database: + +```json +{ + "brand_config": { + "name": "nouns", + "displayName": "Nouns", + "tagline": "A space for Nouns", + "description": "The social hub for Nouns", + "miniAppTags": ["nouns", "client", "customizable"] + }, + "assets_config": { + "logos": { + "main": "/images/nouns/logo.svg", + "icon": "/images/nouns/noggles.svg", + "favicon": "/images/favicon.ico", + "appleTouch": "/images/apple-touch-icon.png", + "og": "/images/nouns/og.png", + "splash": "/images/nouns/splash.png" + } + }, + "theme_config": { + "default": { /* ThemeProperties */ }, + "nounish": { /* ThemeProperties */ } + }, + "community_config": { + "type": "nouns", + "urls": { /* URLs */ }, + "social": { /* Social handles */ }, + "governance": { /* Governance URLs */ }, + "tokens": { /* Token definitions */ }, + "contracts": { /* Contract addresses */ } + }, + "fidgets_config": { + "enabled": ["feed", "cast", "gallery"], + "disabled": [] + }, + "home_page_config": { + "defaultTab": "Nouns", + "tabOrder": ["Nouns", "Social"], + "tabs": { /* Tab configurations */ }, + "layout": { /* Layout settings */ } + }, + "explore_page_config": { + "defaultTab": "Explore", + "tabOrder": ["Explore"], + "tabs": { /* Tab configurations */ }, + "layout": { /* Layout settings */ } + }, + "navigation_config": { + "items": [ /* Navigation items */ ], + "logoTooltip": { /* Tooltip config */ }, + "showMusicPlayer": true, + "showSocials": true + }, + "ui_config": { + "primaryColor": "rgb(37, 99, 235)", + "primaryHoverColor": "rgb(29, 78, 216)", + "primaryActiveColor": "rgb(30, 64, 175)", + "castButton": { /* Cast button colors */ } + } +} +``` + +--- + +## Notes + +1. **All configs are JSONB** - PostgreSQL JSONB type allows: + - Efficient storage + - Indexing on specific keys + - Querying with JSON operators + - Validation with JSON schemas + +2. **Optional fields** - `navigation_config` and `ui_config` are optional: + - Can be `NULL` in database + - Will use defaults if not provided + +3. **Nested structures** - Some configs have deeply nested structures: + - `home_page_config.tabs[tabId].fidgetInstanceDatums[fidgetId]` - Very deep nesting + - Consider flattening if querying becomes complex + +4. **Size considerations** - Large configs (especially `home_page_config` and `explore_page_config`) can be large: + - Monitor JSONB size + - Consider compression if needed + - Index frequently queried fields + +5. **Validation** - Consider adding JSON schema validation: + - At application level (TypeScript types) + - At database level (PostgreSQL CHECK constraints) + - At API level (request validation) + diff --git a/docs/DATABASE_CONFIG_MIGRATION_PLAN.md b/docs/DATABASE_CONFIG_MIGRATION_PLAN.md new file mode 100644 index 000000000..231e3e958 --- /dev/null +++ b/docs/DATABASE_CONFIG_MIGRATION_PLAN.md @@ -0,0 +1,1000 @@ +# Database-Backed Configuration System Migration Plan + +## Overview + +This document outlines the approach for migrating the community configuration system from build-time static files to a database-backed system that can be updated by admins in real-time. + +## Current State + +- **Build-time configuration** via `NEXT_PUBLIC_COMMUNITY` environment variable +- **Static TypeScript files** in `src/config/{community}/` +- **No runtime updates** - requires rebuild to change configuration +- **No admin interface** - changes require code deployment + +## Target State + +- **Database-backed configuration** stored in Supabase +- **Admin interface** for updating configurations +- **Versioning/history** for configuration changes +- **Caching layer** for performance +- **Backward compatibility** with existing code +- **Multi-community support** with active/inactive states + +## Database Schema Design + +### 1. Core Tables + +#### `community_configs` - Main Configuration Table + +```sql +CREATE TABLE "public"."community_configs" ( + "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "community_id" VARCHAR(50) NOT NULL UNIQUE, -- 'nouns', 'clanker', etc. + "is_active" BOOLEAN DEFAULT true, + "version" INTEGER DEFAULT 1, + "created_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), + "updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), + "created_by" VARCHAR(255), -- Admin identity/public key + "updated_by" VARCHAR(255), -- Admin identity/public key + + -- Configuration sections as JSONB columns + "brand_config" JSONB NOT NULL, + "assets_config" JSONB NOT NULL, + "theme_config" JSONB NOT NULL, + "community_config" JSONB NOT NULL, + "fidgets_config" JSONB NOT NULL, + "home_page_config" JSONB NOT NULL, + "explore_page_config" JSONB NOT NULL, + "navigation_config" JSONB, + "ui_config" JSONB, + + -- Metadata + "notes" TEXT, -- Admin notes about this version + "is_published" BOOLEAN DEFAULT false -- Draft vs published +); + +CREATE INDEX "idx_community_configs_community_id" ON "public"."community_configs"("community_id"); +CREATE INDEX "idx_community_configs_active" ON "public"."community_configs"("is_active") WHERE "is_active" = true; +CREATE INDEX "idx_community_configs_published" ON "public"."community_configs"("is_published") WHERE "is_published" = true; +``` + +#### `community_config_history` - Version History + +```sql +CREATE TABLE "public"."community_config_history" ( + "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "community_config_id" UUID NOT NULL REFERENCES "public"."community_configs"("id") ON DELETE CASCADE, + "version" INTEGER NOT NULL, + "config_snapshot" JSONB NOT NULL, -- Full config snapshot + "changed_sections" TEXT[], -- Array of section names that changed + "created_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), + "created_by" VARCHAR(255), + "change_notes" TEXT +); + +CREATE INDEX "idx_config_history_community_config_id" ON "public"."community_config_history"("community_config_id"); +CREATE INDEX "idx_config_history_version" ON "public"."community_config_history"("community_config_id", "version"); +``` + +#### `community_config_admins` - Admin Permissions + +```sql +CREATE TABLE "public"."community_config_admins" ( + "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "community_id" VARCHAR(50) NOT NULL, + "admin_identity_public_key" VARCHAR(255) NOT NULL, -- Cryptographic identity + "admin_fid" BIGINT, -- Optional Farcaster ID + "permissions" TEXT[] DEFAULT ARRAY['read', 'write'], -- 'read', 'write', 'publish', 'delete' + "created_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), + "created_by" VARCHAR(255), + "is_active" BOOLEAN DEFAULT true, + + UNIQUE("community_id", "admin_identity_public_key") +); + +CREATE INDEX "idx_config_admins_community" ON "public"."community_config_admins"("community_id"); +CREATE INDEX "idx_config_admins_identity" ON "public"."community_config_admins"("admin_identity_public_key"); +``` + +#### `community_config_cache` - Cache Table (Optional) + +```sql +CREATE TABLE "public"."community_config_cache" ( + "community_id" VARCHAR(50) PRIMARY KEY, + "config_data" JSONB NOT NULL, + "version" INTEGER NOT NULL, + "cached_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), + "expires_at" TIMESTAMP WITH TIME ZONE NOT NULL +); + +CREATE INDEX "idx_config_cache_expires" ON "public"."community_config_cache"("expires_at"); +``` + +### 2. Row Level Security (RLS) Policies + +```sql +-- Enable RLS on all tables +ALTER TABLE "public"."community_configs" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "public"."community_config_history" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "public"."community_config_admins" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "public"."community_config_cache" ENABLE ROW LEVEL SECURITY; + +-- Public read access for active, published configs +CREATE POLICY "public_read_active_configs" ON "public"."community_configs" + FOR SELECT + USING ("is_active" = true AND "is_published" = true); + +-- Admin read access (can see drafts) +CREATE POLICY "admin_read_all_configs" ON "public"."community_configs" + FOR SELECT + USING ( + EXISTS ( + SELECT 1 FROM "public"."community_config_admins" cca + WHERE cca."community_id" = "community_configs"."community_id" + AND cca."admin_identity_public_key" = current_setting('app.current_identity_public_key', true) + AND cca."is_active" = true + AND 'read' = ANY(cca."permissions") + ) + ); + +-- Admin write access +CREATE POLICY "admin_write_configs" ON "public"."community_configs" + FOR INSERT + WITH CHECK ( + EXISTS ( + SELECT 1 FROM "public"."community_config_admins" cca + WHERE cca."community_id" = "community_configs"."community_id" + AND cca."admin_identity_public_key" = current_setting('app.current_identity_public_key', true) + AND cca."is_active" = true + AND 'write' = ANY(cca."permissions") + ) + ); + +CREATE POLICY "admin_update_configs" ON "public"."community_configs" + FOR UPDATE + USING ( + EXISTS ( + SELECT 1 FROM "public"."community_config_admins" cca + WHERE cca."community_id" = "community_configs"."community_id" + AND cca."admin_identity_public_key" = current_setting('app.current_identity_public_key', true) + AND cca."is_active" = true + AND 'write' = ANY(cca."permissions") + ) + ); + +-- History is readable by admins +CREATE POLICY "admin_read_history" ON "public"."community_config_history" + FOR SELECT + USING ( + EXISTS ( + SELECT 1 FROM "public"."community_config_admins" cca + JOIN "public"."community_configs" cc ON cc."id" = "community_config_history"."community_config_id" + WHERE cca."community_id" = cc."community_id" + AND cca."admin_identity_public_key" = current_setting('app.current_identity_public_key', true) + AND cca."is_active" = true + AND 'read' = ANY(cca."permissions") + ) + ); + +-- Cache is publicly readable +CREATE POLICY "public_read_cache" ON "public"."community_config_cache" + FOR SELECT + USING ("expires_at" > now()); +``` + +### 3. Database Functions + +#### Function to Get Active Config + +```sql +CREATE OR REPLACE FUNCTION "public"."get_active_community_config"( + p_community_id VARCHAR(50) +) +RETURNS JSONB +LANGUAGE plpgsql +SECURITY DEFINER +AS $$ +DECLARE + v_config JSONB; +BEGIN + SELECT jsonb_build_object( + 'brand', "brand_config", + 'assets', "assets_config", + 'theme', "theme_config", + 'community', "community_config", + 'fidgets', "fidgets_config", + 'homePage', "home_page_config", + 'explorePage', "explore_page_config", + 'navigation', "navigation_config", + 'ui', "ui_config" + ) + INTO v_config + FROM "public"."community_configs" + WHERE "community_id" = p_community_id + AND "is_active" = true + AND "is_published" = true + ORDER BY "version" DESC + LIMIT 1; + + RETURN v_config; +END; +$$; +``` + +#### Function to Create Config Version + +```sql +CREATE OR REPLACE FUNCTION "public"."create_config_version"( + p_community_id VARCHAR(50), + p_config_data JSONB, + p_change_notes TEXT DEFAULT NULL, + p_admin_identity VARCHAR(255) DEFAULT NULL +) +RETURNS UUID +LANGUAGE plpgsql +SECURITY DEFINER +AS $$ +DECLARE + v_config_id UUID; + v_new_version INTEGER; + v_old_config JSONB; +BEGIN + -- Get current version + SELECT "id", "version", jsonb_build_object( + 'brand', "brand_config", + 'assets', "assets_config", + 'theme', "theme_config", + 'community', "community_config", + 'fidgets', "fidgets_config", + 'homePage', "home_page_config", + 'explorePage', "explore_page_config", + 'navigation', "navigation_config", + 'ui', "ui_config" + ) + INTO v_config_id, v_new_version, v_old_config + FROM "public"."community_configs" + WHERE "community_id" = p_community_id + AND "is_active" = true + ORDER BY "version" DESC + LIMIT 1; + + -- Increment version + v_new_version := COALESCE(v_new_version, 0) + 1; + + -- If config exists, archive old version + IF v_config_id IS NOT NULL THEN + INSERT INTO "public"."community_config_history" ( + "community_config_id", + "version", + "config_snapshot", + "created_by", + "change_notes" + ) VALUES ( + v_config_id, + v_new_version - 1, + v_old_config, + p_admin_identity, + p_change_notes + ); + + -- Deactivate old version + UPDATE "public"."community_configs" + SET "is_active" = false + WHERE "id" = v_config_id; + END IF; + + -- Create new version + INSERT INTO "public"."community_configs" ( + "community_id", + "version", + "brand_config", + "assets_config", + "theme_config", + "community_config", + "fidgets_config", + "home_page_config", + "explore_page_config", + "navigation_config", + "ui_config", + "created_by", + "updated_by", + "notes", + "is_published" + ) VALUES ( + p_community_id, + v_new_version, + p_config_data->'brand', + p_config_data->'assets', + p_config_data->'theme', + p_config_data->'community', + p_config_data->'fidgets', + p_config_data->'homePage', + p_config_data->'explorePage', + p_config_data->'navigation', + p_config_data->'ui', + p_admin_identity, + p_admin_identity, + p_change_notes, + true -- Auto-publish for now, can be made configurable + ) + RETURNING "id" INTO v_config_id; + + -- Update cache + INSERT INTO "public"."community_config_cache" ( + "community_id", + "config_data", + "version", + "expires_at" + ) VALUES ( + p_community_id, + p_config_data, + v_new_version, + now() + INTERVAL '1 hour' + ) + ON CONFLICT ("community_id") DO UPDATE SET + "config_data" = EXCLUDED."config_data", + "version" = EXCLUDED."version", + "cached_at" = now(), + "expires_at" = now() + INTERVAL '1 hour'; + + RETURN v_config_id; +END; +$$; +``` + +## Application Architecture + +### Build-Time Configuration Generation + +**Key Principle**: Configs are fetched from the database at build time and generated as static TypeScript files. Runtime uses these static files with zero database queries. + +### 1. Build-Time Config Generator Script + +Create a script that runs before the Next.js build to fetch configs from the database and generate static TypeScript files: + +```typescript +// scripts/generate-configs.ts + +import { createClient } from '@supabase/supabase-js'; +import { writeFile, mkdir } from 'fs/promises'; +import { join } from 'path'; +import { SystemConfig } from '../src/config/systemConfig'; + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; +const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + +if (!supabaseUrl || !supabaseKey) { + console.error('Missing Supabase environment variables'); + process.exit(1); +} + +const supabase = createClient(supabaseUrl, supabaseKey); + +/** + * Fetch active config from database + */ +async function fetchConfigFromDB(communityId: string): Promise { + try { + const { data, error } = await supabase + .rpc('get_active_community_config', { p_community_id: communityId }) + .single(); + + if (error) { + console.error(`Failed to fetch config for ${communityId}:`, error); + return null; + } + + return data as SystemConfig; + } catch (error) { + console.error(`Error fetching config for ${communityId}:`, error); + return null; + } +} + +/** + * Generate TypeScript file for a config section + */ +function generateConfigFile( + sectionName: string, + data: any, + communityId: string +): string { + const exportName = `${communityId}${sectionName.charAt(0).toUpperCase() + sectionName.slice(1)}`; + return `export const ${exportName} = ${JSON.stringify(data, null, 2)} as const;\n`; +} + +/** + * Generate the main index file for a community + */ +function generateIndexFile(communityId: string, sections: string[]): string { + const imports = sections.map(section => { + const importName = `${communityId}${section.charAt(0).toUpperCase() + section.slice(1)}`; + return `import { ${importName} } from './${communityId}.${section}';`; + }).join('\n'); + + const exports = sections.map(section => { + const varName = `${communityId}${section.charAt(0).toUpperCase() + section.slice(1)}`; + return ` ${section}: ${varName},`; + }).join('\n'); + + const exportName = `${communityId}SystemConfig`; + + return `${imports} + +export const ${exportName} = { +${exports} +}; + +// Re-export individual sections +${sections.map(section => { + const varName = `${communityId}${section.charAt(0).toUpperCase() + section.slice(1)}`; + return `export { ${varName} } from './${communityId}.${section}';`; +}).join('\n')} +`; +} + +/** + * Generate initial space creator files (if needed) + * These can be kept as static or also generated from DB + */ +async function generateInitialSpaceFiles(communityId: string, config: SystemConfig) { + // For now, we'll keep initial space files as static + // They can be migrated to DB later if needed + console.log(`Skipping initial space files for ${communityId} (keeping static)`); +} + +/** + * Main generation function + */ +async function generateConfigFiles(communityId: string) { + console.log(`Generating config files for ${communityId}...`); + + // Fetch config from database + const config = await fetchConfigFromDB(communityId); + + if (!config) { + console.warn(`No config found in DB for ${communityId}, skipping generation`); + return false; + } + + const configDir = join(process.cwd(), 'src', 'config', communityId); + + // Ensure directory exists + await mkdir(configDir, { recursive: true }); + + // Generate individual section files + const sections = [ + 'brand', + 'assets', + 'theme', + 'community', + 'fidgets', + 'home', + 'explore', + 'navigation', + 'ui' + ]; + + for (const section of sections) { + const sectionData = config[section as keyof SystemConfig]; + if (sectionData !== undefined) { + const content = generateConfigFile(section, sectionData, communityId); + const filePath = join(configDir, `${communityId}.${section}.ts`); + await writeFile(filePath, content, 'utf-8'); + console.log(` ✓ Generated ${filePath}`); + } + } + + // Generate index file + const indexContent = generateIndexFile( + communityId, + sections.filter(s => config[s as keyof SystemConfig] !== undefined) + ); + const indexPath = join(configDir, 'index.ts'); + await writeFile(indexPath, indexContent, 'utf-8'); + console.log(` ✓ Generated ${indexPath}`); + + // Generate initial space files (optional, can keep static) + await generateInitialSpaceFiles(communityId, config); + + console.log(`✓ Successfully generated config files for ${communityId}`); + return true; +} + +/** + * Update main config index to include generated configs + */ +async function updateMainConfigIndex(communityIds: string[]) { + const indexPath = join(process.cwd(), 'src', 'config', 'index.ts'); + + // Read existing file + const fs = await import('fs/promises'); + let content = await fs.readFile(indexPath, 'utf-8'); + + // Add imports for generated configs + const imports = communityIds.map(id => { + const configName = `${id}SystemConfig`; + return `import { ${configName} } from './${id}/index';`; + }).join('\n'); + + // Update switch statement + const switchCases = communityIds.map(id => { + return ` case '${id}':\n return ${id}SystemConfig;`; + }).join('\n'); + + // This is a simplified version - actual implementation would need + // more sophisticated parsing/updating of the existing file + console.log('Note: Main config index may need manual updates'); +} + +/** + * Main execution + */ +async function main() { + const communities = process.env.GENERATE_CONFIGS?.split(',') || + ['nouns', 'clanker', 'example']; + + console.log('Starting config generation from database...'); + console.log(`Communities: ${communities.join(', ')}`); + + const results = await Promise.all( + communities.map(id => generateConfigFiles(id)) + ); + + const successCount = results.filter(Boolean).length; + console.log(`\n✓ Generated ${successCount}/${communities.length} configs`); + + if (successCount < communities.length) { + console.warn('Some configs failed to generate. Build will use static fallbacks.'); + process.exit(0); // Don't fail build, allow fallback + } +} + +main().catch(error => { + console.error('Config generation failed:', error); + process.exit(1); +}); +``` + +### 2. Package.json Scripts + +Add scripts to run the generator before build: + +```json +{ + "scripts": { + "generate-configs": "tsx scripts/generate-configs.ts", + "prebuild": "npm run generate-configs", + "build": "next build", + "dev": "next dev", + "dev:with-configs": "npm run generate-configs && next dev" + } +} +``` + +### 3. Updated Configuration Loader + +The loader remains mostly the same, but now uses the generated files: + +```typescript +// src/config/index.ts + +// Import generated configs (these are created at build time) +import { nounsSystemConfig } from './nouns/index'; +import { exampleSystemConfig } from './example/index'; +import { clankerSystemConfig } from './clanker/index'; +import { SystemConfig } from './systemConfig'; + +// Fallback to static configs if generated ones don't exist +// This provides safety if DB fetch fails during build +const STATIC_FALLBACKS = { + nouns: nounsSystemConfig, + example: exampleSystemConfig, + clanker: clankerSystemConfig as unknown as SystemConfig, +}; + +export const loadSystemConfig = (): SystemConfig => { + const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + + // Use generated config (from build-time DB fetch) + // Falls back to static if generation failed + const config = STATIC_FALLBACKS[communityConfig.toLowerCase() as keyof typeof STATIC_FALLBACKS]; + + if (!config) { + console.warn( + `Invalid community configuration: "${communityConfig}". ` + + `Available options: ${Object.keys(STATIC_FALLBACKS).join(', ')}. ` + + `Falling back to "nouns" configuration.` + ); + return STATIC_FALLBACKS.nouns; + } + + return config; +}; +``` + +### 4. Admin Service (Runtime - For Admin Interface Only) + +Create a service for admin operations (only used in admin interface, not in main app): + +```typescript +// src/common/data/services/adminConfigService.ts + +import { createClient } from '@supabase/supabase-js'; +import { SystemConfig } from '@/config/systemConfig'; + +/** + * Admin-only service for updating configs in database + * This is NOT used by the main application at runtime + */ +export class AdminConfigService { + private supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! // Use service role for admin operations + ); + + /** + * Update configuration (admin only) + */ + async updateConfig( + communityId: string, + config: Partial, + adminIdentity: string, + changeNotes?: string + ): Promise { + // Get current config + const { data: currentData } = await this.supabase + .rpc('get_active_community_config', { p_community_id: communityId }) + .single(); + + if (!currentData) { + throw new Error('Current config not found'); + } + + const currentConfig = currentData as SystemConfig; + + // Merge with new config + const updatedConfig: SystemConfig = { + ...currentConfig, + ...config, + brand: { ...currentConfig.brand, ...config.brand }, + assets: { ...currentConfig.assets, ...config.assets }, + theme: { ...currentConfig.theme, ...config.theme }, + community: { ...currentConfig.community, ...config.community }, + fidgets: { ...currentConfig.fidgets, ...config.fidgets }, + homePage: { ...currentConfig.homePage, ...config.homePage }, + explorePage: { ...currentConfig.explorePage, ...config.explorePage }, + navigation: config.navigation ?? currentConfig.navigation, + ui: config.ui ?? currentConfig.ui, + }; + + // Create new version + const { error } = await this.supabase.rpc('create_config_version', { + p_community_id: communityId, + p_config_data: updatedConfig, + p_change_notes: changeNotes, + p_admin_identity: adminIdentity + }); + + if (error) { + console.error('Failed to update config:', error); + return false; + } + + return true; + } + + /** + * Get config history + */ + async getConfigHistory(communityId: string, limit: number = 10): Promise { + const { data, error } = await this.supabase + .from('community_configs') + .select(` + id, + version, + created_at, + created_by, + notes + `) + .eq('community_id', communityId) + .order('version', { ascending: false }) + .limit(limit); + + if (error) { + console.error('Failed to fetch history:', error); + return []; + } + + return data || []; + } + + /** + * Trigger rebuild (webhook or manual) + */ + async triggerRebuild(communityId: string): Promise { + // This would trigger your CI/CD to rebuild + // Implementation depends on your deployment setup + // Could use GitHub Actions API, Vercel API, etc. + console.log(`Rebuild triggered for ${communityId}`); + return true; + } +} +``` + +### 5. Updated Hook (No Changes Needed) + +The hook remains simple since it's using static files: + +```typescript +// src/common/lib/hooks/useSystemConfig.ts + +import { useMemo } from 'react'; +import { loadSystemConfig, SystemConfig } from '@/config'; + +let cachedConfig: SystemConfig | null = null; + +export const useSystemConfig = (): SystemConfig => { + return useMemo(() => { + if (cachedConfig) { + return cachedConfig; + } + + cachedConfig = loadSystemConfig(); + return cachedConfig; + }, []); +}; + +export { loadSystemConfig }; +``` + +## Migration Strategy + +### Phase 1: Database Setup (Week 1) + +1. **Create migration files** + - Create tables: `community_configs`, `community_config_history`, `community_config_admins` + - Set up RLS policies + - Create database functions (`get_active_community_config`, `create_config_version`) + +2. **Seed initial data** + - Migrate existing static configs to database + - Create admin accounts for each community + - Set up initial published versions + +### Phase 2: Build-Time Generator (Week 1-2) + +1. **Create config generator script** + - `scripts/generate-configs.ts` - Fetches from DB and generates TypeScript files + - Add `generate-configs` script to package.json + - Add `prebuild` hook to run generator before build + +2. **Test generator** + - Test fetching from database + - Test file generation + - Test fallback to static configs if DB unavailable + +### Phase 3: Update Build Process (Week 2) + +1. **Update package.json scripts** + - Add `prebuild` hook + - Update CI/CD to run generator before build + - Ensure environment variables are available during build + +2. **Update config loader** + - Keep existing structure (uses generated files) + - Maintain fallback to static configs + - No runtime changes needed + +### Phase 4: Admin Interface (Week 2-3) + +1. **Create admin pages** + - Config editor UI + - Section-by-section editing + - Preview functionality + - History viewer + +2. **API endpoints** + - `/api/admin/config/[communityId]` - GET/PUT config + - `/api/admin/config/[communityId]/history` - GET history + - `/api/admin/config/[communityId]/rebuild` - Trigger rebuild + +3. **Rebuild trigger** + - Webhook or manual trigger to rebuild after config updates + - Integration with CI/CD (GitHub Actions, Vercel, etc.) + +### Phase 5: Testing & Rollout (Week 3-4) + +1. **Testing** + - Test config generator script + - Test build process with generated configs + - Test admin interface updates + - Test rebuild trigger + +2. **Gradual rollout** + - Start with one community (e.g., 'example') + - Monitor build process and generated files + - Roll out to other communities + +### Phase 6: Cleanup & Optimization (Week 4+) + +1. **Keep static configs as fallback** + - Always maintain static configs as safety net + - Generated configs take precedence + - Static configs used if generation fails + +2. **Optimization** + - Add validation to generator + - Add error handling and logging + - Monitor build times + - Consider caching generated files in CI/CD + +## Admin Interface Design + +### Pages Needed + +1. **Config Dashboard** (`/admin/config`) + - List all communities + - Active/published status + - Quick actions + - Rebuild status/triggers + +2. **Config Editor** (`/admin/config/[communityId]`) + - Section tabs (Brand, Assets, Theme, etc.) + - Form inputs for each section + - Preview pane + - Save/Publish buttons + - "Trigger Rebuild" button after saving + +3. **Config History** (`/admin/config/[communityId]/history`) + - Version list + - Diff viewer + - Rollback functionality + - Rebuild trigger for any version + +4. **Admin Management** (`/admin/config/[communityId]/admins`) + - Add/remove admins + - Permission management + +### Rebuild Trigger Options + +**Option 1: Manual Trigger (Recommended for MVP)** +- Admin clicks "Trigger Rebuild" button +- Calls API endpoint that triggers CI/CD rebuild +- Shows build status/link + +**Option 2: Automatic Trigger** +- Webhook from database (PostgreSQL triggers) +- Calls CI/CD API automatically on config update +- More complex but seamless + +**Option 3: Scheduled Rebuilds** +- Daily/hourly rebuilds to pick up changes +- Simple but less immediate + +**Implementation Example (GitHub Actions):** +```typescript +// API endpoint: /api/admin/config/[communityId]/rebuild +export async function POST(request: Request) { + const { communityId } = await request.json(); + + // Trigger GitHub Actions workflow + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/actions/workflows/${workflowId}/dispatches`, + { + method: 'POST', + headers: { + 'Authorization': `token ${GITHUB_TOKEN}`, + 'Accept': 'application/vnd.github.v3+json', + }, + body: JSON.stringify({ + ref: 'main', + inputs: { + community: communityId, + }, + }), + } + ); + + return Response.json({ success: response.ok }); +} +``` + +## Security Considerations + +1. **Authentication** + - Use existing identity system (cryptographic keys) + - Verify admin permissions before allowing edits + +2. **Authorization** + - RLS policies enforce database-level security + - Application-level checks as backup + +3. **Validation** + - Validate config structure before saving + - Type checking against `SystemConfig` interface + - Sanitize JSON inputs + +4. **Audit Trail** + - All changes logged in history table + - Track who made changes and when + +## Performance Considerations + +1. **Build-Time Generation** + - Configs fetched once during build (not at runtime) + - Zero database queries in production + - Generated files are static TypeScript (fast imports) + - No runtime caching needed + +2. **Database Optimization** + - Indexes on frequently queried columns (for admin interface) + - Build-time queries are infrequent (only during builds) + - Admin queries are lightweight (only used in admin interface) + +3. **Build Process** + - Generator runs in parallel for multiple communities + - Failed generations don't break build (fallback to static) + - Generated files can be cached in CI/CD + - Build time impact is minimal (~1-2 seconds per community) + +## Rollback Plan + +If issues arise: + +1. **Immediate**: Remove `prebuild` hook, build uses static configs directly +2. **Short-term**: Fix generator script or database issues, re-enable generation +3. **Long-term**: Keep static configs as permanent fallback (always available) + +**Advantages of this approach:** +- Static configs are always available as fallback +- No runtime dependencies on database +- Build failures don't affect runtime +- Easy to rollback by simply removing prebuild hook + +## Future Enhancements + +1. **Draft/Preview System** + - Save drafts without publishing + - Preview changes before publishing + - Scheduled publishing + +2. **Multi-environment Support** + - Dev/staging/production configs + - Environment-specific overrides + +3. **Config Templates** + - Save configs as templates + - Clone configs for new communities + +4. **API for External Tools** + - REST API for config management + - Webhook notifications on changes + +5. **Advanced Permissions** + - Section-level permissions + - Read-only admins + - Approval workflows + +## Summary + +This migration plan provides: + +- ✅ **Database schema** for storing configs with versioning +- ✅ **Admin permission system** using existing identity infrastructure +- ✅ **Build-time generation** - Zero runtime database queries +- ✅ **Backward compatibility** - Static configs always available as fallback +- ✅ **Migration path** with phased rollout +- ✅ **Security** through RLS and validation +- ✅ **Audit trail** for all changes +- ✅ **Performance** - Static files at runtime, DB only at build time + +**Key Benefits:** +- **Zero runtime overhead** - Configs are static files at runtime +- **Admin updates** - Changes made in database, reflected in next build +- **Safe fallback** - Static configs always available if generation fails +- **Fast builds** - Generator runs in parallel, minimal build time impact +- **Easy rollback** - Simply remove prebuild hook to use static configs + +The system maintains backward compatibility while enabling admin updates through the database, with changes reflected in builds rather than at runtime. + diff --git a/docs/E2BIG_SOLUTION.md b/docs/E2BIG_SOLUTION.md new file mode 100644 index 000000000..eb931e7b7 --- /dev/null +++ b/docs/E2BIG_SOLUTION.md @@ -0,0 +1,99 @@ +# E2BIG Error Solution: File-Based Config Instead of Env Var + +## Problem + +The `NEXT_PUBLIC_BUILD_TIME_CONFIG` environment variable is too large (E2BIG error). Environment variables have size limits: +- **Linux/macOS**: ~128KB - 2MB (varies by system) +- **Windows**: ~32KB + +Our config includes: +- Multiple theme definitions with HTML/CSS +- Home page configs with fidget instances +- Explore page configs +- All can easily exceed 100KB+ + +## Solution: Generate TypeScript File Instead + +Instead of storing config in an environment variable, generate a TypeScript file at build time. + +### Approach 1: Single Generated Config File (Recommended) + +Generate `src/config/db-config.ts` at build time, import it at runtime. + +**Pros:** +- ✅ No size limits +- ✅ Type-safe +- ✅ Simple to implement +- ✅ Works with Next.js build system + +**Cons:** +- ⚠️ Generates one file + +### Approach 2: Compress Config + +Compress the JSON before storing in env var. + +**Pros:** +- ✅ Keeps env var approach +- ✅ Reduces size significantly + +**Cons:** +- ⚠️ Still has limits +- ⚠️ Requires decompression +- ⚠️ More complex + +### Approach 3: Split Config + +Store different parts in separate env vars. + +**Pros:** +- ✅ Keeps env var approach + +**Cons:** +- ⚠️ Complex to manage +- ⚠️ Still has limits +- ⚠️ Harder to maintain + +## Recommended: File-Based Approach + +Generate a TypeScript file at build time: + +```javascript +// next.config.mjs + +async function generateConfigFile() { + // Fetch config from DB + const config = await fetchConfigFromDB(); + + if (!config) return; // Fall back to static + + // Generate TypeScript file + const content = `// Auto-generated at build time +import { SystemConfig } from './systemConfig'; + +export const dbConfig: SystemConfig | null = ${JSON.stringify(config, null, 2)} as SystemConfig; +`; + + await writeFile('src/config/db-config.ts', content); +} +``` + +```typescript +// src/config/index.ts + +let dbConfig: SystemConfig | null = null; +try { + const dbModule = require('./db-config'); + dbConfig = dbModule.dbConfig; +} catch { + // File doesn't exist, use static +} + +export const loadSystemConfig = (): SystemConfig => { + if (dbConfig) { + return dbConfig; + } + // Fall back to static... +}; +``` + diff --git a/docs/INCREMENTAL_IMPLEMENTATION_PLAN.md b/docs/INCREMENTAL_IMPLEMENTATION_PLAN.md new file mode 100644 index 000000000..5be4ca207 --- /dev/null +++ b/docs/INCREMENTAL_IMPLEMENTATION_PLAN.md @@ -0,0 +1,1753 @@ +# Incremental Implementation Plan + +## Overview + +This plan breaks down the database-backed configuration system into testable phases, allowing you to validate each piece before moving to the next. + +## Phase 0: Preparation & Setup + +**Goal:** Set up foundation without breaking existing system + +**Tasks:** +1. Create feature branch: `feature/database-configs` +2. Document current static config structure +3. Set up test database (local Supabase) +4. Create migration files structure + +**Testing:** +- ✅ Verify static configs still work +- ✅ Local Supabase runs successfully +- ✅ Can connect to database + +**Rollback:** Just switch back to main branch + +**Estimated Time:** 1-2 hours + +--- + +## Phase 1: Database Schema (Week 1, Day 1-2) + +**Goal:** Create database tables and functions + +**Tasks:** +1. Create migration: `community_configs` table (without homePage/explorePage columns) +2. Create migration: `community_config_history` table +3. Create migration: `community_config_admins` table +4. Add `navPage` spaceType to `spaceRegistrations` +5. Create database function: `get_active_community_config` (excludes page configs) +6. Create database function: `create_config_version` +7. Set up RLS policies +8. Seed initial data (copy from static configs, excluding themes/pages) + +**Files to Create:** +``` +supabase/migrations/ + └── YYYYMMDDHHMMSS_create_community_configs.sql + └── YYYYMMDDHHMMSS_add_navpage_space_type.sql +``` + +**Files to Create/Update:** +``` +src/config/shared/ + └── themes.ts # NEW: Shared theme definitions +``` + +**Testing:** +```sql +-- Test 1: Can fetch config (should not include homePage/explorePage) +SELECT * FROM get_active_community_config('nouns'); + +-- Test 2: Verify navPage spaceType exists +SELECT * FROM spaceRegistrations WHERE "spaceType" = 'navPage'; + +-- Test 3: Can create version +SELECT create_config_version( + 'nouns', + '{"brand": {...}, "assets": {...}}'::jsonb, + 'Initial seed' +); + +-- Test 4: Can query history +SELECT * FROM community_config_history +WHERE community_config_id = (SELECT id FROM community_configs WHERE community_id = 'nouns'); +``` + +**Validation:** +- ✅ Database functions work +- ✅ RLS policies enforce access +- ✅ Can seed existing configs (without themes/pages) +- ✅ History tracking works +- ✅ navPage spaceType added +- ✅ Config excludes homePage/explorePage columns + +**Rollback:** Drop migrations, system unchanged + +**Estimated Time:** 4-6 hours + +--- + +## Phase 2: Basic Config Loading (Week 1, Day 3-4) + +**Goal:** Load config from DB at build time, fetch Spaces for nav items, fallback to static + +**Tasks:** +1. Create `src/config/shared/themes.ts` - Move themes to shared file +2. Update community configs to import shared themes +3. Add build-time fetch in `next.config.mjs`: + - Fetch main config from DB + - Extract spaceIds from navigation items + - Fetch Spaces for nav items with spaceId + - Convert Spaces to page configs +4. Generate TypeScript file (not env var - avoids E2BIG) +5. Update `src/config/index.ts` to use generated file +6. Keep static configs as fallback + +**Files to Create:** +``` +src/config/shared/ + └── themes.ts # NEW: Shared theme definitions +``` + +**Files to Modify:** +``` +next.config.mjs # Add config fetch + Space fetching +src/config/index.ts # Add generated file reading +src/config/nouns/nouns.theme.ts # Import from shared +src/config/clanker/clanker.theme.ts # Import from shared +src/config/example/example.theme.ts # Import from shared +src/config/systemConfig.ts # Update NavigationItem interface +``` + +**Implementation:** + +```javascript +// next.config.mjs (add at top) + +import { createClient } from '@supabase/supabase-js'; + +async function loadConfigFromDB() { + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + + if (!supabaseUrl || !supabaseKey) { + console.log('ℹ️ No Supabase credentials, using static configs'); + return; + } + + const supabase = createClient(supabaseUrl, supabaseKey); + const community = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + + try { + const { data, error } = await supabase + .rpc('get_active_community_config', { p_community_id: community }) + .single(); + + if (error || !data) { + console.log('ℹ️ No config in DB, using static configs'); + return; + } + + // Store in env var + process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(data); + console.log('✅ Loaded config from database'); + } catch (error) { + console.warn('⚠️ Error loading config from DB:', error.message); + } +} + +await loadConfigFromDB(); + +// Continue with existing Next.js config... +``` + +```typescript +// src/config/index.ts (modify loadSystemConfig) + +// Try to import DB config (generated at build time) +let dbConfig: SystemConfig | null = null; +try { + const dbModule = require('./db-config'); + dbConfig = dbModule.dbConfig || null; +} catch { + // File doesn't exist, will use static +} + +export const loadSystemConfig = (): SystemConfig => { + const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + + // Try build-time config from generated file + if (dbConfig) { + // Map page configs from pages object to homePage/explorePage + const homePage = dbConfig.pages?.['home'] || staticConfig.homePage; + const explorePage = dbConfig.pages?.['explore'] || staticConfig.explorePage; + + console.log('✅ Using config from database'); + return { + ...dbConfig, + homePage, + explorePage, + }; + } + + // Fall back to static configs (existing behavior) + console.log('ℹ️ Using static configs'); + switch (communityConfig.toLowerCase()) { + case 'nouns': + return nounsSystemConfig; + case 'example': + return exampleSystemConfig; + case 'clanker': + return clankerSystemConfig as unknown as SystemConfig; + default: + return nounsSystemConfig; + } +}; +``` + +**Testing:** +1. **Test with DB available:** + ```bash + NEXT_PUBLIC_SUPABASE_URL=... SUPABASE_SERVICE_ROLE_KEY=... npm run build + # Should see: "✅ Generated config file from database" + # Should see: src/config/db-config.ts created + ``` + +2. **Test without DB:** + ```bash + # Remove env vars + npm run build + # Should see: "ℹ️ Using static configs" + # Should fall back to static configs + ``` + +3. **Test app functionality:** + - Build succeeds + - App loads correctly + - Config values match DB (if loaded) or static (if fallback) + - Themes loaded from shared file + - Pages loaded from Spaces (if nav items have spaceId) + - No runtime errors + +4. **Verify config size:** + ```bash + # Check generated file size + wc -c src/config/db-config.ts + # Should be ~2-3 KB (much smaller than before!) + ``` + +**Validation:** +- ✅ Build succeeds with/without DB +- ✅ App uses DB config when available +- ✅ App falls back to static when DB unavailable +- ✅ Themes loaded from shared file +- ✅ Pages loaded from Spaces (via nav items) +- ✅ Config file generated (not env var - avoids E2BIG) +- ✅ Config size reduced (~2.8 KB vs ~29 KB) +- ✅ All existing functionality works +- ✅ No breaking changes + +**Rollback:** Remove generated file reading, system back to static-only + +**Estimated Time:** 2-3 hours + +--- + +## Phase 3: Admin API Endpoints (Week 1, Day 5) + +**Goal:** Create API endpoints for admin config updates + +**Tasks:** +1. Create `/api/admin/config/[communityId]` - GET/PUT +2. Create `/api/admin/config/[communityId]/history` - GET +3. Create `/api/admin/spaces/[spaceId]` - GET/PUT (for nav page Spaces) +4. Add admin permission checks +5. Add request validation +6. Note: Themes are in shared file (not editable via API for now) + +**Files to Create:** +``` +src/app/api/admin/config/[communityId]/route.ts +src/app/api/admin/config/[communityId]/history/route.ts +src/app/api/admin/spaces/[spaceId]/route.ts # NEW: For nav page Spaces +src/common/data/services/adminConfigService.ts +``` + +**Implementation:** + +```typescript +// src/app/api/admin/config/[communityId]/route.ts + +import { NextRequest, NextResponse } from 'next/server'; +import { createClient } from '@supabase/supabase-js'; +import { SystemConfig } from '@/config/systemConfig'; + +export async function GET( + request: NextRequest, + { params }: { params: { communityId: string } } +) { + // Verify admin (simplified for Phase 3) + const adminIdentity = request.headers.get('x-admin-identity'); + if (!adminIdentity) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! + ); + + // Check admin permissions + const { data: admin } = await supabase + .from('community_config_admins') + .select('id') + .eq('community_id', params.communityId) + .eq('admin_identity_public_key', adminIdentity) + .eq('is_active', true) + .single(); + + if (!admin) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + + // Get config + const { data, error } = await supabase + .rpc('get_active_community_config', { p_community_id: params.communityId }) + .single(); + + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } + + return NextResponse.json({ config: data }); +} + +export async function PUT( + request: NextRequest, + { params }: { params: { communityId: string } } +) { + const adminIdentity = request.headers.get('x-admin-identity'); + if (!adminIdentity) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const body = await request.json(); + const { config, changeNotes } = body; + + // Validate config structure (basic) + if (!config || typeof config !== 'object') { + return NextResponse.json({ error: 'Invalid config' }, { status: 400 }); + } + + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! + ); + + // Check admin permissions + const { data: admin } = await supabase + .from('community_config_admins') + .select('id') + .eq('community_id', params.communityId) + .eq('admin_identity_public_key', adminIdentity) + .eq('is_active', true) + .single(); + + if (!admin) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + + // Create new version + const { data, error } = await supabase.rpc('create_config_version', { + p_community_id: params.communityId, + p_config_data: config, + p_change_notes: changeNotes || 'Updated via admin API', + p_admin_identity: adminIdentity + }); + + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } + + return NextResponse.json({ success: true, versionId: data }); +} +``` + +**Testing:** +```bash +# Test GET +curl -H "x-admin-identity: test-admin" \ + http://localhost:3000/api/admin/config/nouns + +# Test PUT +curl -X PUT \ + -H "x-admin-identity: test-admin" \ + -H "Content-Type: application/json" \ + -d '{"config": {...}, "changeNotes": "Test update"}' \ + http://localhost:3000/api/admin/config/nouns +``` + +**Validation:** +- ✅ GET returns config +- ✅ PUT creates new version +- ✅ Permission checks work +- ✅ History is tracked +- ✅ Invalid requests are rejected + +**Rollback:** Remove API routes, no impact on main app + +**Estimated Time:** 3-4 hours + +--- + +## Phase 4: Basic Admin UI (Week 2, Day 1-2) + +**Goal:** Create simple admin interface to view/edit configs and nav pages + +**Tasks:** +1. Create admin layout/page structure +2. Create config viewer (read-only first) +3. Add basic form for editing config +4. Add Space editor for nav pages (homePage/explorePage) +5. Add save functionality +6. Note: Themes edited in code (shared file) for now + +**Files to Create:** +``` +src/app/admin/config/[communityId]/page.tsx +src/app/admin/config/[communityId]/components/ConfigViewer.tsx +src/app/admin/config/[communityId]/components/ConfigEditor.tsx +src/app/admin/config/[communityId]/components/NavPageEditor.tsx # NEW: Edit nav page Spaces +``` + +**Implementation:** + +```typescript +// src/app/admin/config/[communityId]/page.tsx + +'use client'; + +import { useState, useEffect } from 'react'; +import { useParams } from 'next/navigation'; +import { SystemConfig } from '@/config/systemConfig'; + +export default function AdminConfigPage() { + const params = useParams(); + const communityId = params.communityId as string; + const [config, setConfig] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetch(`/api/admin/config/${communityId}`, { + headers: { + 'x-admin-identity': getAdminIdentity(), // Your auth + }, + }) + .then(res => res.json()) + .then(data => { + setConfig(data.config); + setLoading(false); + }); + }, [communityId]); + + if (loading) return
Loading...
; + if (!config) return
No config found
; + + return ( +
+

Edit Config: {communityId}

+ +
+ ); +} +``` + +**Testing:** +1. Navigate to `/admin/config/nouns` +2. Verify config loads +3. Make a small change (e.g., brand name) +4. Save and verify it updates in DB +5. Rebuild and verify change appears + +**Validation:** +- ✅ Can view config (without themes/pages) +- ✅ Can edit config +- ✅ Can edit nav page Spaces (homePage/explorePage) +- ✅ Can save changes +- ✅ Changes persist in DB/Storage +- ✅ Changes appear after rebuild + +**Rollback:** Remove admin routes, no impact on main app + +**Estimated Time:** 4-6 hours + +--- + +## Phase 5: Asset Upload (Week 2, Day 3-4) + +**Goal:** Add asset upload functionality + +**Tasks:** +1. Create Supabase storage bucket +2. Create upload API endpoint +3. Add upload UI component +4. Update config to store asset paths + +**Files to Create:** +``` +src/app/api/admin/assets/upload/route.ts +src/app/admin/config/[communityId]/components/AssetUpload.tsx +``` + +**Testing:** +1. Upload a logo +2. Verify it's in Supabase Storage +3. Verify config stores storage path +4. Rebuild and verify asset downloads +5. Verify asset appears in app + +**Validation:** +- ✅ Can upload assets +- ✅ Assets stored in Supabase +- ✅ Config updated with paths +- ✅ Assets download at build time +- ✅ Assets appear correctly + +**Rollback:** Remove upload functionality, use static assets + +**Estimated Time:** 4-6 hours + +--- + +## Phase 6: Build-Time Asset Download (Week 2, Day 5) + +**Goal:** Download assets during build + +**Tasks:** +1. Add asset download to `next.config.mjs` +2. Download assets to `public/images/{community}/` +3. Update config paths to public paths +4. Add error handling + +**Files to Modify:** +``` +next.config.mjs # Add asset download +``` + +**Testing:** +1. Upload asset via admin UI +2. Trigger build +3. Verify asset downloads to `public/` +4. Verify config uses public path +5. Verify asset loads in app + +**Validation:** +- ✅ Assets download during build +- ✅ Assets in correct location +- ✅ Config paths updated +- ✅ Assets load correctly +- ✅ Fallback works if download fails + +**Rollback:** Remove download code, use static assets + +**Estimated Time:** 2-3 hours + +--- + +## Phase 7: Asset Optimization (Week 3, Day 1) + +**Goal:** Optimize assets during download + +**Tasks:** +1. Add Sharp optimization +2. Convert PNG/JPG to WebP +3. Resize large images +4. Add optimization logging + +**Files to Modify:** +``` +next.config.mjs # Add Sharp optimization +``` + +**Testing:** +1. Upload large PNG (2MB+) +2. Build and verify WebP created +3. Verify file size reduced +4. Verify image quality acceptable +5. Verify load time improved + +**Validation:** +- ✅ Images optimized +- ✅ File sizes reduced +- ✅ Quality maintained +- ✅ Build time acceptable +- ✅ Performance improved + +**Rollback:** Remove optimization, use raw downloads + +**Estimated Time:** 2-3 hours + +--- + +## Phase 8: Rebuild Trigger (Week 3, Day 2) + +**Goal:** Trigger rebuilds after config updates + +**Tasks:** +1. Create rebuild trigger API +2. Integrate with CI/CD (GitHub Actions/Vercel) +3. Add rebuild status UI +4. Add webhook handling + +**Files to Create:** +``` +src/app/api/admin/config/[communityId]/rebuild/route.ts +.github/workflows/rebuild-on-config-change.yml (if using GitHub Actions) +``` + +**Testing:** +1. Update config via admin UI +2. Click "Trigger Rebuild" +3. Verify build triggered +4. Verify build completes +5. Verify changes deployed + +**Validation:** +- ✅ Rebuild triggers correctly +- ✅ Build completes successfully +- ✅ Changes deployed +- ✅ Status updates correctly + +**Rollback:** Remove trigger, manual rebuilds + +**Estimated Time:** 3-4 hours + +--- + +## Phase 9: Production Rollout (Week 3, Day 3-5) + +**Goal:** Roll out to production gradually + +**Tasks:** +1. Test with one community (e.g., 'example') +2. Monitor build times +3. Monitor performance +4. Roll out to other communities +5. Keep static configs as fallback for now + +**Testing:** +1. Deploy to staging +2. Test all functionality +3. Monitor metrics +4. Deploy to production (one community) +5. Monitor for issues +6. Roll out to all communities + +**Validation:** +- ✅ All communities work +- ✅ Performance acceptable +- ✅ No regressions +- ✅ Admin interface works +- ✅ Assets load correctly + +**Rollback:** Revert to static configs + +**Estimated Time:** 4-8 hours + +--- + +## Phase 10: Phase Out Static Configs (Week 4) + +**Goal:** Remove old static config system, make DB the single source of truth + +**Prerequisites:** +- ✅ Phase 9 complete (all communities in production) +- ✅ Stable for at least 1-2 weeks +- ✅ No critical issues reported +- ✅ Admin interface fully functional +- ✅ All configs migrated to database + +### Phase 10A: Remove Static Config Fallbacks (Week 4, Day 1-2) + +**Goal:** Remove static config imports, keep minimal emergency fallback + +**Tasks:** +1. Update `src/config/index.ts` to remove static imports +2. Keep minimal fallback (hardcoded default config) +3. Update error handling +4. Add monitoring/alerting for config load failures + +**Files to Modify:** +``` +src/config/index.ts # Remove static imports +src/config/nouns/index.ts # Mark as deprecated +src/config/clanker/index.ts # Mark as deprecated +src/config/example/index.ts # Mark as deprecated +``` + +**Implementation:** + +```typescript +// src/config/index.ts + +import { SystemConfig } from './systemConfig'; + +// Minimal emergency fallback (only used if DB completely unavailable) +const EMERGENCY_FALLBACK_CONFIG: SystemConfig = { + brand: { + name: "nounspace", + displayName: "Nounspace", + tagline: "A customizable Farcaster client", + description: "Nounspace - Customizable Farcaster client", + miniAppTags: [], + }, + assets: { + logos: { + main: "/images/logo.png", + icon: "/images/icon.png", + favicon: "/images/favicon.ico", + appleTouch: "/images/apple-touch-icon.png", + og: "/images/og.png", + splash: "/images/splash.png", + }, + }, + // ... minimal required config +}; + +export const loadSystemConfig = (): SystemConfig => { + const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + + // Try build-time config from DB + const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; + if (buildTimeConfig) { + try { + const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; + + // Validate config structure + if (dbConfig && dbConfig.brand && dbConfig.assets) { + return dbConfig; + } else { + console.error('❌ Invalid config structure from DB, using emergency fallback'); + // TODO: Alert monitoring system + } + } catch (error) { + console.error('❌ Failed to parse build-time config:', error); + // TODO: Alert monitoring system + } + } + + // Emergency fallback only (should rarely be used) + console.error('⚠️ Using emergency fallback config - DB config unavailable!'); + console.error('⚠️ This indicates a build configuration issue.'); + // TODO: Alert monitoring system + + return EMERGENCY_FALLBACK_CONFIG; +}; +``` + +**Testing:** +1. **Test with DB available:** + ```bash + # Should use DB config + npm run build + # Check logs: Should see config loaded from DB + ``` + +2. **Test without DB (simulate failure):** + ```bash + # Remove env vars or break DB connection + npm run build + # Should use emergency fallback + # Should log warnings/errors + ``` + +3. **Test app with emergency fallback:** + ```bash + npm run dev + # App should still load (with minimal config) + ``` + +**Validation:** +- ✅ App works with DB config +- ✅ App works with emergency fallback +- ✅ Errors are logged/monitored +- ✅ No references to old static configs +- ✅ Build succeeds in both cases + +**Rollback:** Re-add static config imports + +**Estimated Time:** 2-3 hours + +--- + +### Phase 10B: Remove Static Config Files (Week 4, Day 3) + +**Goal:** Delete old static config files, keep only for reference + +**Tasks:** +1. Archive static configs (move to `src/config/_archived/`) +2. Update imports throughout codebase +3. Remove from exports +4. Update documentation + +**Files to Move:** +``` +src/config/nouns/ → src/config/_archived/nouns/ +src/config/clanker/ → src/config/_archived/clanker/ +src/config/example/ → src/config/_archived/example/ +``` + +**Files to Update:** +``` +src/config/index.ts # Remove exports +src/config/systemConfig.ts # Keep (type definitions) +``` + +**Implementation:** + +```typescript +// src/config/index.ts + +import { SystemConfig } from './systemConfig'; + +// Static configs moved to _archived/ - no longer imported +// Only DB configs are used now + +const EMERGENCY_FALLBACK_CONFIG: SystemConfig = { + // ... minimal config +}; + +export const loadSystemConfig = (): SystemConfig => { + // ... same as Phase 10A +}; + +// Remove all static config exports +// export { nounsSystemConfig } from './nouns/index'; // ❌ Removed +// export { clankerSystemConfig } from './clanker/index'; // ❌ Removed +``` + +**Testing:** +1. **Verify no broken imports:** + ```bash + npm run check-types + # Should pass - no broken imports + ``` + +2. **Verify build works:** + ```bash + npm run build + # Should succeed + ``` + +3. **Search for remaining references:** + ```bash + grep -r "nounsSystemConfig\|clankerSystemConfig\|exampleSystemConfig" src/ + # Should only find references in _archived/ or comments + ``` + +**Validation:** +- ✅ No broken imports +- ✅ Build succeeds +- ✅ No references to old configs (except archived) +- ✅ Type checking passes + +**Rollback:** Restore files from `_archived/` + +**Estimated Time:** 1-2 hours + +--- + +### Phase 10C: Update Space Creators (Week 4, Day 4) + +**Goal:** Migrate initial space creators to use DB configs + +**Tasks:** +1. Move space creator logic to database +2. Or keep as code but reference DB config +3. Update space creator functions +4. Test space creation + +**Options:** + +**Option A: Keep creators in code, reference DB config** +```typescript +// src/config/index.ts + +export const createInitialProfileSpaceConfigForFid = (fid: number, username?: string) => { + const config = loadSystemConfig(); // Uses DB config + + // Use config values in space creation + return { + // ... space config using config.community, config.brand, etc. + }; +}; +``` + +**Option B: Store creators in DB** +```sql +-- Add to community_configs table +ALTER TABLE community_configs ADD COLUMN initial_space_creators JSONB; + +-- Store creator functions/logic as JSON +``` + +**Testing:** +1. Create profile space +2. Create channel space +3. Create token space +4. Create proposal space +5. Create homebase +6. Verify all work correctly + +**Validation:** +- ✅ All space creators work +- ✅ Spaces use DB config values +- ✅ No references to static configs + +**Rollback:** Restore static space creators + +**Estimated Time:** 3-4 hours + +--- + +### Phase 10D: Cleanup & Documentation (Week 4, Day 5) + +**Goal:** Final cleanup and documentation updates + +**Tasks:** +1. Remove deprecated code comments +2. Update all documentation +3. Update README +4. Add migration guide +5. Update contributing guide +6. Remove unused files + +**Files to Update:** +``` +docs/CONFIGURATION.md # Update for DB system +docs/COMMUNITY_CONFIG_SYSTEM.md # Mark as legacy +docs/GETTING_STARTED.md # Update setup steps +README.md # Update config section +``` + +**Files to Create:** +``` +docs/MIGRATION_GUIDE.md # How to migrate from static to DB +docs/ADMIN_GUIDE.md # Admin user guide +``` + +**Cleanup Tasks:** +1. Remove unused imports +2. Remove commented code +3. Remove deprecated functions +4. Clean up `_archived/` folder (or keep for reference) + +**Testing:** +1. **Verify documentation accuracy:** + - All examples work + - All links valid + - All instructions correct + +2. **Verify code cleanliness:** + ```bash + npm run lint + npm run check-types + # Should pass + ``` + +3. **Verify no dead code:** + ```bash + # Search for unused exports + # Check for orphaned files + ``` + +**Validation:** +- ✅ Documentation updated +- ✅ No dead code +- ✅ No lint errors +- ✅ All examples work +- ✅ Migration guide complete + +**Rollback:** Restore old documentation + +**Estimated Time:** 2-3 hours + +--- + +## Phase 10 Summary + +**Total Time:** 8-12 hours over 1 week + +**Phases:** +- **10A:** Remove static fallbacks (2-3h) +- **10B:** Remove static files (1-2h) +- **10C:** Update space creators (3-4h) +- **10D:** Cleanup & docs (2-3h) + +**Final State:** +- ✅ DB is single source of truth +- ✅ No static config dependencies +- ✅ Minimal emergency fallback only +- ✅ Clean codebase +- ✅ Updated documentation + +**Rollback Plan:** +- Can restore static configs from `_archived/` +- Can re-add static imports +- Can revert to Phase 9 state + +--- + +## Complete Timeline + +| Phase | Duration | Week | Risk | +|-------|----------|------|------| +| Phase 0 | 1-2h | Week 1 | Low | +| Phase 1 | 4-6h | Week 1 | Low | +| Phase 2 | 2-3h | Week 1 | Medium | +| Phase 3 | 3-4h | Week 1 | Low | +| Phase 4 | 4-6h | Week 2 | Medium | +| Phase 5 | 4-6h | Week 2 | Medium | +| Phase 6 | 2-3h | Week 2 | Medium | +| Phase 7 | 2-3h | Week 3 | Low | +| Phase 8 | 3-4h | Week 3 | Medium | +| Phase 9 | 4-8h | Week 3 | High | +| **Phase 10** | **8-12h** | **Week 4** | **Medium** | + +**Total: 4-5 weeks** + +--- + +## Phase 10: Phase Out Static Configs (Week 4) + +**Goal:** Remove old static config system, make DB the single source of truth + +**Prerequisites:** +- ✅ Phase 9 complete (all communities in production) +- ✅ Stable for at least 1-2 weeks +- ✅ No critical issues reported +- ✅ Admin interface fully functional +- ✅ All configs migrated to database + +### Phase 10A: Remove Static Config Fallbacks (Week 4, Day 1-2) + +**Goal:** Remove static config imports, keep minimal emergency fallback + +**Tasks:** +1. Update `src/config/index.ts` to remove static imports +2. Keep minimal fallback (hardcoded default config) +3. Update error handling +4. Add monitoring/alerting for config load failures + +**Files to Modify:** +``` +src/config/index.ts # Remove static imports +src/config/nouns/index.ts # Mark as deprecated +src/config/clanker/index.ts # Mark as deprecated +src/config/example/index.ts # Mark as deprecated +``` + +**Implementation:** + +```typescript +// src/config/index.ts + +import { SystemConfig } from './systemConfig'; + +// Minimal emergency fallback (only used if DB completely unavailable) +const EMERGENCY_FALLBACK_CONFIG: SystemConfig = { + brand: { + name: "nounspace", + displayName: "Nounspace", + tagline: "A customizable Farcaster client", + description: "Nounspace - Customizable Farcaster client", + miniAppTags: [], + }, + assets: { + logos: { + main: "/images/logo.png", + icon: "/images/icon.png", + favicon: "/images/favicon.ico", + appleTouch: "/images/apple-touch-icon.png", + og: "/images/og.png", + splash: "/images/splash.png", + }, + }, + theme: { + default: { + id: "default", + name: "Default", + properties: { + font: "Inter", + fontColor: "#000000", + headingsFont: "Inter", + headingsFontColor: "#000000", + background: "#ffffff", + backgroundHTML: "", + musicURL: "", + fidgetBackground: "#ffffff", + fidgetBorderWidth: "1px", + fidgetBorderColor: "#C0C0C0", + fidgetShadow: "none", + fidgetBorderRadius: "12px", + gridSpacing: "16", + }, + }, + // ... minimal required themes + }, + community: { + type: "nounspace", + urls: { + website: "https://nounspace.com", + discord: "", + twitter: "", + github: "", + forum: "", + }, + social: { + farcaster: "", + discord: "", + twitter: "", + }, + governance: { + proposals: "", + delegates: "", + treasury: "", + }, + tokens: {}, + contracts: {}, + }, + fidgets: { + enabled: [], + disabled: [], + }, + homePage: { + defaultTab: "Home", + tabOrder: ["Home"], + tabs: { + Home: { + name: "Home", + displayName: "Home", + layoutID: "default", + layoutDetails: { + layoutConfig: { layout: [] }, + layoutFidget: "grid", + }, + theme: { + id: "default", + name: "Default", + properties: { + font: "Inter", + fontColor: "#000000", + headingsFont: "Inter", + headingsFontColor: "#000000", + background: "#ffffff", + backgroundHTML: "", + musicURL: "", + fidgetBackground: "#ffffff", + fidgetBorderWidth: "1px", + fidgetBorderColor: "#C0C0C0", + fidgetShadow: "none", + fidgetBorderRadius: "12px", + gridSpacing: "16", + }, + }, + fidgetInstanceDatums: {}, + fidgetTrayContents: [], + isEditable: false, + timestamp: new Date().toISOString(), + }, + }, + layout: { + defaultLayoutFidget: "grid", + gridSpacing: 16, + theme: { + background: "#ffffff", + fidgetBackground: "#ffffff", + font: "Inter", + fontColor: "#000000", + }, + }, + }, + explorePage: { + defaultTab: "Explore", + tabOrder: ["Explore"], + tabs: { + Explore: { + // ... minimal explore config + }, + }, + layout: { + defaultLayoutFidget: "grid", + gridSpacing: 16, + theme: { + background: "#ffffff", + fidgetBackground: "#ffffff", + font: "Inter", + fontColor: "#000000", + }, + }, + }, +}; + +export const loadSystemConfig = (): SystemConfig => { + const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + + // Try build-time config from DB + const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; + if (buildTimeConfig) { + try { + const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; + + // Validate config structure + if (dbConfig && dbConfig.brand && dbConfig.assets) { + return dbConfig; + } else { + console.error('❌ Invalid config structure from DB, using emergency fallback'); + // TODO: Alert monitoring system (Sentry, etc.) + } + } catch (error) { + console.error('❌ Failed to parse build-time config:', error); + // TODO: Alert monitoring system + } + } + + // Emergency fallback only (should rarely be used) + console.error('⚠️ Using emergency fallback config - DB config unavailable!'); + console.error('⚠️ This indicates a build configuration issue.'); + // TODO: Alert monitoring system + + return EMERGENCY_FALLBACK_CONFIG; +}; + +// Remove all static config exports +// export { nounsSystemConfig } from './nouns/index'; // ❌ Removed +// export { clankerSystemConfig } from './clanker/index'; // ❌ Removed +// export { exampleSystemConfig } from './example/index'; // ❌ Removed +``` + +**Testing:** +1. **Test with DB available:** + ```bash + npm run build + # Should use DB config, no warnings + ``` + +2. **Test without DB (simulate failure):** + ```bash + # Remove SUPABASE env vars + npm run build + # Should use emergency fallback + # Should log warnings/errors + ``` + +3. **Test app with emergency fallback:** + ```bash + npm run dev + # App should still load (with minimal config) + ``` + +**Validation:** +- ✅ App works with DB config +- ✅ App works with emergency fallback +- ✅ Errors are logged/monitored +- ✅ No references to old static configs in main code +- ✅ Build succeeds in both cases + +**Rollback:** Re-add static config imports + +**Estimated Time:** 2-3 hours + +--- + +### Phase 10B: Archive Static Config Files (Week 4, Day 3) + +**Goal:** Move static configs to archive, remove from active codebase + +**Tasks:** +1. Create archive directory +2. Move static config files to archive +3. Update any remaining references +4. Add deprecation notices + +**Files to Move:** +``` +src/config/nouns/ → src/config/_archived/nouns/ +src/config/clanker/ → src/config/_archived/clanker/ +src/config/example/ → src/config/_archived/example/ +``` + +**Implementation:** + +```bash +# Create archive directory +mkdir -p src/config/_archived + +# Move static configs +mv src/config/nouns src/config/_archived/ +mv src/config/clanker src/config/_archived/ +mv src/config/example src/config/_archived/ + +# Add README in archive +echo "# Archived Static Configs + +These configs are kept for reference only. +All active configs are now stored in the database. + +Last used: [Date] +Migrated to DB: [Date] +" > src/config/_archived/README.md +``` + +**Update Space Creators:** + +```typescript +// src/config/index.ts + +// Space creators now delegate to DB config or use minimal defaults +export const createInitialProfileSpaceConfigForFid = (fid: number, username?: string) => { + const config = loadSystemConfig(); // Always uses DB config now + + // Use config values + return { + // ... space config using config.community, config.brand, etc. + fidgetInstanceDatums: { + "feed:profile": { + config: { + editable: false, + settings: { + feedType: FeedType.Filter, + users: fid, + filterType: FilterType.Fids, + }, + data: {}, + }, + fidgetType: "feed", + id: "feed:profile", + }, + }, + // ... rest of space config + }; +}; + +// Similar updates for other space creators +``` + +**Testing:** +1. **Verify no broken imports:** + ```bash + npm run check-types + # Should pass + ``` + +2. **Search for remaining references:** + ```bash + grep -r "from './nouns\|from './clanker\|from './example" src/ --exclude-dir=_archived + # Should find no results (or only in comments) + ``` + +3. **Verify build works:** + ```bash + npm run build + # Should succeed + ``` + +**Validation:** +- ✅ No broken imports +- ✅ Build succeeds +- ✅ No references to old configs (except archived) +- ✅ Type checking passes +- ✅ Space creators work + +**Rollback:** Restore files from archive + +**Estimated Time:** 1-2 hours + +--- + +### Phase 10C: Update Space Creators (Week 4, Day 4) + +**Goal:** Ensure space creators use DB configs, not static + +**Tasks:** +1. Review all space creator functions +2. Update to use `loadSystemConfig()` (which uses DB) +3. Remove any direct static config references +4. Test all space creation flows + +**Files to Update:** +``` +src/config/index.ts # Space creator functions +``` + +**Implementation:** + +```typescript +// src/config/index.ts + +// All space creators now use loadSystemConfig() which gets from DB +export const createInitialProfileSpaceConfigForFid = (fid: number, username?: string) => { + const config = loadSystemConfig(); // Uses DB config + + // Use config values + return { + // ... space config + theme: config.theme.default, // Use theme from DB config + // ... rest uses config values + }; +}; + +// Similar pattern for all creators +export const createInitialChannelSpaceConfig = (channelId: string) => { + const config = loadSystemConfig(); + // ... use config values +}; + +export const createInitialTokenSpaceConfigForAddress = (...args: any[]) => { + const config = loadSystemConfig(); + // ... use config values +}; + +export const createInitalProposalSpaceConfigForProposalId = (...args: any[]) => { + const config = loadSystemConfig(); + // ... use config values +}; + +export const INITIAL_HOMEBASE_CONFIG = (() => { + const config = loadSystemConfig(); + return { + // ... use config.homePage values + }; +})(); +``` + +**Testing:** +1. Create profile space → Verify uses DB config +2. Create channel space → Verify uses DB config +3. Create token space → Verify uses DB config +4. Create proposal space → Verify uses DB config +5. Create homebase → Verify uses DB config + +**Validation:** +- ✅ All space creators work +- ✅ Spaces use DB config values +- ✅ No references to static configs +- ✅ Themes/styles match DB config + +**Rollback:** Restore static space creators + +**Estimated Time:** 3-4 hours + +--- + +### Phase 10D: Final Cleanup & Documentation (Week 4, Day 5) + +**Goal:** Complete cleanup and update all documentation + +**Tasks:** +1. Remove deprecated code comments +2. Update all documentation +3. Update README +4. Create migration guide +5. Update contributing guide +6. Clean up archived files (optional) + +**Files to Update:** +``` +docs/CONFIGURATION.md # Update for DB system +docs/COMMUNITY_CONFIG_SYSTEM.md # Mark as legacy, add DB section +docs/GETTING_STARTED.md # Update setup steps +README.md # Update config section +docs/CONTRIBUTING.MD # Update for DB configs +``` + +**Files to Create:** +``` +docs/MIGRATION_GUIDE.md # Static → DB migration guide +docs/ADMIN_GUIDE.md # Admin user guide +docs/LEGACY_STATIC_CONFIGS.md # Reference for archived configs +``` + +**Cleanup Tasks:** +1. Remove unused imports +2. Remove commented code +3. Remove deprecated functions +4. Update `.gitignore` if needed +5. Clean up test files + +**Documentation Updates:** + +```markdown +# docs/CONFIGURATION.md (updated) + +## Configuration System + +Nounspace now uses a **database-backed configuration system**. Configurations are stored in Supabase and loaded at build time. + +### For Developers + +Configs are automatically loaded from the database during build. No manual configuration needed. + +### For Admins + +Use the admin interface at `/admin/config/[communityId]` to update configurations. + +### Legacy Static Configs + +Static configs have been archived to `src/config/_archived/`. They are kept for reference only and are no longer used. +``` + +**Testing:** +1. **Verify documentation:** + - All examples work + - All links valid + - All instructions correct + +2. **Verify code cleanliness:** + ```bash + npm run lint + npm run check-types + # Should pass with no errors + ``` + +3. **Verify no dead code:** + ```bash + # Search for unused exports + # Check for orphaned files + ``` + +**Validation:** +- ✅ Documentation updated and accurate +- ✅ No dead code +- ✅ No lint errors +- ✅ All examples work +- ✅ Migration guide complete +- ✅ Admin guide complete + +**Rollback:** Restore old documentation + +**Estimated Time:** 2-3 hours + +--- + +## Phase 10 Summary + +**Total Time:** 8-12 hours over 1 week + +**Sub-Phases:** +- **10A:** Remove static fallbacks (2-3h) +- **10B:** Archive static files (1-2h) +- **10C:** Update space creators (3-4h) +- **10D:** Cleanup & docs (2-3h) + +**Final State:** +- ✅ DB is single source of truth +- ✅ No static config dependencies in active code +- ✅ Minimal emergency fallback only +- ✅ Static configs archived for reference +- ✅ Clean codebase +- ✅ Complete documentation +- ✅ Migration guide available + +**Success Criteria:** +- ✅ App works entirely from DB configs +- ✅ No static config imports in active code +- ✅ Emergency fallback works if DB unavailable +- ✅ All documentation updated +- ✅ Codebase is clean + +**Rollback Plan:** +- Can restore static configs from archive +- Can re-add static imports +- Can revert to Phase 9 state (DB + static fallback) + +--- + +## Testing Strategy Per Phase + +### Unit Tests +```typescript +// tests/config-loader.test.ts +describe('Config Loader', () => { + it('should load from DB when available', () => { + process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(testConfig); + const config = loadSystemConfig(); + expect(config.brand.name).toBe('Test'); + }); + + it('should fallback to static when DB unavailable', () => { + delete process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; + const config = loadSystemConfig(); + expect(config.brand.name).toBe('Nouns'); + }); +}); +``` + +### Integration Tests +```typescript +// tests/admin-api.test.ts +describe('Admin API', () => { + it('should require admin authentication', async () => { + const res = await fetch('/api/admin/config/nouns'); + expect(res.status).toBe(401); + }); + + it('should update config', async () => { + const res = await fetch('/api/admin/config/nouns', { + method: 'PUT', + headers: { + 'x-admin-identity': 'test-admin', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ config: testConfig }), + }); + expect(res.status).toBe(200); + }); +}); +``` + +### E2E Tests +```typescript +// tests/admin-ui.e2e.test.ts +describe('Admin UI', () => { + it('should load and display config', async () => { + await page.goto('/admin/config/nouns'); + await expect(page.locator('h1')).toContainText('Edit Config: nouns'); + }); + + it('should save config changes', async () => { + await page.fill('[name="brand.name"]', 'New Name'); + await page.click('button[type="submit"]'); + await expect(page.locator('.success')).toBeVisible(); + }); +}); +``` + +--- + +## Rollback Plan Per Phase + +Each phase can be rolled back independently: + +1. **Phase 1 (DB Schema)** - Drop migrations +2. **Phase 2 (Config Loading)** - Remove env var reading +3. **Phase 3 (Admin API)** - Remove API routes +4. **Phase 4 (Admin UI)** - Remove admin pages +5. **Phase 5 (Asset Upload)** - Remove upload functionality +6. **Phase 6 (Asset Download)** - Remove download code +7. **Phase 7 (Optimization)** - Remove optimization +8. **Phase 8 (Rebuild Trigger)** - Remove trigger +9. **Phase 9 (Production)** - Revert to static configs + +--- + +## Success Criteria Per Phase + +### Phase 1: Database Schema +- ✅ Tables created successfully (without page/theme columns) +- ✅ Functions work correctly +- ✅ RLS policies enforce access +- ✅ Can seed initial data (without themes/pages) +- ✅ navPage spaceType added + +### Phase 2: Config Loading +- ✅ Shared themes file created +- ✅ Build succeeds with/without DB +- ✅ App uses DB config when available +- ✅ Themes loaded from shared file +- ✅ Pages loaded from Spaces (via nav items) +- ✅ Config file generated (not env var) +- ✅ Config size reduced (~2.8 KB) +- ✅ App falls back to static when unavailable +- ✅ No breaking changes + +### Phase 3: Admin API +- ✅ Can fetch config (without themes/pages) +- ✅ Can update config +- ✅ Can fetch/update nav page Spaces +- ✅ Permission checks work +- ✅ History tracked + +### Phase 4: Admin UI +- ✅ Can view config (without themes/pages) +- ✅ Can edit config +- ✅ Can edit nav page Spaces +- ✅ Can save changes +- ✅ Changes persist + +### Phase 5: Asset Upload +- ✅ Can upload assets +- ✅ Assets stored correctly +- ✅ Config updated with paths + +### Phase 6: Asset Download +- ✅ Assets download at build time +- ✅ Assets in correct location +- ✅ Config paths updated +- ✅ Assets load correctly + +### Phase 7: Asset Optimization +- ✅ Images optimized +- ✅ File sizes reduced +- ✅ Quality maintained +- ✅ Performance improved + +### Phase 8: Rebuild Trigger +- ✅ Rebuild triggers correctly +- ✅ Build completes successfully +- ✅ Changes deployed + +### Phase 9: Production +- ✅ All communities work +- ✅ Performance acceptable +- ✅ No regressions + +### Phase 10: Phase Out Static Configs +- ✅ No static config dependencies +- ✅ DB is single source of truth +- ✅ Emergency fallback works +- ✅ Documentation updated +- ✅ Codebase cleaned + +--- + +## Timeline Summary + +| Phase | Duration | Dependencies | Risk Level | +|-------|----------|--------------|------------| +| Phase 0 | 1-2h | None | Low | +| Phase 1 | 4-6h | Phase 0 | Low | +| Phase 2 | 2-3h | Phase 1 | Medium | +| Phase 3 | 3-4h | Phase 1 | Low | +| Phase 4 | 4-6h | Phase 3 | Medium | +| Phase 5 | 4-6h | Phase 1 | Medium | +| Phase 6 | 2-3h | Phase 5 | Medium | +| Phase 7 | 2-3h | Phase 6 | Low | +| Phase 8 | 3-4h | Phase 4 | Medium | +| Phase 9 | 4-8h | All phases | High | +| **Phase 10** | **8-12h** | **Phase 9** | **Medium** | + +**Total Estimated Time:** 4-5 weeks + +--- + +## Quick Start: Minimal Viable Implementation + +If you want to test the concept quickly: + +1. **Phase 1** - Create DB schema (2h) +2. **Phase 2** - Basic config loading (2h) +3. **Manual testing** - Update DB directly, rebuild, verify + +This gives you a working proof-of-concept in ~4 hours! + +--- + +## Next Steps + +1. **Start with Phase 0** - Set up test environment +2. **Complete Phase 1** - Database foundation +3. **Test Phase 2** - Verify config loading works +4. **Iterate** - Add phases incrementally + +Each phase is independently testable and can be rolled back if issues arise. + diff --git a/docs/NAVIGATION_SPACE_REFERENCE_APPROACH.md b/docs/NAVIGATION_SPACE_REFERENCE_APPROACH.md new file mode 100644 index 000000000..172b1b2b3 --- /dev/null +++ b/docs/NAVIGATION_SPACE_REFERENCE_APPROACH.md @@ -0,0 +1,313 @@ +# Navigation-Space Reference Approach + +## Overview + +Instead of storing `homePage` and `explorePage` configs directly in `community_configs`, navigation items reference Spaces in the database. Pages are fetched at build time based on navigation entries. + +## Architecture + +``` +Navigation Config + ├── items: [ + │ { id: 'home', spaceId: 'uuid-1', ... }, + │ { id: 'explore', spaceId: 'uuid-2', ... }, + │ ... + │ ] + │ + └── Build Time: + ├── Fetch nav config from community_configs + ├── For each nav item with spaceId: + │ └── Fetch Space from database/storage + └── Build page configs from fetched Spaces +``` + +## Benefits + +1. **Dramatically reduces config size** - Removes 71% (20.6 KB) of config +2. **Unified architecture** - Everything is Spaces +3. **Navigation as source of truth** - Nav defines what pages exist +4. **Flexible** - Any nav item can reference a Space +5. **Reuses existing infrastructure** - Uses Space storage/retrieval + +## Implementation + +### 1. Update NavigationItem Interface + +```typescript +// src/config/systemConfig.ts + +export interface NavigationItem { + id: string; + label: string; + href: string; + icon?: 'home' | 'explore' | 'notifications' | 'search' | 'space' | 'robot' | 'custom'; + openInNewTab?: boolean; + requiresAuth?: boolean; + spaceId?: string; // ← NEW: Reference to Space in database +} +``` + +### 2. Add 'navPage' Space Type + +```sql +-- Migration: Add navPage to spaceType enum +ALTER TABLE "public"."spaceRegistrations" + DROP CONSTRAINT IF EXISTS valid_space_type; + +ALTER TABLE "public"."spaceRegistrations" + ADD CONSTRAINT valid_space_type CHECK ( + "spaceType" IN ('profile', 'token', 'proposal', 'channel', 'navPage') + ); +``` + +### 3. Update Navigation Config Structure + +```typescript +// src/config/nouns/nouns.navigation.ts + +export const nounsNavigation: NavigationConfig = { + logoTooltip: { + text: "wtf is nouns?", + href: "https://nouns.wtf", + }, + items: [ + { + id: 'home', + label: 'Home', + href: '/home', + icon: 'home', + spaceId: '550e8400-e29b-41d4-a716-446655440000' // ← Reference to Space + }, + { + id: 'explore', + label: 'Explore', + href: '/explore', + icon: 'explore', + spaceId: '550e8400-e29b-41d4-a716-446655440001' // ← Reference to Space + }, + { + id: 'notifications', + label: 'Notifications', + href: '/notifications', + icon: 'notifications', + requiresAuth: true + // No spaceId - not a Space-based page + }, + ], + showMusicPlayer: true, + showSocials: true, +}; +``` + +### 4. Update Database Schema + +```sql +-- Remove home_page_config and explore_page_config from community_configs +ALTER TABLE "public"."community_configs" + DROP COLUMN IF EXISTS "home_page_config", + DROP COLUMN IF EXISTS "explore_page_config"; + +-- Navigation config now contains spaceId references +-- No schema changes needed - navigation_config JSONB column handles it +``` + +### 5. Update Build-Time Config Generation + +```javascript +// next.config.mjs + +async function generateConfigFile() { + // Fetch main config (now much smaller - no homePage/explorePage!) + const { data: config } = await supabase + .rpc('get_active_community_config', { p_community_id: community }) + .single(); + + // Fetch Spaces for nav items that have spaceId + const navItems = config.navigation?.items || []; + const spaceIds = navItems + .filter(item => item.spaceId) + .map(item => item.spaceId); + + // Fetch Spaces from database/storage + const spaces = {}; + for (const spaceId of spaceIds) { + // Fetch Space based on how Spaces are stored + // (Could be from spaceRegistrations + Storage, or new table) + const space = await fetchSpace(spaceId); + if (space) { + spaces[spaceId] = space; + } + } + + // Build page configs from Spaces + const pageConfigs = {}; + navItems.forEach(item => { + if (item.spaceId && spaces[item.spaceId]) { + // Map Space to page config format + pageConfigs[item.id] = convertSpaceToPageConfig(spaces[item.spaceId]); + } + }); + + // Combine configs + const fullConfig = { + ...config, + // Add page configs based on nav items + pages: pageConfigs, + }; + + // Generate file + await writeFile('src/config/db-config.ts', ...); +} +``` + +### 6. Update Config Loader + +```typescript +// src/config/index.ts + +export const loadSystemConfig = (): SystemConfig => { + const config = dbConfig || staticConfig; + + // Get page configs from nav items + const navItems = config.navigation?.items || []; + const pages = {}; + + navItems.forEach(item => { + if (item.spaceId && config.pages?.[item.id]) { + pages[item.id] = config.pages[item.id]; + } + }); + + // Map to legacy structure for backward compatibility + return { + ...config, + homePage: pages['home'] || staticConfig.homePage, + explorePage: pages['explore'] || staticConfig.explorePage, + }; +}; +``` + +## Space Storage Options + +### Option A: Use Existing Space Storage System + +Spaces stored in Supabase Storage: +- Path: `spaces/{spaceId}/tabs/{tabName}` +- Encrypted/signed files +- Requires decryption at build time + +**Pros:** +- Uses existing infrastructure +- No schema changes needed + +**Cons:** +- Requires encryption/decryption logic at build time +- More complex fetching + +### Option B: New Database Table for Nav Pages + +```sql +CREATE TABLE community_nav_pages ( + id UUID PRIMARY KEY, + community_id VARCHAR(50), + nav_item_id VARCHAR(50), -- 'home', 'explore', etc. + space_config JSONB NOT NULL, + version INTEGER DEFAULT 1, + created_at TIMESTAMP DEFAULT NOW() +); +``` + +**Pros:** +- Simple database queries +- No encryption needed (public pages) +- Easy to version + +**Cons:** +- New table needed +- Duplicates Space structure + +### Option C: Use spaceRegistrations + Storage + +Store nav pages as Spaces in `spaceRegistrations` with `spaceType = 'navPage'`: +- Register in `spaceRegistrations` table +- Store config in Supabase Storage +- Fetch at build time + +**Pros:** +- Uses existing Space infrastructure +- Consistent with other Spaces +- Can reuse Space APIs + +**Cons:** +- Requires spaceRegistrations entry +- Need to handle Storage fetching + +## Recommended: Option C (spaceRegistrations + Storage) + +**Why:** +- ✅ Uses existing Space system +- ✅ Consistent architecture +- ✅ Can reuse Space loading logic +- ✅ No new tables needed + +## Migration Steps + +1. **Add 'navPage' to spaceType enum** +2. **Create Spaces for homePage/explorePage** + - Register in `spaceRegistrations` with `spaceType = 'navPage'` + - Store configs in Supabase Storage +3. **Update navigation configs** + - Add `spaceId` to home/explore nav items +4. **Update build-time fetching** + - Fetch Spaces based on nav items + - Build page configs from Spaces +5. **Remove homePage/explorePage from community_configs** + - Update schema + - Update seed script + +## Size Reduction + +**Before:** +- Config: ~29 KB +- homePage: 19.2 KB +- explorePage: 1.4 KB + +**After:** +- Config: ~8.4 KB (71% reduction!) +- Navigation: ~500 bytes (includes spaceId references) +- Spaces: Fetched separately, no size limit + +**Result:** Config easily fits in env vars or generated file! + +## Example: Updated Navigation Config + +```typescript +export const nounsNavigation: NavigationConfig = { + items: [ + { + id: 'home', + label: 'Home', + href: '/home', + icon: 'home', + spaceId: 'nouns-home-space-uuid' // ← References Space + }, + { + id: 'explore', + label: 'Explore', + href: '/explore', + icon: 'explore', + spaceId: 'nouns-explore-space-uuid' // ← References Space + }, + ], +}; +``` + +## Benefits Summary + +✅ **Solves E2BIG** - Config size reduced by 71% +✅ **Unified architecture** - Everything is Spaces +✅ **Navigation as source of truth** - Nav defines pages +✅ **Flexible** - Any nav item can be a Space +✅ **Reuses infrastructure** - Uses existing Space system +✅ **No breaking changes** - Can maintain backward compatibility + diff --git a/docs/NAVIGATION_SPACE_REFERENCE_IMPLEMENTATION.md b/docs/NAVIGATION_SPACE_REFERENCE_IMPLEMENTATION.md new file mode 100644 index 000000000..cdb95e1c1 --- /dev/null +++ b/docs/NAVIGATION_SPACE_REFERENCE_IMPLEMENTATION.md @@ -0,0 +1,357 @@ +# Navigation-Space Reference Implementation Plan + +## Architecture Overview + +**Key Insight:** Navigation items reference Spaces. Pages are fetched from Spaces at build time. + +``` +Navigation Config (in community_configs) + └── items: [ + { id: 'home', spaceId: 'uuid', ... }, + { id: 'explore', spaceId: 'uuid', ... } + ] + ↓ + Build Time: + ↓ + Fetch Spaces by spaceId + ↓ + Build page configs from Spaces +``` + +## Changes Required + +### 1. Add 'navPage' Space Type + +**File:** `src/common/types/spaceData.ts` + +```typescript +export const SPACE_TYPES = { + PROFILE: 'profile', + TOKEN: 'token', + PROPOSAL: 'proposal', + CHANNEL: 'channel', + NAV_PAGE: 'navPage', // ← NEW +} as const; +``` + +**Migration:** `supabase/migrations/YYYYMMDDHHMMSS_add_navpage_space_type.sql` + +```sql +-- Add navPage to spaceType constraint +ALTER TABLE "public"."spaceRegistrations" + DROP CONSTRAINT IF EXISTS valid_space_type; + +ALTER TABLE "public"."spaceRegistrations" + ADD CONSTRAINT valid_space_type CHECK ( + "spaceType" IN ('profile', 'token', 'proposal', 'channel', 'navPage') + ); +``` + +### 2. Update NavigationItem Interface + +**File:** `src/config/systemConfig.ts` + +```typescript +export interface NavigationItem { + id: string; + label: string; + href: string; + icon?: 'home' | 'explore' | 'notifications' | 'search' | 'space' | 'robot' | 'custom'; + openInNewTab?: boolean; + requiresAuth?: boolean; + spaceId?: string; // ← NEW: Optional reference to Space +} +``` + +### 3. Update Navigation Configs + +**File:** `src/config/nouns/nouns.navigation.ts` + +```typescript +export const nounsNavigation: NavigationConfig = { + logoTooltip: { + text: "wtf is nouns?", + href: "https://nouns.wtf", + }, + items: [ + { + id: 'home', + label: 'Home', + href: '/home', + icon: 'home', + spaceId: 'nouns-home-space-id' // ← Reference to Space + }, + { + id: 'explore', + label: 'Explore', + href: '/explore', + icon: 'explore', + spaceId: 'nouns-explore-space-id' // ← Reference to Space + }, + { + id: 'notifications', + label: 'Notifications', + href: '/notifications', + icon: 'notifications', + requiresAuth: true + // No spaceId - not a Space-based page + }, + ], + showMusicPlayer: true, + showSocials: true, +}; +``` + +### 4. Update Database Schema + +**Migration:** Remove homePage/explorePage columns + +```sql +-- Remove large page config columns +ALTER TABLE "public"."community_configs" + DROP COLUMN IF EXISTS "home_page_config", + DROP COLUMN IF EXISTS "explore_page_config"; + +-- Update function to exclude page configs +CREATE OR REPLACE FUNCTION "public"."get_active_community_config"( + p_community_id VARCHAR(50) +) +RETURNS JSONB +LANGUAGE plpgsql +SECURITY DEFINER +AS $$ +DECLARE + v_config JSONB; +BEGIN + SELECT jsonb_build_object( + 'brand', "brand_config", + 'assets', "assets_config", + 'theme', "theme_config", + 'community', "community_config", + 'fidgets', "fidgets_config", + 'navigation', "navigation_config", -- Contains spaceId references + 'ui', "ui_config" + ) + INTO v_config + FROM "public"."community_configs" + WHERE "community_id" = p_community_id + AND "is_active" = true + AND "is_published" = true + ORDER BY "version" DESC + LIMIT 1; + + RETURN v_config; +END; +$$; +``` + +### 5. Update Build-Time Config Generation + +**File:** `next.config.mjs` + +```javascript +async function generateConfigFile() { + // Fetch main config (now much smaller!) + const { data: config } = await supabase + .rpc('get_active_community_config', { p_community_id: community }) + .single(); + + if (!config) return; + + // Extract spaceIds from navigation items + const navItems = config.navigation?.items || []; + const spaceIds = navItems + .filter(item => item.spaceId) + .map(item => ({ navId: item.id, spaceId: item.spaceId })); + + // Fetch Spaces for nav items + const pageConfigs = {}; + for (const { navId, spaceId } of spaceIds) { + try { + // Fetch Space from spaceRegistrations + Storage + const space = await fetchSpaceBySpaceId(spaceId); + if (space) { + // Convert Space config to page config format + pageConfigs[navId] = convertSpaceToPageConfig(space); + } + } catch (error) { + console.warn(`⚠️ Failed to fetch Space ${spaceId} for nav item ${navId}:`, error.message); + } + } + + // Combine configs + const fullConfig = { + ...config, + pages: pageConfigs, // Add page configs + }; + + // Generate TypeScript file + const configFile = `// Auto-generated at build time +import { SystemConfig } from './systemConfig'; + +export const dbConfig: SystemConfig | null = ${JSON.stringify(fullConfig, null, 2)} as SystemConfig; +`; + + await writeFile('src/config/db-config.ts', configFile, 'utf-8'); +} + +async function fetchSpaceBySpaceId(spaceId: string) { + // Option 1: Fetch from spaceRegistrations + Storage + const { data: registration } = await supabase + .from('spaceRegistrations') + .select('*') + .eq('spaceId', spaceId) + .eq('spaceType', 'navPage') + .single(); + + if (!registration) return null; + + // Fetch Space config from Storage + const { data } = await supabase.storage + .from('spaces') + .download(`${spaceId}/tabs/default`); // Or fetch all tabs + + if (!data) return null; + + // Parse and return Space config + const fileData = JSON.parse(await data.text()); + return fileData; // Return SpaceConfig +} + +function convertSpaceToPageConfig(space: SpaceConfig): HomePageConfig { + // Convert Space config to HomePageConfig/ExplorePageConfig format + // This maps Space tabs to page tabs + return { + defaultTab: space.defaultTab || 'Home', + tabOrder: Object.keys(space.tabs || {}), + tabs: space.tabs || {}, + layout: { + defaultLayoutFidget: space.layout?.defaultLayoutFidget || 'grid', + gridSpacing: space.layout?.gridSpacing || 16, + theme: space.theme || {}, + }, + }; +} +``` + +### 6. Update Config Loader + +**File:** `src/config/index.ts` + +```typescript +export const loadSystemConfig = (): SystemConfig => { + const config = dbConfig || staticConfig; + + // Extract page configs from pages object (built from nav items) + const homePage = config.pages?.['home'] || staticConfig.homePage; + const explorePage = config.pages?.['explore'] || staticConfig.explorePage; + + return { + ...config, + homePage, // Map from pages['home'] + explorePage, // Map from pages['explore'] + }; +}; +``` + +## Space Storage Strategy + +### How Nav Pages Are Stored + +1. **Register in spaceRegistrations:** + ```sql + INSERT INTO spaceRegistrations ( + "spaceId", + "spaceName", + "spaceType", + "identityPublicKey", + "signature", + "timestamp" + ) VALUES ( + 'nouns-home-space-id', + 'nouns-home', + 'navPage', + 'system-identity-key', + 'signature', + NOW() + ); + ``` + +2. **Store config in Supabase Storage:** + - Path: `spaces/{spaceId}/tabs/{tabName}` + - Format: Same as other Spaces (SpaceConfig JSON) + +3. **Fetch at build time:** + - Query `spaceRegistrations` for `spaceType = 'navPage'` + - Download from Storage + - Parse and convert to page config format + +## Size Reduction + +**Before:** +- Config: ~29 KB +- homePage: 19.2 KB (66.5%) +- explorePage: 1.4 KB (4.8%) + +**After:** +- Config: ~8.4 KB (71% reduction!) +- Navigation: ~500 bytes (includes spaceId strings) +- Spaces: Fetched separately, no size limit + +**Result:** Config easily fits in env vars or generated file! + +## Migration Path + +1. **Add navPage spaceType** - Migration + TypeScript constants +2. **Create Spaces for existing pages** - Register homePage/explorePage as Spaces +3. **Update navigation configs** - Add spaceId to nav items +4. **Update build-time fetching** - Fetch Spaces based on nav items +5. **Remove page configs from schema** - Drop home_page_config/explore_page_config columns +6. **Update seed script** - Don't seed page configs, seed Spaces instead + +## Benefits + +✅ **Solves E2BIG** - Config size reduced by 71% +✅ **Unified architecture** - Everything is Spaces +✅ **Navigation as source of truth** - Nav defines what pages exist +✅ **Flexible** - Any nav item can reference a Space +✅ **Reuses infrastructure** - Uses existing Space system +✅ **No breaking changes** - Can maintain backward compatibility during migration + +## Example: Complete Flow + +### 1. Navigation Config (in DB) +```json +{ + "navigation": { + "items": [ + { "id": "home", "label": "Home", "href": "/home", "spaceId": "uuid-1" }, + { "id": "explore", "label": "Explore", "href": "/explore", "spaceId": "uuid-2" } + ] + } +} +``` + +### 2. Build Time +```javascript +// Fetch nav config → see spaceIds +// Fetch Spaces: uuid-1, uuid-2 +// Convert Spaces to page configs +// Generate: +{ + ...config, + pages: { + 'home': { /* Space config converted */ }, + 'explore': { /* Space config converted */ } + } +} +``` + +### 3. Runtime +```typescript +// Load config +const config = loadSystemConfig(); +// config.homePage comes from config.pages['home'] +// config.explorePage comes from config.pages['explore'] +``` + diff --git a/docs/NEXTJS_CONFIG_APPROACHES.md b/docs/NEXTJS_CONFIG_APPROACHES.md new file mode 100644 index 000000000..977038674 --- /dev/null +++ b/docs/NEXTJS_CONFIG_APPROACHES.md @@ -0,0 +1,347 @@ +# Next.js-Specific Approaches for Build-Time Config Generation + +Next.js offers several clever ways to handle build-time configuration generation. Here are the most Next.js-native approaches: + +## Approach 1: Generate in `next.config.mjs` (Recommended) + +**Most Next.js-Native**: Since `next.config.mjs` executes at build time, you can generate files directly there. + +### Implementation + +```javascript +// next.config.mjs + +import { generateConfigs } from './scripts/generate-configs.mjs'; + +// Generate configs before Next.js config is created +await generateConfigs(); + +import bundlerAnalyzer from "@next/bundle-analyzer"; +// ... rest of your config +``` + +**Pros:** +- ✅ Runs automatically before every build +- ✅ No need for `prebuild` hook +- ✅ Guaranteed to run before Next.js processes files +- ✅ Can use async/await +- ✅ Native Next.js approach + +**Cons:** +- ⚠️ Makes `next.config.mjs` more complex +- ⚠️ Harder to test in isolation + +### Example Implementation + +```javascript +// next.config.mjs + +import { createClient } from '@supabase/supabase-js'; +import { writeFile, mkdir } from 'fs/promises'; +import { join } from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +async function generateConfigs() { + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + if (!supabaseUrl || !supabaseKey) { + console.warn('⚠️ Missing Supabase env vars, skipping config generation'); + return; + } + + const supabase = createClient(supabaseUrl, supabaseKey); + const communities = process.env.GENERATE_CONFIGS?.split(',') || ['nouns']; + + console.log('📦 Generating configs from database...'); + + for (const communityId of communities) { + try { + const { data, error } = await supabase + .rpc('get_active_community_config', { p_community_id: communityId }) + .single(); + + if (error || !data) { + console.warn(`⚠️ Failed to fetch ${communityId} config:`, error?.message); + continue; + } + + const configDir = join(__dirname, 'src', 'config', communityId); + await mkdir(configDir, { recursive: true }); + + // Generate config files... + // (same as before) + + console.log(`✅ Generated config for ${communityId}`); + } catch (error) { + console.error(`❌ Error generating ${communityId} config:`, error); + } + } +} + +// Generate configs before creating Next.js config +await generateConfigs(); + +// Now create Next.js config +import bundlerAnalyzer from "@next/bundle-analyzer"; +// ... rest of config +``` + +--- + +## Approach 2: Webpack Plugin (Most Flexible) + +**Most Flexible**: Create a custom webpack plugin that generates files during the build process. + +### Implementation + +```javascript +// scripts/config-generator-plugin.js + +class ConfigGeneratorPlugin { + apply(compiler) { + compiler.hooks.beforeCompile.tapAsync( + 'ConfigGeneratorPlugin', + async (params, callback) => { + try { + await generateConfigs(); + callback(); + } catch (error) { + console.error('Config generation failed:', error); + callback(error); + } + } + ); + } +} + +module.exports = ConfigGeneratorPlugin; +``` + +```javascript +// next.config.mjs + +import ConfigGeneratorPlugin from './scripts/config-generator-plugin.js'; + +const nextConfig = { + webpack: (config, { isServer }) => { + if (!isServer) { + config.plugins.push(new ConfigGeneratorPlugin()); + } + return config; + }, + // ... rest of config +}; +``` + +**Pros:** +- ✅ Runs during webpack compilation +- ✅ Can access webpack context +- ✅ More control over when it runs +- ✅ Can be conditional based on build mode + +**Cons:** +- ⚠️ More complex setup +- ⚠️ Requires understanding webpack hooks +- ⚠️ Runs during compilation (slightly later than `next.config.mjs`) + +--- + +## Approach 3: Next.js Environment Variables with Build Script + +**Simplest**: Use environment variables that are loaded at build time, combined with a build script. + +### Implementation + +```javascript +// next.config.mjs + +const nextConfig = { + env: { + // These are loaded at build time + NEXT_PUBLIC_CONFIG_VERSION: process.env.CONFIG_VERSION || 'static', + }, + // ... rest of config +}; +``` + +```json +// package.json +{ + "scripts": { + "generate-configs": "node scripts/generate-configs.mjs", + "build": "npm run generate-configs && next build" + } +} +``` + +**Pros:** +- ✅ Simple and explicit +- ✅ Easy to understand +- ✅ Can be skipped if needed + +**Cons:** +- ⚠️ Requires remembering to run script +- ⚠️ Can be forgotten in CI/CD + +--- + +## Approach 4: Next.js Server Components (For Runtime Config) + +**Note**: This is for runtime config, not build-time, but worth mentioning. + +If you wanted runtime config (not recommended for your use case), you could use Server Components: + +```typescript +// app/config-provider.tsx (Server Component) + +import { createClient } from '@supabase/supabase-js'; + +export async function ConfigProvider({ children }) { + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + ); + + const { data } = await supabase + .rpc('get_active_community_config', { + p_community_id: process.env.NEXT_PUBLIC_COMMUNITY || 'nouns' + }) + .single(); + + return ( + + {children} + + ); +} +``` + +**Pros:** +- ✅ No build-time generation needed +- ✅ Always up-to-date + +**Cons:** +- ❌ Runtime database queries (not what you want) +- ❌ Slower page loads +- ❌ Requires database connection + +--- + +## Approach 5: Next.js `generateStaticParams` Pattern (For Routes) + +**For Route-Based Configs**: If configs were route-specific, you could use `generateStaticParams`: + +```typescript +// app/[community]/layout.tsx + +export async function generateStaticParams() { + // Fetch all communities from DB at build time + const communities = await fetchCommunitiesFromDB(); + return communities.map(community => ({ + community: community.id, + })); +} + +export default async function CommunityLayout({ params }) { + // This runs at build time for each community + const config = await fetchConfigForCommunity(params.community); + return ...; +} +``` + +**Pros:** +- ✅ Native Next.js pattern +- ✅ Build-time generation +- ✅ Per-route optimization + +**Cons:** +- ⚠️ Only works for route-based configs +- ⚠️ Not ideal for global configs + +--- + +## Recommended Approach: Hybrid + +**Best of Both Worlds**: Combine `next.config.mjs` generation with a fallback script. + +```javascript +// next.config.mjs + +import { generateConfigs } from './scripts/generate-configs.mjs'; + +// Try to generate configs, but don't fail build if it fails +try { + await generateConfigs(); +} catch (error) { + console.warn('⚠️ Config generation failed, using static configs:', error.message); +} + +// Continue with Next.js config... +``` + +```json +// package.json +{ + "scripts": { + "generate-configs": "node scripts/generate-configs.mjs", + "prebuild": "npm run generate-configs || true", // Don't fail if generation fails + "build": "next build" + } +} +``` + +**Benefits:** +- ✅ Automatic generation in `next.config.mjs` +- ✅ Fallback script for manual runs +- ✅ Build doesn't fail if DB unavailable +- ✅ Works in all environments + +--- + +## Comparison Table + +| Approach | When It Runs | Complexity | Next.js Native | Recommended | +|----------|--------------|------------|----------------|-------------| +| `next.config.mjs` | Before config load | Low | ✅ Yes | ⭐⭐⭐⭐⭐ | +| Webpack Plugin | During compilation | Medium | ✅ Yes | ⭐⭐⭐⭐ | +| Prebuild Script | Before build | Low | ⚠️ No | ⭐⭐⭐ | +| Server Components | Runtime | Low | ✅ Yes | ❌ No (runtime) | +| `generateStaticParams` | Build time | Medium | ✅ Yes | ⭐⭐ (route-specific) | + +--- + +## Recommended Implementation + +For your use case, I recommend **Approach 1** (`next.config.mjs`) because: + +1. ✅ **Most Next.js-native** - Uses Next.js's build-time execution +2. ✅ **Automatic** - Runs before every build without extra scripts +3. ✅ **Simple** - No webpack plugins or complex setup +4. ✅ **Reliable** - Guaranteed to run before Next.js processes files +5. ✅ **Flexible** - Can easily add error handling and fallbacks + +### Final Implementation + +```javascript +// next.config.mjs + +import { generateConfigs } from './scripts/generate-configs.mjs'; + +// Generate configs at build time +await generateConfigs().catch(error => { + console.warn('⚠️ Config generation failed, using static configs'); + console.warn(error.message); +}); + +// Continue with your existing Next.js config +import bundlerAnalyzer from "@next/bundle-analyzer"; +// ... rest of config +``` + +This gives you the best of both worlds: automatic generation with graceful fallback to static configs. + diff --git a/docs/QUICK_START_IMPLEMENTATION.md b/docs/QUICK_START_IMPLEMENTATION.md new file mode 100644 index 000000000..f78a62124 --- /dev/null +++ b/docs/QUICK_START_IMPLEMENTATION.md @@ -0,0 +1,655 @@ +# Quick Start: Incremental Implementation Guide + +## 🎯 Goal + +Implement database-backed configs incrementally, testing each piece before moving forward. + +**Updated Architecture:** +- ✅ Configs stored in DB (without themes/pages) +- ✅ Themes in shared file (`src/config/shared/themes.ts`) +- ✅ Pages stored as Spaces (referenced by navigation `spaceId`) +- ✅ Config size reduced by ~90% (from ~29 KB to ~2.8 KB) + +## 📋 Phase Overview + +``` +Phase 0: Setup → Phase 1: DB Schema + ↓ +Phase 2: Config Loading ← Phase 3: Admin API + ↓ +Phase 4: Admin UI → Phase 5: Asset Upload + ↓ +Phase 6: Asset Download → Phase 7: Optimization + ↓ +Phase 8: Rebuild Trigger → Phase 9: Production → Phase 10: Phase Out Static +``` + +## 🚀 Quick Proof of Concept (4 hours) + +Want to test the concept quickly? Do these 3 phases: + +### Step 1: Database Setup (1-2 hours) + +**Reset database from scratch:** +```bash +# Reset Supabase database (applies all migrations + seed data) +supabase db reset +``` + +This will: +1. ✅ Apply migration: `create_community_configs.sql` (creates table without themes/pages) +2. ✅ Apply migration: `add_navpage_space_type.sql` (adds navPage spaceType) +3. ✅ Run `seed.sql` (seeds configs for nouns, example, clanker) + +**Verify:** +```sql +-- Check table exists and has correct columns +SELECT column_name, data_type +FROM information_schema.columns +WHERE table_name = 'community_configs'; + +-- Should see: brand_config, assets_config, community_config, fidgets_config, navigation_config, ui_config +-- Should NOT see: theme_config, home_page_config, explore_page_config + +-- Test function +SELECT get_active_community_config('nouns'); + +-- Verify navPage spaceType exists +SELECT DISTINCT "spaceType" FROM "spaceRegistrations"; +-- Should include: 'navPage' +``` + +**Expected output:** +- ✅ Table created with correct columns +- ✅ Function returns config (without themes/pages) +- ✅ Seed data inserted for all 3 communities +- ✅ navPage spaceType available + +### Step 2: Build-Time Loading (1 hour) + +```javascript +// next.config.mjs (add at top) + +import { createClient } from '@supabase/supabase-js'; +import { writeFile, mkdir } from 'fs/promises'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +async function fetchSpaceBySpaceId(supabase, spaceId) { + // Fetch Space from spaceRegistrations + Storage + const { data: registration } = await supabase + .from('spaceRegistrations') + .select('*') + .eq('spaceId', spaceId) + .eq('spaceType', 'navPage') + .single(); + + if (!registration) return null; + + // Fetch Space config from Storage + const { data } = await supabase.storage + .from('spaces') + .download(`${spaceId}/tabs/default`); + + if (!data) return null; + + const fileData = JSON.parse(await data.text()); + return fileData; +} + +function convertSpaceToPageConfig(space) { + // Convert Space config to HomePageConfig/ExplorePageConfig format + return { + defaultTab: space.defaultTab || 'Home', + tabOrder: Object.keys(space.tabs || {}), + tabs: space.tabs || {}, + layout: { + defaultLayoutFidget: space.layout?.defaultLayoutFidget || 'grid', + gridSpacing: space.layout?.gridSpacing || 16, + theme: space.theme || {}, + }, + }; +} + +async function generateConfigFile() { + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + + if (!supabaseUrl || !supabaseKey) { + console.log('ℹ️ Using static configs (no DB credentials for config generation)'); + return; + } + + const supabase = createClient(supabaseUrl, supabaseKey); + const community = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + + try { + // Fetch main config (now much smaller - no themes/pages!) + const { data: config, error } = await supabase + .rpc('get_active_community_config', { p_community_id: community }) + .single(); + + if (error || !data) { + console.log('ℹ️ No DB config found, skipping db-config.ts generation'); + if (error) { + console.log(` Error: ${error.message}`); + } + return; + } + + // Extract spaceIds from navigation items + const navItems = config.navigation?.items || []; + const spaceIds = navItems + .filter(item => item.spaceId) + .map(item => ({ navId: item.id, spaceId: item.spaceId })); + + // Fetch Spaces for nav items + const pageConfigs = {}; + for (const { navId, spaceId } of spaceIds) { + try { + const space = await fetchSpaceBySpaceId(supabase, spaceId); + if (space) { + pageConfigs[navId] = convertSpaceToPageConfig(space); + } + } catch (error) { + console.warn(`⚠️ Failed to fetch Space ${spaceId}:`, error.message); + } + } + + // Import shared themes (will be created in Step 3) + // For now, we'll add a placeholder - themes will be imported from shared file + const themesPlaceholder = {}; // Will be replaced with import from shared/themes.ts + + // Combine: config + themes + pages + const fullConfig = { + ...config, + theme: themesPlaceholder, // From shared file (Step 3) + pages: pageConfigs, // From Spaces + }; + + // Store config in environment variable (now small enough at ~2.8 KB) + process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(fullConfig); + console.log('✅ Loaded config from database'); + } catch (error) { + console.warn('⚠️ Error generating db-config.ts:', error.message); + } +} + +// Run config generation before Next.js config is created +await generateConfigFile(); + +// Continue with existing Next.js config... +``` + +```typescript +// src/config/index.ts (modify loadSystemConfig) + +import { SystemConfig } from './systemConfig'; +import { nounsSystemConfig } from './nouns/index'; +import { exampleSystemConfig } from './example/index'; +import { clankerSystemConfig } from './clanker/index'; +// Import shared themes (will be created in Step 3) +// import { themes } from './shared/themes'; + +export const loadSystemConfig = (): SystemConfig => { + const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + + // Try build-time config from database (stored in env var at build time) + const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; + if (buildTimeConfig) { + try { + const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; + + // Map page configs from pages object to homePage/explorePage + const homePage = dbConfig.pages?.['home'] || null; + const explorePage = dbConfig.pages?.['explore'] || null; + + // Get themes from shared file (Step 3) + // const themes = require('./shared/themes').themes; + + // Validate config structure + if (dbConfig && dbConfig.brand && dbConfig.assets) { + console.log('✅ Using config from database'); + return { + ...dbConfig, + homePage, + explorePage, + // theme: themes, // From shared file (Step 3) + } as SystemConfig; + } + } catch (error) { + console.warn('⚠️ Failed to parse build-time config, falling back to static:', error); + } + } + + // Fall back to static configs (existing behavior) + console.log('ℹ️ Using static configs'); + switch (communityConfig.toLowerCase()) { + case 'nouns': + return nounsSystemConfig; + case 'example': + return exampleSystemConfig; + case 'clanker': + return clankerSystemConfig as unknown as SystemConfig; + default: + return nounsSystemConfig; + } +}; +``` + +**Test:** +```bash +# Test with DB +NEXT_PUBLIC_SUPABASE_URL=... SUPABASE_SERVICE_ROLE_KEY=... npm run build +# Should see: "✅ Loaded config from database" + +# Test without DB +npm run build +# Should see: "ℹ️ Using static configs" +# Should fall back to static configs + +# Verify app works +npm run dev +# App should load correctly +``` + +### Step 3: Create Shared Themes (30 min) + +```typescript +// src/config/shared/themes.ts + +// Extract themes from nouns.theme.ts (or any community - they're all the same) +import { nounsTheme } from '../nouns/nouns.theme'; + +// Export shared themes (all communities use the same themes) +export const themes = nounsTheme; + +// Update community configs to import from shared +// src/config/nouns/nouns.theme.ts +export { themes as nounsTheme } from '../shared/themes'; + +// src/config/clanker/clanker.theme.ts +export { themes as clankerTheme } from '../shared/themes'; + +// src/config/example/example.theme.ts +export { themes as exampleTheme } from '../shared/themes'; +``` + +**Update next.config.mjs to import themes:** +```javascript +// In generateConfigFile function, replace themesPlaceholder: +import { themes } from './src/config/shared/themes'; + +// Then use: +const fullConfig = { + ...config, + theme: themes, // From shared file + pages: pageConfigs, // From Spaces +}; +``` + +**Update src/config/index.ts to use shared themes:** +```typescript +import { themes } from './shared/themes'; + +// In loadSystemConfig, when using dbConfig: +return { + ...dbConfig, + theme: themes, // Always use shared themes + homePage, + explorePage, +} as SystemConfig; +``` + +**Test:** +```bash +npm run build +# Should see: "✅ Loaded config from database" +# Config is now ~2.8 KB (much smaller!), stored in env var +``` + +**✅ Proof of Concept Complete!** + +If this works, you've validated: +- ✅ Database storage works (without themes/pages) +- ✅ Build-time loading works +- ✅ Shared themes work +- ✅ Space-based pages work (when Spaces are created) +- ✅ Fallback works +- ✅ Config size reduced by ~90% + +--- + +## 📊 Full Implementation Phases + +### Phase 1: Database Foundation (Week 1, Days 1-2) + +**What:** Create tables, functions, seed data + +**Files:** +- `supabase/migrations/20251129172847_create_community_configs.sql` - Creates table (no themes/pages) +- `supabase/migrations/20251129172848_add_navpage_space_type.sql` - Adds navPage spaceType +- `supabase/seed.sql` - Seeds configs for nouns, example, clanker + +**Test:** +- ✅ Can query configs from DB +- ✅ Functions work correctly +- ✅ Config excludes themes/pages +- ✅ navPage spaceType exists +- ✅ Seed data loaded + +**Rollback:** `supabase db reset` (resets to clean state) + +--- + +### Phase 2: Config Loading (Week 1, Days 3-4) + +**What:** Load config from DB at build time, fetch Spaces for nav items + +**Files:** +- `next.config.mjs` - Build-time config generation +- `src/config/index.ts` - Config loader +- `src/config/shared/themes.ts` - Shared themes + +**Test:** +- ✅ Build succeeds with/without DB +- ✅ App uses DB config when available +- ✅ Themes loaded from shared file +- ✅ Pages loaded from Spaces (when available) +- ✅ App falls back to static +- ✅ Config size ~2.8 KB + +**Rollback:** Remove generated file reading + +--- + +### Phase 3: Admin API (Week 1, Day 5) + +**What:** Create API endpoints for updates + +**Test:** +- ✅ Can fetch config via API (without themes/pages) +- ✅ Can update config via API +- ✅ Can fetch/update nav page Spaces +- ✅ Permission checks work + +**Rollback:** Remove API routes + +--- + +### Phase 4: Admin UI (Week 2, Days 1-2) + +**What:** Create admin interface + +**Test:** +- ✅ Can view config (without themes/pages) +- ✅ Can edit config +- ✅ Can edit nav page Spaces +- ✅ Can save changes + +**Rollback:** Remove admin pages + +--- + +### Phase 5: Asset Upload (Week 2, Days 3-4) + +**What:** Upload assets to Supabase Storage + +**Test:** +- ✅ Can upload assets +- ✅ Assets stored correctly +- ✅ Config updated with paths + +**Rollback:** Remove upload functionality + +--- + +### Phase 6: Asset Download (Week 2, Day 5) + +**What:** Download assets during build + +**Test:** +- ✅ Assets download to public/ +- ✅ Config paths updated +- ✅ Assets load correctly + +**Rollback:** Remove download code + +--- + +### Phase 7: Optimization (Week 3, Day 1) + +**What:** Optimize images with Sharp + +**Test:** +- ✅ Images optimized +- ✅ File sizes reduced +- ✅ Quality maintained + +**Rollback:** Remove optimization + +--- + +### Phase 8: Rebuild Trigger (Week 3, Day 2) + +**What:** Trigger rebuilds after updates + +**Test:** +- ✅ Rebuild triggers correctly +- ✅ Build completes successfully +- ✅ Changes deployed + +**Rollback:** Remove trigger + +--- + +### Phase 9: Production (Week 3, Days 3-5) + +**What:** Roll out to production + +**Test:** +- ✅ All communities work +- ✅ Performance acceptable +- ✅ No regressions + +**Rollback:** Revert to static configs + +--- + +## 🧪 Testing Checklist Per Phase + +### Phase 1: Database +- [ ] Tables created (without theme/home/explore columns) +- [ ] Functions work (exclude themes/pages) +- [ ] Seed data loaded +- [ ] navPage spaceType exists +- [ ] RLS policies work + +### Phase 2: Config Loading +- [ ] Build succeeds with DB +- [ ] Build succeeds without DB +- [ ] App uses DB config +- [ ] Themes loaded from shared file +- [ ] Pages loaded from Spaces (when available) +- [ ] App falls back to static +- [ ] Config size ~2.8 KB +- [ ] No breaking changes + +### Phase 3: Admin API +- [ ] GET returns config (without themes/pages) +- [ ] PUT updates config +- [ ] GET/PUT for nav page Spaces +- [ ] Permission checks work +- [ ] Invalid requests rejected + +### Phase 4: Admin UI +- [ ] Can view config (without themes/pages) +- [ ] Can edit config +- [ ] Can edit nav page Spaces +- [ ] Can save changes +- [ ] Changes persist + +### Phase 5: Asset Upload +- [ ] Can upload assets +- [ ] Assets in storage +- [ ] Config updated + +### Phase 6: Asset Download +- [ ] Assets download +- [ ] Assets in public/ +- [ ] Config paths updated +- [ ] Assets load correctly + +### Phase 7: Optimization +- [ ] Images optimized +- [ ] File sizes reduced +- [ ] Quality maintained + +### Phase 8: Rebuild Trigger +- [ ] Rebuild triggers +- [ ] Build completes +- [ ] Changes deployed + +### Phase 9: Production +- [ ] All communities work +- [ ] Performance good +- [ ] No regressions + +--- + +## 🎯 Success Metrics + +### Phase 2 (Config Loading) +- Build time: < 30s +- App loads: < 2s +- Config size: ~2.8 KB (down from ~29 KB) +- Config matches: DB or static + +### Phase 6 (Asset Download) +- Asset download: < 5s per community +- Asset sizes: < 500KB each +- Assets load: < 500ms + +### Phase 7 (Optimization) +- File size reduction: > 50% +- Build time increase: < 5s +- Quality: Acceptable + +### Phase 9 (Production) +- All communities: Working +- Performance: No degradation +- Errors: < 0.1% + +--- + +## 🚨 Risk Mitigation + +### High Risk Phases +- **Phase 2** - Could break builds + - **Mitigation:** Extensive fallback testing + - **Rollback:** Remove generated file reading + +- **Phase 6** - Could break asset loading + - **Mitigation:** Fallback to static assets + - **Rollback:** Remove download code + +- **Phase 9** - Production rollout + - **Mitigation:** Gradual rollout, monitor closely + - **Rollback:** Revert to static configs + +### Low Risk Phases +- **Phase 1** - Database only, no app changes +- **Phase 3** - API only, no app changes +- **Phase 4** - Admin UI only, no app changes +- **Phase 7** - Optimization only, improves performance + +--- + +## 📝 Implementation Notes + +### Environment Variables Needed + +```bash +# Required for all phases +NEXT_PUBLIC_SUPABASE_URL=... +NEXT_PUBLIC_SUPABASE_ANON_KEY=... + +# Required for build-time loading (Phase 2+) +SUPABASE_SERVICE_ROLE_KEY=... + +# Required for admin features (Phase 3+) +# (Admin identity from your auth system) +``` + +### Database Reset + +To reset database from scratch: +```bash +supabase db reset +``` + +This will: +1. Drop all tables +2. Apply all migrations (including community_configs) +3. Run seed.sql (seeds configs for nouns, example, clanker) + +### Git Workflow + +```bash +# Create feature branch +git checkout -b feature/database-configs + +# Work on Phase 1 +git commit -m "Phase 1: Database schema" + +# Work on Phase 2 +git commit -m "Phase 2: Config loading" + +# Test each phase before moving on +# Merge when all phases complete +``` + +### Testing Strategy + +1. **Local testing** - Test each phase locally first +2. **Staging testing** - Deploy to staging after each phase +3. **Production testing** - Test with one community first +4. **Full rollout** - Roll out to all communities + +--- + +## 🎓 Learning Path + +If you're new to any part: + +1. **Supabase** - Start with local Supabase, test queries +2. **Next.js build** - Understand `next.config.mjs` execution +3. **Environment variables** - Learn Next.js env var system +4. **Sharp** - Test image optimization separately first + +--- + +## 📞 Support + +If you get stuck on any phase: + +1. Check the detailed plan in `INCREMENTAL_IMPLEMENTATION_PLAN.md` +2. Review the specific phase documentation +3. Test the rollback procedure +4. Ask for help before proceeding + +--- + +## ✅ Quick Validation + +After each phase, verify: + +1. **Build succeeds** - `npm run build` works +2. **App works** - `npm run dev` loads correctly +3. **No regressions** - Existing features still work +4. **Can rollback** - Know how to revert if needed + +If all ✅, proceed to next phase! diff --git a/docs/QUICK_START_TESTING.md b/docs/QUICK_START_TESTING.md new file mode 100644 index 000000000..e3086bb6a --- /dev/null +++ b/docs/QUICK_START_TESTING.md @@ -0,0 +1,301 @@ +# Quick Start Testing Guide + +## Overview + +This guide walks you through testing the database-backed configuration system. + +## Prerequisites + +- ✅ Supabase project set up +- ✅ Environment variables configured +- ✅ Node.js and npm installed + +## Step 1: Reset Database (Applies Migrations + Seed Data) + +**Updated:** Database is now seeded via SQL in `supabase/seed.sql`, which runs automatically on reset. + +```bash +# Reset database from scratch (applies all migrations + seed data) +supabase db reset +``` + +This will: +1. ✅ Apply migration: `create_community_configs.sql` (creates table without themes/pages) +2. ✅ Apply migration: `add_navpage_space_type.sql` (adds navPage spaceType) +3. ✅ Run `seed.sql` (seeds configs for nouns, example, clanker) + +**Verify:** +```sql +-- Check table exists with correct columns (should NOT have theme_config, home_page_config, explore_page_config) +SELECT column_name, data_type +FROM information_schema.columns +WHERE table_name = 'community_configs'; + +-- Should see: brand_config, assets_config, community_config, fidgets_config, navigation_config, ui_config +-- Should NOT see: theme_config, home_page_config, explore_page_config + +-- Test function (should return config without themes/pages) +SELECT get_active_community_config('nouns'); + +-- Verify seed data loaded +SELECT community_id FROM community_configs; +-- Should see: nouns, example, clanker + +-- Verify navPage spaceType exists +SELECT DISTINCT "spaceType" FROM "spaceRegistrations"; +-- Should include: 'navPage' (if constraint updated) +``` + +**Expected output:** +- ✅ Table created with correct columns (no themes/pages) +- ✅ Function returns config (without themes/pages) +- ✅ Seed data inserted for all 3 communities +- ✅ navPage spaceType available + +## Step 2: Optional - Manual Seed Script + +**Note:** The TypeScript seed script (`scripts/seed-community-configs.ts`) is now **OPTIONAL**. +Use it only if you need to update configs without resetting the database. + +If you need to use it: + +```bash +# Make sure you have these env vars set: +export NEXT_PUBLIC_SUPABASE_URL="your-supabase-url" +export SUPABASE_SERVICE_ROLE_KEY="your-service-role-key" + +# Run seed script (optional - seed.sql already did this) +npx tsx scripts/seed-community-configs.ts +``` + +**Note:** The TypeScript script still includes themes/pages for backward compatibility. +The new architecture stores themes in shared file and pages as Spaces. + +## Step 3: Create Shared Themes (If Not Done) + +Before testing build-time loading, ensure shared themes file exists: + +```typescript +// src/config/shared/themes.ts +import { nounsTheme } from '../nouns/nouns.theme'; +export const themes = nounsTheme; +``` + +Update community configs to import from shared: +- `src/config/nouns/nouns.theme.ts` → `export { themes as nounsTheme } from '../shared/themes';` +- `src/config/clanker/clanker.theme.ts` → `export { themes as clankerTheme } from '../shared/themes';` +- `src/config/example/example.theme.ts` → `export { themes as exampleTheme } from '../shared/themes';` + +## Step 4: Test Build-Time Loading + +### Test with Database Available + +```bash +# Set environment variables +export NEXT_PUBLIC_SUPABASE_URL="your-supabase-url" +export SUPABASE_SERVICE_ROLE_KEY="your-service-role-key" +export NEXT_PUBLIC_COMMUNITY="nouns" + +# Run build +npm run build +``` + +**Expected output:** +``` +✅ Loaded config from database +``` + +**Verify in build output:** +- Should see "✅ Loaded config from database" message +- Build should complete successfully +- No errors about missing config +- Config is stored in `NEXT_PUBLIC_BUILD_TIME_CONFIG` env var (~2.8 KB) + +### Test without Database (Fallback) + +```bash +# Remove Supabase env vars +unset NEXT_PUBLIC_SUPABASE_URL +unset SUPABASE_SERVICE_ROLE_KEY + +# Run build (should fall back to static configs) +npm run build +``` + +**Expected output:** +``` +ℹ️ Using static configs (no DB credentials) +``` + +**Verify:** +- Should see "ℹ️ Using static configs" message +- Build should complete successfully +- App should work with static configs + +## Step 4: Test Runtime + +### Start Dev Server + +```bash +# With DB config +export NEXT_PUBLIC_SUPABASE_URL="your-supabase-url" +export SUPABASE_SERVICE_ROLE_KEY="your-service-role-key" +export NEXT_PUBLIC_COMMUNITY="nouns" +npm run dev +``` + +**Check browser console:** +- Should see "✅ Using config from database" message +- App should load correctly +- Brand name should match database config + +### Test Different Communities + +```bash +# Test with 'example' community +export NEXT_PUBLIC_COMMUNITY="example" +npm run build +npm run dev + +# Test with 'clanker' community +export NEXT_PUBLIC_COMMUNITY="clanker" +npm run build +npm run dev +``` + +## Step 5: Manual Database Update Test + +Update config directly in database: + +```sql +-- Update brand display name +UPDATE community_configs +SET brand_config = jsonb_set( + brand_config, + '{displayName}', + '"Nouns Updated"' +) +WHERE community_id = 'nouns'; +``` + +Then rebuild and verify: + +```bash +npm run build +npm run dev +``` + +**Verify:** +- Brand name should be "Nouns Updated" in the app +- Changes should be reflected after rebuild + +## Troubleshooting + +### Migration Fails + +**Error:** `relation "community_configs" already exists` + +**Solution:** +```sql +DROP TABLE IF EXISTS community_configs CASCADE; +DROP FUNCTION IF EXISTS get_active_community_config; +``` +Then re-run migration. + +### Seed Script Fails + +**Error:** `Missing required environment variables` + +**Solution:** +```bash +# Check env vars are set +echo $NEXT_PUBLIC_SUPABASE_URL +echo $SUPABASE_SERVICE_ROLE_KEY + +# Set them if missing +export NEXT_PUBLIC_SUPABASE_URL="your-url" +export SUPABASE_SERVICE_ROLE_KEY="your-key" +``` + +**Error:** `Error seeding nouns: ...` + +**Solution:** +- Check Supabase connection +- Verify service role key has permissions +- Check table exists: `SELECT * FROM community_configs LIMIT 1;` + +### Build Fails + +**Error:** `Cannot find module '@supabase/supabase-js'` + +**Solution:** +```bash +npm install @supabase/supabase-js +``` + +**Error:** `Function get_active_community_config does not exist` + +**Solution:** +- Re-run migration: `supabase migration up` +- Verify function exists: `SELECT * FROM pg_proc WHERE proname = 'get_active_community_config';` + +### Runtime Issues + +**App shows static config instead of DB config** + +**Check:** +1. Build-time env vars are set +2. Database has config for community +3. Check build logs for "✅ Loaded config from database" +4. Check browser console for config source + +**Config not updating after DB change** + +**Solution:** +- Rebuild required: `npm run build` +- Config is loaded at build time, not runtime +- Changes require rebuild to take effect + +## Success Criteria + +✅ **Migration succeeds** - Table and function created +✅ **Seed succeeds** - All configs inserted +✅ **Build with DB** - Config loaded from database +✅ **Build without DB** - Falls back to static configs +✅ **Runtime works** - App loads correctly +✅ **DB updates work** - Changes reflected after rebuild + +## Next Steps + +Once testing is successful: + +1. ✅ Phase 1 complete - Database schema working +2. ✅ Phase 2 complete - Build-time loading working +3. ➡️ Phase 3 - Create admin API endpoints +4. ➡️ Phase 4 - Create admin UI + +## Quick Commands Reference + +```bash +# Reset database (applies migrations + seed data) +supabase db reset + +# Optional: Seed configs manually (seed.sql already does this) +npx tsx scripts/seed-community-configs.ts + +# Build with DB +NEXT_PUBLIC_SUPABASE_URL=... SUPABASE_SERVICE_ROLE_KEY=... npm run build + +# Build without DB (fallback) +npm run build + +# Dev server +npm run dev + +# Test function +psql -c "SELECT get_active_community_config('nouns');" + +# Verify config is loaded (check env var in build output) +# Config is stored in NEXT_PUBLIC_BUILD_TIME_CONFIG env var (~2.8 KB) +``` + diff --git a/docs/SHARED_THEMES_APPROACH.md b/docs/SHARED_THEMES_APPROACH.md new file mode 100644 index 000000000..5b52cbe84 --- /dev/null +++ b/docs/SHARED_THEMES_APPROACH.md @@ -0,0 +1,255 @@ +# Shared Themes Approach + +## Overview + +Move themes out of individual community configs into a shared file, since themes are reusable across communities with only minor customizations. + +## Current State + +- Each community has its own `{community}.theme.ts` file +- Themes are mostly identical (same structure, different values) +- Themes take up 5.5 KB (66.7% of remaining config) + +## Proposed Location Options + +### Option 1: `src/config/shared/themes.ts` (Recommended) + +**Structure:** +``` +src/config/ +├── shared/ +│ └── themes.ts # Shared theme definitions +├── nouns/ +│ └── nouns.theme.ts # Community-specific overrides (optional) +└── ... +``` + +**Pros:** +- ✅ Clear organization - shared configs in `shared/` folder +- ✅ Easy to find - obvious location +- ✅ Extensible - can add other shared configs later +- ✅ Separates shared from community-specific + +**Cons:** +- ⚠️ New directory structure + +### Option 2: `src/config/themes.ts` + +**Structure:** +``` +src/config/ +├── themes.ts # Shared themes at root +├── nouns/ +└── ... +``` + +**Pros:** +- ✅ Simple - no new directory +- ✅ Easy to import + +**Cons:** +- ⚠️ Mixes shared with community configs +- ⚠️ Less clear organization + +### Option 3: `src/common/config/themes.ts` + +**Structure:** +``` +src/common/ +├── config/ +│ └── themes.ts # Shared themes +└── ... +``` + +**Pros:** +- ✅ In `common/` (shared code) +- ✅ Separated from community configs + +**Cons:** +- ⚠️ New directory structure +- ⚠️ Mixes config with common utilities + +## Recommendation: `src/config/shared/themes.ts` + +**Why:** +1. **Clear organization** - `shared/` folder makes intent obvious +2. **Extensible** - Can add other shared configs (e.g., `shared/defaultFidgets.ts`) +3. **Consistent** - Keeps configs in config directory +4. **Easy imports** - `import { themes } from '@/config/shared/themes'` + +## Implementation Approach + +### Option A: Single Shared File (All Themes) + +Store all theme variants in one shared file: + +```typescript +// src/config/shared/themes.ts + +export const sharedThemes = { + default: { /* ... */ }, + nounish: { /* ... */ }, + gradientAndWave: { /* ... */ }, + colorBlobs: { /* ... */ }, + floatingShapes: { /* ... */ }, + imageParallax: { /* ... */ }, + shootingStar: { /* ... */ }, + squareGrid: { /* ... */ }, + tesseractPattern: { /* ... */ }, + retro: { /* ... */ }, +}; +``` + +**Pros:** +- ✅ Single source of truth +- ✅ Easy to maintain +- ✅ All communities use same themes + +**Cons:** +- ⚠️ No community customization +- ⚠️ Can't override specific themes per community + +### Option B: Shared Base + Community Overrides + +Store base themes in shared file, allow community-specific overrides: + +```typescript +// src/config/shared/themes.ts + +export const baseThemes = { + default: { /* ... */ }, + nounish: { /* ... */ }, + // ... all themes +}; + +// src/config/nouns/nouns.theme.ts + +import { baseThemes } from '../../shared/themes'; + +export const nounsTheme = { + ...baseThemes, + // Override specific themes if needed + default: { + ...baseThemes.default, + properties: { + ...baseThemes.default.properties, + musicURL: "https://...", // Nouns-specific music + }, + }, +}; +``` + +**Pros:** +- ✅ Shared base themes +- ✅ Allows community customization +- ✅ Best of both worlds + +**Cons:** +- ⚠️ More complex +- ⚠️ Need merge logic + +### Option C: Shared Themes + Community Theme Config + +Store themes in shared file, reference from community config: + +```typescript +// src/config/shared/themes.ts + +export const themes = { + default: { /* ... */ }, + nounish: { /* ... */ }, + // ... all themes +}; + +// src/config/nouns/nouns.theme.ts + +import { themes } from '../../shared/themes'; + +// Just export shared themes (or override if needed) +export const nounsTheme = themes; +``` + +**Pros:** +- ✅ Simplest approach +- ✅ Single source of truth +- ✅ Easy to use + +**Cons:** +- ⚠️ No community customization (but maybe that's fine?) + +## Recommended: Option C (Simple Shared Reference) + +**Why:** +- Themes are visual templates, not community-specific +- Communities can customize via theme editor at runtime +- Keeps config simple and maintainable + +## Database Storage + +**Option 1: Store in Database as Shared Resource** + +```sql +CREATE TABLE shared_themes ( + id UUID PRIMARY KEY, + theme_id VARCHAR(50) UNIQUE, -- 'default', 'nounish', etc. + theme_config JSONB NOT NULL, + version INTEGER DEFAULT 1, + created_at TIMESTAMP DEFAULT NOW() +); +``` + +**Option 2: Store in Code (Recommended for Now)** + +Keep themes in code, reference from config: +- Themes are code/templates, not data +- Easier to version control +- Can move to DB later if needed + +## Size Impact + +**Before:** +- theme: 5.5 KB (66.7% of config) + +**After:** +- theme: Removed from config (0 KB) +- Config size: ~2.8 KB (down from 8.3 KB) +- **Total reduction: 90%** (from 29 KB to 2.8 KB) + +## Migration Steps + +1. **Create `src/config/shared/themes.ts`** + - Move theme definitions from nouns.theme.ts + - Export as `themes` object + +2. **Update community configs** + - Change `nouns.theme.ts` to import from shared + - Update other communities similarly + +3. **Update SystemConfig interface** + - Keep `theme: ThemeConfig` in interface + - But it now references shared themes + +4. **Update database schema** + - Remove `theme_config` column (or keep as reference) + - Or store theme reference IDs + +5. **Update build-time config** + - Themes loaded from shared file, not DB + - Or fetch from DB if storing there + +## Final Config Size + +After removing homePage, explorePage, and theme: + +| Section | Size | Percentage | +|---------|------|------------| +| community | 1.4 KB | 50% | +| navigation | 0.5 KB | 18% | +| assets | 0.3 KB | 11% | +| brand | 0.2 KB | 7% | +| fidgets | 0.2 KB | 7% | +| ui | 0.2 KB | 7% | +| **TOTAL** | **~2.8 KB** | **100%** | + +**Result:** Config is now tiny! Easily fits in env vars or generated file. + diff --git a/docs/SIMPLE_BUILD_TIME_CONFIG.md b/docs/SIMPLE_BUILD_TIME_CONFIG.md new file mode 100644 index 000000000..cd256b262 --- /dev/null +++ b/docs/SIMPLE_BUILD_TIME_CONFIG.md @@ -0,0 +1,424 @@ +# Simple Build-Time Config: Variable Assignment Approach + +## Overview + +Instead of generating multiple TypeScript files, we can simply fetch configs from the database at build time and assign them to variables. Much simpler! + +## Approach 1: Single Generated Config Module (Recommended) + +Create a single module that fetches and exports configs at build time. + +### Implementation + +```typescript +// src/config/generated.ts +// This file is generated at build time by next.config.mjs + +import { SystemConfig } from './systemConfig'; + +// Fetch from DB at build time and assign here +// If DB fetch fails, these will be undefined and we fall back to static configs + +export const generatedConfigs: Record = { + // These are populated at build time by next.config.mjs + nouns: undefined, // Will be replaced with DB config if available + clanker: undefined, + example: undefined, +}; +``` + +```javascript +// next.config.mjs + +import { createClient } from '@supabase/supabase-js'; +import { writeFile } from 'fs/promises'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +async function generateConfigModule() { + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + if (!supabaseUrl || !supabaseKey) { + console.warn('⚠️ Missing Supabase env vars, skipping config generation'); + return; + } + + const supabase = createClient(supabaseUrl, supabaseKey); + const communities = ['nouns', 'clanker', 'example']; + const configs = {}; + + console.log('📦 Fetching configs from database...'); + + for (const communityId of communities) { + try { + const { data, error } = await supabase + .rpc('get_active_community_config', { p_community_id: communityId }) + .single(); + + if (error || !data) { + console.warn(`⚠️ Failed to fetch ${communityId} config:`, error?.message); + configs[communityId] = null; // Will use static fallback + continue; + } + + configs[communityId] = data; + console.log(`✅ Fetched config for ${communityId}`); + } catch (error) { + console.error(`❌ Error fetching ${communityId} config:`, error); + configs[communityId] = null; + } + } + + // Generate the config module + const configModule = `// Auto-generated at build time - DO NOT EDIT +import { SystemConfig } from './systemConfig'; + +export const generatedConfigs: Record = ${JSON.stringify(configs, null, 2)}; +`; + + const filePath = join(__dirname, 'src', 'config', 'generated.ts'); + await writeFile(filePath, configModule, 'utf-8'); + console.log('✅ Generated config module'); +} + +// Generate config module before Next.js config +await generateConfigModule().catch(error => { + console.warn('⚠️ Config generation failed, will use static configs'); +}); + +// Continue with Next.js config... +import bundlerAnalyzer from "@next/bundle-analyzer"; +// ... rest of config +``` + +```typescript +// src/config/index.ts + +import { SystemConfig } from './systemConfig'; +import { nounsSystemConfig } from './nouns/index'; +import { exampleSystemConfig } from './example/index'; +import { clankerSystemConfig } from './clanker/index'; + +// Import generated configs (may not exist if generation failed) +let generatedConfigs: Record = {}; +try { + const generated = await import('./generated'); + generatedConfigs = generated.generatedConfigs || {}; +} catch { + // Generated file doesn't exist, use static configs +} + +// Static fallbacks +const STATIC_CONFIGS: Record = { + nouns: nounsSystemConfig, + example: exampleSystemConfig, + clanker: clankerSystemConfig as unknown as SystemConfig, +}; + +export const loadSystemConfig = (): SystemConfig => { + const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + const community = communityConfig.toLowerCase(); + + // Use generated config if available, otherwise fall back to static + const config = generatedConfigs[community] || STATIC_CONFIGS[community]; + + if (!config) { + console.warn( + `Invalid community configuration: "${communityConfig}". ` + + `Falling back to "nouns" configuration.` + ); + return STATIC_CONFIGS.nouns; + } + + return config; +}; +``` + +--- + +## Approach 2: Direct Assignment in `next.config.mjs` + +Even simpler - assign configs directly in `next.config.mjs` and export them. + +### Implementation + +```javascript +// next.config.mjs + +import { createClient } from '@supabase/supabase-js'; + +// Fetch configs at build time +let dbConfigs = {}; + +async function fetchConfigs() { + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + if (!supabaseUrl || !supabaseKey) { + console.warn('⚠️ Missing Supabase env vars, using static configs'); + return {}; + } + + const supabase = createClient(supabaseUrl, supabaseKey); + const community = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + + try { + const { data, error } = await supabase + .rpc('get_active_community_config', { p_community_id: community }) + .single(); + + if (error || !data) { + console.warn('⚠️ Failed to fetch config, using static'); + return {}; + } + + return { [community]: data }; + } catch (error) { + console.warn('⚠️ Error fetching config:', error.message); + return {}; + } +} + +// Fetch configs before creating Next.js config +dbConfigs = await fetchConfigs().catch(() => ({})); + +// Export configs for use in the app +// We'll make them available via environment variables or a module + +// Continue with Next.js config... +import bundlerAnalyzer from "@next/bundle-analyzer"; +// ... rest of config + +export default withBundleAnalyzer(nextConfig); +``` + +**Problem:** `next.config.mjs` exports Next.js config, not app config. We need a different approach. + +--- + +## Approach 3: Environment Variables (Simplest!) + +Set configs as environment variables at build time. + +### Implementation + +```javascript +// next.config.mjs + +import { createClient } from '@supabase/supabase-js'; + +async function setConfigEnvVars() { + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + if (!supabaseUrl || !supabaseKey) { + return; // Use static configs + } + + const supabase = createClient(supabaseUrl, supabaseKey); + const community = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + + try { + const { data, error } = await supabase + .rpc('get_active_community_config', { p_community_id: community }) + .single(); + + if (!error && data) { + // Set as environment variable (available at build time) + process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(data); + console.log('✅ Loaded config from database'); + } + } catch (error) { + console.warn('⚠️ Failed to load config from DB:', error.message); + } +} + +await setConfigEnvVars(); + +// Continue with Next.js config... +``` + +```typescript +// src/config/index.ts + +import { SystemConfig } from './systemConfig'; +import { nounsSystemConfig } from './nouns/index'; +// ... other static configs + +export const loadSystemConfig = (): SystemConfig => { + const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + + // Try to use build-time config from env var + const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; + if (buildTimeConfig) { + try { + const config = JSON.parse(buildTimeConfig) as SystemConfig; + if (config) return config; + } catch (error) { + console.warn('Failed to parse build-time config:', error); + } + } + + // Fall back to static configs + switch (communityConfig.toLowerCase()) { + case 'nouns': + return nounsSystemConfig; + case 'example': + return exampleSystemConfig; + case 'clanker': + return clankerSystemConfig as unknown as SystemConfig; + default: + return nounsSystemConfig; + } +}; +``` + +**Pros:** +- ✅ Simplest approach +- ✅ No file generation +- ✅ No new modules +- ✅ Works with Next.js env var system + +**Cons:** +- ⚠️ Large configs in env vars (but Next.js handles this fine) +- ⚠️ Need to parse JSON + +--- + +## Approach 4: Single Config Module with Direct Import (Best!) + +Create a single module that's generated at build time, but keep it simple. + +### Implementation + +```javascript +// next.config.mjs + +import { createClient } from '@supabase/supabase-js'; +import { writeFile } from 'fs/promises'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +async function generateConfig() { + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + if (!supabaseUrl || !supabaseKey) { + return; // Will use static configs + } + + const supabase = createClient(supabaseUrl, supabaseKey); + const community = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + + try { + const { data, error } = await supabase + .rpc('get_active_community_config', { p_community_id: community }) + .single(); + + if (error || !data) { + console.warn('⚠️ No config found in DB, using static'); + return; + } + + // Generate simple module with just the config + const moduleContent = `// Auto-generated at build time +import { SystemConfig } from './systemConfig'; + +export const dbConfig: SystemConfig | null = ${JSON.stringify(data, null, 2)} as SystemConfig; +`; + + const filePath = join(__dirname, 'src', 'config', 'db-config.ts'); + await writeFile(filePath, moduleContent, 'utf-8'); + console.log('✅ Generated config from database'); + } catch (error) { + console.warn('⚠️ Error generating config:', error.message); + } +} + +await generateConfig(); + +// Continue with Next.js config... +``` + +```typescript +// src/config/index.ts + +import { SystemConfig } from './systemConfig'; +import { nounsSystemConfig } from './nouns/index'; +import { exampleSystemConfig } from './example/index'; +import { clankerSystemConfig } from './clanker/index'; + +// Try to import DB config (may not exist) +let dbConfig: SystemConfig | null = null; +try { + const dbModule = require('./db-config'); + dbConfig = dbModule.dbConfig; +} catch { + // File doesn't exist, will use static +} + +// Static fallbacks +const STATIC_CONFIGS: Record = { + nouns: nounsSystemConfig, + example: exampleSystemConfig, + clanker: clankerSystemConfig as unknown as SystemConfig, +}; + +export const loadSystemConfig = (): SystemConfig => { + const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + + // Use DB config if available and matches current community + if (dbConfig) { + return dbConfig; + } + + // Fall back to static configs + const config = STATIC_CONFIGS[communityConfig.toLowerCase()]; + return config || STATIC_CONFIGS.nouns; +}; +``` + +**Pros:** +- ✅ Single file generation (simple) +- ✅ Direct variable assignment +- ✅ Type-safe +- ✅ Easy to understand +- ✅ Clean fallback + +--- + +## Comparison + +| Approach | Files Generated | Complexity | Type Safety | Recommended | +|----------|----------------|------------|-------------|-------------| +| **Single Module** | 1 file | Low | ✅ Yes | ⭐⭐⭐⭐⭐ | +| **Env Variables** | 0 files | Very Low | ⚠️ Manual | ⭐⭐⭐⭐ | +| **Multiple Files** | Many files | High | ✅ Yes | ⭐⭐⭐ | + +--- + +## Recommended: Approach 4 (Single Config Module) + +**Why:** +- ✅ Generates only ONE file (`db-config.ts`) +- ✅ Simple variable assignment (`dbConfig`) +- ✅ Type-safe (imports `SystemConfig`) +- ✅ Easy to understand +- ✅ Clean fallback to static configs +- ✅ No complex file structure + +**Implementation:** + +1. **`next.config.mjs`** - Fetches config, generates single `db-config.ts` file +2. **`src/config/index.ts`** - Imports `dbConfig`, falls back to static if not available +3. **That's it!** - Much simpler than generating multiple files + +This gives you all the benefits with minimal complexity! + diff --git a/docs/SPACE_REFERENCE_APPROACH.md b/docs/SPACE_REFERENCE_APPROACH.md new file mode 100644 index 000000000..873f1cafc --- /dev/null +++ b/docs/SPACE_REFERENCE_APPROACH.md @@ -0,0 +1,202 @@ +# Space Reference Approach for Page Configs + +## The Idea + +Instead of storing `homePage` and `explorePage` configs (71% of total size) directly in `community_configs`, store them as Spaces and reference them by ID. + +## Current Problem + +- `homePage`: 19.2 KB (66.5% of config) +- `explorePage`: 1.4 KB (4.8% of config) +- **Combined: 20.6 KB (71.3% of total)** + +## Proposed Solution + +### Option A: Store as Spaces in Supabase Storage + +**How it works:** +1. Create Spaces for `homePage` and `explorePage` in Supabase Storage +2. Store Space IDs in `community_configs`: + ```json + { + "homePageSpaceId": "uuid-here", + "explorePageSpaceId": "uuid-here" + } + ``` +3. Fetch Spaces at build time along with config + +**Pros:** +- ✅ Uses existing Space infrastructure +- ✅ Dramatically reduces config size (~71% reduction) +- ✅ Spaces can be edited independently +- ✅ Reuses Space storage/retrieval logic + +**Cons:** +- ⚠️ Requires fetching Spaces at build time (additional DB calls) +- ⚠️ Spaces stored in Storage, not database (need to check how they're accessed) +- ⚠️ More complex build-time logic + +### Option B: Store in Separate Database Table + +**How it works:** +1. Create `community_page_configs` table: + ```sql + CREATE TABLE community_page_configs ( + id UUID PRIMARY KEY, + community_id VARCHAR(50), + page_type VARCHAR(20), -- 'homePage' or 'explorePage' + config JSONB NOT NULL, + version INTEGER DEFAULT 1, + created_at TIMESTAMP DEFAULT NOW() + ); + ``` +2. Store page configs there +3. Reference by ID in `community_configs`: + ```json + { + "homePageConfigId": "uuid-here", + "explorePageConfigId": "uuid-here" + } + ``` +4. Fetch at build time with a JOIN or separate query + +**Pros:** +- ✅ Database-backed (easier to query/manage) +- ✅ Version history possible +- ✅ Dramatically reduces config size +- ✅ Can use JOINs for efficient fetching + +**Cons:** +- ⚠️ Requires new table +- ⚠️ Additional build-time query + +### Option C: Store in Same Table, Separate Columns (Simplest) + +**How it works:** +1. Keep `home_page_config` and `explore_page_config` columns +2. But fetch them separately at build time +3. Only include IDs in the main config JSONB + +**Pros:** +- ✅ Simplest implementation +- ✅ No schema changes needed +- ✅ Still reduces size if we only store IDs + +**Cons:** +- ⚠️ Still stores full configs in database (just separately) +- ⚠️ Doesn't solve the E2BIG issue if we're putting them in env vars + +## Size Reduction Analysis + +### Current: +```json +{ + "homePage": { /* 19.2 KB */ }, + "explorePage": { /* 1.4 KB */ } +} +``` +**Total: 20.6 KB** + +### With References: +```json +{ + "homePageSpaceId": "550e8400-e29b-41d4-a716-446655440000", // ~36 bytes + "explorePageSpaceId": "550e8400-e29b-41d4-a716-446655440001" // ~36 bytes +} +``` +**Total: ~72 bytes** + +**Reduction: 99.6%** (from 20.6 KB to 72 bytes) + +### New Total Config Size: +- Current: ~29 KB +- With references: ~8.4 KB (29 KB - 20.6 KB + 72 bytes) +- **Reduction: 71%** + +## Implementation Considerations + +### Build-Time Fetching + +```javascript +// next.config.mjs + +async function generateConfigFile() { + // Fetch main config + const { data: config } = await supabase + .rpc('get_active_community_config', { p_community_id: community }) + .single(); + + // Fetch page configs separately + const { data: homePage } = await supabase + .from('community_page_configs') + .select('config') + .eq('community_id', community) + .eq('page_type', 'homePage') + .single(); + + const { data: explorePage } = await supabase + .from('community_page_configs') + .select('config') + .eq('community_id', community) + .eq('page_type', 'explorePage') + .single(); + + // Combine + const fullConfig = { + ...config, + homePage: homePage?.config, + explorePage: explorePage?.config, + }; + + // Generate file (now much smaller!) + await writeFile('src/config/db-config.ts', ...); +} +``` + +### Database Function Update + +```sql +-- Updated function that excludes page configs +CREATE OR REPLACE FUNCTION get_active_community_config(...) +RETURNS JSONB AS $$ + SELECT jsonb_build_object( + 'brand', brand_config, + 'assets', assets_config, + 'theme', theme_config, + 'community', community_config, + 'fidgets', fidgets_config, + 'homePageSpaceId', home_page_space_id, -- Just the ID + 'explorePageSpaceId', explore_page_space_id, -- Just the ID + 'navigation', navigation_config, + 'ui', ui_config + ) + FROM community_configs + WHERE ... +$$; +``` + +## Recommendation + +**Option B (Separate Table)** because: +1. ✅ Database-backed (easier to manage) +2. ✅ Can add versioning/history +3. ✅ Efficient queries +4. ✅ Dramatically reduces config size +5. ✅ Clear separation of concerns + +## Migration Path + +1. Create `community_page_configs` table +2. Migrate existing `homePage` and `explorePage` configs to new table +3. Update `community_configs` to store IDs instead +4. Update build-time fetching to include page configs +5. Update `get_active_community_config` function + +## Benefits + +- ✅ **Solves E2BIG** - Config now ~8.4 KB (well under limits) +- ✅ **Better organization** - Page configs separate from community config +- ✅ **Easier editing** - Admins can edit pages independently +- ✅ **Versioning** - Can version page configs separately +- ✅ **Reusability** - Page configs could be shared/referenced + diff --git a/docs/UPDATED_IMPLEMENTATION_SUMMARY.md b/docs/UPDATED_IMPLEMENTATION_SUMMARY.md new file mode 100644 index 000000000..c62f9d0fd --- /dev/null +++ b/docs/UPDATED_IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,147 @@ +# Updated Implementation Plan Summary + +## Key Architectural Changes + +### 1. **Navigation-Space Reference** +- `homePage` and `explorePage` removed from `community_configs` +- Navigation items reference Spaces via `spaceId` +- Pages fetched from Spaces at build time +- **Size reduction: 71%** (removes 20.6 KB) + +### 2. **Shared Themes** +- Themes moved to `src/config/shared/themes.ts` +- All communities use same shared themes +- Themes removed from `community_configs` +- **Size reduction: 66%** (removes 5.5 KB) + +### 3. **File-Based Config (Not Env Var)** +- Config generated as TypeScript file (`src/config/db-config.ts`) +- Avoids E2BIG error (env var size limits) +- No size restrictions + +## Final Config Structure + +### In Database (`community_configs`): +```json +{ + "brand_config": { /* 0.2 KB */ }, + "assets_config": { /* 0.3 KB */ }, + "community_config": { /* 1.4 KB */ }, + "fidgets_config": { /* 0.2 KB */ }, + "navigation_config": { /* 0.5 KB - includes spaceId references */ }, + "ui_config": { /* 0.2 KB */ } + // NO theme_config (in shared file) + // NO home_page_config (in Spaces) + // NO explore_page_config (in Spaces) +} +``` + +**Total: ~2.8 KB** (down from ~29 KB - **90% reduction!**) + +### In Code (`src/config/shared/themes.ts`): +```typescript +export const themes = { + default: { /* ... */ }, + nounish: { /* ... */ }, + // ... all 10 themes +}; +``` + +### In Spaces (via navigation): +- `homePage` → Space referenced by nav item `spaceId` +- `explorePage` → Space referenced by nav item `spaceId` +- Stored in `spaceRegistrations` with `spaceType = 'navPage'` +- Config stored in Supabase Storage + +## Updated Database Schema + +### `community_configs` Table: +```sql +CREATE TABLE community_configs ( + id UUID PRIMARY KEY, + community_id VARCHAR(50) UNIQUE, + brand_config JSONB, -- ✅ Kept + assets_config JSONB, -- ✅ Kept + community_config JSONB, -- ✅ Kept + fidgets_config JSONB, -- ✅ Kept + navigation_config JSONB, -- ✅ Kept (now includes spaceId) + ui_config JSONB, -- ✅ Kept + -- ❌ REMOVED: theme_config (in shared file) + -- ❌ REMOVED: home_page_config (in Spaces) + -- ❌ REMOVED: explore_page_config (in Spaces) +); +``` + +### `spaceRegistrations` Table: +```sql +-- Add navPage spaceType +ALTER TABLE spaceRegistrations + ADD CONSTRAINT valid_space_type CHECK ( + "spaceType" IN ('profile', 'token', 'proposal', 'channel', 'navPage') + ); +``` + +## Updated Build Process + +```javascript +// next.config.mjs + +1. Fetch main config from DB (small - ~2.8 KB) +2. Import shared themes from code +3. Extract spaceIds from navigation items +4. Fetch Spaces for nav items +5. Convert Spaces to page configs +6. Combine: config + themes + pages +7. Generate TypeScript file +``` + +## Updated Config Loader + +```typescript +// src/config/index.ts + +1. Try to import db-config.ts (generated file) +2. If exists: + - Use DB config + - Add shared themes + - Map pages['home'] → homePage + - Map pages['explore'] → explorePage +3. If not exists: + - Fall back to static configs +``` + +## Size Comparison + +| Stage | Config Size | Reduction | +|-------|-------------|-----------| +| **Original** | ~29 KB | - | +| **After removing pages** | ~8.3 KB | 71% | +| **After removing themes** | **~2.8 KB** | **90%** | + +## Migration Impact + +### Phase 1 (Database Schema): +- ✅ Create `community_configs` (without page/theme columns) +- ✅ Add `navPage` spaceType +- ✅ Seed configs (without themes/pages) + +### Phase 2 (Config Loading): +- ✅ Create `src/config/shared/themes.ts` +- ✅ Update community configs to import shared themes +- ✅ Fetch Spaces for nav items at build time +- ✅ Generate TypeScript file (not env var) + +### Phase 3+ (Admin/UI): +- ✅ Admin can edit config (smaller now) +- ✅ Admin can edit nav page Spaces +- ✅ Themes edited in code (shared file) + +## Benefits Summary + +✅ **Solves E2BIG** - Config now ~2.8 KB (well under limits) +✅ **Unified architecture** - Pages are Spaces +✅ **Shared themes** - Single source of truth +✅ **Navigation as source of truth** - Nav defines pages +✅ **Flexible** - Any nav item can reference a Space +✅ **Maintainable** - Clear separation of concerns + diff --git a/next.config.mjs b/next.config.mjs index 34387c9d0..25351b687 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,9 +1,48 @@ import bundlerAnalyzer from "@next/bundle-analyzer"; import packageInfo from "./package.json" with { type: "json" }; import { createRequire } from "node:module"; +import { createClient } from '@supabase/supabase-js'; const require = createRequire(import.meta.url); +// Load config from database at build time and set as environment variable +// Config is now ~2.8 KB (down from ~29 KB), so env var approach works fine +async function loadConfigFromDB() { + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + + if (!supabaseUrl || !supabaseKey) { + console.log('ℹ️ Using static configs (no DB credentials)'); + return; + } + + const supabase = createClient(supabaseUrl, supabaseKey); + const community = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + + try { + const { data, error } = await supabase + .rpc('get_active_community_config', { p_community_id: community }) + .single(); + + if (error || !data) { + console.log('ℹ️ Using static configs (no DB config found)'); + if (error) { + console.log(` Error: ${error.message}`); + } + return; + } + + // Store config in environment variable (now small enough at ~2.8 KB) + process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(data); + console.log('✅ Loaded config from database'); + } catch (error) { + console.warn('⚠️ Error loading config from DB:', error.message); + } +} + +// Load config before Next.js config is created +await loadConfigFromDB(); + const withBundleAnalyzer = bundlerAnalyzer({ enabled: process.env.ANALYZE === "true", }); diff --git a/scripts/analyze-config-size.ts b/scripts/analyze-config-size.ts new file mode 100644 index 000000000..3b5976014 --- /dev/null +++ b/scripts/analyze-config-size.ts @@ -0,0 +1,83 @@ +#!/usr/bin/env tsx +/** + * Analyze config sizes to identify largest sections + */ + +import { nounsSystemConfig } from '../src/config/nouns/index'; + +function getSize(obj: any): number { + return JSON.stringify(obj).length; +} + +function formatSize(bytes: number): string { + if (bytes < 1024) return `${bytes} bytes`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; +} + +const config = nounsSystemConfig; + +const sizes = { + brand: getSize(config.brand), + assets: getSize(config.assets), + theme: getSize(config.theme), + community: getSize(config.community), + fidgets: getSize(config.fidgets), + homePage: getSize(config.homePage), + explorePage: getSize(config.explorePage), + navigation: getSize(config.navigation || {}), + ui: getSize(config.ui || {}), +}; + +const total = Object.values(sizes).reduce((a, b) => a + b, 0); + +console.log('\n📊 Config Size Analysis\n'); +console.log('Section sizes:'); +console.log('─'.repeat(50)); + +const sorted = Object.entries(sizes) + .sort(([, a], [, b]) => b - a) + .map(([key, size]) => ({ + section: key, + size, + percentage: ((size / total) * 100).toFixed(1), + formatted: formatSize(size), + })); + +sorted.forEach(({ section, formatted, percentage }) => { + console.log(`${section.padEnd(15)} ${formatted.padStart(10)} (${percentage}%)`); +}); + +console.log('─'.repeat(50)); +console.log(`Total: ${formatSize(total).padStart(10)}`); + +// Analyze theme config in detail +console.log('\n🎨 Theme Config Breakdown:\n'); +const themeKeys = Object.keys(config.theme); +themeKeys.forEach((key) => { + const themeSize = getSize(config.theme[key as keyof typeof config.theme]); + console.log(` ${key.padEnd(20)} ${formatSize(themeSize)}`); +}); + +// Analyze homePage tabs +console.log('\n🏠 Home Page Tabs:\n'); +const homeTabs = Object.keys(config.homePage.tabs); +homeTabs.forEach((tab) => { + const tabSize = getSize(config.homePage.tabs[tab]); + console.log(` ${tab.padEnd(20)} ${formatSize(tabSize)}`); +}); + +// Check for large strings (like HTML) +console.log('\n🔍 Large String Analysis:\n'); +function findLargeStrings(obj: any, path = '', threshold = 1000): void { + if (typeof obj === 'string' && obj.length > threshold) { + console.log(` ${path}: ${formatSize(obj.length)}`); + } else if (typeof obj === 'object' && obj !== null) { + Object.keys(obj).forEach((key) => { + findLargeStrings(obj[key], path ? `${path}.${key}` : key, threshold); + }); + } +} + +findLargeStrings(config, '', 1000); + diff --git a/scripts/seed-community-configs.ts b/scripts/seed-community-configs.ts new file mode 100644 index 000000000..f72453f63 --- /dev/null +++ b/scripts/seed-community-configs.ts @@ -0,0 +1,224 @@ +#!/usr/bin/env tsx +/** + * OPTIONAL: Seed script to populate community_configs table from static configs + * + * NOTE: This script is now OPTIONAL. The database is seeded via SQL in: + * - supabase/seed.sql (runs automatically on `supabase db reset`) + * + * Use this script only if you need to: + * - Update configs without resetting the database + * - Seed configs in a production environment + * - Migrate from old schema (with themes/pages) to new schema + * + * Usage: + * tsx scripts/seed-community-configs.ts + * + * Requires: + * - NEXT_PUBLIC_SUPABASE_URL + * - SUPABASE_SERVICE_ROLE_KEY + * + * IMPORTANT: This script still includes themes/pages for backward compatibility. + * The new architecture stores themes in shared file and pages as Spaces. + * Update this script if you need to exclude themes/pages. + */ + +import { createClient } from '@supabase/supabase-js'; +// Import configs without assets (which import SVGs) +import { nounsBrand } from '../src/config/nouns/nouns.brand'; +import { nounsTheme } from '../src/config/nouns/nouns.theme'; +import { nounsCommunity } from '../src/config/nouns/nouns.community'; +import { nounsFidgets } from '../src/config/nouns/nouns.fidgets'; +import { nounsHomePage } from '../src/config/nouns/nouns.home'; +import { nounsExplorePage } from '../src/config/nouns/nouns.explore'; +import { nounsNavigation } from '../src/config/nouns/nouns.navigation'; +import { nounsUI } from '../src/config/nouns/nouns.ui'; + +import { exampleBrand } from '../src/config/example/example.brand'; +import { exampleTheme } from '../src/config/example/example.theme'; +import { exampleCommunity } from '../src/config/example/example.community'; +import { exampleFidgets } from '../src/config/example/example.fidgets'; +import { exampleHomePage } from '../src/config/example/example.home'; +import { exampleExplorePage } from '../src/config/example/example.explore'; +import { exampleUI } from '../src/config/example/example.ui'; +// Example doesn't have navigation config - use null +const exampleNavigation = null; + +import { clankerBrand } from '../src/config/clanker/clanker.brand'; +import { clankerTheme } from '../src/config/clanker/clanker.theme'; +import { clankerCommunity } from '../src/config/clanker/clanker.community'; +import { clankerFidgets } from '../src/config/clanker/clanker.fidgets'; +import { clankerHomePage } from '../src/config/clanker/clanker.home'; +import { clankerExplorePage } from '../src/config/clanker/clanker.explore'; +import { clankerNavigation } from '../src/config/clanker/clanker.navigation'; +import { clankerUI } from '../src/config/clanker/clanker.ui'; + +// Manually construct assets configs to avoid SVG imports +// These use the actual public paths that match what Next.js resolves +// For now, we'll use placeholder paths that match the static config structure +// In production, these will be replaced with actual uploaded asset paths +const nounsAssets = { + logos: { + main: "/images/nouns/logo.svg", // Matches static config structure + icon: "/images/nouns/noggles.svg", // Matches static config structure + favicon: "/images/favicon.ico", + appleTouch: "/images/apple-touch-icon.png", + og: "/images/nouns/og.svg", + splash: "/images/nouns/splash.svg", + }, +}; + +const exampleAssets = { + logos: { + main: "/images/example_logo.png", + icon: "/images/example_icon.png", + favicon: "/images/example_favicon.ico", + appleTouch: "/images/example_apple_touch.png", + og: "/images/example_og.png", + splash: "/images/example_splash.png", + }, +}; + +const clankerAssets = { + logos: { + main: "/images/clanker/logo.svg", // Will be resolved from clanker assets + icon: "/images/clanker/logo.svg", + favicon: "/images/clanker/favicon.ico", + appleTouch: "/images/clanker/apple.png", + og: "/images/clanker/og.jpg", + splash: "/images/clanker/og.jpg", + }, +}; + +// Construct system configs manually +const nounsSystemConfig = { + brand: nounsBrand, + assets: nounsAssets, + theme: nounsTheme, + community: nounsCommunity, + fidgets: nounsFidgets, + homePage: nounsHomePage, + explorePage: nounsExplorePage, + navigation: nounsNavigation, + ui: nounsUI, +}; + +const exampleSystemConfig = { + brand: exampleBrand, + assets: exampleAssets, + theme: exampleTheme, + community: exampleCommunity, + fidgets: exampleFidgets, + homePage: exampleHomePage, + explorePage: exampleExplorePage, + navigation: exampleNavigation, + ui: exampleUI, +}; + +const clankerSystemConfig = { + brand: clankerBrand, + assets: clankerAssets, + theme: clankerTheme, + community: clankerCommunity, + fidgets: clankerFidgets, + homePage: clankerHomePage, + explorePage: clankerExplorePage, + navigation: clankerNavigation, + ui: clankerUI, +}; + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; +const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + +if (!supabaseUrl || !supabaseKey) { + console.error('❌ Missing required environment variables:'); + console.error(' NEXT_PUBLIC_SUPABASE_URL'); + console.error(' SUPABASE_SERVICE_ROLE_KEY'); + process.exit(1); +} + +const supabase = createClient(supabaseUrl, supabaseKey); + +async function seedConfig(communityId: string, config: any) { + console.log(`\n📦 Seeding config for: ${communityId}`); + + // Transform config to match database schema + const dbConfig = { + community_id: communityId, + version: 1, + is_active: true, + is_published: true, + brand_config: config.brand, + assets_config: config.assets, + theme_config: config.theme, + community_config: config.community, + fidgets_config: config.fidgets, + home_page_config: config.homePage, + explore_page_config: config.explorePage, + navigation_config: config.navigation || null, + ui_config: config.ui || null, + }; + + // Upsert (insert or update) + const { data, error } = await supabase + .from('community_configs') + .upsert(dbConfig, { + onConflict: 'community_id', + ignoreDuplicates: false, + }) + .select() + .single(); + + if (error) { + console.error(`❌ Error seeding ${communityId}:`, error.message); + return false; + } + + console.log(`✅ Seeded config for ${communityId} (id: ${data.id})`); + return true; +} + +async function main() { + console.log('🚀 Starting community config seeding...\n'); + + const results = await Promise.allSettled([ + seedConfig('nouns', nounsSystemConfig), + seedConfig('example', exampleSystemConfig), + seedConfig('clanker', clankerSystemConfig), + ]); + + const successCount = results.filter(r => r.status === 'fulfilled' && r.value).length; + const failCount = results.length - successCount; + + console.log(`\n📊 Results: ${successCount} succeeded, ${failCount} failed`); + + if (failCount > 0) { + console.error('\n❌ Some configs failed to seed. Check errors above.'); + process.exit(1); + } + + console.log('\n✅ All configs seeded successfully!'); + + // Test the function + console.log('\n🧪 Testing get_active_community_config function...'); + const { data: testConfig, error: testError } = await supabase + .rpc('get_active_community_config', { p_community_id: 'nouns' }) + .single(); + + if (testError) { + console.error('❌ Function test failed:', testError.message); + process.exit(1); + } + + if (testConfig && testConfig.brand) { + console.log(`✅ Function works! Retrieved config for: ${testConfig.brand.displayName}`); + } else { + console.error('❌ Function returned invalid config'); + process.exit(1); + } +} + +main().catch((error) => { + console.error('❌ Fatal error:', error); + process.exit(1); +}); + diff --git a/src/config/index.ts b/src/config/index.ts index 00e9757ba..b33231ef2 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -12,6 +12,26 @@ export const loadSystemConfig = (): SystemConfig => { // Get the community configuration from environment variable const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + // Try build-time config from database (stored in env var at build time) + const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; + if (buildTimeConfig) { + try { + const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; + // Validate config structure + if (dbConfig && dbConfig.brand && dbConfig.assets) { + console.log('✅ Using config from database'); + return dbConfig; + } else { + console.warn('⚠️ Invalid config structure from DB, falling back to static'); + } + } catch (error) { + console.warn('⚠️ Failed to parse build-time config, falling back to static:', error); + } + } + + // Fall back to static configs + console.log('ℹ️ Using static configs'); + // Validate the configuration if (!isValidCommunityConfig(communityConfig)) { console.warn( diff --git a/supabase/migrations/20251129172847_create_community_configs.sql b/supabase/migrations/20251129172847_create_community_configs.sql new file mode 100644 index 000000000..6473e2f0d --- /dev/null +++ b/supabase/migrations/20251129172847_create_community_configs.sql @@ -0,0 +1,57 @@ +-- Create community_configs table (updated architecture: no themes/pages) +CREATE TABLE IF NOT EXISTS "public"."community_configs" ( + "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "community_id" VARCHAR(50) NOT NULL UNIQUE, + "is_active" BOOLEAN DEFAULT true, + "version" INTEGER DEFAULT 1, + "created_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), + "updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), + "brand_config" JSONB NOT NULL, + "assets_config" JSONB NOT NULL, + "community_config" JSONB NOT NULL, + "fidgets_config" JSONB NOT NULL, + "navigation_config" JSONB, + "ui_config" JSONB, + "is_published" BOOLEAN DEFAULT true +); + +-- Create indexes +CREATE INDEX IF NOT EXISTS "idx_community_configs_community_id" ON "public"."community_configs"("community_id"); +CREATE INDEX IF NOT EXISTS "idx_community_configs_active" ON "public"."community_configs"("is_active") WHERE "is_active" = true; + +-- Create function to get active community config (excludes themes/pages) +CREATE OR REPLACE FUNCTION "public"."get_active_community_config"( + p_community_id VARCHAR(50) +) +RETURNS JSONB +LANGUAGE plpgsql +SECURITY DEFINER +AS $$ +DECLARE + v_config JSONB; +BEGIN + SELECT jsonb_build_object( + 'brand', "brand_config", + 'assets', "assets_config", + 'community', "community_config", + 'fidgets', "fidgets_config", + 'navigation', "navigation_config", + 'ui', "ui_config" + ) + INTO v_config + FROM "public"."community_configs" + WHERE "community_id" = p_community_id + AND "is_active" = true + AND "is_published" = true + ORDER BY "version" DESC + LIMIT 1; + + RETURN v_config; +END; +$$; + +-- Grant permissions +GRANT SELECT ON "public"."community_configs" TO authenticated; +GRANT SELECT ON "public"."community_configs" TO anon; +GRANT EXECUTE ON FUNCTION "public"."get_active_community_config" TO authenticated; +GRANT EXECUTE ON FUNCTION "public"."get_active_community_config" TO anon; diff --git a/supabase/migrations/20251129172848_add_navpage_space_type.sql b/supabase/migrations/20251129172848_add_navpage_space_type.sql new file mode 100644 index 000000000..990d5e165 --- /dev/null +++ b/supabase/migrations/20251129172848_add_navpage_space_type.sql @@ -0,0 +1,15 @@ +-- Add navPage spaceType to spaceRegistrations +-- This allows navigation items to reference Spaces for pages like homePage/explorePage + +-- Update the spaceType constraint to include 'navPage' +ALTER TABLE "public"."spaceRegistrations" +DROP CONSTRAINT IF EXISTS "spaceRegistrations_spaceType_check"; + +ALTER TABLE "public"."spaceRegistrations" +ADD CONSTRAINT "spaceRegistrations_spaceType_check" +CHECK ("spaceType" IN ('profile', 'token', 'proposal', 'channel', 'navPage')); + +-- Add comment explaining navPage usage +COMMENT ON COLUMN "public"."spaceRegistrations"."spaceType" IS +'Type of space: profile (user profile), token (token page), proposal (governance proposal), channel (Farcaster channel), navPage (navigation page like homePage/explorePage)'; + diff --git a/supabase/seed.sql b/supabase/seed.sql index 31ce8322d..bcb06376b 100644 --- a/supabase/seed.sql +++ b/supabase/seed.sql @@ -6,3 +6,104 @@ VALUES ('private', 'private', '2024-05-21 23:43:16.762796+00', '2024-05-21 23:43:16.762796+00', true, false), ('spaces', 'spaces', '2024-05-21 23:43:16.762796+00', '2024-05-21 23:43:16.762796+00', true, false) ON CONFLICT ("id") DO NOTHING; + +-- Seed community configs (without themes/pages - those are in shared file and Spaces) +-- Note: Themes are in src/config/shared/themes.ts +-- Note: Pages (homePage/explorePage) are stored as Spaces with spaceType='navPage' + +-- Nouns community config +INSERT INTO "public"."community_configs" ( + "community_id", + "version", + "is_active", + "is_published", + "brand_config", + "assets_config", + "community_config", + "fidgets_config", + "navigation_config", + "ui_config" +) VALUES ( + 'nouns', + 1, + true, + true, + '{"name": "Nouns", "displayName": "Nouns", "tagline": "A space for Nouns", "description": "The social hub for Nouns", "miniAppTags": ["nouns", "client", "customizable", "social", "link"]}'::jsonb, + '{"logos": {"main": "/images/nouns/logo.svg", "icon": "/images/nouns/noggles.svg", "favicon": "/images/favicon.ico", "appleTouch": "/images/apple-touch-icon.png", "og": "/images/nouns/og.svg", "splash": "/images/nouns/splash.svg"}}'::jsonb, + '{"type": "nouns", "urls": {"website": "https://nouns.com", "discord": "https://discord.gg/nouns", "twitter": "https://twitter.com/nounsdao", "github": "https://github.com/nounsDAO", "forum": "https://discourse.nouns.wtf"}, "social": {"farcaster": "nouns", "discord": "nouns", "twitter": "nounsdao"}, "governance": {"proposals": "https://nouns.wtf/vote", "delegates": "https://nouns.wtf/delegates", "treasury": "https://nouns.wtf/treasury"}, "tokens": {"erc20Tokens": [{"address": "0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab", "symbol": "$SPACE", "decimals": 18, "network": "base"}], "nftTokens": [{"address": "0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03", "symbol": "Nouns", "type": "erc721", "network": "eth"}]}, "contracts": {"nouns": "0x9c8ff314c9bc7f6e59a9d9225fb22946427edc03", "auctionHouse": "0x830bd73e4184cef73443c15111a1df14e495c706", "space": "0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab", "nogs": "0xD094D5D45c06c1581f5f429462eE7cCe72215616"}}'::jsonb, + '{"enabled": ["nounsHome", "governance", "feed", "cast", "gallery", "text", "iframe", "links", "video", "channel", "profile", "snapshot", "swap", "rss", "market", "portfolio", "chat", "builderScore", "framesV2"], "disabled": ["example"]}'::jsonb, + '{"logoTooltip": {"text": "wtf is nouns?", "href": "https://nouns.wtf"}, "items": [{"id": "home", "label": "Home", "href": "/home", "icon": "home"}, {"id": "explore", "label": "Explore", "href": "/explore", "icon": "explore"}, {"id": "notifications", "label": "Notifications", "href": "/notifications", "icon": "notifications", "requiresAuth": true}, {"id": "space-token", "label": "$SPACE", "href": "/t/base/0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab/Profile", "icon": "space"}], "showMusicPlayer": true, "showSocials": true}'::jsonb, + '{"primaryColor": "rgb(37, 99, 235)", "primaryHoverColor": "rgb(29, 78, 216)", "primaryActiveColor": "rgb(30, 64, 175)", "castButton": {"backgroundColor": "rgb(37, 99, 235)", "hoverColor": "rgb(29, 78, 216)", "activeColor": "rgb(30, 64, 175)"}}'::jsonb +) ON CONFLICT ("community_id") DO UPDATE SET + "brand_config" = EXCLUDED."brand_config", + "assets_config" = EXCLUDED."assets_config", + "community_config" = EXCLUDED."community_config", + "fidgets_config" = EXCLUDED."fidgets_config", + "navigation_config" = EXCLUDED."navigation_config", + "ui_config" = EXCLUDED."ui_config", + "updated_at" = now(); + +-- Example community config +INSERT INTO "public"."community_configs" ( + "community_id", + "version", + "is_active", + "is_published", + "brand_config", + "assets_config", + "community_config", + "fidgets_config", + "navigation_config", + "ui_config" +) VALUES ( + 'example', + 1, + true, + true, + '{"name": "Example", "displayName": "Example Community", "tagline": "A space for Example Community", "description": "The social hub for Example Community", "miniAppTags": []}'::jsonb, + '{"logos": {"main": "/images/example_logo.png", "icon": "/images/example_icon.png", "favicon": "/images/example_favicon.ico", "appleTouch": "/images/example_apple_touch.png", "og": "/images/example_og.png", "splash": "/images/example_splash.png"}}'::jsonb, + '{"type": "example", "urls": {"website": "https://example.com", "discord": "https://discord.gg/example", "twitter": "https://twitter.com/example", "github": "https://github.com/example", "forum": "https://forum.example.com"}, "social": {"farcaster": "example", "discord": "example", "twitter": "example"}, "governance": {"proposals": "https://governance.example.com/proposals", "delegates": "https://governance.example.com/delegates", "treasury": "https://governance.example.com/treasury"}, "tokens": {"erc20Tokens": [{"address": "0x1234567890123456789012345678901234567890", "symbol": "$EXAMPLE", "decimals": 18, "network": "mainnet"}], "nftTokens": [{"address": "0x1234567890123456789012345678901234567890", "symbol": "Example NFT", "type": "erc721", "network": "eth"}]}, "contracts": {"nouns": "0x1234567890123456789012345678901234567890", "auctionHouse": "0x1234567890123456789012345678901234567890", "space": "0x1234567890123456789012345678901234567890", "nogs": "0x1234567890123456789012345678901234567890"}}'::jsonb, + '{"enabled": ["feed", "cast", "gallery", "text", "iframe", "links", "video", "channel", "profile", "swap", "rss", "market", "portfolio", "chat", "framesV2"], "disabled": ["example", "nounsHome", "governance", "snapshot", "builderScore"]}'::jsonb, + NULL, + '{"primaryColor": "rgb(37, 99, 235)", "primaryHoverColor": "rgb(29, 78, 216)", "primaryActiveColor": "rgb(30, 64, 175)", "castButton": {"backgroundColor": "rgb(37, 99, 235)", "hoverColor": "rgb(29, 78, 216)", "activeColor": "rgb(30, 64, 175)"}}'::jsonb +) ON CONFLICT ("community_id") DO UPDATE SET + "brand_config" = EXCLUDED."brand_config", + "assets_config" = EXCLUDED."assets_config", + "community_config" = EXCLUDED."community_config", + "fidgets_config" = EXCLUDED."fidgets_config", + "navigation_config" = EXCLUDED."navigation_config", + "ui_config" = EXCLUDED."ui_config", + "updated_at" = now(); + +-- Clanker community config +INSERT INTO "public"."community_configs" ( + "community_id", + "version", + "is_active", + "is_published", + "brand_config", + "assets_config", + "community_config", + "fidgets_config", + "navigation_config", + "ui_config" +) VALUES ( + 'clanker', + 1, + true, + true, + '{"name": "clanker", "displayName": "Clanker", "tagline": "Clank Clank", "description": "Explore, launch and trade tokens in the Clanker ecosystem. Create your own tokens and discover trending projects in the community-driven token economy."}'::jsonb, + '{"logos": {"main": "/images/clanker/logo.svg", "icon": "/images/clanker/logo.svg", "favicon": "/images/clanker/favicon.ico", "appleTouch": "/images/clanker/apple.png", "og": "/images/clanker/og.jpg", "splash": "/images/clanker/og.jpg"}}'::jsonb, + '{"type": "token_platform", "urls": {"website": "https://clanker.world", "discord": "https://discord.gg/clanker", "twitter": "https://twitter.com/clankerworld", "github": "https://github.com/clanker", "forum": "https://forum.clanker.world"}, "social": {"farcaster": "clanker", "discord": "clanker", "twitter": "clankerworld"}, "governance": {"proposals": "https://proposals.clanker.world", "delegates": "https://delegates.clanker.world", "treasury": "https://treasury.clanker.world"}, "tokens": {"erc20Tokens": [{"address": "0x1bc0c42215582d5a085795f4badbac3ff36d1bcb", "symbol": "$CLANKER", "decimals": 18, "network": "base"}], "nftTokens": []}, "contracts": {"clanker": "0x1bc0c42215582d5a085795f4badbac3ff36d1bcb", "tokenFactory": "0x0000000000000000000000000000000000000000", "space": "0x0000000000000000000000000000000000000000", "trading": "0x0000000000000000000000000000000000000000", "nouns": "0x0000000000000000000000000000000000000000", "auctionHouse": "0x0000000000000000000000000000000000000000", "nogs": "0x0000000000000000000000000000000000000000"}}'::jsonb, + '{"enabled": ["Market", "Portfolio", "Swap", "feed", "cast", "gallery", "text", "iframe", "links", "Video", "Chat", "BuilderScore", "FramesV2", "Rss", "SnapShot"], "disabled": ["nounsHome", "governance"]}'::jsonb, + '{"logoTooltip": {"text": "clanker.world", "href": "https://www.clanker.world"}, "items": [{"id": "home", "label": "Home", "href": "/home", "icon": "home"}, {"id": "notifications", "label": "Notifications", "href": "/notifications", "icon": "notifications", "requiresAuth": true}, {"id": "clanker-token", "label": "$CLANKER", "href": "/t/base/0x1bc0c42215582d5a085795f4badbac3ff36d1bcb/Profile", "icon": "robot"}], "showMusicPlayer": false, "showSocials": false}'::jsonb, + '{"primaryColor": "rgba(136, 131, 252, 1)", "primaryHoverColor": "rgba(116, 111, 232, 1)", "primaryActiveColor": "rgba(96, 91, 212, 1)", "castButton": {"backgroundColor": "rgba(136, 131, 252, 1)", "hoverColor": "rgba(116, 111, 232, 1)", "activeColor": "rgba(96, 91, 212, 1)"}}'::jsonb +) ON CONFLICT ("community_id") DO UPDATE SET + "brand_config" = EXCLUDED."brand_config", + "assets_config" = EXCLUDED."assets_config", + "community_config" = EXCLUDED."community_config", + "fidgets_config" = EXCLUDED."fidgets_config", + "navigation_config" = EXCLUDED."navigation_config", + "ui_config" = EXCLUDED."ui_config", + "updated_at" = now(); + From 75b9c05b594811979aa314eec3093d1db8028049 Mon Sep 17 00:00:00 2001 From: Jesse Paterson Date: Sat, 29 Nov 2025 20:50:31 -0600 Subject: [PATCH 109/155] builds --- docs/BUILD_FIX_SUMMARY.md | 93 +++ docs/DATABASE_CONFIG/DATABASE_CONFIG_GUIDE.md | 335 ++++++++++ .../DATABASE_CONFIG_IMPLEMENTATION.md | 437 +++++++++++++ .../INCREMENTAL_IMPLEMENTATION_PLAN.md | 182 +++--- .../QUICK_START_IMPLEMENTATION.md | 143 +++-- .../QUICK_START_TESTING.md | 88 ++- docs/DATABASE_CONFIG/README.md | 49 ++ docs/README.md | 8 + docs/SYSTEM_WALKTHROUGH.md | 578 ++++++++++++++++++ .../ADMIN_ASSET_UPLOAD_STRATEGY.md | 0 .../ASSETS_CONFIG_STORAGE_RELATIONSHIP.md | 0 .../ASSET_HANDLING_STRATEGY.md | 0 .../ASSET_PERFORMANCE_OPTIMIZATION.md | 0 .../BUILD_TIME_CONFIG_SUMMARY.md | 0 .../CONFIG_DATABASE_SCHEMA_DETAILED.md | 0 .../DATABASE_CONFIG_MIGRATION_PLAN.md | 0 .../database-config}/E2BIG_SOLUTION.md | 0 .../NAVIGATION_SPACE_REFERENCE_APPROACH.md | 313 ++++++++++ ...VIGATION_SPACE_REFERENCE_IMPLEMENTATION.md | 357 +++++++++++ .../NEXTJS_CONFIG_APPROACHES.md | 0 docs/_archived/database-config/README.md | 43 ++ .../database-config/SHARED_THEMES_APPROACH.md | 255 ++++++++ .../SIMPLE_BUILD_TIME_CONFIG.md | 0 .../SPACE_REFERENCE_APPROACH.md | 0 .../UPDATED_IMPLEMENTATION_SUMMARY.md | 147 +++++ scripts/seed-community-configs.ts | 24 +- scripts/seed-navpage-spaces.ts | 252 ++++++++ .../[[...tabName]]/NavPageClient.tsx} | 51 +- src/app/[navSlug]/[[...tabName]]/page.tsx | 265 ++++++++ src/app/explore/[slug]/page.tsx | 27 - src/app/explore/page.tsx | 8 - src/app/home/page.tsx | 10 - src/app/page.tsx | 13 +- src/app/pwa/page.tsx | 47 +- src/config/index.ts | 10 +- src/config/systemConfig.ts | 8 +- ...0251129172847_create_community_configs.sql | 7 +- .../20251129172848_add_navpage_space_type.sql | 10 +- supabase/seed.sql | 110 +++- 39 files changed, 3626 insertions(+), 244 deletions(-) create mode 100644 docs/BUILD_FIX_SUMMARY.md create mode 100644 docs/DATABASE_CONFIG/DATABASE_CONFIG_GUIDE.md create mode 100644 docs/DATABASE_CONFIG/DATABASE_CONFIG_IMPLEMENTATION.md rename docs/{ => DATABASE_CONFIG}/INCREMENTAL_IMPLEMENTATION_PLAN.md (90%) rename docs/{ => DATABASE_CONFIG}/QUICK_START_IMPLEMENTATION.md (79%) rename docs/{ => DATABASE_CONFIG}/QUICK_START_TESTING.md (72%) create mode 100644 docs/DATABASE_CONFIG/README.md create mode 100644 docs/SYSTEM_WALKTHROUGH.md rename docs/{ => _archived/database-config}/ADMIN_ASSET_UPLOAD_STRATEGY.md (100%) rename docs/{ => _archived/database-config}/ASSETS_CONFIG_STORAGE_RELATIONSHIP.md (100%) rename docs/{ => _archived/database-config}/ASSET_HANDLING_STRATEGY.md (100%) rename docs/{ => _archived/database-config}/ASSET_PERFORMANCE_OPTIMIZATION.md (100%) rename docs/{ => _archived/database-config}/BUILD_TIME_CONFIG_SUMMARY.md (100%) rename docs/{ => _archived/database-config}/CONFIG_DATABASE_SCHEMA_DETAILED.md (100%) rename docs/{ => _archived/database-config}/DATABASE_CONFIG_MIGRATION_PLAN.md (100%) rename docs/{ => _archived/database-config}/E2BIG_SOLUTION.md (100%) create mode 100644 docs/_archived/database-config/NAVIGATION_SPACE_REFERENCE_APPROACH.md create mode 100644 docs/_archived/database-config/NAVIGATION_SPACE_REFERENCE_IMPLEMENTATION.md rename docs/{ => _archived/database-config}/NEXTJS_CONFIG_APPROACHES.md (100%) create mode 100644 docs/_archived/database-config/README.md create mode 100644 docs/_archived/database-config/SHARED_THEMES_APPROACH.md rename docs/{ => _archived/database-config}/SIMPLE_BUILD_TIME_CONFIG.md (100%) rename docs/{ => _archived/database-config}/SPACE_REFERENCE_APPROACH.md (100%) create mode 100644 docs/_archived/database-config/UPDATED_IMPLEMENTATION_SUMMARY.md create mode 100755 scripts/seed-navpage-spaces.ts rename src/app/{home/[tabname]/page.tsx => [navSlug]/[[...tabName]]/NavPageClient.tsx} (55%) create mode 100644 src/app/[navSlug]/[[...tabName]]/page.tsx delete mode 100644 src/app/explore/[slug]/page.tsx delete mode 100644 src/app/explore/page.tsx delete mode 100644 src/app/home/page.tsx diff --git a/docs/BUILD_FIX_SUMMARY.md b/docs/BUILD_FIX_SUMMARY.md new file mode 100644 index 000000000..9c598140c --- /dev/null +++ b/docs/BUILD_FIX_SUMMARY.md @@ -0,0 +1,93 @@ +# Build Error Fix Summary + +## Issues Fixed + +### 1. ✅ Removed Old Route Files + +The following old route files were removed (they conflicted with the new dynamic routing): + +- `src/app/explore/[slug]/page.tsx` - Was trying to access `config.explorePage.tabOrder` which doesn't exist in DB config +- `src/app/explore/page.tsx` - Was trying to access `config.explorePage.defaultTab` +- `src/app/home/[tabname]/page.tsx` - Old home route handler +- `src/app/home/page.tsx` - Old home redirect + +These are all replaced by the dynamic route: `src/app/[navSlug]/[[...tabName]]/page.tsx` + +### 2. ✅ Cleaned Up Empty Directories + +Removed empty directories: +- `src/app/explore/[slug]/` +- `src/app/home/[tabname]/` + +## Remaining Issue + +### ⚠️ Space Config Files Not Uploaded to Storage + +The errors you're seeing: +``` +Failed to load tabOrder for space a68308a2-9dae-4ff6-9d46-fe165623be79: Error [StorageUnknownError]: {} +``` + +This means the space config files haven't been uploaded to Supabase Storage yet. The database has the space registrations (created by `seed.sql`), but the actual config files need to be uploaded. + +## Solution: Upload Space Configs + +Run the seed script to upload space config files: + +```bash +# Make sure you have environment variables set +export NEXT_PUBLIC_SUPABASE_URL="your-supabase-url" +export SUPABASE_SERVICE_ROLE_KEY="your-service-role-key" + +# Upload space configs to Supabase Storage +tsx scripts/seed-navpage-spaces.ts +``` + +This script will: +1. Read space registrations from the database +2. Import page configs from TypeScript (`nounsHomePage`, `nounsExplorePage`, etc.) +3. Upload each tab as `{spaceId}/tabs/{tabName}` to Supabase Storage +4. Upload tab order as `{spaceId}/tabOrder` to Supabase Storage + +**Expected output:** +``` +🚀 Starting navPage space config seeding... + +📦 Uploading space: nouns-home + 📍 Space ID: a68308a2-9dae-4ff6-9d46-fe165623be79 + 📄 Uploading 6 tabs... + ✅ Uploaded tab: Nouns + ✅ Uploaded tab: Social + ... + ✅ Uploaded tab order + ✅ Successfully uploaded nouns-home + +📦 Uploading space: nouns-explore + ... + +✅ All navPage spaces seeded successfully! +``` + +## After Uploading + +Once the space configs are uploaded, the build should succeed. The dynamic route will be able to: +- Load spaces from Storage at build time (for static generation) +- Load spaces from Storage at runtime (for dynamic rendering) + +## Verify Upload + +You can verify the files were uploaded by: + +1. **Supabase Dashboard**: Check the `spaces` bucket in Storage +2. **Via SQL**: The files are in Storage, not the database +3. **Build test**: Try building again - errors should be gone + +## Note + +The `loadSpaceAsPageConfig()` function gracefully handles missing files: +- If files don't exist, it returns `null` +- `generateStaticParams()` skips static generation for that space +- Pages will still render at runtime (with a small delay to fetch from Storage) + +But for best performance, upload the files before building! + diff --git a/docs/DATABASE_CONFIG/DATABASE_CONFIG_GUIDE.md b/docs/DATABASE_CONFIG/DATABASE_CONFIG_GUIDE.md new file mode 100644 index 000000000..03c178723 --- /dev/null +++ b/docs/DATABASE_CONFIG/DATABASE_CONFIG_GUIDE.md @@ -0,0 +1,335 @@ +# Database-Backed Configuration System Guide + +## Overview + +Nounspace uses a **database-backed configuration system** that allows admins to update community configurations without code changes. Configurations are stored in Supabase and loaded at **build time** (not runtime), ensuring zero database queries in production. + +## Architecture + +### Current Approach + +``` +┌─────────────────┐ +│ Admin UI │ +│ (Updates DB) │ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│ Database │ +│ (Stores Config)│ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ ┌──────────────────┐ +│ Build Process │─────▶│ Set Env Var │ +│ (next.config.mjs)│ │ NEXT_PUBLIC_ │ +│ │ │ BUILD_TIME_ │ +│ │ │ CONFIG │ +└─────────────────┘ └─────────┬──────────┘ + │ + ▼ + ┌──────────────────┐ + │ Runtime App │ + │ (Reads Env Var │ + │ Zero DB Queries)│ + └──────────────────┘ +``` + +### Key Design Decisions + +1. **Build-Time Loading** - Configs fetched from DB during build, stored in env var +2. **Shared Themes** - Themes stored in `src/config/shared/themes.ts` (not in DB) +3. **Navigation-Space References** - Pages (homePage/explorePage) stored as Spaces, referenced by navigation items +4. **Environment Variables** - Config stored in `NEXT_PUBLIC_BUILD_TIME_CONFIG` (~2.8 KB) + +## Config Size Reduction + +| Stage | Config Size | Reduction | +|-------|-------------|-----------| +| **Original** | ~29 KB | - | +| **After removing pages** | ~8.3 KB | 71% | +| **After removing themes** | **~2.8 KB** | **90%** | + +### What Was Removed + +- **Themes** (5.5 KB) → Moved to `src/config/shared/themes.ts` +- **homePage** (19.2 KB) → Stored as Space, referenced by nav item +- **explorePage** (1.4 KB) → Stored as Space, referenced by nav item + +## Database Schema + +### `community_configs` Table + +```sql +CREATE TABLE "public"."community_configs" ( + "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "community_id" VARCHAR(50) NOT NULL UNIQUE, + "updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), + "brand_config" JSONB NOT NULL, -- Brand identity + "assets_config" JSONB NOT NULL, -- Asset paths + "community_config" JSONB NOT NULL, -- Community integration data + "fidgets_config" JSONB NOT NULL, -- Enabled/disabled fidgets + "navigation_config" JSONB, -- Navigation items (with spaceId refs) + "ui_config" JSONB, -- UI colors + "is_published" BOOLEAN DEFAULT true -- Draft vs published +); +``` + +**Note:** No `theme_config`, `home_page_config`, or `explore_page_config` columns. + +### Config Sections + +#### `brand_config` +- `name`: Internal identifier (e.g., "nouns") +- `displayName`: User-facing name +- `tagline`: Short tagline +- `description`: Full description +- `miniAppTags`: Farcaster Mini App discovery tags + +#### `assets_config` +- `logos`: Paths to logo files (main, icon, favicon, etc.) +- All paths are public URLs (e.g., `/images/nouns/logo.svg`) + +#### `community_config` +- `type`: Community type +- `urls`: Website, Discord, Twitter, GitHub, Forum links +- `social`: Social handles +- `governance`: Proposals, delegates, treasury links +- `tokens`: ERC20 and NFT token definitions +- `contracts`: Contract addresses + +#### `fidgets_config` +- `enabled`: Array of enabled fidget IDs +- `disabled`: Array of disabled fidget IDs + +#### `navigation_config` +- `logoTooltip`: Logo tooltip text and href +- `items`: Navigation items array + - Each item can have `spaceId` to reference a Space for page content +- `showMusicPlayer`: Boolean +- `showSocials`: Boolean + +#### `ui_config` +- `primaryColor`: Primary UI color +- `primaryHoverColor`: Hover state color +- `primaryActiveColor`: Active state color +- `castButton`: Cast button color config + +### Navigation-Space References + +Navigation items can reference Spaces for page content: + +```typescript +{ + id: 'home', + label: 'Home', + href: '/home', + icon: 'home', + spaceId: 'uuid-of-home-space' // ← References Space +} +``` + +- Spaces stored in `spaceRegistrations` with `spaceType = 'navPage'` (system-owned, fid=NULL) +- Space configs stored in Supabase Storage at `{spaceId}/tabs/{tabName}` and `{spaceId}/tabOrder` +- Uploaded via `scripts/seed-navpage-spaces.ts` script after database reset +- Stored as unencrypted SignedFile objects (readable by existing code) +- Fetched at build time and converted to page configs + +### Shared Themes + +Themes are stored in `src/config/shared/themes.ts`: + +```typescript +export const themes = { + default: { /* ... */ }, + nounish: { /* ... */ }, + // ... all 10 themes +}; +``` + +All communities import from this shared file. + +## Build Process + +### 1. Fetch Config from Database + +```javascript +// next.config.mjs + +const { data } = await supabase + .rpc('get_active_community_config', { p_community_id: community }) + .single(); +``` + +### 2. Fetch Spaces for Navigation Items + +```javascript +// Extract spaceIds from navigation items +const spaceIds = navItems + .filter(item => item.spaceId) + .map(item => ({ navId: item.id, spaceId: item.spaceId })); + +// Fetch Spaces from database/storage +const pageConfigs = {}; +for (const { navId, spaceId } of spaceIds) { + // Fetch tab order first + const { data: tabOrderData } = await supabase.storage + .from('spaces') + .download(`${spaceId}/tabOrder`); + + if (!tabOrderData) continue; + + const tabOrderFile = JSON.parse(await tabOrderData.text()); + const tabOrder = tabOrderFile.tabOrder || []; + + // Fetch each tab config + const tabs = {}; + for (const tabName of tabOrder) { + const { data: tabData } = await supabase.storage + .from('spaces') + .download(`${spaceId}/tabs/${tabName}`); + + if (tabData) { + const tabFile = JSON.parse(await tabData.text()); + const tabConfig = JSON.parse(tabFile.fileData); // Unencrypted SignedFile + tabs[tabName] = tabConfig; + } + } + + // Reconstruct HomePageConfig/ExplorePageConfig format + if (Object.keys(tabs).length > 0) { + pageConfigs[navId] = { + defaultTab: tabOrder[0] || 'Home', + tabOrder, + tabs, + layout: { + defaultLayoutFidget: 'grid', + gridSpacing: 16, + theme: {}, + }, + }; + } +} +``` + +### 3. Import Shared Themes + +```javascript +import { themes } from './src/config/shared/themes'; +``` + +### 4. Combine and Store in Env Var + +```javascript +const fullConfig = { + ...config, + theme: themes, // From shared file + pages: pageConfigs, // From Spaces +}; + +process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(fullConfig); +``` + +### 5. Runtime Usage + +```typescript +// src/config/index.ts + +const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; +if (buildTimeConfig) { + const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; + return dbConfig; +} +// Fall back to static configs +``` + +## Quick Start + +### 1. Reset Database + +```bash +# Applies migrations + seed data +supabase db reset +``` + +This will: +- Create `community_configs` table +- Add `navPage` spaceType +- Seed configs for nouns, example, clanker +- Create navPage space registrations (nouns-home, nouns-explore, clanker-home) +- Link navigation items to spaces via spaceId + +### 2. Upload Space Configs + +```bash +# Set environment variables +export NEXT_PUBLIC_SUPABASE_URL="your-supabase-url" +export SUPABASE_SERVICE_ROLE_KEY="your-service-role-key" + +# Upload space config files to Supabase Storage +tsx scripts/seed-navpage-spaces.ts +``` + +This uploads: +- Each tab config as `{spaceId}/tabs/{tabName}` (SignedFile format) +- Tab order as `{spaceId}/tabOrder` (SignedFile format) + +**Note:** Space configs are stored as unencrypted SignedFile objects and can be read by the existing space loading code. + +### 3. Build Application + +```bash +NEXT_PUBLIC_SUPABASE_URL=... \ +SUPABASE_SERVICE_ROLE_KEY=... \ +npm run build +``` + +Expected output: +``` +✅ Loaded config from database +``` + +### 4. Verify + +```bash +npm run dev +# App should load with DB config +# Pages should load from Spaces +``` + +## Environment Variables + +**Required for Build:** +```bash +NEXT_PUBLIC_SUPABASE_URL=... +SUPABASE_SERVICE_ROLE_KEY=... +NEXT_PUBLIC_COMMUNITY=nouns # Optional, defaults to 'nouns' +``` + +**Optional:** +- If DB credentials missing → Falls back to static configs +- If DB config not found → Falls back to static configs + +## Benefits + +✅ **Zero Runtime Overhead** - No database queries in production +✅ **Admin Updates** - Changes made through database/admin UI +✅ **Fast Runtime** - Config loaded from env var (instant) +✅ **Safe Fallback** - Static configs always available +✅ **Small Config** - Only ~2.8 KB (fits in env vars) +✅ **Unified Architecture** - Pages are Spaces +✅ **Shared Themes** - Single source of truth + +## Migration Path + +See `DATABASE_CONFIG_IMPLEMENTATION.md` for detailed phase-by-phase implementation plan. + +## Related Documentation + +- `DATABASE_CONFIG_IMPLEMENTATION.md` - Detailed implementation plan +- `QUICK_START_IMPLEMENTATION.md` - Quick start guide +- `QUICK_START_TESTING.md` - Testing guide +- `INCREMENTAL_IMPLEMENTATION_PLAN.md` - Complete phase-by-phase plan +- `README.md` - Documentation index + diff --git a/docs/DATABASE_CONFIG/DATABASE_CONFIG_IMPLEMENTATION.md b/docs/DATABASE_CONFIG/DATABASE_CONFIG_IMPLEMENTATION.md new file mode 100644 index 000000000..51de92f20 --- /dev/null +++ b/docs/DATABASE_CONFIG/DATABASE_CONFIG_IMPLEMENTATION.md @@ -0,0 +1,437 @@ +# Database-Backed Configuration Implementation Plan + +## Overview + +This document provides a detailed, phase-by-phase implementation plan for migrating to database-backed configurations. Each phase is independently testable and can be rolled back if issues arise. + +## Architecture Summary + +- **Config Storage**: Supabase `community_configs` table (~2.8 KB per config) +- **Build-Time Loading**: Configs fetched during build, stored in `NEXT_PUBLIC_BUILD_TIME_CONFIG` env var +- **Shared Themes**: Stored in `src/config/shared/themes.ts` (not in DB) +- **Pages as Spaces**: homePage/explorePage stored as Spaces, referenced by navigation items +- **Zero Runtime Queries**: All configs loaded at build time + +## Phase 0: Preparation & Setup + +**Goal:** Set up foundation without breaking existing system + +**Tasks:** +1. Create feature branch: `feature/database-configs` +2. Document current static config structure +3. Set up test database (local Supabase) +4. Create migration files structure + +**Testing:** +- ✅ Verify static configs still work +- ✅ Local Supabase runs successfully +- ✅ Can connect to database + +**Rollback:** Just switch back to main branch + +**Estimated Time:** 1-2 hours + +--- + +## Phase 1: Database Schema + +**Goal:** Create database tables and functions + +**Tasks:** +1. Create migration: `community_configs` table (without theme/home/explore columns) +2. Create migration: `add_navpage_space_type` (adds navPage spaceType) +3. Create database function: `get_active_community_config` (excludes themes/pages) +4. Set up RLS policies +5. Seed initial data via `supabase/seed.sql`: + - Seeds configs for nouns, example, clanker + - Creates navPage space registrations (nouns-home, nouns-explore, clanker-home) + - Links navigation items to spaces via spaceId +6. Create space seeding script: `scripts/seed-navpage-spaces.ts` +7. Upload space configs to Supabase Storage + +**Files:** +- `supabase/migrations/20251129172847_create_community_configs.sql` +- `supabase/migrations/20251129172848_add_navpage_space_type.sql` +- `supabase/seed.sql` (includes seed data and space registrations) +- `scripts/seed-navpage-spaces.ts` (uploads space config files) + +**Testing:** +```sql +-- Test 1: Can fetch config (should not include themes/pages) +SELECT get_active_community_config('nouns'); + +-- Test 2: Verify navPage spaceType exists +SELECT DISTINCT "spaceType" FROM "spaceRegistrations"; +-- Should include: 'navPage' + +-- Test 3: Verify seed data loaded +SELECT community_id FROM community_configs; +-- Should see: nouns, example, clanker + +-- Test 4: Verify navPage spaces created +SELECT "spaceId", "spaceName", "spaceType" +FROM "spaceRegistrations" +WHERE "spaceType" = 'navPage'; +-- Should see: nouns-home, nouns-explore, clanker-home + +-- Test 5: Verify navigation references spaces +SELECT "navigation_config"->'items' as nav_items +FROM "community_configs" +WHERE "community_id" = 'nouns'; +-- Should see spaceId references +``` + +**After seed.sql, upload space configs:** +```bash +tsx scripts/seed-navpage-spaces.ts +``` + +**Validation:** +- ✅ Database functions work +- ✅ RLS policies enforce access +- ✅ Seed data loaded +- ✅ navPage spaceType added +- ✅ Config excludes theme/home/explore columns +- ✅ navPage space registrations created +- ✅ Space config files uploaded to storage + +**Rollback:** `supabase db reset` (resets to clean state, deletes storage files) + +**Estimated Time:** 4-6 hours + +--- + +## Phase 2: Build-Time Config Loading + +**Goal:** Load config from DB at build time, store in env var + +**Tasks:** +1. Create `src/config/shared/themes.ts` - Move themes to shared file +2. Update community configs to import shared themes +3. Add build-time fetch in `next.config.mjs`: + - Fetch main config from DB + - Import shared themes + - Extract spaceIds from navigation items + - Fetch Spaces for nav items (if implemented) + - Combine config + themes + pages + - Store in `NEXT_PUBLIC_BUILD_TIME_CONFIG` env var +4. Update `src/config/index.ts` to read from env var +5. Keep static configs as fallback + +**Files to Create:** +- `src/config/shared/themes.ts` + +**Files to Modify:** +- `next.config.mjs` - Add config fetch +- `src/config/index.ts` - Read from env var +- `src/config/nouns/nouns.theme.ts` - Import from shared +- `src/config/clanker/clanker.theme.ts` - Import from shared +- `src/config/example/example.theme.ts` - Import from shared + +**Implementation:** + +```javascript +// next.config.mjs + +import { createClient } from '@supabase/supabase-js'; +import { themes } from './src/config/shared/themes'; + +async function loadConfigFromDB() { + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + + if (!supabaseUrl || !supabaseKey) { + console.log('ℹ️ Using static configs (no DB credentials)'); + return; + } + + const supabase = createClient(supabaseUrl, supabaseKey); + const community = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + + try { + // Fetch main config (now much smaller - no themes/pages!) + const { data: config, error } = await supabase + .rpc('get_active_community_config', { p_community_id: community }) + .single(); + + if (error || !data) { + console.log('ℹ️ Using static configs (no DB config found)'); + return; + } + + // Combine: config + shared themes + pages (if Spaces implemented) + const fullConfig = { + ...config, + theme: themes, // From shared file + pages: {}, // Will be populated when Spaces are implemented + }; + + // Store in env var (now small enough at ~2.8 KB) + process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(fullConfig); + console.log('✅ Loaded config from database'); + } catch (error) { + console.warn('⚠️ Error loading config from DB:', error.message); + } +} + +await loadConfigFromDB(); +``` + +```typescript +// src/config/index.ts + +export const loadSystemConfig = (): SystemConfig => { + const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + + // Try build-time config from database (stored in env var) + const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; + if (buildTimeConfig) { + try { + const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; + // Validate config structure + if (dbConfig && dbConfig.brand && dbConfig.assets) { + console.log('✅ Using config from database'); + return dbConfig; + } + } catch (error) { + console.warn('⚠️ Failed to parse build-time config, falling back to static:', error); + } + } + + // Fall back to static configs (existing behavior) + console.log('ℹ️ Using static configs'); + switch (communityConfig.toLowerCase()) { + case 'nouns': + return nounsSystemConfig; + case 'example': + return exampleSystemConfig; + case 'clanker': + return clankerSystemConfig as unknown as SystemConfig; + default: + return nounsSystemConfig; + } +}; +``` + +**Testing:** +```bash +# Test with DB available +NEXT_PUBLIC_SUPABASE_URL=... SUPABASE_SERVICE_ROLE_KEY=... npm run build +# Should see: "✅ Loaded config from database" + +# Test without DB +npm run build +# Should see: "ℹ️ Using static configs" + +# Verify app works +npm run dev +# App should load correctly +``` + +**Validation:** +- ✅ Build succeeds with/without DB +- ✅ App uses DB config when available +- ✅ Themes loaded from shared file +- ✅ App falls back to static when unavailable +- ✅ Config size ~2.8 KB +- ✅ All existing functionality works +- ✅ No breaking changes + +**Rollback:** Remove env var reading, system back to static-only + +**Estimated Time:** 2-3 hours + +--- + +## Phase 3: Admin API Endpoints + +**Goal:** Create API endpoints for admin config updates + +**Tasks:** +1. Create `/api/admin/config/[communityId]` - GET/PUT +2. Create `/api/admin/config/[communityId]/history` - GET +3. Create `/api/admin/spaces/[spaceId]` - GET/PUT (for nav page Spaces) +4. Add admin permission checks +5. Add request validation + +**Files to Create:** +- `src/app/api/admin/config/[communityId]/route.ts` +- `src/app/api/admin/config/[communityId]/history/route.ts` +- `src/app/api/admin/spaces/[spaceId]/route.ts` +- `src/common/data/services/adminConfigService.ts` + +**Testing:** +```bash +# Test GET +curl -H "x-admin-identity: test-admin" \ + http://localhost:3000/api/admin/config/nouns + +# Test PUT +curl -X PUT \ + -H "x-admin-identity: test-admin" \ + -H "Content-Type: application/json" \ + -d '{"config": {...}, "changeNotes": "Test update"}' \ + http://localhost:3000/api/admin/config/nouns +``` + +**Validation:** +- ✅ GET returns config (without themes/pages) +- ✅ PUT updates config +- ✅ GET/PUT for Spaces (nav pages) +- ✅ Permission checks work +- ✅ History tracked +- ✅ Invalid requests rejected + +**Rollback:** Remove API routes, no impact on main app + +**Estimated Time:** 3-4 hours + +--- + +## Phase 4: Basic Admin UI + +**Goal:** Create simple admin interface to view/edit configs + +**Tasks:** +1. Create admin layout/page structure +2. Create config viewer (read-only first) +3. Add basic form for editing config +4. Add Space editor for nav pages (homePage/explorePage) +5. Add save functionality + +**Files to Create:** +- `src/app/admin/config/[communityId]/page.tsx` +- `src/app/admin/config/[communityId]/components/ConfigViewer.tsx` +- `src/app/admin/config/[communityId]/components/ConfigEditor.tsx` +- `src/app/admin/config/[communityId]/components/NavPageEditor.tsx` + +**Testing:** +1. Navigate to `/admin/config/nouns` +2. Verify config loads +3. Make a small change (e.g., brand name) +4. Save and verify it updates in DB +5. Rebuild and verify change appears + +**Validation:** +- ✅ Can view config (without themes/pages) +- ✅ Can edit config +- ✅ Can edit nav page Spaces +- ✅ Can save changes +- ✅ Changes persist in DB/Storage +- ✅ Changes appear after rebuild + +**Rollback:** Remove admin routes, no impact on main app + +**Estimated Time:** 4-6 hours + +--- + +## Phase 5-10: Additional Features + +See `INCREMENTAL_IMPLEMENTATION_PLAN.md` for detailed phases covering: +- Phase 5: Asset Upload +- Phase 6: Build-Time Asset Download +- Phase 7: Asset Optimization +- Phase 8: Rebuild Trigger +- Phase 9: Production Rollout +- Phase 10: Phase Out Static Configs + +--- + +## Testing Checklist + +### Phase 1: Database +- [ ] Tables created (without theme/home/explore columns) +- [ ] Functions work (exclude themes/pages) +- [ ] Seed data loaded +- [ ] navPage spaceType exists +- [ ] RLS policies work + +### Phase 2: Config Loading +- [ ] Build succeeds with DB +- [ ] Build succeeds without DB +- [ ] App uses DB config +- [ ] Themes loaded from shared file +- [ ] App falls back to static +- [ ] Config size ~2.8 KB +- [ ] No breaking changes + +### Phase 3: Admin API +- [ ] GET returns config (without themes/pages) +- [ ] PUT updates config +- [ ] GET/PUT for nav page Spaces +- [ ] Permission checks work +- [ ] Invalid requests rejected + +### Phase 4: Admin UI +- [ ] Can view config (without themes/pages) +- [ ] Can edit config +- [ ] Can edit nav page Spaces +- [ ] Can save changes +- [ ] Changes persist + +--- + +## Rollback Procedures + +### Phase 1 Rollback +```bash +supabase db reset # Resets to clean state +``` + +### Phase 2 Rollback +Remove env var reading from `src/config/index.ts`, system uses static configs + +### Phase 3 Rollback +Remove API routes, no impact on main app + +### Phase 4 Rollback +Remove admin routes, no impact on main app + +--- + +## Success Criteria + +### Phase 1 +- ✅ Database functions work +- ✅ Seed data loaded +- ✅ navPage spaceType exists + +### Phase 2 +- ✅ Build succeeds with/without DB +- ✅ App uses DB config when available +- ✅ Themes loaded from shared file +- ✅ Config size reduced to ~2.8 KB +- ✅ No breaking changes + +### Phase 3 +- ✅ Admin can fetch/update configs +- ✅ Permission checks work +- ✅ History tracked + +### Phase 4 +- ✅ Admin can view/edit configs +- ✅ Changes persist and appear after rebuild + +--- + +## Quick Start Path + +For a quick proof-of-concept: + +1. **Phase 1** - Create DB schema (2h) +2. **Phase 2** - Basic config loading (2h) +3. **Manual testing** - Update DB directly, rebuild, verify + +This gives you a working POC in ~4 hours! + +--- + +## Related Documentation + +- `DATABASE_CONFIG_GUIDE.md` - Architecture and overview +- `QUICK_START_IMPLEMENTATION.md` - Quick start guide +- `QUICK_START_TESTING.md` - Testing guide +- `INCREMENTAL_IMPLEMENTATION_PLAN.md` - Complete phase-by-phase plan (all 10 phases) +- `README.md` - Documentation index + diff --git a/docs/INCREMENTAL_IMPLEMENTATION_PLAN.md b/docs/DATABASE_CONFIG/INCREMENTAL_IMPLEMENTATION_PLAN.md similarity index 90% rename from docs/INCREMENTAL_IMPLEMENTATION_PLAN.md rename to docs/DATABASE_CONFIG/INCREMENTAL_IMPLEMENTATION_PLAN.md index 5be4ca207..332fbfe93 100644 --- a/docs/INCREMENTAL_IMPLEMENTATION_PLAN.md +++ b/docs/DATABASE_CONFIG/INCREMENTAL_IMPLEMENTATION_PLAN.md @@ -1,9 +1,20 @@ # Incremental Implementation Plan +> **Note:** This is a detailed phase-by-phase plan. For a quick overview, see: +> - `DATABASE_CONFIG_GUIDE.md` - Architecture and overview +> - `DATABASE_CONFIG_IMPLEMENTATION.md` - Consolidated implementation plan +> - `QUICK_START_IMPLEMENTATION.md` - Quick start guide +> - `README.md` - Documentation index + ## Overview This plan breaks down the database-backed configuration system into testable phases, allowing you to validate each piece before moving to the next. +**Current Approach:** +- Configs stored in DB (~2.8 KB), loaded at build time into `NEXT_PUBLIC_BUILD_TIME_CONFIG` env var +- Themes in shared file (`src/config/shared/themes.ts`) +- Pages as Spaces (referenced by navigation items) + ## Phase 0: Preparation & Setup **Goal:** Set up foundation without breaking existing system @@ -31,19 +42,25 @@ This plan breaks down the database-backed configuration system into testable pha **Tasks:** 1. Create migration: `community_configs` table (without homePage/explorePage columns) -2. Create migration: `community_config_history` table -3. Create migration: `community_config_admins` table -4. Add `navPage` spaceType to `spaceRegistrations` -5. Create database function: `get_active_community_config` (excludes page configs) -6. Create database function: `create_config_version` -7. Set up RLS policies -8. Seed initial data (copy from static configs, excluding themes/pages) +2. Create migration: `community_config_admins` table +3. Add `navPage` spaceType to `spaceRegistrations` +4. Create database function: `get_active_community_config` (excludes page configs) +5. Set up RLS policies +6. Seed initial data via `supabase/seed.sql`: + - Seeds configs (copy from static configs, excluding themes/pages) + - Creates navPage space registrations (nouns-home, nouns-explore, clanker-home) + - Links navigation items to spaces via spaceId +7. Create space seeding script: `scripts/seed-navpage-spaces.ts` +8. Upload space config files to Supabase Storage **Files to Create:** ``` supabase/migrations/ └── YYYYMMDDHHMMSS_create_community_configs.sql └── YYYYMMDDHHMMSS_add_navpage_space_type.sql +supabase/seed.sql # Seeds configs and creates space registrations +scripts/ + └── seed-navpage-spaces.ts # NEW: Uploads space config files ``` **Files to Create/Update:** @@ -55,30 +72,42 @@ src/config/shared/ **Testing:** ```sql -- Test 1: Can fetch config (should not include homePage/explorePage) -SELECT * FROM get_active_community_config('nouns'); +SELECT get_active_community_config('nouns'); -- Test 2: Verify navPage spaceType exists -SELECT * FROM spaceRegistrations WHERE "spaceType" = 'navPage'; - --- Test 3: Can create version -SELECT create_config_version( - 'nouns', - '{"brand": {...}, "assets": {...}}'::jsonb, - 'Initial seed' -); - --- Test 4: Can query history -SELECT * FROM community_config_history -WHERE community_config_id = (SELECT id FROM community_configs WHERE community_id = 'nouns'); +SELECT DISTINCT "spaceType" FROM "spaceRegistrations"; +-- Should include: 'navPage' + +-- Test 3: Verify seed data loaded +SELECT community_id FROM community_configs; +-- Should see: nouns, example, clanker + +-- Test 4: Verify navPage spaces created +SELECT "spaceId", "spaceName", "spaceType" +FROM "spaceRegistrations" +WHERE "spaceType" = 'navPage'; +-- Should see: nouns-home, nouns-explore, clanker-home + +-- Test 5: Verify navigation references spaces +SELECT "navigation_config"->'items' as nav_items +FROM "community_configs" +WHERE "community_id" = 'nouns'; +-- Should see spaceId references +``` + +**After seed.sql, upload space configs:** +```bash +tsx scripts/seed-navpage-spaces.ts ``` **Validation:** - ✅ Database functions work - ✅ RLS policies enforce access - ✅ Can seed existing configs (without themes/pages) -- ✅ History tracking works - ✅ navPage spaceType added - ✅ Config excludes homePage/explorePage columns +- ✅ navPage space registrations created +- ✅ Space config files uploaded to storage **Rollback:** Drop migrations, system unchanged @@ -98,8 +127,8 @@ WHERE community_config_id = (SELECT id FROM community_configs WHERE community_id - Extract spaceIds from navigation items - Fetch Spaces for nav items with spaceId - Convert Spaces to page configs -4. Generate TypeScript file (not env var - avoids E2BIG) -5. Update `src/config/index.ts` to use generated file +4. Store config in `NEXT_PUBLIC_BUILD_TIME_CONFIG` env var (now small enough at ~2.8 KB) +5. Update `src/config/index.ts` to read from env var 6. Keep static configs as fallback **Files to Create:** @@ -110,8 +139,8 @@ src/config/shared/ **Files to Modify:** ``` -next.config.mjs # Add config fetch + Space fetching -src/config/index.ts # Add generated file reading +next.config.mjs # Add config fetch + Space fetching, set env var +src/config/index.ts # Read from env var src/config/nouns/nouns.theme.ts # Import from shared src/config/clanker/clanker.theme.ts # Import from shared src/config/example/example.theme.ts # Import from shared @@ -163,30 +192,30 @@ await loadConfigFromDB(); ```typescript // src/config/index.ts (modify loadSystemConfig) -// Try to import DB config (generated at build time) -let dbConfig: SystemConfig | null = null; -try { - const dbModule = require('./db-config'); - dbConfig = dbModule.dbConfig || null; -} catch { - // File doesn't exist, will use static -} - export const loadSystemConfig = (): SystemConfig => { const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - // Try build-time config from generated file - if (dbConfig) { - // Map page configs from pages object to homePage/explorePage - const homePage = dbConfig.pages?.['home'] || staticConfig.homePage; - const explorePage = dbConfig.pages?.['explore'] || staticConfig.explorePage; - - console.log('✅ Using config from database'); - return { - ...dbConfig, - homePage, - explorePage, - }; + // Try build-time config from database (stored in env var at build time) + const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; + if (buildTimeConfig) { + try { + const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; + // Map page configs from pages object to homePage/explorePage + const homePage = dbConfig.pages?.['home'] || null; + const explorePage = dbConfig.pages?.['explore'] || null; + + // Validate config structure + if (dbConfig && dbConfig.brand && dbConfig.assets) { + console.log('✅ Using config from database'); + return { + ...dbConfig, + homePage, + explorePage, + }; + } + } catch (error) { + console.warn('⚠️ Failed to parse build-time config, falling back to static:', error); + } } // Fall back to static configs (existing behavior) @@ -208,8 +237,7 @@ export const loadSystemConfig = (): SystemConfig => { 1. **Test with DB available:** ```bash NEXT_PUBLIC_SUPABASE_URL=... SUPABASE_SERVICE_ROLE_KEY=... npm run build - # Should see: "✅ Generated config file from database" - # Should see: src/config/db-config.ts created + # Should see: "✅ Loaded config from database" ``` 2. **Test without DB:** @@ -228,11 +256,10 @@ export const loadSystemConfig = (): SystemConfig => { - Pages loaded from Spaces (if nav items have spaceId) - No runtime errors -4. **Verify config size:** +4. **Verify config loaded:** ```bash - # Check generated file size - wc -c src/config/db-config.ts - # Should be ~2-3 KB (much smaller than before!) + # Config is stored in NEXT_PUBLIC_BUILD_TIME_CONFIG env var (~2.8 KB) + # Check build logs for "✅ Loaded config from database" ``` **Validation:** @@ -241,12 +268,12 @@ export const loadSystemConfig = (): SystemConfig => { - ✅ App falls back to static when DB unavailable - ✅ Themes loaded from shared file - ✅ Pages loaded from Spaces (via nav items) -- ✅ Config file generated (not env var - avoids E2BIG) +- ✅ Config stored in env var (`NEXT_PUBLIC_BUILD_TIME_CONFIG`) - ✅ Config size reduced (~2.8 KB vs ~29 KB) - ✅ All existing functionality works - ✅ No breaking changes -**Rollback:** Remove generated file reading, system back to static-only +**Rollback:** Remove env var reading, system back to static-only **Estimated Time:** 2-3 hours @@ -258,16 +285,14 @@ export const loadSystemConfig = (): SystemConfig => { **Tasks:** 1. Create `/api/admin/config/[communityId]` - GET/PUT -2. Create `/api/admin/config/[communityId]/history` - GET -3. Create `/api/admin/spaces/[spaceId]` - GET/PUT (for nav page Spaces) -4. Add admin permission checks -5. Add request validation -6. Note: Themes are in shared file (not editable via API for now) +2. Create `/api/admin/spaces/[spaceId]` - GET/PUT (for nav page Spaces) +3. Add admin permission checks +4. Add request validation +5. Note: Themes are in shared file (not editable via API for now) **Files to Create:** ``` src/app/api/admin/config/[communityId]/route.ts -src/app/api/admin/config/[communityId]/history/route.ts src/app/api/admin/spaces/[spaceId]/route.ts # NEW: For nav page Spaces src/common/data/services/adminConfigService.ts ``` @@ -302,7 +327,6 @@ export async function GET( .select('id') .eq('community_id', params.communityId) .eq('admin_identity_public_key', adminIdentity) - .eq('is_active', true) .single(); if (!admin) { @@ -331,7 +355,7 @@ export async function PUT( } const body = await request.json(); - const { config, changeNotes } = body; + const { config } = body; // Validate config structure (basic) if (!config || typeof config !== 'object') { @@ -349,26 +373,36 @@ export async function PUT( .select('id') .eq('community_id', params.communityId) .eq('admin_identity_public_key', adminIdentity) - .eq('is_active', true) .single(); if (!admin) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } - // Create new version - const { data, error } = await supabase.rpc('create_config_version', { - p_community_id: params.communityId, - p_config_data: config, - p_change_notes: changeNotes || 'Updated via admin API', - p_admin_identity: adminIdentity - }); + // Update config directly (using ON CONFLICT to update existing row) + const { data, error } = await supabase + .from('community_configs') + .upsert({ + community_id: params.communityId, + brand_config: config.brand, + assets_config: config.assets, + community_config: config.community, + fidgets_config: config.fidgets, + navigation_config: config.navigation, + ui_config: config.ui, + is_published: config.is_published ?? true, + updated_at: new Date().toISOString(), + }, { + onConflict: 'community_id', + }) + .select() + .single(); if (error) { return NextResponse.json({ error: error.message }, { status: 500 }); } - return NextResponse.json({ success: true, versionId: data }); + return NextResponse.json({ success: true, config: data }); } ``` @@ -382,15 +416,14 @@ curl -H "x-admin-identity: test-admin" \ curl -X PUT \ -H "x-admin-identity: test-admin" \ -H "Content-Type: application/json" \ - -d '{"config": {...}, "changeNotes": "Test update"}' \ + -d '{"config": {...}}' \ http://localhost:3000/api/admin/config/nouns ``` **Validation:** - ✅ GET returns config -- ✅ PUT creates new version +- ✅ PUT updates config directly - ✅ Permission checks work -- ✅ History is tracked - ✅ Invalid requests are rejected **Rollback:** Remove API routes, no impact on main app @@ -1665,7 +1698,6 @@ Each phase can be rolled back independently: - ✅ Can update config - ✅ Can fetch/update nav page Spaces - ✅ Permission checks work -- ✅ History tracked ### Phase 4: Admin UI - ✅ Can view config (without themes/pages) diff --git a/docs/QUICK_START_IMPLEMENTATION.md b/docs/DATABASE_CONFIG/QUICK_START_IMPLEMENTATION.md similarity index 79% rename from docs/QUICK_START_IMPLEMENTATION.md rename to docs/DATABASE_CONFIG/QUICK_START_IMPLEMENTATION.md index f78a62124..4c58d0ba0 100644 --- a/docs/QUICK_START_IMPLEMENTATION.md +++ b/docs/DATABASE_CONFIG/QUICK_START_IMPLEMENTATION.md @@ -1,11 +1,18 @@ # Quick Start: Incremental Implementation Guide +> **See also:** +> - `DATABASE_CONFIG_GUIDE.md` - Architecture and overview +> - `DATABASE_CONFIG_IMPLEMENTATION.md` - Detailed implementation plan +> - `QUICK_START_TESTING.md` - Testing guide +> - `README.md` - Documentation index + ## 🎯 Goal Implement database-backed configs incrementally, testing each piece before moving forward. -**Updated Architecture:** -- ✅ Configs stored in DB (without themes/pages) +**Current Architecture:** +- ✅ Configs stored in DB (without themes/pages) - ~2.8 KB +- ✅ Configs loaded at build time, stored in `NEXT_PUBLIC_BUILD_TIME_CONFIG` env var - ✅ Themes in shared file (`src/config/shared/themes.ts`) - ✅ Pages stored as Spaces (referenced by navigation `spaceId`) - ✅ Config size reduced by ~90% (from ~29 KB to ~2.8 KB) @@ -39,7 +46,10 @@ supabase db reset This will: 1. ✅ Apply migration: `create_community_configs.sql` (creates table without themes/pages) 2. ✅ Apply migration: `add_navpage_space_type.sql` (adds navPage spaceType) -3. ✅ Run `seed.sql` (seeds configs for nouns, example, clanker) +3. ✅ Run `seed.sql`: + - Seeds configs for nouns, example, clanker + - Creates navPage space registrations (nouns-home, nouns-explore, clanker-home) + - Links navigation items to spaces via spaceId **Verify:** ```sql @@ -57,6 +67,18 @@ SELECT get_active_community_config('nouns'); -- Verify navPage spaceType exists SELECT DISTINCT "spaceType" FROM "spaceRegistrations"; -- Should include: 'navPage' + +-- Verify navPage spaces were created +SELECT "spaceId", "spaceName", "spaceType" +FROM "spaceRegistrations" +WHERE "spaceType" = 'navPage'; +-- Should see: nouns-home, nouns-explore, clanker-home + +-- Verify navigation configs reference spaces +SELECT "community_id", "navigation_config"->'items' as nav_items +FROM "community_configs" +WHERE "community_id" = 'nouns'; +-- Should see spaceId references in navigation items ``` **Expected output:** @@ -64,6 +86,8 @@ SELECT DISTINCT "spaceType" FROM "spaceRegistrations"; - ✅ Function returns config (without themes/pages) - ✅ Seed data inserted for all 3 communities - ✅ navPage spaceType available +- ✅ navPage space registrations created +- ✅ Navigation items reference spaceIds ### Step 2: Build-Time Loading (1 hour) @@ -79,47 +103,61 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); async function fetchSpaceBySpaceId(supabase, spaceId) { - // Fetch Space from spaceRegistrations + Storage - const { data: registration } = await supabase - .from('spaceRegistrations') - .select('*') - .eq('spaceId', spaceId) - .eq('spaceType', 'navPage') - .single(); + // Fetch tab order first + const { data: tabOrderData } = await supabase.storage + .from('spaces') + .download(`${spaceId}/tabOrder`); - if (!registration) return null; + if (!tabOrderData) return null; - // Fetch Space config from Storage - const { data } = await supabase.storage - .from('spaces') - .download(`${spaceId}/tabs/default`); + const tabOrderFile = JSON.parse(await tabOrderData.text()); + const tabOrder = tabOrderFile.tabOrder || []; - if (!data) return null; + // Fetch each tab config + const tabs = {}; + for (const tabName of tabOrder) { + try { + const { data: tabData } = await supabase.storage + .from('spaces') + .download(`${spaceId}/tabs/${tabName}`); + + if (tabData) { + const tabFile = JSON.parse(await tabData.text()); + const tabConfig = JSON.parse(tabFile.fileData); // Unencrypted SignedFile + tabs[tabName] = tabConfig; + } + } catch (error) { + console.warn(`Failed to fetch tab ${tabName}:`, error.message); + } + } - const fileData = JSON.parse(await data.text()); - return fileData; -} - -function convertSpaceToPageConfig(space) { - // Convert Space config to HomePageConfig/ExplorePageConfig format + if (Object.keys(tabs).length === 0) return null; + + // Reconstruct HomePageConfig/ExplorePageConfig format return { - defaultTab: space.defaultTab || 'Home', - tabOrder: Object.keys(space.tabs || {}), - tabs: space.tabs || {}, + defaultTab: tabOrder[0] || 'Home', + tabOrder, + tabs, layout: { - defaultLayoutFidget: space.layout?.defaultLayoutFidget || 'grid', - gridSpacing: space.layout?.gridSpacing || 16, - theme: space.theme || {}, + defaultLayoutFidget: 'grid', + gridSpacing: 16, + theme: {}, }, }; } -async function generateConfigFile() { +function convertSpaceToPageConfig(spacePageConfig) { + // Space is already in HomePageConfig/ExplorePageConfig format + // (reconstructed from tabs in fetchSpaceBySpaceId) + return spacePageConfig; +} + +async function loadConfigFromDB() { const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; - if (!supabaseUrl || !supabaseKey) { - console.log('ℹ️ Using static configs (no DB credentials for config generation)'); + if (!supabaseUrl || !supabaseKey) { + console.log('ℹ️ Using static configs (no DB credentials)'); return; } @@ -133,7 +171,7 @@ async function generateConfigFile() { .single(); if (error || !data) { - console.log('ℹ️ No DB config found, skipping db-config.ts generation'); + console.log('ℹ️ No DB config found, using static configs'); if (error) { console.log(` Error: ${error.message}`); } @@ -174,12 +212,12 @@ async function generateConfigFile() { process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(fullConfig); console.log('✅ Loaded config from database'); } catch (error) { - console.warn('⚠️ Error generating db-config.ts:', error.message); + console.warn('⚠️ Error loading config from DB:', error.message); } } -// Run config generation before Next.js config is created -await generateConfigFile(); +// Load config before Next.js config is created +await loadConfigFromDB(); // Continue with existing Next.js config... ``` @@ -256,6 +294,8 @@ npm run dev # App should load correctly ``` +**Note:** Build-time loading requires space configs to be uploaded (Step 1.5). Without them, page configs won't be available. + ### Step 3: Create Shared Themes (30 min) ```typescript @@ -280,7 +320,7 @@ export { themes as exampleTheme } from '../shared/themes'; **Update next.config.mjs to import themes:** ```javascript -// In generateConfigFile function, replace themesPlaceholder: +// In loadConfigFromDB function, replace themesPlaceholder: import { themes } from './src/config/shared/themes'; // Then use: @@ -289,6 +329,9 @@ const fullConfig = { theme: themes, // From shared file pages: pageConfigs, // From Spaces }; + +// Store in env var +process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(fullConfig); ``` **Update src/config/index.ts to use shared themes:** @@ -315,9 +358,11 @@ npm run build If this works, you've validated: - ✅ Database storage works (without themes/pages) +- ✅ Space registrations created in database +- ✅ Space configs uploaded to storage - ✅ Build-time loading works - ✅ Shared themes work -- ✅ Space-based pages work (when Spaces are created) +- ✅ Space-based pages work (loaded from storage) - ✅ Fallback works - ✅ Config size reduced by ~90% @@ -332,7 +377,8 @@ If this works, you've validated: **Files:** - `supabase/migrations/20251129172847_create_community_configs.sql` - Creates table (no themes/pages) - `supabase/migrations/20251129172848_add_navpage_space_type.sql` - Adds navPage spaceType -- `supabase/seed.sql` - Seeds configs for nouns, example, clanker +- `supabase/seed.sql` - Seeds configs and creates navPage space registrations +- `scripts/seed-navpage-spaces.ts` - Uploads space config files to storage **Test:** - ✅ Can query configs from DB @@ -340,6 +386,8 @@ If this works, you've validated: - ✅ Config excludes themes/pages - ✅ navPage spaceType exists - ✅ Seed data loaded +- ✅ navPage space registrations created +- ✅ Space config files uploaded to storage **Rollback:** `supabase db reset` (resets to clean state) @@ -358,11 +406,13 @@ If this works, you've validated: - ✅ Build succeeds with/without DB - ✅ App uses DB config when available - ✅ Themes loaded from shared file -- ✅ Pages loaded from Spaces (when available) +- ✅ Pages loaded from Spaces (when space configs uploaded) - ✅ App falls back to static - ✅ Config size ~2.8 KB -**Rollback:** Remove generated file reading +**Note:** Pages require space configs to be uploaded (via `scripts/seed-navpage-spaces.ts`) before they can be loaded at build time. + +**Rollback:** Remove env var reading --- @@ -466,6 +516,8 @@ If this works, you've validated: - [ ] Functions work (exclude themes/pages) - [ ] Seed data loaded - [ ] navPage spaceType exists +- [ ] navPage space registrations created +- [ ] Space config files uploaded to storage - [ ] RLS policies work ### Phase 2: Config Loading @@ -550,7 +602,7 @@ If this works, you've validated: ### High Risk Phases - **Phase 2** - Could break builds - **Mitigation:** Extensive fallback testing - - **Rollback:** Remove generated file reading + - **Rollback:** Remove env var reading - **Phase 6** - Could break asset loading - **Mitigation:** Fallback to static assets @@ -594,7 +646,14 @@ supabase db reset This will: 1. Drop all tables 2. Apply all migrations (including community_configs) -3. Run seed.sql (seeds configs for nouns, example, clanker) +3. Run seed.sql (seeds configs and creates navPage space registrations) + +**After reset, upload space configs:** +```bash +tsx scripts/seed-navpage-spaces.ts +``` + +This uploads the actual space config files (tabs and tabOrder) to Supabase Storage. ### Git Workflow diff --git a/docs/QUICK_START_TESTING.md b/docs/DATABASE_CONFIG/QUICK_START_TESTING.md similarity index 72% rename from docs/QUICK_START_TESTING.md rename to docs/DATABASE_CONFIG/QUICK_START_TESTING.md index e3086bb6a..efa9fd17a 100644 --- a/docs/QUICK_START_TESTING.md +++ b/docs/DATABASE_CONFIG/QUICK_START_TESTING.md @@ -1,5 +1,11 @@ # Quick Start Testing Guide +> **See also:** +> - `DATABASE_CONFIG_GUIDE.md` - Architecture and overview +> - `DATABASE_CONFIG_IMPLEMENTATION.md` - Detailed implementation plan +> - `QUICK_START_IMPLEMENTATION.md` - Quick start guide +> - `README.md` - Documentation index + ## Overview This guide walks you through testing the database-backed configuration system. @@ -22,7 +28,10 @@ supabase db reset This will: 1. ✅ Apply migration: `create_community_configs.sql` (creates table without themes/pages) 2. ✅ Apply migration: `add_navpage_space_type.sql` (adds navPage spaceType) -3. ✅ Run `seed.sql` (seeds configs for nouns, example, clanker) +3. ✅ Run `seed.sql`: + - Seeds configs for nouns, example, clanker + - Creates navPage space registrations (nouns-home, nouns-explore, clanker-home) + - Links navigation items to spaces via spaceId **Verify:** ```sql @@ -43,7 +52,19 @@ SELECT community_id FROM community_configs; -- Verify navPage spaceType exists SELECT DISTINCT "spaceType" FROM "spaceRegistrations"; --- Should include: 'navPage' (if constraint updated) +-- Should include: 'navPage' + +-- Verify navPage spaces were created +SELECT "spaceId", "spaceName", "spaceType" +FROM "spaceRegistrations" +WHERE "spaceType" = 'navPage'; +-- Should see: nouns-home, nouns-explore, clanker-home + +-- Verify navigation configs reference spaces +SELECT "community_id", "navigation_config"->'items' as nav_items +FROM "community_configs" +WHERE "community_id" = 'nouns'; +-- Should see spaceId references in navigation items ``` **Expected output:** @@ -51,8 +72,57 @@ SELECT DISTINCT "spaceType" FROM "spaceRegistrations"; - ✅ Function returns config (without themes/pages) - ✅ Seed data inserted for all 3 communities - ✅ navPage spaceType available +- ✅ navPage space registrations created +- ✅ Navigation items reference spaceIds + +## Step 2: Upload Space Configs to Storage + +**After database reset, upload the actual space config files:** + +```bash +# Set environment variables +export NEXT_PUBLIC_SUPABASE_URL="your-supabase-url" +export SUPABASE_SERVICE_ROLE_KEY="your-service-role-key" + +# Upload space configs to Supabase Storage +tsx scripts/seed-navpage-spaces.ts +``` -## Step 2: Optional - Manual Seed Script +This script will: +1. ✅ Read space registrations from database (created in seed.sql) +2. ✅ Import page configs from TypeScript (nounsHomePage, nounsExplorePage, etc.) +3. ✅ Upload each tab as `{spaceId}/tabs/{tabName}` to Supabase Storage as SignedFile +4. ✅ Upload tab order as `{spaceId}/tabOrder` to Supabase Storage as SignedFile + +**Expected output:** +``` +🚀 Starting navPage space config seeding... + +📦 Uploading space: nouns-home + 📍 Space ID: + 📄 Uploading 6 tabs... + ✅ Uploaded tab: Nouns + ✅ Uploaded tab: Social + ✅ Uploaded tab: Governance + ✅ Uploaded tab: Resources + ✅ Uploaded tab: Funded Works + ✅ Uploaded tab: Places + ✅ Uploaded tab order: [Nouns, Social, Governance, Resources, Funded Works, Places] + ✅ Successfully uploaded nouns-home + +📦 Uploading space: nouns-explore + ... + +✅ All navPage spaces seeded successfully! +``` + +**Verify space configs uploaded:** +- Check Supabase Storage dashboard: `spaces` bucket should contain files +- Or verify via API/CLI that files exist at `{spaceId}/tabs/{tabName}` paths + +**Note:** Space configs are stored as unencrypted SignedFile objects (`isEncrypted: false`). They can be read by anyone, and the `decryptEncryptedSignedFile` function handles unencrypted files correctly. + +## Step 3: Optional - Manual Seed Script **Note:** The TypeScript seed script (`scripts/seed-community-configs.ts`) is now **OPTIONAL**. Use it only if you need to update configs without resetting the database. @@ -71,7 +141,7 @@ npx tsx scripts/seed-community-configs.ts **Note:** The TypeScript script still includes themes/pages for backward compatibility. The new architecture stores themes in shared file and pages as Spaces. -## Step 3: Create Shared Themes (If Not Done) +## Step 4: Create Shared Themes (If Not Done) Before testing build-time loading, ensure shared themes file exists: @@ -86,7 +156,7 @@ Update community configs to import from shared: - `src/config/clanker/clanker.theme.ts` → `export { themes as clankerTheme } from '../shared/themes';` - `src/config/example/example.theme.ts` → `export { themes as exampleTheme } from '../shared/themes';` -## Step 4: Test Build-Time Loading +## Step 5: Test Build-Time Loading ### Test with Database Available @@ -132,7 +202,7 @@ npm run build - Build should complete successfully - App should work with static configs -## Step 4: Test Runtime +## Step 6: Test Runtime ### Start Dev Server @@ -163,7 +233,7 @@ npm run build npm run dev ``` -## Step 5: Manual Database Update Test +## Step 7: Manual Database Update Test Update config directly in database: @@ -276,10 +346,12 @@ Once testing is successful: ## Quick Commands Reference -```bash # Reset database (applies migrations + seed data) supabase db reset +# Upload space configs to storage (required after reset) +tsx scripts/seed-navpage-spaces.ts + # Optional: Seed configs manually (seed.sql already does this) npx tsx scripts/seed-community-configs.ts diff --git a/docs/DATABASE_CONFIG/README.md b/docs/DATABASE_CONFIG/README.md new file mode 100644 index 000000000..bf571321e --- /dev/null +++ b/docs/DATABASE_CONFIG/README.md @@ -0,0 +1,49 @@ +# Database-Backed Configuration System + +This directory contains all documentation for the database-backed configuration system. + +## Documentation Files + +### Getting Started +- **`QUICK_START_IMPLEMENTATION.md`** - Quick start guide for a 4-hour proof of concept +- **`QUICK_START_TESTING.md`** - Step-by-step testing guide + +### Architecture & Implementation +- **`DATABASE_CONFIG_GUIDE.md`** - Architecture overview, database schema, and key design decisions +- **`DATABASE_CONFIG_IMPLEMENTATION.md`** - Detailed phase-by-phase implementation plan (Phases 0-4) +- **`INCREMENTAL_IMPLEMENTATION_PLAN.md`** - Complete implementation plan with all 10 phases + +### Reference +- **`DATABASE_CONFIG_CONSOLIDATION.md`** - Summary of documentation consolidation and what changed + +## Quick Navigation + +**New to the system?** +1. Start with `DATABASE_CONFIG_GUIDE.md` - Understand the architecture +2. Then `QUICK_START_IMPLEMENTATION.md` - Try the quick POC +3. Then `QUICK_START_TESTING.md` - Test it + +**Ready to implement?** +1. Read `DATABASE_CONFIG_IMPLEMENTATION.md` - Phases 0-4 +2. Or `INCREMENTAL_IMPLEMENTATION_PLAN.md` - All 10 phases + +## Current Architecture + +- **Config Storage**: Supabase `community_configs` table (~2.8 KB per config) +- **Build-Time Loading**: Configs fetched during build, stored in `NEXT_PUBLIC_BUILD_TIME_CONFIG` env var +- **Shared Themes**: Stored in `src/config/shared/themes.ts` (not in DB) +- **Pages as Spaces**: homePage/explorePage stored as Spaces, referenced by navigation items +- **Space Seeding**: Space registrations created in `seed.sql`, config files uploaded via `scripts/seed-navpage-spaces.ts` +- **Zero Runtime Queries**: All configs loaded at build time + +## Related Files + +- **Migrations**: + - `supabase/migrations/20251129172847_create_community_configs.sql` - Creates config table + - `supabase/migrations/20251129172848_add_navpage_space_type.sql` - Adds navPage spaceType +- **Seed Data**: + - `supabase/seed.sql` - Seeds configs and creates navPage space registrations + - `scripts/seed-navpage-spaces.ts` - Uploads space config files to storage (run after seed.sql) +- **Build Config**: `next.config.mjs` (loads config at build time) +- **Config Loader**: `src/config/index.ts` (reads from env var) + diff --git a/docs/README.md b/docs/README.md index d385c3d57..e176a475b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -52,6 +52,14 @@ Nounspace is a highly customizable Farcaster client funded by Nouns DAO. This do - [Supabase](INTEGRATIONS/SUPABASE.md) - Supabase integration - [Neynar](INTEGRATIONS/NEYNAR.md) - Neynar API integration +## Database-Backed Configuration + +- [Database Config Guide](DATABASE_CONFIG/DATABASE_CONFIG_GUIDE.md) - Architecture and overview +- [Quick Start](DATABASE_CONFIG/QUICK_START_IMPLEMENTATION.md) - 4-hour proof of concept guide +- [Testing Guide](DATABASE_CONFIG/QUICK_START_TESTING.md) - Step-by-step testing +- [Implementation Plan](DATABASE_CONFIG/DATABASE_CONFIG_IMPLEMENTATION.md) - Detailed implementation (Phases 0-4) +- [Complete Plan](DATABASE_CONFIG/INCREMENTAL_IMPLEMENTATION_PLAN.md) - Full phase-by-phase plan (all 10 phases) + ## Development - [Development Guide](DEVELOPMENT/DEVELOPMENT_GUIDE.md) - Comprehensive development guide diff --git a/docs/SYSTEM_WALKTHROUGH.md b/docs/SYSTEM_WALKTHROUGH.md new file mode 100644 index 000000000..cac220e58 --- /dev/null +++ b/docs/SYSTEM_WALKTHROUGH.md @@ -0,0 +1,578 @@ +# System Walkthrough: How Nounspace Works Now + +This document provides a comprehensive walkthrough of how the Nounspace system currently works, focusing on the database-backed configuration system and dynamic navigation routing. + +## Table of Contents + +1. [Overview](#overview) +2. [Architecture Flow](#architecture-flow) +3. [Build-Time Configuration Loading](#build-time-configuration-loading) +4. [Runtime Configuration Access](#runtime-configuration-access) +5. [Navigation Pages as Spaces](#navigation-pages-as-spaces) +6. [Dynamic Routing](#dynamic-routing) +7. [Data Flow Examples](#data-flow-examples) +8. [Key Components](#key-components) + +--- + +## Overview + +Nounspace is a **customizable Farcaster client** that can be whitelabeled for different communities (Nouns, Clanker, etc.). The system uses a **database-backed configuration** approach where: + +- **Configurations are stored in Supabase** (PostgreSQL database) +- **Configs are loaded at build time** (not runtime), ensuring zero database queries in production +- **Navigation pages are stored as Spaces** in Supabase Storage, referenced by navigation items +- **Themes are shared** across communities in a TypeScript file +- **Zero runtime queries** - everything is baked into the build + +### Key Benefits + +- ✅ **Admin-editable**: Admins can update configs via database without code changes +- ✅ **Performance**: No runtime database queries +- ✅ **Flexible**: Navigation items can reference Spaces for page content +- ✅ **Maintainable**: Shared themes reduce duplication + +--- + +## Architecture Flow + +``` +┌─────────────────────────────────────────────────────────────┐ +│ BUILD TIME │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ 1. next.config.mjs runs │ +│ ├─> Fetches config from community_configs table │ +│ ├─> Extracts spaceIds from navigation items │ +│ ├─> Fetches Spaces from Supabase Storage │ +│ └─> Stores everything in NEXT_PUBLIC_BUILD_TIME_CONFIG │ +│ │ +│ 2. Next.js builds static pages │ +│ ├─> generateStaticParams() runs │ +│ ├─> Generates static paths for navigation pages │ +│ └─> Creates static HTML/JS bundles │ +│ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ RUNTIME │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ 1. User visits /home or /explore │ +│ ├─> Next.js routes to [navSlug]/[[...tabName]]/page.tsx │ +│ ├─> loadSystemConfig() reads from env var │ +│ └─> No database queries! │ +│ │ +│ 2. Page component loads │ +│ ├─> Finds navigation item by slug │ +│ ├─> If spaceId exists, loads Space from Storage │ +│ ├─> Converts Space config to PageConfig │ +│ └─> Renders NavPageClient with tabs │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Build-Time Configuration Loading + +### Step 1: Database Query + +When you run `npm run build`, `next.config.mjs` runs first: + +```javascript +// next.config.mjs + +async function loadConfigFromDB() { + // 1. Get credentials + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + + if (!supabaseUrl || !supabaseKey) { + console.log('ℹ️ Using static configs (no DB credentials)'); + return; // Falls back to static configs + } + + // 2. Create Supabase client + const supabase = createClient(supabaseUrl, supabaseKey); + const community = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + + // 3. Fetch config from database + const { data, error } = await supabase + .rpc('get_active_community_config', { p_community_id: community }) + .single(); + + if (error || !data) { + console.log('ℹ️ Using static configs (no DB config found)'); + return; // Falls back to static configs + } + + // 4. Store in environment variable + process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(data); + console.log('✅ Loaded config from database'); +} +``` + +### Step 2: What Gets Fetched + +The `get_active_community_config` function returns: + +```json +{ + "brand": { /* Brand identity */ }, + "assets": { /* Asset paths */ }, + "community": { /* Community integration data */ }, + "fidgets": { /* Enabled/disabled fidgets */ }, + "navigation": { + "items": [ + { + "id": "home", + "label": "Home", + "href": "/home", + "icon": "home", + "spaceId": "uuid-123" // ← References a Space! + }, + { + "id": "explore", + "label": "Explore", + "href": "/explore", + "icon": "explore", + "spaceId": "uuid-456" // ← References a Space! + } + ] + }, + "ui": { /* UI colors */ } +} +``` + +**Note:** +- No `theme` (stored in shared file) +- No `homePage` or `explorePage` (stored as Spaces) +- Navigation items have `spaceId` references + +### Step 3: Space Fetching (Future Enhancement) + +Currently, Spaces are fetched at **runtime** when pages load. In the future, we could fetch them at build time too: + +```javascript +// For each navigation item with spaceId: +const spaceIds = navigation.items + .filter(item => item.spaceId) + .map(item => item.spaceId); + +// Fetch each Space from Storage +for (const spaceId of spaceIds) { + const tabOrder = await fetchTabOrder(spaceId); + const tabs = await fetchAllTabs(spaceId, tabOrder); + // Store in config... +} +``` + +--- + +## Runtime Configuration Access + +### Step 1: Config Loader + +When the app runs, `loadSystemConfig()` is called: + +```typescript +// src/config/index.ts + +export const loadSystemConfig = (): SystemConfig => { + // 1. Try build-time config from database + const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; + if (buildTimeConfig) { + try { + const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; + if (dbConfig && dbConfig.brand && dbConfig.assets) { + console.log('✅ Using config from database'); + return dbConfig; // ← Returns DB config + } + } catch (error) { + console.warn('⚠️ Failed to parse build-time config'); + } + } + + // 2. Fall back to static configs + console.log('ℹ️ Using static configs'); + const community = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + + switch (community) { + case 'nouns': + return nounsSystemConfig; + case 'clanker': + return clankerSystemConfig; + default: + return nounsSystemConfig; + } +}; +``` + +### Step 2: Component Usage + +Components use `loadSystemConfig()` or the `useSystemConfig()` hook: + +```typescript +// In a component +import { loadSystemConfig } from '@/config'; + +const config = loadSystemConfig(); +const brandName = config.brand.displayName; +const logo = config.assets.logos.main; +const navItems = config.navigation?.items || []; +``` + +```typescript +// In a React component +import { useSystemConfig } from '@/common/lib/hooks/useSystemConfig'; + +function Navigation() { + const config = useSystemConfig(); + // Use config.brand, config.navigation, etc. +} +``` + +**Important:** This happens at runtime, but **no database queries occur** - the config is already in the environment variable! + +--- + +## Navigation Pages as Spaces + +### Concept + +Navigation pages (like `/home` and `/explore`) are stored as **Spaces** in Supabase Storage, not directly in the config. This allows: + +- ✅ Pages to be updated independently +- ✅ Reusing existing Space infrastructure +- ✅ Dramatically reducing config size (from ~29 KB to ~2.8 KB) + +### Storage Structure + +**1. Space Registration** (in `spaceRegistrations` table): + +```sql +INSERT INTO spaceRegistrations ( + spaceId, + fid, -- NULL for system-owned pages + spaceName, -- 'nouns-home', 'nouns-explore' + spaceType, -- 'navPage' + identityPublicKey,-- 'system' + signature, -- 'system-seed' + timestamp +) VALUES (...); +``` + +**2. Space Config Files** (in Supabase Storage bucket `spaces`): + +``` +spaces/ + {spaceId}/ + tabOrder ← JSON: { tabOrder: ["Nouns", "Socials", ...] } + tabs/ + Nouns ← SpaceConfig JSON (fidgets, layout, etc.) + Socials ← SpaceConfig JSON + ... +``` + +**3. File Format** (SignedFile wrapper): + +```json +{ + "fileData": "{...SpaceConfig JSON as string...}", + "fileType": "json", + "isEncrypted": false, + "timestamp": "2024-01-01T00:00:00Z", + "publicKey": "nounspace", + "signature": "not applicable, machine generated file" +} +``` + +### How It Works + +1. **Navigation Config** references Spaces: + ```typescript + { + id: 'home', + href: '/home', + spaceId: 'uuid-123' // ← References Space + } + ``` + +2. **At Runtime**, when user visits `/home`: + - Route handler finds navigation item + - Extracts `spaceId` + - Fetches Space from Storage + - Converts Space config to PageConfig + - Renders page with tabs + +--- + +## Dynamic Routing + +### Route Structure + +``` +src/app/[navSlug]/[[...tabName]]/page.tsx +``` + +This handles: +- `/home` → redirects to `/home/{defaultTab}` +- `/home/Nouns` → renders Nouns tab +- `/explore` → redirects to `/explore/{defaultTab}` +- `/explore/Featured` → renders Featured tab +- Any custom nav item with a Space + +### Request Flow + +``` +User visits /home + │ + ▼ +┌─────────────────────────────────────────┐ +│ [navSlug]/[[...tabName]]/page.tsx │ +├─────────────────────────────────────────┤ +│ │ +│ 1. Extract navSlug = "home" │ +│ │ +│ 2. Load system config │ +│ const config = loadSystemConfig(); │ +│ │ +│ 3. Find navigation item │ +│ const navItem = config.navigation │ +│ .items.find(i => i.href === "/home")│ +│ │ +│ 4. Check if spaceId exists │ +│ if (navItem.spaceId) { │ +│ // Load Space from Storage │ +│ } │ +│ │ +└─────────────────────────────────────────┘ +``` + +### Space Loading + +When a navigation item has a `spaceId`, the system loads it: + +```typescript +async function loadSpaceAsPageConfig(spaceId: string): Promise { + // 1. Create Supabase client (with credentials check) + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = process.env.SUPABASE_SERVICE_KEY || ...; + + if (!supabaseUrl || !supabaseKey) { + return null; // Will render at runtime + } + + const supabase = createClient(supabaseUrl, supabaseKey); + + // 2. Fetch tab order + const { data: tabOrderData } = await supabase.storage + .from('spaces') + .download(`${spaceId}/tabOrder`); + + const tabOrderFile = JSON.parse(await tabOrderData.text()) as SignedFile; + const tabOrderObj = JSON.parse(tabOrderFile.fileData); + const tabOrder = tabOrderObj.tabOrder; + + // 3. Fetch each tab config + const tabs = {}; + for (const tabName of tabOrder) { + const { data: tabData } = await supabase.storage + .from('spaces') + .download(`${spaceId}/tabs/${tabName}`); + + const tabFile = JSON.parse(await tabData.text()) as SignedFile; + const tabConfig = JSON.parse(tabFile.fileData); + tabs[tabName] = tabConfig; + } + + // 4. Reconstruct PageConfig + return { + defaultTab: tabOrder[0], + tabOrder, + tabs, + layout: { /* defaults */ } + }; +} +``` + +### Redirect Logic + +If no tab is specified, redirect to default: + +```typescript +// If no tab name provided, redirect to default tab +if (!tabName || tabName.length === 0) { + const pageConfig = await loadSpaceAsPageConfig(navItem.spaceId); + if (pageConfig) { + const defaultTab = encodeURIComponent(pageConfig.defaultTab); + redirect(`/${navSlug}/${defaultTab}`); // e.g., /home/Nouns + return null; + } +} +``` + +--- + +## Data Flow Examples + +### Example 1: Building the App + +``` +1. Developer runs: npm run build + │ + ▼ +2. next.config.mjs executes + ├─> loadConfigFromDB() runs + ├─> Queries: SELECT get_active_community_config('nouns') + ├─> Gets JSON config from database + └─> Sets: process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = "{...}" + │ + ▼ +3. Next.js build process starts + ├─> generateStaticParams() runs for [navSlug] route + ├─> loadSystemConfig() reads from env var + ├─> Extracts navigation items + ├─> (Optionally) Generates static paths + └─> Builds static pages + │ + ▼ +4. Build completes + └─> App is ready to deploy (no DB needed!) +``` + +### Example 2: User Visits /home + +``` +1. User navigates to: https://nounspace.com/home + │ + ▼ +2. Next.js routes to: [navSlug]/[[...tabName]]/page.tsx + ├─> navSlug = "home" + └─> tabName = undefined (no tab specified) + │ + ▼ +3. Page component executes: + ├─> const config = loadSystemConfig(); + │ └─> Reads from NEXT_PUBLIC_BUILD_TIME_CONFIG (no DB query!) + │ + ├─> const navItem = config.navigation.items.find(...) + │ └─> Finds: { id: 'home', spaceId: 'uuid-123', ... } + │ + ├─> if (!tabName) { redirect to default tab } + │ └─> loadSpaceAsPageConfig('uuid-123') + │ ├─> Fetches: spaces/uuid-123/tabOrder + │ ├─> Fetches: spaces/uuid-123/tabs/Nouns + │ ├─> Fetches: spaces/uuid-123/tabs/Socials + │ └─> Returns PageConfig + │ + └─> redirect(`/home/${defaultTab}`) // e.g., /home/Nouns + │ + ▼ +4. User redirected to: /home/Nouns + └─> Same component, but now tabName = ["Nouns"] + │ + ▼ +5. Page renders: + ├─> Loads Space config (same as above) + ├─> Renders NavPageClient with: + │ ├─> pageConfig: { tabs: {...}, tabOrder: [...] } + │ ├─> activeTabName: "Nouns" + │ └─> navSlug: "home" + └─> User sees Home page with Nouns tab active +``` + +### Example 3: Admin Updates Config + +``` +1. Admin opens admin UI (future feature) + │ + ▼ +2. Admin updates brand name in UI + ├─> UI calls: UPDATE community_configs + │ SET brand_config = '{...new config...}' + │ WHERE community_id = 'nouns' + │ + ▼ +3. Admin triggers rebuild + ├─> CI/CD system runs: npm run build + ├─> Build process fetches new config from DB + └─> New config is baked into build + │ + ▼ +4. New build deployed + └─> Users see updated brand name (no code changes!) +``` + +--- + +## Key Components + +### 1. Configuration System + +**Files:** +- `src/config/index.ts` - Main config loader +- `src/config/systemConfig.ts` - TypeScript interfaces +- `src/config/shared/themes.ts` - Shared theme definitions +- `src/config/nouns/` - Static fallback configs + +**Key Functions:** +- `loadSystemConfig()` - Returns SystemConfig (DB or static) +- `useSystemConfig()` - React hook for components + +### 2. Build-Time Loading + +**Files:** +- `next.config.mjs` - Fetches config from DB at build time + +**Key Functions:** +- `loadConfigFromDB()` - Queries database, stores in env var + +### 3. Database Schema + +**Tables:** +- `community_configs` - Stores brand, assets, navigation, etc. +- `spaceRegistrations` - Registers navPage Spaces +- `storage.buckets.spaces` - Stores Space config files + +**Functions:** +- `get_active_community_config(community_id)` - Returns combined config + +### 4. Dynamic Routing + +**Files:** +- `src/app/[navSlug]/[[...tabName]]/page.tsx` - Main route handler +- `src/app/[navSlug]/[[...tabName]]/NavPageClient.tsx` - Client component + +**Key Functions:** +- `loadSpaceAsPageConfig(spaceId)` - Fetches Space from Storage +- `generateStaticParams()` - Generates static paths (optional) + +### 5. Space Storage + +**Structure:** +``` +Supabase Storage: spaces/ + {spaceId}/ + tabOrder ← Tab order JSON + tabs/ + {tabName} ← SpaceConfig JSON (fidgets, layout, etc.) +``` + +**Format:** SignedFile wrapper (unencrypted for system files) + +--- + +## Summary + +1. **Configs are stored in Supabase** but loaded at build time, not runtime +2. **Navigation pages are Spaces** stored in Supabase Storage, referenced by navigation items +3. **Themes are shared** across communities in a TypeScript file +4. **Zero runtime queries** - everything is in environment variables +5. **Dynamic routing** handles any navigation item that references a Space +6. **Graceful fallbacks** - if DB/config unavailable, falls back to static configs + +This architecture provides the flexibility of database-backed configs with the performance of static builds! + diff --git a/docs/ADMIN_ASSET_UPLOAD_STRATEGY.md b/docs/_archived/database-config/ADMIN_ASSET_UPLOAD_STRATEGY.md similarity index 100% rename from docs/ADMIN_ASSET_UPLOAD_STRATEGY.md rename to docs/_archived/database-config/ADMIN_ASSET_UPLOAD_STRATEGY.md diff --git a/docs/ASSETS_CONFIG_STORAGE_RELATIONSHIP.md b/docs/_archived/database-config/ASSETS_CONFIG_STORAGE_RELATIONSHIP.md similarity index 100% rename from docs/ASSETS_CONFIG_STORAGE_RELATIONSHIP.md rename to docs/_archived/database-config/ASSETS_CONFIG_STORAGE_RELATIONSHIP.md diff --git a/docs/ASSET_HANDLING_STRATEGY.md b/docs/_archived/database-config/ASSET_HANDLING_STRATEGY.md similarity index 100% rename from docs/ASSET_HANDLING_STRATEGY.md rename to docs/_archived/database-config/ASSET_HANDLING_STRATEGY.md diff --git a/docs/ASSET_PERFORMANCE_OPTIMIZATION.md b/docs/_archived/database-config/ASSET_PERFORMANCE_OPTIMIZATION.md similarity index 100% rename from docs/ASSET_PERFORMANCE_OPTIMIZATION.md rename to docs/_archived/database-config/ASSET_PERFORMANCE_OPTIMIZATION.md diff --git a/docs/BUILD_TIME_CONFIG_SUMMARY.md b/docs/_archived/database-config/BUILD_TIME_CONFIG_SUMMARY.md similarity index 100% rename from docs/BUILD_TIME_CONFIG_SUMMARY.md rename to docs/_archived/database-config/BUILD_TIME_CONFIG_SUMMARY.md diff --git a/docs/CONFIG_DATABASE_SCHEMA_DETAILED.md b/docs/_archived/database-config/CONFIG_DATABASE_SCHEMA_DETAILED.md similarity index 100% rename from docs/CONFIG_DATABASE_SCHEMA_DETAILED.md rename to docs/_archived/database-config/CONFIG_DATABASE_SCHEMA_DETAILED.md diff --git a/docs/DATABASE_CONFIG_MIGRATION_PLAN.md b/docs/_archived/database-config/DATABASE_CONFIG_MIGRATION_PLAN.md similarity index 100% rename from docs/DATABASE_CONFIG_MIGRATION_PLAN.md rename to docs/_archived/database-config/DATABASE_CONFIG_MIGRATION_PLAN.md diff --git a/docs/E2BIG_SOLUTION.md b/docs/_archived/database-config/E2BIG_SOLUTION.md similarity index 100% rename from docs/E2BIG_SOLUTION.md rename to docs/_archived/database-config/E2BIG_SOLUTION.md diff --git a/docs/_archived/database-config/NAVIGATION_SPACE_REFERENCE_APPROACH.md b/docs/_archived/database-config/NAVIGATION_SPACE_REFERENCE_APPROACH.md new file mode 100644 index 000000000..172b1b2b3 --- /dev/null +++ b/docs/_archived/database-config/NAVIGATION_SPACE_REFERENCE_APPROACH.md @@ -0,0 +1,313 @@ +# Navigation-Space Reference Approach + +## Overview + +Instead of storing `homePage` and `explorePage` configs directly in `community_configs`, navigation items reference Spaces in the database. Pages are fetched at build time based on navigation entries. + +## Architecture + +``` +Navigation Config + ├── items: [ + │ { id: 'home', spaceId: 'uuid-1', ... }, + │ { id: 'explore', spaceId: 'uuid-2', ... }, + │ ... + │ ] + │ + └── Build Time: + ├── Fetch nav config from community_configs + ├── For each nav item with spaceId: + │ └── Fetch Space from database/storage + └── Build page configs from fetched Spaces +``` + +## Benefits + +1. **Dramatically reduces config size** - Removes 71% (20.6 KB) of config +2. **Unified architecture** - Everything is Spaces +3. **Navigation as source of truth** - Nav defines what pages exist +4. **Flexible** - Any nav item can reference a Space +5. **Reuses existing infrastructure** - Uses Space storage/retrieval + +## Implementation + +### 1. Update NavigationItem Interface + +```typescript +// src/config/systemConfig.ts + +export interface NavigationItem { + id: string; + label: string; + href: string; + icon?: 'home' | 'explore' | 'notifications' | 'search' | 'space' | 'robot' | 'custom'; + openInNewTab?: boolean; + requiresAuth?: boolean; + spaceId?: string; // ← NEW: Reference to Space in database +} +``` + +### 2. Add 'navPage' Space Type + +```sql +-- Migration: Add navPage to spaceType enum +ALTER TABLE "public"."spaceRegistrations" + DROP CONSTRAINT IF EXISTS valid_space_type; + +ALTER TABLE "public"."spaceRegistrations" + ADD CONSTRAINT valid_space_type CHECK ( + "spaceType" IN ('profile', 'token', 'proposal', 'channel', 'navPage') + ); +``` + +### 3. Update Navigation Config Structure + +```typescript +// src/config/nouns/nouns.navigation.ts + +export const nounsNavigation: NavigationConfig = { + logoTooltip: { + text: "wtf is nouns?", + href: "https://nouns.wtf", + }, + items: [ + { + id: 'home', + label: 'Home', + href: '/home', + icon: 'home', + spaceId: '550e8400-e29b-41d4-a716-446655440000' // ← Reference to Space + }, + { + id: 'explore', + label: 'Explore', + href: '/explore', + icon: 'explore', + spaceId: '550e8400-e29b-41d4-a716-446655440001' // ← Reference to Space + }, + { + id: 'notifications', + label: 'Notifications', + href: '/notifications', + icon: 'notifications', + requiresAuth: true + // No spaceId - not a Space-based page + }, + ], + showMusicPlayer: true, + showSocials: true, +}; +``` + +### 4. Update Database Schema + +```sql +-- Remove home_page_config and explore_page_config from community_configs +ALTER TABLE "public"."community_configs" + DROP COLUMN IF EXISTS "home_page_config", + DROP COLUMN IF EXISTS "explore_page_config"; + +-- Navigation config now contains spaceId references +-- No schema changes needed - navigation_config JSONB column handles it +``` + +### 5. Update Build-Time Config Generation + +```javascript +// next.config.mjs + +async function generateConfigFile() { + // Fetch main config (now much smaller - no homePage/explorePage!) + const { data: config } = await supabase + .rpc('get_active_community_config', { p_community_id: community }) + .single(); + + // Fetch Spaces for nav items that have spaceId + const navItems = config.navigation?.items || []; + const spaceIds = navItems + .filter(item => item.spaceId) + .map(item => item.spaceId); + + // Fetch Spaces from database/storage + const spaces = {}; + for (const spaceId of spaceIds) { + // Fetch Space based on how Spaces are stored + // (Could be from spaceRegistrations + Storage, or new table) + const space = await fetchSpace(spaceId); + if (space) { + spaces[spaceId] = space; + } + } + + // Build page configs from Spaces + const pageConfigs = {}; + navItems.forEach(item => { + if (item.spaceId && spaces[item.spaceId]) { + // Map Space to page config format + pageConfigs[item.id] = convertSpaceToPageConfig(spaces[item.spaceId]); + } + }); + + // Combine configs + const fullConfig = { + ...config, + // Add page configs based on nav items + pages: pageConfigs, + }; + + // Generate file + await writeFile('src/config/db-config.ts', ...); +} +``` + +### 6. Update Config Loader + +```typescript +// src/config/index.ts + +export const loadSystemConfig = (): SystemConfig => { + const config = dbConfig || staticConfig; + + // Get page configs from nav items + const navItems = config.navigation?.items || []; + const pages = {}; + + navItems.forEach(item => { + if (item.spaceId && config.pages?.[item.id]) { + pages[item.id] = config.pages[item.id]; + } + }); + + // Map to legacy structure for backward compatibility + return { + ...config, + homePage: pages['home'] || staticConfig.homePage, + explorePage: pages['explore'] || staticConfig.explorePage, + }; +}; +``` + +## Space Storage Options + +### Option A: Use Existing Space Storage System + +Spaces stored in Supabase Storage: +- Path: `spaces/{spaceId}/tabs/{tabName}` +- Encrypted/signed files +- Requires decryption at build time + +**Pros:** +- Uses existing infrastructure +- No schema changes needed + +**Cons:** +- Requires encryption/decryption logic at build time +- More complex fetching + +### Option B: New Database Table for Nav Pages + +```sql +CREATE TABLE community_nav_pages ( + id UUID PRIMARY KEY, + community_id VARCHAR(50), + nav_item_id VARCHAR(50), -- 'home', 'explore', etc. + space_config JSONB NOT NULL, + version INTEGER DEFAULT 1, + created_at TIMESTAMP DEFAULT NOW() +); +``` + +**Pros:** +- Simple database queries +- No encryption needed (public pages) +- Easy to version + +**Cons:** +- New table needed +- Duplicates Space structure + +### Option C: Use spaceRegistrations + Storage + +Store nav pages as Spaces in `spaceRegistrations` with `spaceType = 'navPage'`: +- Register in `spaceRegistrations` table +- Store config in Supabase Storage +- Fetch at build time + +**Pros:** +- Uses existing Space infrastructure +- Consistent with other Spaces +- Can reuse Space APIs + +**Cons:** +- Requires spaceRegistrations entry +- Need to handle Storage fetching + +## Recommended: Option C (spaceRegistrations + Storage) + +**Why:** +- ✅ Uses existing Space system +- ✅ Consistent architecture +- ✅ Can reuse Space loading logic +- ✅ No new tables needed + +## Migration Steps + +1. **Add 'navPage' to spaceType enum** +2. **Create Spaces for homePage/explorePage** + - Register in `spaceRegistrations` with `spaceType = 'navPage'` + - Store configs in Supabase Storage +3. **Update navigation configs** + - Add `spaceId` to home/explore nav items +4. **Update build-time fetching** + - Fetch Spaces based on nav items + - Build page configs from Spaces +5. **Remove homePage/explorePage from community_configs** + - Update schema + - Update seed script + +## Size Reduction + +**Before:** +- Config: ~29 KB +- homePage: 19.2 KB +- explorePage: 1.4 KB + +**After:** +- Config: ~8.4 KB (71% reduction!) +- Navigation: ~500 bytes (includes spaceId references) +- Spaces: Fetched separately, no size limit + +**Result:** Config easily fits in env vars or generated file! + +## Example: Updated Navigation Config + +```typescript +export const nounsNavigation: NavigationConfig = { + items: [ + { + id: 'home', + label: 'Home', + href: '/home', + icon: 'home', + spaceId: 'nouns-home-space-uuid' // ← References Space + }, + { + id: 'explore', + label: 'Explore', + href: '/explore', + icon: 'explore', + spaceId: 'nouns-explore-space-uuid' // ← References Space + }, + ], +}; +``` + +## Benefits Summary + +✅ **Solves E2BIG** - Config size reduced by 71% +✅ **Unified architecture** - Everything is Spaces +✅ **Navigation as source of truth** - Nav defines pages +✅ **Flexible** - Any nav item can be a Space +✅ **Reuses infrastructure** - Uses existing Space system +✅ **No breaking changes** - Can maintain backward compatibility + diff --git a/docs/_archived/database-config/NAVIGATION_SPACE_REFERENCE_IMPLEMENTATION.md b/docs/_archived/database-config/NAVIGATION_SPACE_REFERENCE_IMPLEMENTATION.md new file mode 100644 index 000000000..cdb95e1c1 --- /dev/null +++ b/docs/_archived/database-config/NAVIGATION_SPACE_REFERENCE_IMPLEMENTATION.md @@ -0,0 +1,357 @@ +# Navigation-Space Reference Implementation Plan + +## Architecture Overview + +**Key Insight:** Navigation items reference Spaces. Pages are fetched from Spaces at build time. + +``` +Navigation Config (in community_configs) + └── items: [ + { id: 'home', spaceId: 'uuid', ... }, + { id: 'explore', spaceId: 'uuid', ... } + ] + ↓ + Build Time: + ↓ + Fetch Spaces by spaceId + ↓ + Build page configs from Spaces +``` + +## Changes Required + +### 1. Add 'navPage' Space Type + +**File:** `src/common/types/spaceData.ts` + +```typescript +export const SPACE_TYPES = { + PROFILE: 'profile', + TOKEN: 'token', + PROPOSAL: 'proposal', + CHANNEL: 'channel', + NAV_PAGE: 'navPage', // ← NEW +} as const; +``` + +**Migration:** `supabase/migrations/YYYYMMDDHHMMSS_add_navpage_space_type.sql` + +```sql +-- Add navPage to spaceType constraint +ALTER TABLE "public"."spaceRegistrations" + DROP CONSTRAINT IF EXISTS valid_space_type; + +ALTER TABLE "public"."spaceRegistrations" + ADD CONSTRAINT valid_space_type CHECK ( + "spaceType" IN ('profile', 'token', 'proposal', 'channel', 'navPage') + ); +``` + +### 2. Update NavigationItem Interface + +**File:** `src/config/systemConfig.ts` + +```typescript +export interface NavigationItem { + id: string; + label: string; + href: string; + icon?: 'home' | 'explore' | 'notifications' | 'search' | 'space' | 'robot' | 'custom'; + openInNewTab?: boolean; + requiresAuth?: boolean; + spaceId?: string; // ← NEW: Optional reference to Space +} +``` + +### 3. Update Navigation Configs + +**File:** `src/config/nouns/nouns.navigation.ts` + +```typescript +export const nounsNavigation: NavigationConfig = { + logoTooltip: { + text: "wtf is nouns?", + href: "https://nouns.wtf", + }, + items: [ + { + id: 'home', + label: 'Home', + href: '/home', + icon: 'home', + spaceId: 'nouns-home-space-id' // ← Reference to Space + }, + { + id: 'explore', + label: 'Explore', + href: '/explore', + icon: 'explore', + spaceId: 'nouns-explore-space-id' // ← Reference to Space + }, + { + id: 'notifications', + label: 'Notifications', + href: '/notifications', + icon: 'notifications', + requiresAuth: true + // No spaceId - not a Space-based page + }, + ], + showMusicPlayer: true, + showSocials: true, +}; +``` + +### 4. Update Database Schema + +**Migration:** Remove homePage/explorePage columns + +```sql +-- Remove large page config columns +ALTER TABLE "public"."community_configs" + DROP COLUMN IF EXISTS "home_page_config", + DROP COLUMN IF EXISTS "explore_page_config"; + +-- Update function to exclude page configs +CREATE OR REPLACE FUNCTION "public"."get_active_community_config"( + p_community_id VARCHAR(50) +) +RETURNS JSONB +LANGUAGE plpgsql +SECURITY DEFINER +AS $$ +DECLARE + v_config JSONB; +BEGIN + SELECT jsonb_build_object( + 'brand', "brand_config", + 'assets', "assets_config", + 'theme', "theme_config", + 'community', "community_config", + 'fidgets', "fidgets_config", + 'navigation', "navigation_config", -- Contains spaceId references + 'ui', "ui_config" + ) + INTO v_config + FROM "public"."community_configs" + WHERE "community_id" = p_community_id + AND "is_active" = true + AND "is_published" = true + ORDER BY "version" DESC + LIMIT 1; + + RETURN v_config; +END; +$$; +``` + +### 5. Update Build-Time Config Generation + +**File:** `next.config.mjs` + +```javascript +async function generateConfigFile() { + // Fetch main config (now much smaller!) + const { data: config } = await supabase + .rpc('get_active_community_config', { p_community_id: community }) + .single(); + + if (!config) return; + + // Extract spaceIds from navigation items + const navItems = config.navigation?.items || []; + const spaceIds = navItems + .filter(item => item.spaceId) + .map(item => ({ navId: item.id, spaceId: item.spaceId })); + + // Fetch Spaces for nav items + const pageConfigs = {}; + for (const { navId, spaceId } of spaceIds) { + try { + // Fetch Space from spaceRegistrations + Storage + const space = await fetchSpaceBySpaceId(spaceId); + if (space) { + // Convert Space config to page config format + pageConfigs[navId] = convertSpaceToPageConfig(space); + } + } catch (error) { + console.warn(`⚠️ Failed to fetch Space ${spaceId} for nav item ${navId}:`, error.message); + } + } + + // Combine configs + const fullConfig = { + ...config, + pages: pageConfigs, // Add page configs + }; + + // Generate TypeScript file + const configFile = `// Auto-generated at build time +import { SystemConfig } from './systemConfig'; + +export const dbConfig: SystemConfig | null = ${JSON.stringify(fullConfig, null, 2)} as SystemConfig; +`; + + await writeFile('src/config/db-config.ts', configFile, 'utf-8'); +} + +async function fetchSpaceBySpaceId(spaceId: string) { + // Option 1: Fetch from spaceRegistrations + Storage + const { data: registration } = await supabase + .from('spaceRegistrations') + .select('*') + .eq('spaceId', spaceId) + .eq('spaceType', 'navPage') + .single(); + + if (!registration) return null; + + // Fetch Space config from Storage + const { data } = await supabase.storage + .from('spaces') + .download(`${spaceId}/tabs/default`); // Or fetch all tabs + + if (!data) return null; + + // Parse and return Space config + const fileData = JSON.parse(await data.text()); + return fileData; // Return SpaceConfig +} + +function convertSpaceToPageConfig(space: SpaceConfig): HomePageConfig { + // Convert Space config to HomePageConfig/ExplorePageConfig format + // This maps Space tabs to page tabs + return { + defaultTab: space.defaultTab || 'Home', + tabOrder: Object.keys(space.tabs || {}), + tabs: space.tabs || {}, + layout: { + defaultLayoutFidget: space.layout?.defaultLayoutFidget || 'grid', + gridSpacing: space.layout?.gridSpacing || 16, + theme: space.theme || {}, + }, + }; +} +``` + +### 6. Update Config Loader + +**File:** `src/config/index.ts` + +```typescript +export const loadSystemConfig = (): SystemConfig => { + const config = dbConfig || staticConfig; + + // Extract page configs from pages object (built from nav items) + const homePage = config.pages?.['home'] || staticConfig.homePage; + const explorePage = config.pages?.['explore'] || staticConfig.explorePage; + + return { + ...config, + homePage, // Map from pages['home'] + explorePage, // Map from pages['explore'] + }; +}; +``` + +## Space Storage Strategy + +### How Nav Pages Are Stored + +1. **Register in spaceRegistrations:** + ```sql + INSERT INTO spaceRegistrations ( + "spaceId", + "spaceName", + "spaceType", + "identityPublicKey", + "signature", + "timestamp" + ) VALUES ( + 'nouns-home-space-id', + 'nouns-home', + 'navPage', + 'system-identity-key', + 'signature', + NOW() + ); + ``` + +2. **Store config in Supabase Storage:** + - Path: `spaces/{spaceId}/tabs/{tabName}` + - Format: Same as other Spaces (SpaceConfig JSON) + +3. **Fetch at build time:** + - Query `spaceRegistrations` for `spaceType = 'navPage'` + - Download from Storage + - Parse and convert to page config format + +## Size Reduction + +**Before:** +- Config: ~29 KB +- homePage: 19.2 KB (66.5%) +- explorePage: 1.4 KB (4.8%) + +**After:** +- Config: ~8.4 KB (71% reduction!) +- Navigation: ~500 bytes (includes spaceId strings) +- Spaces: Fetched separately, no size limit + +**Result:** Config easily fits in env vars or generated file! + +## Migration Path + +1. **Add navPage spaceType** - Migration + TypeScript constants +2. **Create Spaces for existing pages** - Register homePage/explorePage as Spaces +3. **Update navigation configs** - Add spaceId to nav items +4. **Update build-time fetching** - Fetch Spaces based on nav items +5. **Remove page configs from schema** - Drop home_page_config/explore_page_config columns +6. **Update seed script** - Don't seed page configs, seed Spaces instead + +## Benefits + +✅ **Solves E2BIG** - Config size reduced by 71% +✅ **Unified architecture** - Everything is Spaces +✅ **Navigation as source of truth** - Nav defines what pages exist +✅ **Flexible** - Any nav item can reference a Space +✅ **Reuses infrastructure** - Uses existing Space system +✅ **No breaking changes** - Can maintain backward compatibility during migration + +## Example: Complete Flow + +### 1. Navigation Config (in DB) +```json +{ + "navigation": { + "items": [ + { "id": "home", "label": "Home", "href": "/home", "spaceId": "uuid-1" }, + { "id": "explore", "label": "Explore", "href": "/explore", "spaceId": "uuid-2" } + ] + } +} +``` + +### 2. Build Time +```javascript +// Fetch nav config → see spaceIds +// Fetch Spaces: uuid-1, uuid-2 +// Convert Spaces to page configs +// Generate: +{ + ...config, + pages: { + 'home': { /* Space config converted */ }, + 'explore': { /* Space config converted */ } + } +} +``` + +### 3. Runtime +```typescript +// Load config +const config = loadSystemConfig(); +// config.homePage comes from config.pages['home'] +// config.explorePage comes from config.pages['explore'] +``` + diff --git a/docs/NEXTJS_CONFIG_APPROACHES.md b/docs/_archived/database-config/NEXTJS_CONFIG_APPROACHES.md similarity index 100% rename from docs/NEXTJS_CONFIG_APPROACHES.md rename to docs/_archived/database-config/NEXTJS_CONFIG_APPROACHES.md diff --git a/docs/_archived/database-config/README.md b/docs/_archived/database-config/README.md new file mode 100644 index 000000000..4c079081c --- /dev/null +++ b/docs/_archived/database-config/README.md @@ -0,0 +1,43 @@ +# Archived Database Config Documentation + +This directory contains documentation files that were created during the exploration and planning phase of the database-backed configuration system. These files have been consolidated into: + +- `DATABASE_CONFIG_GUIDE.md` - Main architecture and overview guide +- `DATABASE_CONFIG_IMPLEMENTATION.md` - Detailed implementation plan +- `QUICK_START_IMPLEMENTATION.md` - Quick start guide +- `QUICK_START_TESTING.md` - Testing guide + +## Why These Were Archived + +These files contain: +- Exploration of different approaches (some we decided against) +- Outdated information (e.g., TypeScript file generation instead of env vars) +- Redundant information (consolidated into main docs) +- Early planning details (superseded by final implementation) + +## Files Archived + +- `DATABASE_CONFIG_MIGRATION_PLAN.md` - Original migration plan (had themes/pages in DB) +- `BUILD_TIME_CONFIG_SUMMARY.md` - Mentioned TS file generation (outdated) +- `E2BIG_SOLUTION.md` - Solution for E2BIG error (now use env vars) +- `UPDATED_IMPLEMENTATION_SUMMARY.md` - Summary with outdated TS file info +- `NAVIGATION_SPACE_REFERENCE_APPROACH.md` - Consolidated into main guide +- `NAVIGATION_SPACE_REFERENCE_IMPLEMENTATION.md` - Consolidated into main guide +- `SHARED_THEMES_APPROACH.md` - Consolidated into main guide +- `SPACE_REFERENCE_APPROACH.md` - Consolidated into main guide +- `SIMPLE_BUILD_TIME_CONFIG.md` - Exploration doc +- `NEXTJS_CONFIG_APPROACHES.md` - Exploration doc +- `CONFIG_DATABASE_SCHEMA_DETAILED.md` - Consolidated into main guide +- `ASSET_HANDLING_STRATEGY.md` - Consolidated (future phase) +- `ADMIN_ASSET_UPLOAD_STRATEGY.md` - Consolidated (future phase) +- `ASSET_PERFORMANCE_OPTIMIZATION.md` - Consolidated (future phase) +- `ASSETS_CONFIG_STORAGE_RELATIONSHIP.md` - Consolidated (future phase) + +## Current Approach + +See the main documentation files for the current, finalized approach: +- Configs stored in DB (~2.8 KB) +- Loaded at build time into `NEXT_PUBLIC_BUILD_TIME_CONFIG` env var +- Themes in shared file (`src/config/shared/themes.ts`) +- Pages as Spaces (referenced by navigation items) + diff --git a/docs/_archived/database-config/SHARED_THEMES_APPROACH.md b/docs/_archived/database-config/SHARED_THEMES_APPROACH.md new file mode 100644 index 000000000..5b52cbe84 --- /dev/null +++ b/docs/_archived/database-config/SHARED_THEMES_APPROACH.md @@ -0,0 +1,255 @@ +# Shared Themes Approach + +## Overview + +Move themes out of individual community configs into a shared file, since themes are reusable across communities with only minor customizations. + +## Current State + +- Each community has its own `{community}.theme.ts` file +- Themes are mostly identical (same structure, different values) +- Themes take up 5.5 KB (66.7% of remaining config) + +## Proposed Location Options + +### Option 1: `src/config/shared/themes.ts` (Recommended) + +**Structure:** +``` +src/config/ +├── shared/ +│ └── themes.ts # Shared theme definitions +├── nouns/ +│ └── nouns.theme.ts # Community-specific overrides (optional) +└── ... +``` + +**Pros:** +- ✅ Clear organization - shared configs in `shared/` folder +- ✅ Easy to find - obvious location +- ✅ Extensible - can add other shared configs later +- ✅ Separates shared from community-specific + +**Cons:** +- ⚠️ New directory structure + +### Option 2: `src/config/themes.ts` + +**Structure:** +``` +src/config/ +├── themes.ts # Shared themes at root +├── nouns/ +└── ... +``` + +**Pros:** +- ✅ Simple - no new directory +- ✅ Easy to import + +**Cons:** +- ⚠️ Mixes shared with community configs +- ⚠️ Less clear organization + +### Option 3: `src/common/config/themes.ts` + +**Structure:** +``` +src/common/ +├── config/ +│ └── themes.ts # Shared themes +└── ... +``` + +**Pros:** +- ✅ In `common/` (shared code) +- ✅ Separated from community configs + +**Cons:** +- ⚠️ New directory structure +- ⚠️ Mixes config with common utilities + +## Recommendation: `src/config/shared/themes.ts` + +**Why:** +1. **Clear organization** - `shared/` folder makes intent obvious +2. **Extensible** - Can add other shared configs (e.g., `shared/defaultFidgets.ts`) +3. **Consistent** - Keeps configs in config directory +4. **Easy imports** - `import { themes } from '@/config/shared/themes'` + +## Implementation Approach + +### Option A: Single Shared File (All Themes) + +Store all theme variants in one shared file: + +```typescript +// src/config/shared/themes.ts + +export const sharedThemes = { + default: { /* ... */ }, + nounish: { /* ... */ }, + gradientAndWave: { /* ... */ }, + colorBlobs: { /* ... */ }, + floatingShapes: { /* ... */ }, + imageParallax: { /* ... */ }, + shootingStar: { /* ... */ }, + squareGrid: { /* ... */ }, + tesseractPattern: { /* ... */ }, + retro: { /* ... */ }, +}; +``` + +**Pros:** +- ✅ Single source of truth +- ✅ Easy to maintain +- ✅ All communities use same themes + +**Cons:** +- ⚠️ No community customization +- ⚠️ Can't override specific themes per community + +### Option B: Shared Base + Community Overrides + +Store base themes in shared file, allow community-specific overrides: + +```typescript +// src/config/shared/themes.ts + +export const baseThemes = { + default: { /* ... */ }, + nounish: { /* ... */ }, + // ... all themes +}; + +// src/config/nouns/nouns.theme.ts + +import { baseThemes } from '../../shared/themes'; + +export const nounsTheme = { + ...baseThemes, + // Override specific themes if needed + default: { + ...baseThemes.default, + properties: { + ...baseThemes.default.properties, + musicURL: "https://...", // Nouns-specific music + }, + }, +}; +``` + +**Pros:** +- ✅ Shared base themes +- ✅ Allows community customization +- ✅ Best of both worlds + +**Cons:** +- ⚠️ More complex +- ⚠️ Need merge logic + +### Option C: Shared Themes + Community Theme Config + +Store themes in shared file, reference from community config: + +```typescript +// src/config/shared/themes.ts + +export const themes = { + default: { /* ... */ }, + nounish: { /* ... */ }, + // ... all themes +}; + +// src/config/nouns/nouns.theme.ts + +import { themes } from '../../shared/themes'; + +// Just export shared themes (or override if needed) +export const nounsTheme = themes; +``` + +**Pros:** +- ✅ Simplest approach +- ✅ Single source of truth +- ✅ Easy to use + +**Cons:** +- ⚠️ No community customization (but maybe that's fine?) + +## Recommended: Option C (Simple Shared Reference) + +**Why:** +- Themes are visual templates, not community-specific +- Communities can customize via theme editor at runtime +- Keeps config simple and maintainable + +## Database Storage + +**Option 1: Store in Database as Shared Resource** + +```sql +CREATE TABLE shared_themes ( + id UUID PRIMARY KEY, + theme_id VARCHAR(50) UNIQUE, -- 'default', 'nounish', etc. + theme_config JSONB NOT NULL, + version INTEGER DEFAULT 1, + created_at TIMESTAMP DEFAULT NOW() +); +``` + +**Option 2: Store in Code (Recommended for Now)** + +Keep themes in code, reference from config: +- Themes are code/templates, not data +- Easier to version control +- Can move to DB later if needed + +## Size Impact + +**Before:** +- theme: 5.5 KB (66.7% of config) + +**After:** +- theme: Removed from config (0 KB) +- Config size: ~2.8 KB (down from 8.3 KB) +- **Total reduction: 90%** (from 29 KB to 2.8 KB) + +## Migration Steps + +1. **Create `src/config/shared/themes.ts`** + - Move theme definitions from nouns.theme.ts + - Export as `themes` object + +2. **Update community configs** + - Change `nouns.theme.ts` to import from shared + - Update other communities similarly + +3. **Update SystemConfig interface** + - Keep `theme: ThemeConfig` in interface + - But it now references shared themes + +4. **Update database schema** + - Remove `theme_config` column (or keep as reference) + - Or store theme reference IDs + +5. **Update build-time config** + - Themes loaded from shared file, not DB + - Or fetch from DB if storing there + +## Final Config Size + +After removing homePage, explorePage, and theme: + +| Section | Size | Percentage | +|---------|------|------------| +| community | 1.4 KB | 50% | +| navigation | 0.5 KB | 18% | +| assets | 0.3 KB | 11% | +| brand | 0.2 KB | 7% | +| fidgets | 0.2 KB | 7% | +| ui | 0.2 KB | 7% | +| **TOTAL** | **~2.8 KB** | **100%** | + +**Result:** Config is now tiny! Easily fits in env vars or generated file. + diff --git a/docs/SIMPLE_BUILD_TIME_CONFIG.md b/docs/_archived/database-config/SIMPLE_BUILD_TIME_CONFIG.md similarity index 100% rename from docs/SIMPLE_BUILD_TIME_CONFIG.md rename to docs/_archived/database-config/SIMPLE_BUILD_TIME_CONFIG.md diff --git a/docs/SPACE_REFERENCE_APPROACH.md b/docs/_archived/database-config/SPACE_REFERENCE_APPROACH.md similarity index 100% rename from docs/SPACE_REFERENCE_APPROACH.md rename to docs/_archived/database-config/SPACE_REFERENCE_APPROACH.md diff --git a/docs/_archived/database-config/UPDATED_IMPLEMENTATION_SUMMARY.md b/docs/_archived/database-config/UPDATED_IMPLEMENTATION_SUMMARY.md new file mode 100644 index 000000000..c62f9d0fd --- /dev/null +++ b/docs/_archived/database-config/UPDATED_IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,147 @@ +# Updated Implementation Plan Summary + +## Key Architectural Changes + +### 1. **Navigation-Space Reference** +- `homePage` and `explorePage` removed from `community_configs` +- Navigation items reference Spaces via `spaceId` +- Pages fetched from Spaces at build time +- **Size reduction: 71%** (removes 20.6 KB) + +### 2. **Shared Themes** +- Themes moved to `src/config/shared/themes.ts` +- All communities use same shared themes +- Themes removed from `community_configs` +- **Size reduction: 66%** (removes 5.5 KB) + +### 3. **File-Based Config (Not Env Var)** +- Config generated as TypeScript file (`src/config/db-config.ts`) +- Avoids E2BIG error (env var size limits) +- No size restrictions + +## Final Config Structure + +### In Database (`community_configs`): +```json +{ + "brand_config": { /* 0.2 KB */ }, + "assets_config": { /* 0.3 KB */ }, + "community_config": { /* 1.4 KB */ }, + "fidgets_config": { /* 0.2 KB */ }, + "navigation_config": { /* 0.5 KB - includes spaceId references */ }, + "ui_config": { /* 0.2 KB */ } + // NO theme_config (in shared file) + // NO home_page_config (in Spaces) + // NO explore_page_config (in Spaces) +} +``` + +**Total: ~2.8 KB** (down from ~29 KB - **90% reduction!**) + +### In Code (`src/config/shared/themes.ts`): +```typescript +export const themes = { + default: { /* ... */ }, + nounish: { /* ... */ }, + // ... all 10 themes +}; +``` + +### In Spaces (via navigation): +- `homePage` → Space referenced by nav item `spaceId` +- `explorePage` → Space referenced by nav item `spaceId` +- Stored in `spaceRegistrations` with `spaceType = 'navPage'` +- Config stored in Supabase Storage + +## Updated Database Schema + +### `community_configs` Table: +```sql +CREATE TABLE community_configs ( + id UUID PRIMARY KEY, + community_id VARCHAR(50) UNIQUE, + brand_config JSONB, -- ✅ Kept + assets_config JSONB, -- ✅ Kept + community_config JSONB, -- ✅ Kept + fidgets_config JSONB, -- ✅ Kept + navigation_config JSONB, -- ✅ Kept (now includes spaceId) + ui_config JSONB, -- ✅ Kept + -- ❌ REMOVED: theme_config (in shared file) + -- ❌ REMOVED: home_page_config (in Spaces) + -- ❌ REMOVED: explore_page_config (in Spaces) +); +``` + +### `spaceRegistrations` Table: +```sql +-- Add navPage spaceType +ALTER TABLE spaceRegistrations + ADD CONSTRAINT valid_space_type CHECK ( + "spaceType" IN ('profile', 'token', 'proposal', 'channel', 'navPage') + ); +``` + +## Updated Build Process + +```javascript +// next.config.mjs + +1. Fetch main config from DB (small - ~2.8 KB) +2. Import shared themes from code +3. Extract spaceIds from navigation items +4. Fetch Spaces for nav items +5. Convert Spaces to page configs +6. Combine: config + themes + pages +7. Generate TypeScript file +``` + +## Updated Config Loader + +```typescript +// src/config/index.ts + +1. Try to import db-config.ts (generated file) +2. If exists: + - Use DB config + - Add shared themes + - Map pages['home'] → homePage + - Map pages['explore'] → explorePage +3. If not exists: + - Fall back to static configs +``` + +## Size Comparison + +| Stage | Config Size | Reduction | +|-------|-------------|-----------| +| **Original** | ~29 KB | - | +| **After removing pages** | ~8.3 KB | 71% | +| **After removing themes** | **~2.8 KB** | **90%** | + +## Migration Impact + +### Phase 1 (Database Schema): +- ✅ Create `community_configs` (without page/theme columns) +- ✅ Add `navPage` spaceType +- ✅ Seed configs (without themes/pages) + +### Phase 2 (Config Loading): +- ✅ Create `src/config/shared/themes.ts` +- ✅ Update community configs to import shared themes +- ✅ Fetch Spaces for nav items at build time +- ✅ Generate TypeScript file (not env var) + +### Phase 3+ (Admin/UI): +- ✅ Admin can edit config (smaller now) +- ✅ Admin can edit nav page Spaces +- ✅ Themes edited in code (shared file) + +## Benefits Summary + +✅ **Solves E2BIG** - Config now ~2.8 KB (well under limits) +✅ **Unified architecture** - Pages are Spaces +✅ **Shared themes** - Single source of truth +✅ **Navigation as source of truth** - Nav defines pages +✅ **Flexible** - Any nav item can reference a Space +✅ **Maintainable** - Clear separation of concerns + diff --git a/scripts/seed-community-configs.ts b/scripts/seed-community-configs.ts index f72453f63..3a96ab9c8 100644 --- a/scripts/seed-community-configs.ts +++ b/scripts/seed-community-configs.ts @@ -142,18 +142,16 @@ async function seedConfig(communityId: string, config: any) { console.log(`\n📦 Seeding config for: ${communityId}`); // Transform config to match database schema + // Note: Schema excludes theme_config, home_page_config, explore_page_config + // - Themes are in src/config/shared/themes.ts + // - Pages are stored as Spaces (navPage type) const dbConfig = { community_id: communityId, - version: 1, - is_active: true, is_published: true, brand_config: config.brand, assets_config: config.assets, - theme_config: config.theme, community_config: config.community, fidgets_config: config.fidgets, - home_page_config: config.homePage, - explore_page_config: config.explorePage, navigation_config: config.navigation || null, ui_config: config.ui || null, }; @@ -209,8 +207,20 @@ async function main() { process.exit(1); } - if (testConfig && testConfig.brand) { - console.log(`✅ Function works! Retrieved config for: ${testConfig.brand.displayName}`); + // Type assertion: function returns JSONB with brand, assets, community, etc. + type ConfigFromDB = { + brand?: { displayName?: string }; + assets?: unknown; + community?: unknown; + fidgets?: unknown; + navigation?: unknown; + ui?: unknown; + }; + + const config = testConfig as ConfigFromDB | null; + + if (config && config.brand && config.brand.displayName) { + console.log(`✅ Function works! Retrieved config for: ${config.brand.displayName}`); } else { console.error('❌ Function returned invalid config'); process.exit(1); diff --git a/scripts/seed-navpage-spaces.ts b/scripts/seed-navpage-spaces.ts new file mode 100755 index 000000000..6cc860c27 --- /dev/null +++ b/scripts/seed-navpage-spaces.ts @@ -0,0 +1,252 @@ +#!/usr/bin/env tsx +/** + * Seed script to upload navPage space configs to Supabase Storage + * + * This script: + * 1. Reads space registrations from the database (created by seed.sql) + * 2. Imports page configs (homePage, explorePage) from TypeScript configs + * 3. Converts page configs to SpaceConfig format + * 4. Uploads each tab to Supabase Storage as SignedFile + * 5. Uploads tabOrder to Supabase Storage + * + * Note: Not all communities have explore pages (e.g., Clanker only has home) + * + * Usage: + * tsx scripts/seed-navpage-spaces.ts + * + * Requires: + * - NEXT_PUBLIC_SUPABASE_URL + * - SUPABASE_SERVICE_ROLE_KEY + * + * Run this AFTER running seed.sql to upload the actual space config files. + */ + +import { createClient } from '@supabase/supabase-js'; +import stringify from 'fast-json-stable-stringify'; +import moment from 'moment'; +import { SignedFile } from '../src/common/lib/signedFiles'; +import { SpaceConfig } from '../src/app/(spaces)/Space'; + +// Import page configs +import { nounsHomePage } from '../src/config/nouns/nouns.home'; +import { nounsExplorePage } from '../src/config/nouns/nouns.explore'; +import { clankerHomePage } from '../src/config/clanker/clanker.home'; + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; +const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + +if (!supabaseUrl || !supabaseKey) { + console.error('❌ Missing required environment variables:'); + console.error(' NEXT_PUBLIC_SUPABASE_URL'); + console.error(' SUPABASE_SERVICE_ROLE_KEY'); + process.exit(1); +} + +const supabase = createClient(supabaseUrl, supabaseKey); + +/** + * Creates a SignedFile wrapper for system-generated files + * + * Note: These files are stored unencrypted (isEncrypted: false) so they can be + * read by anyone. The decryptEncryptedSignedFile function handles unencrypted + * files by returning fileData directly without attempting decryption. + * Signature validation is only performed on write/update, not read, so the + * placeholder signature is acceptable. + */ +function createSystemSignedFile(fileData: string): SignedFile { + return { + fileData, + fileType: 'json', + isEncrypted: false, + timestamp: moment().toISOString(), + publicKey: 'nounspace', + signature: 'not applicable, machine generated file', + }; +} + +/** + * Creates a SignedFile for tab order + */ +function createTabOrderSignedFile(spaceId: string, tabOrder: string[]): SignedFile { + const tabOrderData = { + spaceId, + timestamp: moment().toISOString(), + tabOrder, + publicKey: 'nounspace', + signature: 'not applicable, machine generated file', + }; + return createSystemSignedFile(stringify(tabOrderData)); +} + +/** + * Uploads a single tab config to Supabase Storage + */ +async function uploadTab( + spaceId: string, + tabName: string, + tabConfig: SpaceConfig, +): Promise { + const signedFile = createSystemSignedFile(stringify(tabConfig)); + const filePath = `${spaceId}/tabs/${tabName}`; + + const { error } = await supabase.storage + .from('spaces') + .upload(filePath, new Blob([stringify(signedFile)], { type: 'application/json' }), { + upsert: true, + }); + + if (error) { + console.error(` ❌ Failed to upload tab ${tabName}:`, error.message); + return false; + } + + console.log(` ✅ Uploaded tab: ${tabName}`); + return true; +} + +/** + * Uploads tab order to Supabase Storage + */ +async function uploadTabOrder(spaceId: string, tabOrder: string[]): Promise { + const signedFile = createTabOrderSignedFile(spaceId, tabOrder); + const filePath = `${spaceId}/tabOrder`; + + const { error } = await supabase.storage + .from('spaces') + .upload(filePath, new Blob([stringify(signedFile)], { type: 'application/json' }), { + upsert: true, + }); + + if (error) { + console.error(` ❌ Failed to upload tab order:`, error.message); + return false; + } + + console.log(` ✅ Uploaded tab order: [${tabOrder.join(', ')}]`); + return true; +} + +/** + * Gets spaceId from database by spaceName + */ +async function getSpaceId(spaceName: string): Promise { + const { data, error } = await supabase + .from('spaceRegistrations') + .select('spaceId') + .eq('spaceName', spaceName) + .eq('spaceType', 'navPage') + .single(); + + if (error || !data) { + console.error(` ❌ Space not found: ${spaceName}`, error?.message); + return null; + } + + return data.spaceId; +} + +/** + * Type for page configs with tabs - accepts any structure where tabs contain SpaceConfig-like objects + * The actual configs may have extra properties that we'll extract at runtime + */ +type PageConfigWithSpaceTabs = { + defaultTab: string; + tabOrder: string[]; + tabs: Record; // Tabs contain SpaceConfig properties (and possibly extras) +}; + +/** + * Uploads a page config (homePage or explorePage) as a Space + * Extracts SpaceConfig from tabs which may have additional properties + */ +async function uploadPageConfig( + spaceName: string, + pageConfig: PageConfigWithSpaceTabs, +): Promise { + console.log(`\n📦 Uploading space: ${spaceName}`); + + const spaceId = await getSpaceId(spaceName); + if (!spaceId) { + return false; + } + + console.log(` 📍 Space ID: ${spaceId}`); + + // Upload each tab + const tabNames = Object.keys(pageConfig.tabs); + console.log(` 📄 Uploading ${tabNames.length} tabs...`); + + const tabResults = await Promise.all( + tabNames.map((tabName) => { + const tabConfig = pageConfig.tabs[tabName]; + // Tabs are already SpaceConfig objects (they may have extra properties like name/displayName) + // Extract just the SpaceConfig properties for upload + const spaceConfig: SpaceConfig = { + fidgetInstanceDatums: tabConfig.fidgetInstanceDatums, + layoutID: tabConfig.layoutID, + layoutDetails: tabConfig.layoutDetails, + isEditable: tabConfig.isEditable ?? false, + fidgetTrayContents: tabConfig.fidgetTrayContents, + theme: tabConfig.theme, + timestamp: tabConfig.timestamp, + tabNames: tabConfig.tabNames, + fid: tabConfig.fid, + }; + return uploadTab(spaceId, tabName, spaceConfig); + }), + ); + + const allTabsUploaded = tabResults.every((result) => result); + if (!allTabsUploaded) { + console.error(` ❌ Some tabs failed to upload for ${spaceName}`); + return false; + } + + // Upload tab order + const tabOrderUploaded = await uploadTabOrder(spaceId, pageConfig.tabOrder); + if (!tabOrderUploaded) { + console.error(` ❌ Failed to upload tab order for ${spaceName}`); + return false; + } + + console.log(` ✅ Successfully uploaded ${spaceName}`); + return true; +} + +/** + * Main seeding function + */ +async function main() { + console.log('🚀 Starting navPage space config seeding...\n'); + + // Map of spaceName -> page config + // Note: Using PageConfigWithSpaceTabs type that accepts configs with SpaceConfig tabs + // Note: Clanker doesn't have an explore page, only home + const spaceConfigs: Array<{ spaceName: string; config: PageConfigWithSpaceTabs }> = [ + { spaceName: 'nouns-home', config: nounsHomePage }, + { spaceName: 'nouns-explore', config: nounsExplorePage }, + { spaceName: 'clanker-home', config: clankerHomePage }, + ]; + + const results = await Promise.allSettled( + spaceConfigs.map(({ spaceName, config }) => uploadPageConfig(spaceName, config)), + ); + + const successCount = results.filter((r) => r.status === 'fulfilled' && r.value).length; + const failCount = results.length - successCount; + + console.log(`\n📊 Results: ${successCount} succeeded, ${failCount} failed`); + + if (failCount > 0) { + console.error('\n❌ Some spaces failed to seed. Check errors above.'); + process.exit(1); + } + + console.log('\n✅ All navPage spaces seeded successfully!'); +} + +main().catch((error) => { + console.error('❌ Fatal error:', error); + process.exit(1); +}); + diff --git a/src/app/home/[tabname]/page.tsx b/src/app/[navSlug]/[[...tabName]]/NavPageClient.tsx similarity index 55% rename from src/app/home/[tabname]/page.tsx rename to src/app/[navSlug]/[[...tabName]]/NavPageClient.tsx index bf95e32c7..855590188 100644 --- a/src/app/home/[tabname]/page.tsx +++ b/src/app/[navSlug]/[[...tabName]]/NavPageClient.tsx @@ -1,30 +1,35 @@ "use client"; import React, { useEffect } from "react"; -import { useParams } from "next/navigation"; import { useAppStore } from "@/common/data/stores/app"; import SpacePage, { SpacePageArgs } from "@/app/(spaces)/SpacePage"; import { SpaceConfig } from "@/app/(spaces)/Space"; import TabBar from "@/common/components/organisms/TabBar"; import { useMiniKit } from "@coinbase/onchainkit/minikit"; +import type { HomePageConfig, ExplorePageConfig } from "@/config/systemConfig"; -import { useSystemConfig } from "@/common/lib/hooks/useSystemConfig"; +type PageConfig = HomePageConfig | ExplorePageConfig; -const getTabConfig = (tabName: string, config: any): SpaceConfig => { - return config.homePage.tabs[tabName] || config.homePage.tabs[config.homePage.defaultTab]; +type NavPageClientProps = { + pageConfig: PageConfig; + activeTabName: string; + navSlug: string; }; -const Home = () => { - const params = useParams(); - const config = useSystemConfig(); +const getTabConfig = (tabName: string, config: PageConfig): SpaceConfig => { + return (config.tabs[tabName] || config.tabs[config.defaultTab]) as SpaceConfig; +}; + +const NavPageClient: React.FC = ({ + pageConfig, + activeTabName, + navSlug, +}) => { const { setCurrentTabName, currentTabName } = useAppStore((state) => ({ setCurrentTabName: state.currentSpace.setCurrentTabName, currentTabName: state.currentSpace.currentTabName, })); - // Tab ordering for homepage from configuration - const tabOrdering = config.homePage.tabOrder; - const { setFrameReady, isFrameReady } = useMiniKit(); useEffect(() => { @@ -32,25 +37,20 @@ const Home = () => { }, [isFrameReady, setFrameReady]); useEffect(() => { - const newTabName = params?.tabname - ? decodeURIComponent(params.tabname as string) - : config.homePage.defaultTab; - - setCurrentTabName(newTabName); - }, [params?.tabname, setCurrentTabName, config.homePage.defaultTab]); - + setCurrentTabName(activeTabName); + }, [activeTabName, setCurrentTabName]); const tabBar = ( `/home/${tab}`} + getSpacePageUrl={(tab) => `/${navSlug}/${encodeURIComponent(tab)}`} inHomebase={false} - currentTab={currentTabName ?? config.homePage.defaultTab} - tabList={tabOrdering} - defaultTab={config.homePage.defaultTab} + currentTab={currentTabName ?? activeTabName} + tabList={pageConfig.tabOrder} + defaultTab={pageConfig.defaultTab} inEditMode={false} updateTabOrder={async () => Promise.resolve()} deleteTab={async () => Promise.resolve()} - createTab={async () => Promise.resolve({ tabName: currentTabName ?? config.homePage.defaultTab })} + createTab={async () => Promise.resolve({ tabName: currentTabName ?? activeTabName })} renameTab={async () => Promise.resolve(void 0)} commitTab={async () => Promise.resolve()} commitTabOrder={async () => Promise.resolve()} @@ -59,7 +59,7 @@ const Home = () => { ); const args: SpacePageArgs = { - config: getTabConfig(currentTabName ?? config.homePage.defaultTab, config) as SpaceConfig, + config: getTabConfig(currentTabName ?? activeTabName, pageConfig) as SpaceConfig, saveConfig: async () => {}, commitConfig: async () => {}, resetConfig: async () => {}, @@ -67,7 +67,8 @@ const Home = () => { showFeedOnMobile: false, }; - return ; + return ; }; -export default Home; +export default NavPageClient; + diff --git a/src/app/[navSlug]/[[...tabName]]/page.tsx b/src/app/[navSlug]/[[...tabName]]/page.tsx new file mode 100644 index 000000000..f0fd1aeb6 --- /dev/null +++ b/src/app/[navSlug]/[[...tabName]]/page.tsx @@ -0,0 +1,265 @@ +import React from "react"; +import { notFound, redirect } from "next/navigation"; +import { loadSystemConfig } from "@/config"; +import NavPageClient from "./NavPageClient"; +import { createClient } from '@supabase/supabase-js'; +import { SignedFile } from "@/common/lib/signedFiles"; +import type { HomePageConfig, ExplorePageConfig } from "@/config/systemConfig"; + +type PageConfig = HomePageConfig | ExplorePageConfig; + +/** + * Convert a Space stored in storage to a PageConfig + * Returns null if Supabase credentials are not available or if the space can't be loaded + */ +async function loadSpaceAsPageConfig(spaceId: string): Promise { + // Check if Supabase credentials are available + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = process.env.SUPABASE_SERVICE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY; + + if (!supabaseUrl || !supabaseKey) { + // Skip during build if credentials aren't available - will render at runtime + return null; + } + + // Double-check that both are strings (not undefined/null) + if (typeof supabaseUrl !== 'string' || typeof supabaseKey !== 'string') { + return null; + } + + try { + // Create Supabase client with checked credentials + const supabase = createClient(supabaseUrl, supabaseKey); + + // Fetch tab order + const { data: tabOrderData, error: tabOrderError } = await supabase.storage + .from('spaces') + .download(`${spaceId}/tabOrder`); + + if (tabOrderError || !tabOrderData) { + console.warn(`Failed to load tabOrder for space ${spaceId}:`, tabOrderError); + return null; + } + + const tabOrderFile = JSON.parse(await tabOrderData.text()) as SignedFile; + const tabOrderObj = JSON.parse(tabOrderFile.fileData) as { tabOrder: string[] }; + const tabOrder = tabOrderObj.tabOrder || []; + + // Fetch each tab config + const tabs: Record = {}; + for (const tabName of tabOrder) { + try { + const { data: tabData, error: tabError } = await supabase.storage + .from('spaces') + .download(`${spaceId}/tabs/${tabName}`); + + if (tabError || !tabData) { + console.warn(`Failed to load tab ${tabName} for space ${spaceId}:`, tabError); + continue; + } + + const tabFile = JSON.parse(await tabData.text()) as SignedFile; + const tabConfig = JSON.parse(tabFile.fileData); + tabs[tabName] = tabConfig; + } catch (error) { + console.warn(`Error parsing tab ${tabName} for space ${spaceId}:`, error); + } + } + + if (Object.keys(tabs).length === 0) { + return null; + } + + // Reconstruct PageConfig format + return { + defaultTab: tabOrder[0] || 'Home', + tabOrder, + tabs, + layout: { + defaultLayoutFidget: 'grid', + gridSpacing: 16, + theme: { + background: '#ffffff', + fidgetBackground: '#ffffff', + font: 'Inter', + fontColor: '#000000', + }, + }, + } as PageConfig; + } catch (error) { + console.error(`Error loading space ${spaceId} as page config:`, error); + return null; + } +} + +export async function generateStaticParams() { + try { + const config = loadSystemConfig(); + const navItems = config.navigation?.items || []; + const params: Array<{ navSlug: string; tabName?: string[] }> = []; + + // Check if Supabase credentials are available for space loading + const hasSupabaseCredentials = !!( + process.env.NEXT_PUBLIC_SUPABASE_URL && + (process.env.SUPABASE_SERVICE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY) + ); + + // For each navigation item, generate params for all its tabs + // Note: We don't generate params for base path (no tabName) - it will redirect at runtime + // Note: If Supabase credentials aren't available, we skip spaces and only generate for legacy configs + for (const item of navItems) { + if (!item.href?.startsWith('/')) continue; + + const navSlug = item.href.slice(1); // Remove leading '/' + + // If nav item has a spaceId and we have credentials, try to load from storage + if (item.spaceId && hasSupabaseCredentials) { + try { + const pageConfig = await loadSpaceAsPageConfig(item.spaceId); + if (pageConfig) { + // Generate params for each tab + for (const tabName of pageConfig.tabOrder) { + params.push({ navSlug, tabName: [tabName] }); + } + } + // If loadSpaceAsPageConfig returns null (error loading), skip static generation + // The page will render at runtime instead + } catch (error) { + // Skip this space if there's an error - will render at runtime + console.warn(`Skipping static generation for ${navSlug} (space ${item.spaceId}):`, error); + } + } else if (!item.spaceId) { + // Fallback: check legacy configs for tabs (when no spaceId) + if (navSlug === 'home' && config.homePage) { + for (const tabName of config.homePage.tabOrder) { + params.push({ navSlug, tabName: [tabName] }); + } + } else if (navSlug === 'explore' && config.explorePage) { + for (const tabName of config.explorePage.tabOrder) { + params.push({ navSlug, tabName: [tabName] }); + } + } + } + // If item has spaceId but no credentials, skip static generation - will render at runtime + } + + return params; + } catch (error) { + // If anything fails in generateStaticParams, return empty array + // Pages will render at runtime instead + console.warn('Error in generateStaticParams, skipping static generation:', error); + return []; + } +} + +export default async function NavPage({ + params, +}: { + params: Promise<{ navSlug: string; tabName?: string[] }>; +}) { + const { navSlug, tabName } = await params; + + // Early rejection for known non-nav routes (Next.js will match more specific routes first, + // but this adds an extra safety check) + const reservedRoutes = ['api', 'notifications', 'privacy', 'terms', 'pwa', 'manifest', '.well-known']; + if (reservedRoutes.includes(navSlug)) { + notFound(); + } + + const config = loadSystemConfig(); + + // Find navigation item by href + const navItems = config.navigation?.items || []; + const navItem = navItems.find(item => item.href === `/${navSlug}`); + + if (!navItem) { + notFound(); + } + + // If no tab name provided, redirect to default tab + if (!tabName || tabName.length === 0) { + let defaultTab: string; + + // If nav item has a spaceId, load default tab from storage + if (navItem.spaceId) { + const pageConfig = await loadSpaceAsPageConfig(navItem.spaceId); + if (pageConfig) { + defaultTab = encodeURIComponent(pageConfig.defaultTab); + redirect(`/${navSlug}/${defaultTab}`); + return null; + } + } + + // Fallback: check legacy configs + if (navSlug === 'home' && config.homePage) { + defaultTab = encodeURIComponent(config.homePage.defaultTab); + redirect(`/${navSlug}/${defaultTab}`); + return null; + } + + if (navSlug === 'explore' && config.explorePage) { + defaultTab = encodeURIComponent(config.explorePage.defaultTab); + redirect(`/${navSlug}/${defaultTab}`); + return null; + } + + notFound(); + } + + // Tab name provided, render the page + const activeTabName = decodeURIComponent(tabName[0]); + + // If nav item has a spaceId, load from storage + if (navItem.spaceId) { + const pageConfig = await loadSpaceAsPageConfig(navItem.spaceId); + if (!pageConfig) { + notFound(); + } + + // Validate tab exists + if (!pageConfig.tabs[activeTabName]) { + notFound(); + } + + return ( + + ); + } + + // Fallback: check if it's a legacy homePage/explorePage + // (for backward compatibility during transition) + if (navSlug === 'home' && config.homePage) { + if (!config.homePage.tabs[activeTabName]) { + notFound(); + } + + return ( + + ); + } + + if (navSlug === 'explore' && config.explorePage) { + if (!config.explorePage.tabs[activeTabName]) { + notFound(); + } + + return ( + + ); + } + + notFound(); +} + diff --git a/src/app/explore/[slug]/page.tsx b/src/app/explore/[slug]/page.tsx deleted file mode 100644 index 80b3e8e8d..000000000 --- a/src/app/explore/[slug]/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from "react"; -import { notFound } from "next/navigation"; - -import { loadSystemConfig } from "@/config"; -import ExploreTabPage from "../ExploreTabPage"; - -export async function generateStaticParams() { - const config = loadSystemConfig(); - const tabParams = config.explorePage.tabOrder.map((tab) => ({ slug: tab })); - return tabParams; -} - -export default async function ExploreEntry({ - params, -}: { - params: Promise<{ slug: string }>; -}) { - const config = loadSystemConfig(); - const { slug } = await params; - const tabName = decodeURIComponent(slug); - - if (!config.explorePage.tabs[tabName]) { - notFound(); - } - - return ; -} diff --git a/src/app/explore/page.tsx b/src/app/explore/page.tsx deleted file mode 100644 index de09483c8..000000000 --- a/src/app/explore/page.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { redirect } from "next/navigation"; -import { loadSystemConfig } from "@/config"; - -export default function ExploreRootRedirect() { - const config = loadSystemConfig(); - const defaultTab = encodeURIComponent(config.explorePage.defaultTab); - redirect(`/explore/${defaultTab}`); -} diff --git a/src/app/home/page.tsx b/src/app/home/page.tsx deleted file mode 100644 index 82fd600a5..000000000 --- a/src/app/home/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { redirect } from "next/navigation"; -import { loadSystemConfig } from "@/config"; - -export default function HomeBaseRedirect() { - const config = loadSystemConfig(); - const tab = encodeURIComponent(config.homePage.defaultTab); - redirect(`/home/${tab}`); -} - - diff --git a/src/app/page.tsx b/src/app/page.tsx index 89ca15b93..3805e83d6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,8 +3,17 @@ import { loadSystemConfig } from "@/config"; export default function RootRedirect() { const config = loadSystemConfig(); - const tab = encodeURIComponent(config.homePage.defaultTab); - redirect(`/home/${tab}`); + + // If homePage exists (legacy config), redirect to its default tab + if (config.homePage?.defaultTab) { + const tab = encodeURIComponent(config.homePage.defaultTab); + redirect(`/home/${tab}`); + return null; + } + + // Otherwise, redirect to /home and let the navigation handler figure out the default tab + redirect('/home'); + return null; } diff --git a/src/app/pwa/page.tsx b/src/app/pwa/page.tsx index 441c33cfd..ad7992a8d 100644 --- a/src/app/pwa/page.tsx +++ b/src/app/pwa/page.tsx @@ -3,18 +3,9 @@ import React, { useState, useEffect } from 'react' import { subscribeUser, unsubscribeUser, sendNotification } from './actions' -function urlBase64ToUint8Array(base64String: string): Uint8Array { - const padding = '='.repeat((4 - (base64String.length % 4)) % 4) - const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/') - - const rawData = window.atob(base64) - const outputArray = new Uint8Array(rawData.length) - - for (let i = 0; i < rawData.length; ++i) { - outputArray[i] = rawData.charCodeAt(i) - } - return outputArray -} +// Opt out of static generation - this page is client-only and uses browser APIs +// Note: revalidate is not needed for client components +export const dynamic = 'force-dynamic' function PushNotificationManager() { const [isSupported, setIsSupported] = useState(false) @@ -23,6 +14,8 @@ function PushNotificationManager() { const [userId, setUserId] = useState(null) useEffect(() => { + if (typeof window === 'undefined') return; // Guard against SSR + if ('serviceWorker' in navigator && 'PushManager' in window) { setIsSupported(true) registerServiceWorker() @@ -46,6 +39,23 @@ function PushNotificationManager() { } } + function urlBase64ToUint8Array(base64String: string): Uint8Array { + const padding = '='.repeat((4 - (base64String.length % 4)) % 4) + const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/') + + if (typeof window === 'undefined') { + throw new Error('window is not available'); + } + + const rawData = window.atob(base64) + const outputArray = new Uint8Array(rawData.length) + + for (let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i) + } + return outputArray + } + async function subscribeToPush() { try { const registration = await navigator.serviceWorker.ready @@ -116,6 +126,8 @@ function InstallPrompt() { const [isStandalone, setIsStandalone] = useState(false) useEffect(() => { + if (typeof window === 'undefined') return; // Guard against SSR + setIsIOS(/iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream) setIsStandalone(window.matchMedia('(display-mode: standalone)').matches) }, []) @@ -146,6 +158,17 @@ function InstallPrompt() { } export default function Page() { + // Client-side only - browser APIs required + const [isClient, setIsClient] = React.useState(false); + + React.useEffect(() => { + setIsClient(true); + }, []); + + if (!isClient) { + return null; // Don't render anything during SSR/prerender + } + return (
diff --git a/src/config/index.ts b/src/config/index.ts index b33231ef2..2317a3651 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -16,11 +16,17 @@ export const loadSystemConfig = (): SystemConfig => { const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; if (buildTimeConfig) { try { - const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; + const dbConfig = JSON.parse(buildTimeConfig) as any; // Validate config structure if (dbConfig && dbConfig.brand && dbConfig.assets) { console.log('✅ Using config from database'); - return dbConfig; + // Map pages object to homePage/explorePage for backward compatibility + const mappedConfig: SystemConfig = { + ...dbConfig, + homePage: dbConfig.pages?.['home'] || dbConfig.homePage || null, + explorePage: dbConfig.pages?.['explore'] || dbConfig.explorePage || null, + }; + return mappedConfig as SystemConfig; } else { console.warn('⚠️ Invalid config structure from DB, falling back to static'); } diff --git a/src/config/systemConfig.ts b/src/config/systemConfig.ts index f2a9d1aaf..6b94e392d 100644 --- a/src/config/systemConfig.ts +++ b/src/config/systemConfig.ts @@ -30,10 +30,13 @@ export interface SystemConfig { theme: ThemeConfig; community: CommunityConfig; fidgets: FidgetConfig; - homePage: HomePageConfig; - explorePage: ExplorePageConfig; + homePage: HomePageConfig | null; // Nullable for navigation-space approach + explorePage: ExplorePageConfig | null; // Nullable for navigation-space approach navigation?: NavigationConfig; ui?: UIConfig; + pages?: { + [key: string]: HomePageConfig | ExplorePageConfig; + }; // For database config structure } export interface UIConfig { @@ -174,6 +177,7 @@ export interface NavigationItem { icon?: 'home' | 'explore' | 'notifications' | 'search' | 'space' | 'robot' | 'custom'; openInNewTab?: boolean; requiresAuth?: boolean; + spaceId?: string; // Optional reference to Space for page content (navPage type) } export interface TabConfig { diff --git a/supabase/migrations/20251129172847_create_community_configs.sql b/supabase/migrations/20251129172847_create_community_configs.sql index 6473e2f0d..e24067a77 100644 --- a/supabase/migrations/20251129172847_create_community_configs.sql +++ b/supabase/migrations/20251129172847_create_community_configs.sql @@ -2,9 +2,6 @@ CREATE TABLE IF NOT EXISTS "public"."community_configs" ( "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), "community_id" VARCHAR(50) NOT NULL UNIQUE, - "is_active" BOOLEAN DEFAULT true, - "version" INTEGER DEFAULT 1, - "created_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "brand_config" JSONB NOT NULL, "assets_config" JSONB NOT NULL, @@ -17,7 +14,7 @@ CREATE TABLE IF NOT EXISTS "public"."community_configs" ( -- Create indexes CREATE INDEX IF NOT EXISTS "idx_community_configs_community_id" ON "public"."community_configs"("community_id"); -CREATE INDEX IF NOT EXISTS "idx_community_configs_active" ON "public"."community_configs"("is_active") WHERE "is_active" = true; +CREATE INDEX IF NOT EXISTS "idx_community_configs_published" ON "public"."community_configs"("is_published") WHERE "is_published" = true; -- Create function to get active community config (excludes themes/pages) CREATE OR REPLACE FUNCTION "public"."get_active_community_config"( @@ -41,9 +38,7 @@ BEGIN INTO v_config FROM "public"."community_configs" WHERE "community_id" = p_community_id - AND "is_active" = true AND "is_published" = true - ORDER BY "version" DESC LIMIT 1; RETURN v_config; diff --git a/supabase/migrations/20251129172848_add_navpage_space_type.sql b/supabase/migrations/20251129172848_add_navpage_space_type.sql index 990d5e165..fc38ed342 100644 --- a/supabase/migrations/20251129172848_add_navpage_space_type.sql +++ b/supabase/migrations/20251129172848_add_navpage_space_type.sql @@ -2,12 +2,14 @@ -- This allows navigation items to reference Spaces for pages like homePage/explorePage -- Update the spaceType constraint to include 'navPage' -ALTER TABLE "public"."spaceRegistrations" -DROP CONSTRAINT IF EXISTS "spaceRegistrations_spaceType_check"; +-- Note: The constraint is named 'valid_space_type' (created in earlier migrations) +ALTER TABLE "public"."spaceRegistrations" + DROP CONSTRAINT IF EXISTS valid_space_type; ALTER TABLE "public"."spaceRegistrations" -ADD CONSTRAINT "spaceRegistrations_spaceType_check" -CHECK ("spaceType" IN ('profile', 'token', 'proposal', 'channel', 'navPage')); + ADD CONSTRAINT valid_space_type CHECK ( + "spaceType" IN ('profile', 'token', 'proposal', 'channel', 'navPage') + ); -- Add comment explaining navPage usage COMMENT ON COLUMN "public"."spaceRegistrations"."spaceType" IS diff --git a/supabase/seed.sql b/supabase/seed.sql index bcb06376b..a7d7c08b2 100644 --- a/supabase/seed.sql +++ b/supabase/seed.sql @@ -10,12 +10,83 @@ ON CONFLICT ("id") DO NOTHING; -- Seed community configs (without themes/pages - those are in shared file and Spaces) -- Note: Themes are in src/config/shared/themes.ts -- Note: Pages (homePage/explorePage) are stored as Spaces with spaceType='navPage' +-- +-- IMPORTANT: After seeding, you must upload space configs to Supabase Storage. +-- The spaceRegistrations are created above, but the actual space config files +-- need to be uploaded to the 'spaces' bucket. +-- +-- Run this script AFTER seed.sql completes: +-- tsx scripts/seed-navpage-spaces.ts +-- +-- This script will: +-- 1. Read spaceRegistrations from database (created above) +-- 2. Import page configs from TypeScript (nounsHomePage, nounsExplorePage, etc.) +-- 3. Upload each tab as {spaceId}/tabs/{tabName} +-- 4. Upload tabOrder as {spaceId}/tabOrder + +-- Create navPage spaces for each community (system-owned, fid=NULL) +-- These must be created BEFORE community_configs so they can be referenced + +-- Nouns home page space +INSERT INTO "public"."spaceRegistrations" ( + "spaceId", + "fid", + "spaceName", + "spaceType", + "identityPublicKey", + "signature", + "timestamp" +) VALUES ( + gen_random_uuid(), + NULL, + 'nouns-home', + 'navPage', + 'system', + 'system-seed', + now() +) ON CONFLICT DO NOTHING; + +-- Nouns explore page space +INSERT INTO "public"."spaceRegistrations" ( + "spaceId", + "fid", + "spaceName", + "spaceType", + "identityPublicKey", + "signature", + "timestamp" +) VALUES ( + gen_random_uuid(), + NULL, + 'nouns-explore', + 'navPage', + 'system', + 'system-seed', + now() +) ON CONFLICT DO NOTHING; + +-- Clanker home page space +INSERT INTO "public"."spaceRegistrations" ( + "spaceId", + "fid", + "spaceName", + "spaceType", + "identityPublicKey", + "signature", + "timestamp" +) VALUES ( + gen_random_uuid(), + NULL, + 'clanker-home', + 'navPage', + 'system', + 'system-seed', + now() +) ON CONFLICT DO NOTHING; -- Nouns community config INSERT INTO "public"."community_configs" ( "community_id", - "version", - "is_active", "is_published", "brand_config", "assets_config", @@ -25,14 +96,23 @@ INSERT INTO "public"."community_configs" ( "ui_config" ) VALUES ( 'nouns', - 1, - true, true, '{"name": "Nouns", "displayName": "Nouns", "tagline": "A space for Nouns", "description": "The social hub for Nouns", "miniAppTags": ["nouns", "client", "customizable", "social", "link"]}'::jsonb, '{"logos": {"main": "/images/nouns/logo.svg", "icon": "/images/nouns/noggles.svg", "favicon": "/images/favicon.ico", "appleTouch": "/images/apple-touch-icon.png", "og": "/images/nouns/og.svg", "splash": "/images/nouns/splash.svg"}}'::jsonb, '{"type": "nouns", "urls": {"website": "https://nouns.com", "discord": "https://discord.gg/nouns", "twitter": "https://twitter.com/nounsdao", "github": "https://github.com/nounsDAO", "forum": "https://discourse.nouns.wtf"}, "social": {"farcaster": "nouns", "discord": "nouns", "twitter": "nounsdao"}, "governance": {"proposals": "https://nouns.wtf/vote", "delegates": "https://nouns.wtf/delegates", "treasury": "https://nouns.wtf/treasury"}, "tokens": {"erc20Tokens": [{"address": "0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab", "symbol": "$SPACE", "decimals": 18, "network": "base"}], "nftTokens": [{"address": "0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03", "symbol": "Nouns", "type": "erc721", "network": "eth"}]}, "contracts": {"nouns": "0x9c8ff314c9bc7f6e59a9d9225fb22946427edc03", "auctionHouse": "0x830bd73e4184cef73443c15111a1df14e495c706", "space": "0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab", "nogs": "0xD094D5D45c06c1581f5f429462eE7cCe72215616"}}'::jsonb, '{"enabled": ["nounsHome", "governance", "feed", "cast", "gallery", "text", "iframe", "links", "video", "channel", "profile", "snapshot", "swap", "rss", "market", "portfolio", "chat", "builderScore", "framesV2"], "disabled": ["example"]}'::jsonb, - '{"logoTooltip": {"text": "wtf is nouns?", "href": "https://nouns.wtf"}, "items": [{"id": "home", "label": "Home", "href": "/home", "icon": "home"}, {"id": "explore", "label": "Explore", "href": "/explore", "icon": "explore"}, {"id": "notifications", "label": "Notifications", "href": "/notifications", "icon": "notifications", "requiresAuth": true}, {"id": "space-token", "label": "$SPACE", "href": "/t/base/0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab/Profile", "icon": "space"}], "showMusicPlayer": true, "showSocials": true}'::jsonb, + -- Navigation config with spaceId references + (SELECT jsonb_build_object( + 'logoTooltip', jsonb_build_object('text', 'wtf is nouns?', 'href', 'https://nouns.wtf'), + 'items', jsonb_build_array( + jsonb_build_object('id', 'home', 'label', 'Home', 'href', '/home', 'icon', 'home', 'spaceId', (SELECT "spaceId"::text FROM "public"."spaceRegistrations" WHERE "spaceName" = 'nouns-home' AND "spaceType" = 'navPage' LIMIT 1)), + jsonb_build_object('id', 'explore', 'label', 'Explore', 'href', '/explore', 'icon', 'explore', 'spaceId', (SELECT "spaceId"::text FROM "public"."spaceRegistrations" WHERE "spaceName" = 'nouns-explore' AND "spaceType" = 'navPage' LIMIT 1)), + jsonb_build_object('id', 'notifications', 'label', 'Notifications', 'href', '/notifications', 'icon', 'notifications', 'requiresAuth', true), + jsonb_build_object('id', 'space-token', 'label', '$SPACE', 'href', '/t/base/0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab/Profile', 'icon', 'space') + ), + 'showMusicPlayer', true, + 'showSocials', true + ))::jsonb, '{"primaryColor": "rgb(37, 99, 235)", "primaryHoverColor": "rgb(29, 78, 216)", "primaryActiveColor": "rgb(30, 64, 175)", "castButton": {"backgroundColor": "rgb(37, 99, 235)", "hoverColor": "rgb(29, 78, 216)", "activeColor": "rgb(30, 64, 175)"}}'::jsonb ) ON CONFLICT ("community_id") DO UPDATE SET "brand_config" = EXCLUDED."brand_config", @@ -46,8 +126,6 @@ INSERT INTO "public"."community_configs" ( -- Example community config INSERT INTO "public"."community_configs" ( "community_id", - "version", - "is_active", "is_published", "brand_config", "assets_config", @@ -57,8 +135,6 @@ INSERT INTO "public"."community_configs" ( "ui_config" ) VALUES ( 'example', - 1, - true, true, '{"name": "Example", "displayName": "Example Community", "tagline": "A space for Example Community", "description": "The social hub for Example Community", "miniAppTags": []}'::jsonb, '{"logos": {"main": "/images/example_logo.png", "icon": "/images/example_icon.png", "favicon": "/images/example_favicon.ico", "appleTouch": "/images/example_apple_touch.png", "og": "/images/example_og.png", "splash": "/images/example_splash.png"}}'::jsonb, @@ -78,8 +154,6 @@ INSERT INTO "public"."community_configs" ( -- Clanker community config INSERT INTO "public"."community_configs" ( "community_id", - "version", - "is_active", "is_published", "brand_config", "assets_config", @@ -89,14 +163,22 @@ INSERT INTO "public"."community_configs" ( "ui_config" ) VALUES ( 'clanker', - 1, - true, true, '{"name": "clanker", "displayName": "Clanker", "tagline": "Clank Clank", "description": "Explore, launch and trade tokens in the Clanker ecosystem. Create your own tokens and discover trending projects in the community-driven token economy."}'::jsonb, '{"logos": {"main": "/images/clanker/logo.svg", "icon": "/images/clanker/logo.svg", "favicon": "/images/clanker/favicon.ico", "appleTouch": "/images/clanker/apple.png", "og": "/images/clanker/og.jpg", "splash": "/images/clanker/og.jpg"}}'::jsonb, '{"type": "token_platform", "urls": {"website": "https://clanker.world", "discord": "https://discord.gg/clanker", "twitter": "https://twitter.com/clankerworld", "github": "https://github.com/clanker", "forum": "https://forum.clanker.world"}, "social": {"farcaster": "clanker", "discord": "clanker", "twitter": "clankerworld"}, "governance": {"proposals": "https://proposals.clanker.world", "delegates": "https://delegates.clanker.world", "treasury": "https://treasury.clanker.world"}, "tokens": {"erc20Tokens": [{"address": "0x1bc0c42215582d5a085795f4badbac3ff36d1bcb", "symbol": "$CLANKER", "decimals": 18, "network": "base"}], "nftTokens": []}, "contracts": {"clanker": "0x1bc0c42215582d5a085795f4badbac3ff36d1bcb", "tokenFactory": "0x0000000000000000000000000000000000000000", "space": "0x0000000000000000000000000000000000000000", "trading": "0x0000000000000000000000000000000000000000", "nouns": "0x0000000000000000000000000000000000000000", "auctionHouse": "0x0000000000000000000000000000000000000000", "nogs": "0x0000000000000000000000000000000000000000"}}'::jsonb, '{"enabled": ["Market", "Portfolio", "Swap", "feed", "cast", "gallery", "text", "iframe", "links", "Video", "Chat", "BuilderScore", "FramesV2", "Rss", "SnapShot"], "disabled": ["nounsHome", "governance"]}'::jsonb, - '{"logoTooltip": {"text": "clanker.world", "href": "https://www.clanker.world"}, "items": [{"id": "home", "label": "Home", "href": "/home", "icon": "home"}, {"id": "notifications", "label": "Notifications", "href": "/notifications", "icon": "notifications", "requiresAuth": true}, {"id": "clanker-token", "label": "$CLANKER", "href": "/t/base/0x1bc0c42215582d5a085795f4badbac3ff36d1bcb/Profile", "icon": "robot"}], "showMusicPlayer": false, "showSocials": false}'::jsonb, + -- Navigation config with spaceId reference + (SELECT jsonb_build_object( + 'logoTooltip', jsonb_build_object('text', 'clanker.world', 'href', 'https://www.clanker.world'), + 'items', jsonb_build_array( + jsonb_build_object('id', 'home', 'label', 'Home', 'href', '/home', 'icon', 'home', 'spaceId', (SELECT "spaceId"::text FROM "public"."spaceRegistrations" WHERE "spaceName" = 'clanker-home' AND "spaceType" = 'navPage' LIMIT 1)), + jsonb_build_object('id', 'notifications', 'label', 'Notifications', 'href', '/notifications', 'icon', 'notifications', 'requiresAuth', true), + jsonb_build_object('id', 'clanker-token', 'label', '$CLANKER', 'href', '/t/base/0x1bc0c42215582d5a085795f4badbac3ff36d1bcb/Profile', 'icon', 'robot') + ), + 'showMusicPlayer', false, + 'showSocials', false + ))::jsonb, '{"primaryColor": "rgba(136, 131, 252, 1)", "primaryHoverColor": "rgba(116, 111, 232, 1)", "primaryActiveColor": "rgba(96, 91, 212, 1)", "castButton": {"backgroundColor": "rgba(136, 131, 252, 1)", "hoverColor": "rgba(116, 111, 232, 1)", "activeColor": "rgba(96, 91, 212, 1)"}}'::jsonb ) ON CONFLICT ("community_id") DO UPDATE SET "brand_config" = EXCLUDED."brand_config", From c8e117579c5885ca8e3187fe47b4bac03e841073 Mon Sep 17 00:00:00 2001 From: Jesse Paterson Date: Sun, 30 Nov 2025 15:44:15 -0600 Subject: [PATCH 110/155] consolidate documentation --- docs/BUILD_FIX_SUMMARY.md | 93 - docs/COMMUNITY_CONFIG_SYSTEM.md | 8 +- docs/CONFIGURATION.md | 75 +- docs/DATABASE_CONFIG/DATABASE_CONFIG_GUIDE.md | 335 ---- .../DATABASE_CONFIG_IMPLEMENTATION.md | 437 ---- .../INCREMENTAL_IMPLEMENTATION_PLAN.md | 1785 ----------------- .../QUICK_START_IMPLEMENTATION.md | 714 ------- docs/DATABASE_CONFIG/QUICK_START_TESTING.md | 373 ---- docs/DATABASE_CONFIG/README.md | 49 - docs/DOCUMENTATION_OVERVIEW.md | 3 + docs/INTEGRATIONS/SUPABASE.md | 65 +- docs/NAVIGATION_SPACE_REFERENCE_APPROACH.md | 313 --- ...VIGATION_SPACE_REFERENCE_IMPLEMENTATION.md | 357 ---- docs/PROJECT_STRUCTURE.md | 38 +- docs/README.md | 11 +- docs/SHARED_THEMES_APPROACH.md | 255 --- docs/SYSTEMS/CONFIGURATION/OVERVIEW.md | 312 +++ docs/SYSTEM_WALKTHROUGH.md | 578 ------ docs/UPDATED_IMPLEMENTATION_SUMMARY.md | 147 -- src/app/page.tsx | 4 +- 20 files changed, 449 insertions(+), 5503 deletions(-) delete mode 100644 docs/BUILD_FIX_SUMMARY.md delete mode 100644 docs/DATABASE_CONFIG/DATABASE_CONFIG_GUIDE.md delete mode 100644 docs/DATABASE_CONFIG/DATABASE_CONFIG_IMPLEMENTATION.md delete mode 100644 docs/DATABASE_CONFIG/INCREMENTAL_IMPLEMENTATION_PLAN.md delete mode 100644 docs/DATABASE_CONFIG/QUICK_START_IMPLEMENTATION.md delete mode 100644 docs/DATABASE_CONFIG/QUICK_START_TESTING.md delete mode 100644 docs/DATABASE_CONFIG/README.md delete mode 100644 docs/NAVIGATION_SPACE_REFERENCE_APPROACH.md delete mode 100644 docs/NAVIGATION_SPACE_REFERENCE_IMPLEMENTATION.md delete mode 100644 docs/SHARED_THEMES_APPROACH.md create mode 100644 docs/SYSTEMS/CONFIGURATION/OVERVIEW.md delete mode 100644 docs/SYSTEM_WALKTHROUGH.md delete mode 100644 docs/UPDATED_IMPLEMENTATION_SUMMARY.md diff --git a/docs/BUILD_FIX_SUMMARY.md b/docs/BUILD_FIX_SUMMARY.md deleted file mode 100644 index 9c598140c..000000000 --- a/docs/BUILD_FIX_SUMMARY.md +++ /dev/null @@ -1,93 +0,0 @@ -# Build Error Fix Summary - -## Issues Fixed - -### 1. ✅ Removed Old Route Files - -The following old route files were removed (they conflicted with the new dynamic routing): - -- `src/app/explore/[slug]/page.tsx` - Was trying to access `config.explorePage.tabOrder` which doesn't exist in DB config -- `src/app/explore/page.tsx` - Was trying to access `config.explorePage.defaultTab` -- `src/app/home/[tabname]/page.tsx` - Old home route handler -- `src/app/home/page.tsx` - Old home redirect - -These are all replaced by the dynamic route: `src/app/[navSlug]/[[...tabName]]/page.tsx` - -### 2. ✅ Cleaned Up Empty Directories - -Removed empty directories: -- `src/app/explore/[slug]/` -- `src/app/home/[tabname]/` - -## Remaining Issue - -### ⚠️ Space Config Files Not Uploaded to Storage - -The errors you're seeing: -``` -Failed to load tabOrder for space a68308a2-9dae-4ff6-9d46-fe165623be79: Error [StorageUnknownError]: {} -``` - -This means the space config files haven't been uploaded to Supabase Storage yet. The database has the space registrations (created by `seed.sql`), but the actual config files need to be uploaded. - -## Solution: Upload Space Configs - -Run the seed script to upload space config files: - -```bash -# Make sure you have environment variables set -export NEXT_PUBLIC_SUPABASE_URL="your-supabase-url" -export SUPABASE_SERVICE_ROLE_KEY="your-service-role-key" - -# Upload space configs to Supabase Storage -tsx scripts/seed-navpage-spaces.ts -``` - -This script will: -1. Read space registrations from the database -2. Import page configs from TypeScript (`nounsHomePage`, `nounsExplorePage`, etc.) -3. Upload each tab as `{spaceId}/tabs/{tabName}` to Supabase Storage -4. Upload tab order as `{spaceId}/tabOrder` to Supabase Storage - -**Expected output:** -``` -🚀 Starting navPage space config seeding... - -📦 Uploading space: nouns-home - 📍 Space ID: a68308a2-9dae-4ff6-9d46-fe165623be79 - 📄 Uploading 6 tabs... - ✅ Uploaded tab: Nouns - ✅ Uploaded tab: Social - ... - ✅ Uploaded tab order - ✅ Successfully uploaded nouns-home - -📦 Uploading space: nouns-explore - ... - -✅ All navPage spaces seeded successfully! -``` - -## After Uploading - -Once the space configs are uploaded, the build should succeed. The dynamic route will be able to: -- Load spaces from Storage at build time (for static generation) -- Load spaces from Storage at runtime (for dynamic rendering) - -## Verify Upload - -You can verify the files were uploaded by: - -1. **Supabase Dashboard**: Check the `spaces` bucket in Storage -2. **Via SQL**: The files are in Storage, not the database -3. **Build test**: Try building again - errors should be gone - -## Note - -The `loadSpaceAsPageConfig()` function gracefully handles missing files: -- If files don't exist, it returns `null` -- `generateStaticParams()` skips static generation for that space -- Pages will still render at runtime (with a small delay to fetch from Storage) - -But for best performance, upload the files before building! - diff --git a/docs/COMMUNITY_CONFIG_SYSTEM.md b/docs/COMMUNITY_CONFIG_SYSTEM.md index 9c4c3d41d..93dcab8bf 100644 --- a/docs/COMMUNITY_CONFIG_SYSTEM.md +++ b/docs/COMMUNITY_CONFIG_SYSTEM.md @@ -1,8 +1,10 @@ # Community Configuration System +> **Note:** This document describes the static configuration system structure. For information about the current database-backed configuration system, see [Configuration System Overview](SYSTEMS/CONFIGURATION/OVERVIEW.md). + ## Overview -The Community Configuration System is a comprehensive whitelabeling solution that allows Nounspace to be customized for different communities (Nouns, Clanker, Example, etc.) through a build-time configuration system. Each community can have its own branding, assets, themes, fidgets, navigation, and initial space configurations. +The Community Configuration System is a comprehensive whitelabeling solution that allows Nounspace to be customized for different communities (Nouns, Clanker, Example, etc.). Configurations are stored in Supabase and loaded at build time, with fallback to static TypeScript configs. ## Architecture @@ -433,9 +435,11 @@ interface UIConfig { ## Configuration Loading +The system uses **database-backed configuration** with build-time loading. Configs are fetched from Supabase during build and stored in an environment variable. If the database is unavailable, the system falls back to static TypeScript configs. + ### Build-Time Configuration -The system uses **build-time configuration** - the community is determined at build time via the `NEXT_PUBLIC_COMMUNITY` environment variable: +The community is determined at build time via the `NEXT_PUBLIC_COMMUNITY` environment variable: ```bash # Set community at build time diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 64aeef79c..5219307c1 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -1,5 +1,12 @@ # Configuration Guide +Nounspace uses a **database-backed configuration system** that allows community configurations to be stored in Supabase and loaded at build time. This provides admin-editable configs with zero runtime database queries. + +## Database-Backed Configuration System + +For complete documentation on how the configuration system works, see: +- **[Configuration System Overview](SYSTEMS/CONFIGURATION/OVERVIEW.md)** - Complete description of the database-backed configuration system + ## Environment Variables The application uses environment variables to configure community-specific settings and external services. @@ -43,51 +50,42 @@ NEXT_PUBLIC_WEBSITE_URL=http://localhost:3000 ## Configuration Loading -The application automatically loads the appropriate configuration based on the `NEXT_PUBLIC_COMMUNITY` environment variable: +The application loads configurations in the following order: -1. **Environment Variable Reading**: The system reads `NEXT_PUBLIC_COMMUNITY` from environment variables -2. **Validation**: Validates that the configuration exists in the available configurations -3. **Fallback**: Falls back to the `nouns` configuration if an invalid or missing value is provided -4. **Loading**: Loads the corresponding community configuration +1. **Database Config** (if available): Fetched from Supabase during build, stored in `NEXT_PUBLIC_BUILD_TIME_CONFIG` env var +2. **Static Config Fallback**: Falls back to static TypeScript configs if database is unavailable + +See [Configuration System Overview](SYSTEMS/CONFIGURATION/OVERVIEW.md) for details on the build-time loading process. ## Configuration Structure -The configuration is organized into community-specific folders: +Configurations are stored in Supabase and loaded at build time. Static TypeScript configs serve as fallback. The structure is organized into community-specific folders: ``` src/config/ -├── nouns/ # Nouns community configuration +├── nouns/ # Nouns community configuration (fallback) │ ├── nouns.brand.ts # Brand identity │ ├── nouns.assets.ts # Visual assets -│ ├── nouns.theme.ts # Theme definitions │ ├── nouns.community.ts # Community integration │ ├── nouns.fidgets.ts # Fidget management -│ ├── nouns.home.ts # Home page configuration -│ ├── initial*.ts # Initial space templates -│ └── index.ts # Configuration export -├── example/ # Example community configuration -│ ├── example.brand.ts # Brand identity template -│ ├── example.assets.ts # Visual assets template -│ ├── example.theme.ts # Theme definitions template -│ ├── example.community.ts # Community integration template -│ ├── example.fidgets.ts # Fidget management template -│ ├── example.home.ts # Home page configuration template -│ ├── example.initialSpaces.ts # Initial space templates -│ └── index.ts # Configuration export -├── clanker/ # Clanker community configuration -│ ├── clanker.brand.ts # Brand identity -│ ├── clanker.assets.ts # Visual assets -│ ├── clanker.theme.ts # Theme definitions -│ ├── clanker.community.ts # Community integration -│ ├── clanker.fidgets.ts # Fidget management -│ ├── clanker.home.ts # Home page configuration +│ ├── nouns.home.ts # Home page configuration (legacy) +│ ├── nouns.explore.ts # Explore page configuration (legacy) +│ ├── nouns.navigation.ts # Navigation config +│ ├── nouns.theme.ts # Theme config (references shared) +│ ├── nouns.ui.ts # UI colors │ ├── initialSpaces/ # Initial space templates │ └── index.ts # Configuration export +├── clanker/ # Clanker community configuration (fallback) +├── example/ # Example community configuration (fallback) +├── shared/ # Shared configuration +│ └── themes.ts # Shared theme definitions (all communities) ├── systemConfig.ts # System configuration interface ├── initialSpaceConfig.ts # Base space configuration -└── index.ts # Main configuration loader +└── index.ts # Main configuration loader (reads from DB or static) ``` +**Note:** Themes are stored in `shared/themes.ts` and pages (homePage/explorePage) are stored as Spaces in Supabase Storage. See [Configuration System Overview](SYSTEMS/CONFIGURATION/OVERVIEW.md) for details. + ## Build-Time Configuration The system uses build-time configuration to determine which community configuration to use. This means: @@ -133,21 +131,24 @@ NEXT_PUBLIC_COMMUNITY=example npm run dev When you set `NEXT_PUBLIC_COMMUNITY=example` (or `clanker`), the system will: -1. **Load that community's system config** (brand, assets, theme, community settings) -2. **Use that community's fidget configurations** -3. **Use that community's home page configuration** +1. **Load that community's system config** from Supabase (brand, assets, community settings, fidgets, navigation, UI) +2. **Load shared themes** from `src/config/shared/themes.ts` +3. **Load navigation pages** as Spaces from Supabase Storage (referenced by navigation items) 4. **Use that community's initial space templates** (profile, channel, token, proposal, homebase) +If the database is unavailable, the system falls back to static TypeScript configs. + ## Adding New Community Configurations To add a new community configuration: -1. Create a new folder under `src/config/` (e.g., `src/config/mycommunity/`) -2. Copy the structure from `src/config/example/` as a template -3. Update all configuration files with your community's specific values -4. Add the new configuration to the `AVAILABLE_CONFIGURATIONS` array in `src/config/index.ts` -5. Add a new case to the switch statement in `loadSystemConfig()` and the runtime delegates for space creators -6. Update this documentation +1. **Create database entry**: Insert a new row in the `community_configs` table with your community's configuration +2. **Create static fallback** (optional): Create a new folder under `src/config/` (e.g., `src/config/mycommunity/`) as a fallback +3. **Add to available configs**: Add the new configuration to the `AVAILABLE_CONFIGURATIONS` array in `src/config/index.ts` +4. **Create navigation spaces**: If your community has navigation pages, create `navPage` type spaces in `spaceRegistrations` and upload their configs to Storage +5. **Update seed data**: Add seed data in `supabase/seed.sql` for the new community + +See [Configuration System Overview](SYSTEMS/CONFIGURATION/OVERVIEW.md) for detailed instructions. ## Development vs Production diff --git a/docs/DATABASE_CONFIG/DATABASE_CONFIG_GUIDE.md b/docs/DATABASE_CONFIG/DATABASE_CONFIG_GUIDE.md deleted file mode 100644 index 03c178723..000000000 --- a/docs/DATABASE_CONFIG/DATABASE_CONFIG_GUIDE.md +++ /dev/null @@ -1,335 +0,0 @@ -# Database-Backed Configuration System Guide - -## Overview - -Nounspace uses a **database-backed configuration system** that allows admins to update community configurations without code changes. Configurations are stored in Supabase and loaded at **build time** (not runtime), ensuring zero database queries in production. - -## Architecture - -### Current Approach - -``` -┌─────────────────┐ -│ Admin UI │ -│ (Updates DB) │ -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ Database │ -│ (Stores Config)│ -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ ┌──────────────────┐ -│ Build Process │─────▶│ Set Env Var │ -│ (next.config.mjs)│ │ NEXT_PUBLIC_ │ -│ │ │ BUILD_TIME_ │ -│ │ │ CONFIG │ -└─────────────────┘ └─────────┬──────────┘ - │ - ▼ - ┌──────────────────┐ - │ Runtime App │ - │ (Reads Env Var │ - │ Zero DB Queries)│ - └──────────────────┘ -``` - -### Key Design Decisions - -1. **Build-Time Loading** - Configs fetched from DB during build, stored in env var -2. **Shared Themes** - Themes stored in `src/config/shared/themes.ts` (not in DB) -3. **Navigation-Space References** - Pages (homePage/explorePage) stored as Spaces, referenced by navigation items -4. **Environment Variables** - Config stored in `NEXT_PUBLIC_BUILD_TIME_CONFIG` (~2.8 KB) - -## Config Size Reduction - -| Stage | Config Size | Reduction | -|-------|-------------|-----------| -| **Original** | ~29 KB | - | -| **After removing pages** | ~8.3 KB | 71% | -| **After removing themes** | **~2.8 KB** | **90%** | - -### What Was Removed - -- **Themes** (5.5 KB) → Moved to `src/config/shared/themes.ts` -- **homePage** (19.2 KB) → Stored as Space, referenced by nav item -- **explorePage** (1.4 KB) → Stored as Space, referenced by nav item - -## Database Schema - -### `community_configs` Table - -```sql -CREATE TABLE "public"."community_configs" ( - "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), - "community_id" VARCHAR(50) NOT NULL UNIQUE, - "updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), - "brand_config" JSONB NOT NULL, -- Brand identity - "assets_config" JSONB NOT NULL, -- Asset paths - "community_config" JSONB NOT NULL, -- Community integration data - "fidgets_config" JSONB NOT NULL, -- Enabled/disabled fidgets - "navigation_config" JSONB, -- Navigation items (with spaceId refs) - "ui_config" JSONB, -- UI colors - "is_published" BOOLEAN DEFAULT true -- Draft vs published -); -``` - -**Note:** No `theme_config`, `home_page_config`, or `explore_page_config` columns. - -### Config Sections - -#### `brand_config` -- `name`: Internal identifier (e.g., "nouns") -- `displayName`: User-facing name -- `tagline`: Short tagline -- `description`: Full description -- `miniAppTags`: Farcaster Mini App discovery tags - -#### `assets_config` -- `logos`: Paths to logo files (main, icon, favicon, etc.) -- All paths are public URLs (e.g., `/images/nouns/logo.svg`) - -#### `community_config` -- `type`: Community type -- `urls`: Website, Discord, Twitter, GitHub, Forum links -- `social`: Social handles -- `governance`: Proposals, delegates, treasury links -- `tokens`: ERC20 and NFT token definitions -- `contracts`: Contract addresses - -#### `fidgets_config` -- `enabled`: Array of enabled fidget IDs -- `disabled`: Array of disabled fidget IDs - -#### `navigation_config` -- `logoTooltip`: Logo tooltip text and href -- `items`: Navigation items array - - Each item can have `spaceId` to reference a Space for page content -- `showMusicPlayer`: Boolean -- `showSocials`: Boolean - -#### `ui_config` -- `primaryColor`: Primary UI color -- `primaryHoverColor`: Hover state color -- `primaryActiveColor`: Active state color -- `castButton`: Cast button color config - -### Navigation-Space References - -Navigation items can reference Spaces for page content: - -```typescript -{ - id: 'home', - label: 'Home', - href: '/home', - icon: 'home', - spaceId: 'uuid-of-home-space' // ← References Space -} -``` - -- Spaces stored in `spaceRegistrations` with `spaceType = 'navPage'` (system-owned, fid=NULL) -- Space configs stored in Supabase Storage at `{spaceId}/tabs/{tabName}` and `{spaceId}/tabOrder` -- Uploaded via `scripts/seed-navpage-spaces.ts` script after database reset -- Stored as unencrypted SignedFile objects (readable by existing code) -- Fetched at build time and converted to page configs - -### Shared Themes - -Themes are stored in `src/config/shared/themes.ts`: - -```typescript -export const themes = { - default: { /* ... */ }, - nounish: { /* ... */ }, - // ... all 10 themes -}; -``` - -All communities import from this shared file. - -## Build Process - -### 1. Fetch Config from Database - -```javascript -// next.config.mjs - -const { data } = await supabase - .rpc('get_active_community_config', { p_community_id: community }) - .single(); -``` - -### 2. Fetch Spaces for Navigation Items - -```javascript -// Extract spaceIds from navigation items -const spaceIds = navItems - .filter(item => item.spaceId) - .map(item => ({ navId: item.id, spaceId: item.spaceId })); - -// Fetch Spaces from database/storage -const pageConfigs = {}; -for (const { navId, spaceId } of spaceIds) { - // Fetch tab order first - const { data: tabOrderData } = await supabase.storage - .from('spaces') - .download(`${spaceId}/tabOrder`); - - if (!tabOrderData) continue; - - const tabOrderFile = JSON.parse(await tabOrderData.text()); - const tabOrder = tabOrderFile.tabOrder || []; - - // Fetch each tab config - const tabs = {}; - for (const tabName of tabOrder) { - const { data: tabData } = await supabase.storage - .from('spaces') - .download(`${spaceId}/tabs/${tabName}`); - - if (tabData) { - const tabFile = JSON.parse(await tabData.text()); - const tabConfig = JSON.parse(tabFile.fileData); // Unencrypted SignedFile - tabs[tabName] = tabConfig; - } - } - - // Reconstruct HomePageConfig/ExplorePageConfig format - if (Object.keys(tabs).length > 0) { - pageConfigs[navId] = { - defaultTab: tabOrder[0] || 'Home', - tabOrder, - tabs, - layout: { - defaultLayoutFidget: 'grid', - gridSpacing: 16, - theme: {}, - }, - }; - } -} -``` - -### 3. Import Shared Themes - -```javascript -import { themes } from './src/config/shared/themes'; -``` - -### 4. Combine and Store in Env Var - -```javascript -const fullConfig = { - ...config, - theme: themes, // From shared file - pages: pageConfigs, // From Spaces -}; - -process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(fullConfig); -``` - -### 5. Runtime Usage - -```typescript -// src/config/index.ts - -const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; -if (buildTimeConfig) { - const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; - return dbConfig; -} -// Fall back to static configs -``` - -## Quick Start - -### 1. Reset Database - -```bash -# Applies migrations + seed data -supabase db reset -``` - -This will: -- Create `community_configs` table -- Add `navPage` spaceType -- Seed configs for nouns, example, clanker -- Create navPage space registrations (nouns-home, nouns-explore, clanker-home) -- Link navigation items to spaces via spaceId - -### 2. Upload Space Configs - -```bash -# Set environment variables -export NEXT_PUBLIC_SUPABASE_URL="your-supabase-url" -export SUPABASE_SERVICE_ROLE_KEY="your-service-role-key" - -# Upload space config files to Supabase Storage -tsx scripts/seed-navpage-spaces.ts -``` - -This uploads: -- Each tab config as `{spaceId}/tabs/{tabName}` (SignedFile format) -- Tab order as `{spaceId}/tabOrder` (SignedFile format) - -**Note:** Space configs are stored as unencrypted SignedFile objects and can be read by the existing space loading code. - -### 3. Build Application - -```bash -NEXT_PUBLIC_SUPABASE_URL=... \ -SUPABASE_SERVICE_ROLE_KEY=... \ -npm run build -``` - -Expected output: -``` -✅ Loaded config from database -``` - -### 4. Verify - -```bash -npm run dev -# App should load with DB config -# Pages should load from Spaces -``` - -## Environment Variables - -**Required for Build:** -```bash -NEXT_PUBLIC_SUPABASE_URL=... -SUPABASE_SERVICE_ROLE_KEY=... -NEXT_PUBLIC_COMMUNITY=nouns # Optional, defaults to 'nouns' -``` - -**Optional:** -- If DB credentials missing → Falls back to static configs -- If DB config not found → Falls back to static configs - -## Benefits - -✅ **Zero Runtime Overhead** - No database queries in production -✅ **Admin Updates** - Changes made through database/admin UI -✅ **Fast Runtime** - Config loaded from env var (instant) -✅ **Safe Fallback** - Static configs always available -✅ **Small Config** - Only ~2.8 KB (fits in env vars) -✅ **Unified Architecture** - Pages are Spaces -✅ **Shared Themes** - Single source of truth - -## Migration Path - -See `DATABASE_CONFIG_IMPLEMENTATION.md` for detailed phase-by-phase implementation plan. - -## Related Documentation - -- `DATABASE_CONFIG_IMPLEMENTATION.md` - Detailed implementation plan -- `QUICK_START_IMPLEMENTATION.md` - Quick start guide -- `QUICK_START_TESTING.md` - Testing guide -- `INCREMENTAL_IMPLEMENTATION_PLAN.md` - Complete phase-by-phase plan -- `README.md` - Documentation index - diff --git a/docs/DATABASE_CONFIG/DATABASE_CONFIG_IMPLEMENTATION.md b/docs/DATABASE_CONFIG/DATABASE_CONFIG_IMPLEMENTATION.md deleted file mode 100644 index 51de92f20..000000000 --- a/docs/DATABASE_CONFIG/DATABASE_CONFIG_IMPLEMENTATION.md +++ /dev/null @@ -1,437 +0,0 @@ -# Database-Backed Configuration Implementation Plan - -## Overview - -This document provides a detailed, phase-by-phase implementation plan for migrating to database-backed configurations. Each phase is independently testable and can be rolled back if issues arise. - -## Architecture Summary - -- **Config Storage**: Supabase `community_configs` table (~2.8 KB per config) -- **Build-Time Loading**: Configs fetched during build, stored in `NEXT_PUBLIC_BUILD_TIME_CONFIG` env var -- **Shared Themes**: Stored in `src/config/shared/themes.ts` (not in DB) -- **Pages as Spaces**: homePage/explorePage stored as Spaces, referenced by navigation items -- **Zero Runtime Queries**: All configs loaded at build time - -## Phase 0: Preparation & Setup - -**Goal:** Set up foundation without breaking existing system - -**Tasks:** -1. Create feature branch: `feature/database-configs` -2. Document current static config structure -3. Set up test database (local Supabase) -4. Create migration files structure - -**Testing:** -- ✅ Verify static configs still work -- ✅ Local Supabase runs successfully -- ✅ Can connect to database - -**Rollback:** Just switch back to main branch - -**Estimated Time:** 1-2 hours - ---- - -## Phase 1: Database Schema - -**Goal:** Create database tables and functions - -**Tasks:** -1. Create migration: `community_configs` table (without theme/home/explore columns) -2. Create migration: `add_navpage_space_type` (adds navPage spaceType) -3. Create database function: `get_active_community_config` (excludes themes/pages) -4. Set up RLS policies -5. Seed initial data via `supabase/seed.sql`: - - Seeds configs for nouns, example, clanker - - Creates navPage space registrations (nouns-home, nouns-explore, clanker-home) - - Links navigation items to spaces via spaceId -6. Create space seeding script: `scripts/seed-navpage-spaces.ts` -7. Upload space configs to Supabase Storage - -**Files:** -- `supabase/migrations/20251129172847_create_community_configs.sql` -- `supabase/migrations/20251129172848_add_navpage_space_type.sql` -- `supabase/seed.sql` (includes seed data and space registrations) -- `scripts/seed-navpage-spaces.ts` (uploads space config files) - -**Testing:** -```sql --- Test 1: Can fetch config (should not include themes/pages) -SELECT get_active_community_config('nouns'); - --- Test 2: Verify navPage spaceType exists -SELECT DISTINCT "spaceType" FROM "spaceRegistrations"; --- Should include: 'navPage' - --- Test 3: Verify seed data loaded -SELECT community_id FROM community_configs; --- Should see: nouns, example, clanker - --- Test 4: Verify navPage spaces created -SELECT "spaceId", "spaceName", "spaceType" -FROM "spaceRegistrations" -WHERE "spaceType" = 'navPage'; --- Should see: nouns-home, nouns-explore, clanker-home - --- Test 5: Verify navigation references spaces -SELECT "navigation_config"->'items' as nav_items -FROM "community_configs" -WHERE "community_id" = 'nouns'; --- Should see spaceId references -``` - -**After seed.sql, upload space configs:** -```bash -tsx scripts/seed-navpage-spaces.ts -``` - -**Validation:** -- ✅ Database functions work -- ✅ RLS policies enforce access -- ✅ Seed data loaded -- ✅ navPage spaceType added -- ✅ Config excludes theme/home/explore columns -- ✅ navPage space registrations created -- ✅ Space config files uploaded to storage - -**Rollback:** `supabase db reset` (resets to clean state, deletes storage files) - -**Estimated Time:** 4-6 hours - ---- - -## Phase 2: Build-Time Config Loading - -**Goal:** Load config from DB at build time, store in env var - -**Tasks:** -1. Create `src/config/shared/themes.ts` - Move themes to shared file -2. Update community configs to import shared themes -3. Add build-time fetch in `next.config.mjs`: - - Fetch main config from DB - - Import shared themes - - Extract spaceIds from navigation items - - Fetch Spaces for nav items (if implemented) - - Combine config + themes + pages - - Store in `NEXT_PUBLIC_BUILD_TIME_CONFIG` env var -4. Update `src/config/index.ts` to read from env var -5. Keep static configs as fallback - -**Files to Create:** -- `src/config/shared/themes.ts` - -**Files to Modify:** -- `next.config.mjs` - Add config fetch -- `src/config/index.ts` - Read from env var -- `src/config/nouns/nouns.theme.ts` - Import from shared -- `src/config/clanker/clanker.theme.ts` - Import from shared -- `src/config/example/example.theme.ts` - Import from shared - -**Implementation:** - -```javascript -// next.config.mjs - -import { createClient } from '@supabase/supabase-js'; -import { themes } from './src/config/shared/themes'; - -async function loadConfigFromDB() { - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; - - if (!supabaseUrl || !supabaseKey) { - console.log('ℹ️ Using static configs (no DB credentials)'); - return; - } - - const supabase = createClient(supabaseUrl, supabaseKey); - const community = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - - try { - // Fetch main config (now much smaller - no themes/pages!) - const { data: config, error } = await supabase - .rpc('get_active_community_config', { p_community_id: community }) - .single(); - - if (error || !data) { - console.log('ℹ️ Using static configs (no DB config found)'); - return; - } - - // Combine: config + shared themes + pages (if Spaces implemented) - const fullConfig = { - ...config, - theme: themes, // From shared file - pages: {}, // Will be populated when Spaces are implemented - }; - - // Store in env var (now small enough at ~2.8 KB) - process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(fullConfig); - console.log('✅ Loaded config from database'); - } catch (error) { - console.warn('⚠️ Error loading config from DB:', error.message); - } -} - -await loadConfigFromDB(); -``` - -```typescript -// src/config/index.ts - -export const loadSystemConfig = (): SystemConfig => { - const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - - // Try build-time config from database (stored in env var) - const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; - if (buildTimeConfig) { - try { - const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; - // Validate config structure - if (dbConfig && dbConfig.brand && dbConfig.assets) { - console.log('✅ Using config from database'); - return dbConfig; - } - } catch (error) { - console.warn('⚠️ Failed to parse build-time config, falling back to static:', error); - } - } - - // Fall back to static configs (existing behavior) - console.log('ℹ️ Using static configs'); - switch (communityConfig.toLowerCase()) { - case 'nouns': - return nounsSystemConfig; - case 'example': - return exampleSystemConfig; - case 'clanker': - return clankerSystemConfig as unknown as SystemConfig; - default: - return nounsSystemConfig; - } -}; -``` - -**Testing:** -```bash -# Test with DB available -NEXT_PUBLIC_SUPABASE_URL=... SUPABASE_SERVICE_ROLE_KEY=... npm run build -# Should see: "✅ Loaded config from database" - -# Test without DB -npm run build -# Should see: "ℹ️ Using static configs" - -# Verify app works -npm run dev -# App should load correctly -``` - -**Validation:** -- ✅ Build succeeds with/without DB -- ✅ App uses DB config when available -- ✅ Themes loaded from shared file -- ✅ App falls back to static when unavailable -- ✅ Config size ~2.8 KB -- ✅ All existing functionality works -- ✅ No breaking changes - -**Rollback:** Remove env var reading, system back to static-only - -**Estimated Time:** 2-3 hours - ---- - -## Phase 3: Admin API Endpoints - -**Goal:** Create API endpoints for admin config updates - -**Tasks:** -1. Create `/api/admin/config/[communityId]` - GET/PUT -2. Create `/api/admin/config/[communityId]/history` - GET -3. Create `/api/admin/spaces/[spaceId]` - GET/PUT (for nav page Spaces) -4. Add admin permission checks -5. Add request validation - -**Files to Create:** -- `src/app/api/admin/config/[communityId]/route.ts` -- `src/app/api/admin/config/[communityId]/history/route.ts` -- `src/app/api/admin/spaces/[spaceId]/route.ts` -- `src/common/data/services/adminConfigService.ts` - -**Testing:** -```bash -# Test GET -curl -H "x-admin-identity: test-admin" \ - http://localhost:3000/api/admin/config/nouns - -# Test PUT -curl -X PUT \ - -H "x-admin-identity: test-admin" \ - -H "Content-Type: application/json" \ - -d '{"config": {...}, "changeNotes": "Test update"}' \ - http://localhost:3000/api/admin/config/nouns -``` - -**Validation:** -- ✅ GET returns config (without themes/pages) -- ✅ PUT updates config -- ✅ GET/PUT for Spaces (nav pages) -- ✅ Permission checks work -- ✅ History tracked -- ✅ Invalid requests rejected - -**Rollback:** Remove API routes, no impact on main app - -**Estimated Time:** 3-4 hours - ---- - -## Phase 4: Basic Admin UI - -**Goal:** Create simple admin interface to view/edit configs - -**Tasks:** -1. Create admin layout/page structure -2. Create config viewer (read-only first) -3. Add basic form for editing config -4. Add Space editor for nav pages (homePage/explorePage) -5. Add save functionality - -**Files to Create:** -- `src/app/admin/config/[communityId]/page.tsx` -- `src/app/admin/config/[communityId]/components/ConfigViewer.tsx` -- `src/app/admin/config/[communityId]/components/ConfigEditor.tsx` -- `src/app/admin/config/[communityId]/components/NavPageEditor.tsx` - -**Testing:** -1. Navigate to `/admin/config/nouns` -2. Verify config loads -3. Make a small change (e.g., brand name) -4. Save and verify it updates in DB -5. Rebuild and verify change appears - -**Validation:** -- ✅ Can view config (without themes/pages) -- ✅ Can edit config -- ✅ Can edit nav page Spaces -- ✅ Can save changes -- ✅ Changes persist in DB/Storage -- ✅ Changes appear after rebuild - -**Rollback:** Remove admin routes, no impact on main app - -**Estimated Time:** 4-6 hours - ---- - -## Phase 5-10: Additional Features - -See `INCREMENTAL_IMPLEMENTATION_PLAN.md` for detailed phases covering: -- Phase 5: Asset Upload -- Phase 6: Build-Time Asset Download -- Phase 7: Asset Optimization -- Phase 8: Rebuild Trigger -- Phase 9: Production Rollout -- Phase 10: Phase Out Static Configs - ---- - -## Testing Checklist - -### Phase 1: Database -- [ ] Tables created (without theme/home/explore columns) -- [ ] Functions work (exclude themes/pages) -- [ ] Seed data loaded -- [ ] navPage spaceType exists -- [ ] RLS policies work - -### Phase 2: Config Loading -- [ ] Build succeeds with DB -- [ ] Build succeeds without DB -- [ ] App uses DB config -- [ ] Themes loaded from shared file -- [ ] App falls back to static -- [ ] Config size ~2.8 KB -- [ ] No breaking changes - -### Phase 3: Admin API -- [ ] GET returns config (without themes/pages) -- [ ] PUT updates config -- [ ] GET/PUT for nav page Spaces -- [ ] Permission checks work -- [ ] Invalid requests rejected - -### Phase 4: Admin UI -- [ ] Can view config (without themes/pages) -- [ ] Can edit config -- [ ] Can edit nav page Spaces -- [ ] Can save changes -- [ ] Changes persist - ---- - -## Rollback Procedures - -### Phase 1 Rollback -```bash -supabase db reset # Resets to clean state -``` - -### Phase 2 Rollback -Remove env var reading from `src/config/index.ts`, system uses static configs - -### Phase 3 Rollback -Remove API routes, no impact on main app - -### Phase 4 Rollback -Remove admin routes, no impact on main app - ---- - -## Success Criteria - -### Phase 1 -- ✅ Database functions work -- ✅ Seed data loaded -- ✅ navPage spaceType exists - -### Phase 2 -- ✅ Build succeeds with/without DB -- ✅ App uses DB config when available -- ✅ Themes loaded from shared file -- ✅ Config size reduced to ~2.8 KB -- ✅ No breaking changes - -### Phase 3 -- ✅ Admin can fetch/update configs -- ✅ Permission checks work -- ✅ History tracked - -### Phase 4 -- ✅ Admin can view/edit configs -- ✅ Changes persist and appear after rebuild - ---- - -## Quick Start Path - -For a quick proof-of-concept: - -1. **Phase 1** - Create DB schema (2h) -2. **Phase 2** - Basic config loading (2h) -3. **Manual testing** - Update DB directly, rebuild, verify - -This gives you a working POC in ~4 hours! - ---- - -## Related Documentation - -- `DATABASE_CONFIG_GUIDE.md` - Architecture and overview -- `QUICK_START_IMPLEMENTATION.md` - Quick start guide -- `QUICK_START_TESTING.md` - Testing guide -- `INCREMENTAL_IMPLEMENTATION_PLAN.md` - Complete phase-by-phase plan (all 10 phases) -- `README.md` - Documentation index - diff --git a/docs/DATABASE_CONFIG/INCREMENTAL_IMPLEMENTATION_PLAN.md b/docs/DATABASE_CONFIG/INCREMENTAL_IMPLEMENTATION_PLAN.md deleted file mode 100644 index 332fbfe93..000000000 --- a/docs/DATABASE_CONFIG/INCREMENTAL_IMPLEMENTATION_PLAN.md +++ /dev/null @@ -1,1785 +0,0 @@ -# Incremental Implementation Plan - -> **Note:** This is a detailed phase-by-phase plan. For a quick overview, see: -> - `DATABASE_CONFIG_GUIDE.md` - Architecture and overview -> - `DATABASE_CONFIG_IMPLEMENTATION.md` - Consolidated implementation plan -> - `QUICK_START_IMPLEMENTATION.md` - Quick start guide -> - `README.md` - Documentation index - -## Overview - -This plan breaks down the database-backed configuration system into testable phases, allowing you to validate each piece before moving to the next. - -**Current Approach:** -- Configs stored in DB (~2.8 KB), loaded at build time into `NEXT_PUBLIC_BUILD_TIME_CONFIG` env var -- Themes in shared file (`src/config/shared/themes.ts`) -- Pages as Spaces (referenced by navigation items) - -## Phase 0: Preparation & Setup - -**Goal:** Set up foundation without breaking existing system - -**Tasks:** -1. Create feature branch: `feature/database-configs` -2. Document current static config structure -3. Set up test database (local Supabase) -4. Create migration files structure - -**Testing:** -- ✅ Verify static configs still work -- ✅ Local Supabase runs successfully -- ✅ Can connect to database - -**Rollback:** Just switch back to main branch - -**Estimated Time:** 1-2 hours - ---- - -## Phase 1: Database Schema (Week 1, Day 1-2) - -**Goal:** Create database tables and functions - -**Tasks:** -1. Create migration: `community_configs` table (without homePage/explorePage columns) -2. Create migration: `community_config_admins` table -3. Add `navPage` spaceType to `spaceRegistrations` -4. Create database function: `get_active_community_config` (excludes page configs) -5. Set up RLS policies -6. Seed initial data via `supabase/seed.sql`: - - Seeds configs (copy from static configs, excluding themes/pages) - - Creates navPage space registrations (nouns-home, nouns-explore, clanker-home) - - Links navigation items to spaces via spaceId -7. Create space seeding script: `scripts/seed-navpage-spaces.ts` -8. Upload space config files to Supabase Storage - -**Files to Create:** -``` -supabase/migrations/ - └── YYYYMMDDHHMMSS_create_community_configs.sql - └── YYYYMMDDHHMMSS_add_navpage_space_type.sql -supabase/seed.sql # Seeds configs and creates space registrations -scripts/ - └── seed-navpage-spaces.ts # NEW: Uploads space config files -``` - -**Files to Create/Update:** -``` -src/config/shared/ - └── themes.ts # NEW: Shared theme definitions -``` - -**Testing:** -```sql --- Test 1: Can fetch config (should not include homePage/explorePage) -SELECT get_active_community_config('nouns'); - --- Test 2: Verify navPage spaceType exists -SELECT DISTINCT "spaceType" FROM "spaceRegistrations"; --- Should include: 'navPage' - --- Test 3: Verify seed data loaded -SELECT community_id FROM community_configs; --- Should see: nouns, example, clanker - --- Test 4: Verify navPage spaces created -SELECT "spaceId", "spaceName", "spaceType" -FROM "spaceRegistrations" -WHERE "spaceType" = 'navPage'; --- Should see: nouns-home, nouns-explore, clanker-home - --- Test 5: Verify navigation references spaces -SELECT "navigation_config"->'items' as nav_items -FROM "community_configs" -WHERE "community_id" = 'nouns'; --- Should see spaceId references -``` - -**After seed.sql, upload space configs:** -```bash -tsx scripts/seed-navpage-spaces.ts -``` - -**Validation:** -- ✅ Database functions work -- ✅ RLS policies enforce access -- ✅ Can seed existing configs (without themes/pages) -- ✅ navPage spaceType added -- ✅ Config excludes homePage/explorePage columns -- ✅ navPage space registrations created -- ✅ Space config files uploaded to storage - -**Rollback:** Drop migrations, system unchanged - -**Estimated Time:** 4-6 hours - ---- - -## Phase 2: Basic Config Loading (Week 1, Day 3-4) - -**Goal:** Load config from DB at build time, fetch Spaces for nav items, fallback to static - -**Tasks:** -1. Create `src/config/shared/themes.ts` - Move themes to shared file -2. Update community configs to import shared themes -3. Add build-time fetch in `next.config.mjs`: - - Fetch main config from DB - - Extract spaceIds from navigation items - - Fetch Spaces for nav items with spaceId - - Convert Spaces to page configs -4. Store config in `NEXT_PUBLIC_BUILD_TIME_CONFIG` env var (now small enough at ~2.8 KB) -5. Update `src/config/index.ts` to read from env var -6. Keep static configs as fallback - -**Files to Create:** -``` -src/config/shared/ - └── themes.ts # NEW: Shared theme definitions -``` - -**Files to Modify:** -``` -next.config.mjs # Add config fetch + Space fetching, set env var -src/config/index.ts # Read from env var -src/config/nouns/nouns.theme.ts # Import from shared -src/config/clanker/clanker.theme.ts # Import from shared -src/config/example/example.theme.ts # Import from shared -src/config/systemConfig.ts # Update NavigationItem interface -``` - -**Implementation:** - -```javascript -// next.config.mjs (add at top) - -import { createClient } from '@supabase/supabase-js'; - -async function loadConfigFromDB() { - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; - - if (!supabaseUrl || !supabaseKey) { - console.log('ℹ️ No Supabase credentials, using static configs'); - return; - } - - const supabase = createClient(supabaseUrl, supabaseKey); - const community = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - - try { - const { data, error } = await supabase - .rpc('get_active_community_config', { p_community_id: community }) - .single(); - - if (error || !data) { - console.log('ℹ️ No config in DB, using static configs'); - return; - } - - // Store in env var - process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(data); - console.log('✅ Loaded config from database'); - } catch (error) { - console.warn('⚠️ Error loading config from DB:', error.message); - } -} - -await loadConfigFromDB(); - -// Continue with existing Next.js config... -``` - -```typescript -// src/config/index.ts (modify loadSystemConfig) - -export const loadSystemConfig = (): SystemConfig => { - const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - - // Try build-time config from database (stored in env var at build time) - const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; - if (buildTimeConfig) { - try { - const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; - // Map page configs from pages object to homePage/explorePage - const homePage = dbConfig.pages?.['home'] || null; - const explorePage = dbConfig.pages?.['explore'] || null; - - // Validate config structure - if (dbConfig && dbConfig.brand && dbConfig.assets) { - console.log('✅ Using config from database'); - return { - ...dbConfig, - homePage, - explorePage, - }; - } - } catch (error) { - console.warn('⚠️ Failed to parse build-time config, falling back to static:', error); - } - } - - // Fall back to static configs (existing behavior) - console.log('ℹ️ Using static configs'); - switch (communityConfig.toLowerCase()) { - case 'nouns': - return nounsSystemConfig; - case 'example': - return exampleSystemConfig; - case 'clanker': - return clankerSystemConfig as unknown as SystemConfig; - default: - return nounsSystemConfig; - } -}; -``` - -**Testing:** -1. **Test with DB available:** - ```bash - NEXT_PUBLIC_SUPABASE_URL=... SUPABASE_SERVICE_ROLE_KEY=... npm run build - # Should see: "✅ Loaded config from database" - ``` - -2. **Test without DB:** - ```bash - # Remove env vars - npm run build - # Should see: "ℹ️ Using static configs" - # Should fall back to static configs - ``` - -3. **Test app functionality:** - - Build succeeds - - App loads correctly - - Config values match DB (if loaded) or static (if fallback) - - Themes loaded from shared file - - Pages loaded from Spaces (if nav items have spaceId) - - No runtime errors - -4. **Verify config loaded:** - ```bash - # Config is stored in NEXT_PUBLIC_BUILD_TIME_CONFIG env var (~2.8 KB) - # Check build logs for "✅ Loaded config from database" - ``` - -**Validation:** -- ✅ Build succeeds with/without DB -- ✅ App uses DB config when available -- ✅ App falls back to static when DB unavailable -- ✅ Themes loaded from shared file -- ✅ Pages loaded from Spaces (via nav items) -- ✅ Config stored in env var (`NEXT_PUBLIC_BUILD_TIME_CONFIG`) -- ✅ Config size reduced (~2.8 KB vs ~29 KB) -- ✅ All existing functionality works -- ✅ No breaking changes - -**Rollback:** Remove env var reading, system back to static-only - -**Estimated Time:** 2-3 hours - ---- - -## Phase 3: Admin API Endpoints (Week 1, Day 5) - -**Goal:** Create API endpoints for admin config updates - -**Tasks:** -1. Create `/api/admin/config/[communityId]` - GET/PUT -2. Create `/api/admin/spaces/[spaceId]` - GET/PUT (for nav page Spaces) -3. Add admin permission checks -4. Add request validation -5. Note: Themes are in shared file (not editable via API for now) - -**Files to Create:** -``` -src/app/api/admin/config/[communityId]/route.ts -src/app/api/admin/spaces/[spaceId]/route.ts # NEW: For nav page Spaces -src/common/data/services/adminConfigService.ts -``` - -**Implementation:** - -```typescript -// src/app/api/admin/config/[communityId]/route.ts - -import { NextRequest, NextResponse } from 'next/server'; -import { createClient } from '@supabase/supabase-js'; -import { SystemConfig } from '@/config/systemConfig'; - -export async function GET( - request: NextRequest, - { params }: { params: { communityId: string } } -) { - // Verify admin (simplified for Phase 3) - const adminIdentity = request.headers.get('x-admin-identity'); - if (!adminIdentity) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - - const supabase = createClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.SUPABASE_SERVICE_ROLE_KEY! - ); - - // Check admin permissions - const { data: admin } = await supabase - .from('community_config_admins') - .select('id') - .eq('community_id', params.communityId) - .eq('admin_identity_public_key', adminIdentity) - .single(); - - if (!admin) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } - - // Get config - const { data, error } = await supabase - .rpc('get_active_community_config', { p_community_id: params.communityId }) - .single(); - - if (error) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } - - return NextResponse.json({ config: data }); -} - -export async function PUT( - request: NextRequest, - { params }: { params: { communityId: string } } -) { - const adminIdentity = request.headers.get('x-admin-identity'); - if (!adminIdentity) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - - const body = await request.json(); - const { config } = body; - - // Validate config structure (basic) - if (!config || typeof config !== 'object') { - return NextResponse.json({ error: 'Invalid config' }, { status: 400 }); - } - - const supabase = createClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.SUPABASE_SERVICE_ROLE_KEY! - ); - - // Check admin permissions - const { data: admin } = await supabase - .from('community_config_admins') - .select('id') - .eq('community_id', params.communityId) - .eq('admin_identity_public_key', adminIdentity) - .single(); - - if (!admin) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } - - // Update config directly (using ON CONFLICT to update existing row) - const { data, error } = await supabase - .from('community_configs') - .upsert({ - community_id: params.communityId, - brand_config: config.brand, - assets_config: config.assets, - community_config: config.community, - fidgets_config: config.fidgets, - navigation_config: config.navigation, - ui_config: config.ui, - is_published: config.is_published ?? true, - updated_at: new Date().toISOString(), - }, { - onConflict: 'community_id', - }) - .select() - .single(); - - if (error) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } - - return NextResponse.json({ success: true, config: data }); -} -``` - -**Testing:** -```bash -# Test GET -curl -H "x-admin-identity: test-admin" \ - http://localhost:3000/api/admin/config/nouns - -# Test PUT -curl -X PUT \ - -H "x-admin-identity: test-admin" \ - -H "Content-Type: application/json" \ - -d '{"config": {...}}' \ - http://localhost:3000/api/admin/config/nouns -``` - -**Validation:** -- ✅ GET returns config -- ✅ PUT updates config directly -- ✅ Permission checks work -- ✅ Invalid requests are rejected - -**Rollback:** Remove API routes, no impact on main app - -**Estimated Time:** 3-4 hours - ---- - -## Phase 4: Basic Admin UI (Week 2, Day 1-2) - -**Goal:** Create simple admin interface to view/edit configs and nav pages - -**Tasks:** -1. Create admin layout/page structure -2. Create config viewer (read-only first) -3. Add basic form for editing config -4. Add Space editor for nav pages (homePage/explorePage) -5. Add save functionality -6. Note: Themes edited in code (shared file) for now - -**Files to Create:** -``` -src/app/admin/config/[communityId]/page.tsx -src/app/admin/config/[communityId]/components/ConfigViewer.tsx -src/app/admin/config/[communityId]/components/ConfigEditor.tsx -src/app/admin/config/[communityId]/components/NavPageEditor.tsx # NEW: Edit nav page Spaces -``` - -**Implementation:** - -```typescript -// src/app/admin/config/[communityId]/page.tsx - -'use client'; - -import { useState, useEffect } from 'react'; -import { useParams } from 'next/navigation'; -import { SystemConfig } from '@/config/systemConfig'; - -export default function AdminConfigPage() { - const params = useParams(); - const communityId = params.communityId as string; - const [config, setConfig] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - fetch(`/api/admin/config/${communityId}`, { - headers: { - 'x-admin-identity': getAdminIdentity(), // Your auth - }, - }) - .then(res => res.json()) - .then(data => { - setConfig(data.config); - setLoading(false); - }); - }, [communityId]); - - if (loading) return
Loading...
; - if (!config) return
No config found
; - - return ( -
-

Edit Config: {communityId}

- -
- ); -} -``` - -**Testing:** -1. Navigate to `/admin/config/nouns` -2. Verify config loads -3. Make a small change (e.g., brand name) -4. Save and verify it updates in DB -5. Rebuild and verify change appears - -**Validation:** -- ✅ Can view config (without themes/pages) -- ✅ Can edit config -- ✅ Can edit nav page Spaces (homePage/explorePage) -- ✅ Can save changes -- ✅ Changes persist in DB/Storage -- ✅ Changes appear after rebuild - -**Rollback:** Remove admin routes, no impact on main app - -**Estimated Time:** 4-6 hours - ---- - -## Phase 5: Asset Upload (Week 2, Day 3-4) - -**Goal:** Add asset upload functionality - -**Tasks:** -1. Create Supabase storage bucket -2. Create upload API endpoint -3. Add upload UI component -4. Update config to store asset paths - -**Files to Create:** -``` -src/app/api/admin/assets/upload/route.ts -src/app/admin/config/[communityId]/components/AssetUpload.tsx -``` - -**Testing:** -1. Upload a logo -2. Verify it's in Supabase Storage -3. Verify config stores storage path -4. Rebuild and verify asset downloads -5. Verify asset appears in app - -**Validation:** -- ✅ Can upload assets -- ✅ Assets stored in Supabase -- ✅ Config updated with paths -- ✅ Assets download at build time -- ✅ Assets appear correctly - -**Rollback:** Remove upload functionality, use static assets - -**Estimated Time:** 4-6 hours - ---- - -## Phase 6: Build-Time Asset Download (Week 2, Day 5) - -**Goal:** Download assets during build - -**Tasks:** -1. Add asset download to `next.config.mjs` -2. Download assets to `public/images/{community}/` -3. Update config paths to public paths -4. Add error handling - -**Files to Modify:** -``` -next.config.mjs # Add asset download -``` - -**Testing:** -1. Upload asset via admin UI -2. Trigger build -3. Verify asset downloads to `public/` -4. Verify config uses public path -5. Verify asset loads in app - -**Validation:** -- ✅ Assets download during build -- ✅ Assets in correct location -- ✅ Config paths updated -- ✅ Assets load correctly -- ✅ Fallback works if download fails - -**Rollback:** Remove download code, use static assets - -**Estimated Time:** 2-3 hours - ---- - -## Phase 7: Asset Optimization (Week 3, Day 1) - -**Goal:** Optimize assets during download - -**Tasks:** -1. Add Sharp optimization -2. Convert PNG/JPG to WebP -3. Resize large images -4. Add optimization logging - -**Files to Modify:** -``` -next.config.mjs # Add Sharp optimization -``` - -**Testing:** -1. Upload large PNG (2MB+) -2. Build and verify WebP created -3. Verify file size reduced -4. Verify image quality acceptable -5. Verify load time improved - -**Validation:** -- ✅ Images optimized -- ✅ File sizes reduced -- ✅ Quality maintained -- ✅ Build time acceptable -- ✅ Performance improved - -**Rollback:** Remove optimization, use raw downloads - -**Estimated Time:** 2-3 hours - ---- - -## Phase 8: Rebuild Trigger (Week 3, Day 2) - -**Goal:** Trigger rebuilds after config updates - -**Tasks:** -1. Create rebuild trigger API -2. Integrate with CI/CD (GitHub Actions/Vercel) -3. Add rebuild status UI -4. Add webhook handling - -**Files to Create:** -``` -src/app/api/admin/config/[communityId]/rebuild/route.ts -.github/workflows/rebuild-on-config-change.yml (if using GitHub Actions) -``` - -**Testing:** -1. Update config via admin UI -2. Click "Trigger Rebuild" -3. Verify build triggered -4. Verify build completes -5. Verify changes deployed - -**Validation:** -- ✅ Rebuild triggers correctly -- ✅ Build completes successfully -- ✅ Changes deployed -- ✅ Status updates correctly - -**Rollback:** Remove trigger, manual rebuilds - -**Estimated Time:** 3-4 hours - ---- - -## Phase 9: Production Rollout (Week 3, Day 3-5) - -**Goal:** Roll out to production gradually - -**Tasks:** -1. Test with one community (e.g., 'example') -2. Monitor build times -3. Monitor performance -4. Roll out to other communities -5. Keep static configs as fallback for now - -**Testing:** -1. Deploy to staging -2. Test all functionality -3. Monitor metrics -4. Deploy to production (one community) -5. Monitor for issues -6. Roll out to all communities - -**Validation:** -- ✅ All communities work -- ✅ Performance acceptable -- ✅ No regressions -- ✅ Admin interface works -- ✅ Assets load correctly - -**Rollback:** Revert to static configs - -**Estimated Time:** 4-8 hours - ---- - -## Phase 10: Phase Out Static Configs (Week 4) - -**Goal:** Remove old static config system, make DB the single source of truth - -**Prerequisites:** -- ✅ Phase 9 complete (all communities in production) -- ✅ Stable for at least 1-2 weeks -- ✅ No critical issues reported -- ✅ Admin interface fully functional -- ✅ All configs migrated to database - -### Phase 10A: Remove Static Config Fallbacks (Week 4, Day 1-2) - -**Goal:** Remove static config imports, keep minimal emergency fallback - -**Tasks:** -1. Update `src/config/index.ts` to remove static imports -2. Keep minimal fallback (hardcoded default config) -3. Update error handling -4. Add monitoring/alerting for config load failures - -**Files to Modify:** -``` -src/config/index.ts # Remove static imports -src/config/nouns/index.ts # Mark as deprecated -src/config/clanker/index.ts # Mark as deprecated -src/config/example/index.ts # Mark as deprecated -``` - -**Implementation:** - -```typescript -// src/config/index.ts - -import { SystemConfig } from './systemConfig'; - -// Minimal emergency fallback (only used if DB completely unavailable) -const EMERGENCY_FALLBACK_CONFIG: SystemConfig = { - brand: { - name: "nounspace", - displayName: "Nounspace", - tagline: "A customizable Farcaster client", - description: "Nounspace - Customizable Farcaster client", - miniAppTags: [], - }, - assets: { - logos: { - main: "/images/logo.png", - icon: "/images/icon.png", - favicon: "/images/favicon.ico", - appleTouch: "/images/apple-touch-icon.png", - og: "/images/og.png", - splash: "/images/splash.png", - }, - }, - // ... minimal required config -}; - -export const loadSystemConfig = (): SystemConfig => { - const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - - // Try build-time config from DB - const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; - if (buildTimeConfig) { - try { - const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; - - // Validate config structure - if (dbConfig && dbConfig.brand && dbConfig.assets) { - return dbConfig; - } else { - console.error('❌ Invalid config structure from DB, using emergency fallback'); - // TODO: Alert monitoring system - } - } catch (error) { - console.error('❌ Failed to parse build-time config:', error); - // TODO: Alert monitoring system - } - } - - // Emergency fallback only (should rarely be used) - console.error('⚠️ Using emergency fallback config - DB config unavailable!'); - console.error('⚠️ This indicates a build configuration issue.'); - // TODO: Alert monitoring system - - return EMERGENCY_FALLBACK_CONFIG; -}; -``` - -**Testing:** -1. **Test with DB available:** - ```bash - # Should use DB config - npm run build - # Check logs: Should see config loaded from DB - ``` - -2. **Test without DB (simulate failure):** - ```bash - # Remove env vars or break DB connection - npm run build - # Should use emergency fallback - # Should log warnings/errors - ``` - -3. **Test app with emergency fallback:** - ```bash - npm run dev - # App should still load (with minimal config) - ``` - -**Validation:** -- ✅ App works with DB config -- ✅ App works with emergency fallback -- ✅ Errors are logged/monitored -- ✅ No references to old static configs -- ✅ Build succeeds in both cases - -**Rollback:** Re-add static config imports - -**Estimated Time:** 2-3 hours - ---- - -### Phase 10B: Remove Static Config Files (Week 4, Day 3) - -**Goal:** Delete old static config files, keep only for reference - -**Tasks:** -1. Archive static configs (move to `src/config/_archived/`) -2. Update imports throughout codebase -3. Remove from exports -4. Update documentation - -**Files to Move:** -``` -src/config/nouns/ → src/config/_archived/nouns/ -src/config/clanker/ → src/config/_archived/clanker/ -src/config/example/ → src/config/_archived/example/ -``` - -**Files to Update:** -``` -src/config/index.ts # Remove exports -src/config/systemConfig.ts # Keep (type definitions) -``` - -**Implementation:** - -```typescript -// src/config/index.ts - -import { SystemConfig } from './systemConfig'; - -// Static configs moved to _archived/ - no longer imported -// Only DB configs are used now - -const EMERGENCY_FALLBACK_CONFIG: SystemConfig = { - // ... minimal config -}; - -export const loadSystemConfig = (): SystemConfig => { - // ... same as Phase 10A -}; - -// Remove all static config exports -// export { nounsSystemConfig } from './nouns/index'; // ❌ Removed -// export { clankerSystemConfig } from './clanker/index'; // ❌ Removed -``` - -**Testing:** -1. **Verify no broken imports:** - ```bash - npm run check-types - # Should pass - no broken imports - ``` - -2. **Verify build works:** - ```bash - npm run build - # Should succeed - ``` - -3. **Search for remaining references:** - ```bash - grep -r "nounsSystemConfig\|clankerSystemConfig\|exampleSystemConfig" src/ - # Should only find references in _archived/ or comments - ``` - -**Validation:** -- ✅ No broken imports -- ✅ Build succeeds -- ✅ No references to old configs (except archived) -- ✅ Type checking passes - -**Rollback:** Restore files from `_archived/` - -**Estimated Time:** 1-2 hours - ---- - -### Phase 10C: Update Space Creators (Week 4, Day 4) - -**Goal:** Migrate initial space creators to use DB configs - -**Tasks:** -1. Move space creator logic to database -2. Or keep as code but reference DB config -3. Update space creator functions -4. Test space creation - -**Options:** - -**Option A: Keep creators in code, reference DB config** -```typescript -// src/config/index.ts - -export const createInitialProfileSpaceConfigForFid = (fid: number, username?: string) => { - const config = loadSystemConfig(); // Uses DB config - - // Use config values in space creation - return { - // ... space config using config.community, config.brand, etc. - }; -}; -``` - -**Option B: Store creators in DB** -```sql --- Add to community_configs table -ALTER TABLE community_configs ADD COLUMN initial_space_creators JSONB; - --- Store creator functions/logic as JSON -``` - -**Testing:** -1. Create profile space -2. Create channel space -3. Create token space -4. Create proposal space -5. Create homebase -6. Verify all work correctly - -**Validation:** -- ✅ All space creators work -- ✅ Spaces use DB config values -- ✅ No references to static configs - -**Rollback:** Restore static space creators - -**Estimated Time:** 3-4 hours - ---- - -### Phase 10D: Cleanup & Documentation (Week 4, Day 5) - -**Goal:** Final cleanup and documentation updates - -**Tasks:** -1. Remove deprecated code comments -2. Update all documentation -3. Update README -4. Add migration guide -5. Update contributing guide -6. Remove unused files - -**Files to Update:** -``` -docs/CONFIGURATION.md # Update for DB system -docs/COMMUNITY_CONFIG_SYSTEM.md # Mark as legacy -docs/GETTING_STARTED.md # Update setup steps -README.md # Update config section -``` - -**Files to Create:** -``` -docs/MIGRATION_GUIDE.md # How to migrate from static to DB -docs/ADMIN_GUIDE.md # Admin user guide -``` - -**Cleanup Tasks:** -1. Remove unused imports -2. Remove commented code -3. Remove deprecated functions -4. Clean up `_archived/` folder (or keep for reference) - -**Testing:** -1. **Verify documentation accuracy:** - - All examples work - - All links valid - - All instructions correct - -2. **Verify code cleanliness:** - ```bash - npm run lint - npm run check-types - # Should pass - ``` - -3. **Verify no dead code:** - ```bash - # Search for unused exports - # Check for orphaned files - ``` - -**Validation:** -- ✅ Documentation updated -- ✅ No dead code -- ✅ No lint errors -- ✅ All examples work -- ✅ Migration guide complete - -**Rollback:** Restore old documentation - -**Estimated Time:** 2-3 hours - ---- - -## Phase 10 Summary - -**Total Time:** 8-12 hours over 1 week - -**Phases:** -- **10A:** Remove static fallbacks (2-3h) -- **10B:** Remove static files (1-2h) -- **10C:** Update space creators (3-4h) -- **10D:** Cleanup & docs (2-3h) - -**Final State:** -- ✅ DB is single source of truth -- ✅ No static config dependencies -- ✅ Minimal emergency fallback only -- ✅ Clean codebase -- ✅ Updated documentation - -**Rollback Plan:** -- Can restore static configs from `_archived/` -- Can re-add static imports -- Can revert to Phase 9 state - ---- - -## Complete Timeline - -| Phase | Duration | Week | Risk | -|-------|----------|------|------| -| Phase 0 | 1-2h | Week 1 | Low | -| Phase 1 | 4-6h | Week 1 | Low | -| Phase 2 | 2-3h | Week 1 | Medium | -| Phase 3 | 3-4h | Week 1 | Low | -| Phase 4 | 4-6h | Week 2 | Medium | -| Phase 5 | 4-6h | Week 2 | Medium | -| Phase 6 | 2-3h | Week 2 | Medium | -| Phase 7 | 2-3h | Week 3 | Low | -| Phase 8 | 3-4h | Week 3 | Medium | -| Phase 9 | 4-8h | Week 3 | High | -| **Phase 10** | **8-12h** | **Week 4** | **Medium** | - -**Total: 4-5 weeks** - ---- - -## Phase 10: Phase Out Static Configs (Week 4) - -**Goal:** Remove old static config system, make DB the single source of truth - -**Prerequisites:** -- ✅ Phase 9 complete (all communities in production) -- ✅ Stable for at least 1-2 weeks -- ✅ No critical issues reported -- ✅ Admin interface fully functional -- ✅ All configs migrated to database - -### Phase 10A: Remove Static Config Fallbacks (Week 4, Day 1-2) - -**Goal:** Remove static config imports, keep minimal emergency fallback - -**Tasks:** -1. Update `src/config/index.ts` to remove static imports -2. Keep minimal fallback (hardcoded default config) -3. Update error handling -4. Add monitoring/alerting for config load failures - -**Files to Modify:** -``` -src/config/index.ts # Remove static imports -src/config/nouns/index.ts # Mark as deprecated -src/config/clanker/index.ts # Mark as deprecated -src/config/example/index.ts # Mark as deprecated -``` - -**Implementation:** - -```typescript -// src/config/index.ts - -import { SystemConfig } from './systemConfig'; - -// Minimal emergency fallback (only used if DB completely unavailable) -const EMERGENCY_FALLBACK_CONFIG: SystemConfig = { - brand: { - name: "nounspace", - displayName: "Nounspace", - tagline: "A customizable Farcaster client", - description: "Nounspace - Customizable Farcaster client", - miniAppTags: [], - }, - assets: { - logos: { - main: "/images/logo.png", - icon: "/images/icon.png", - favicon: "/images/favicon.ico", - appleTouch: "/images/apple-touch-icon.png", - og: "/images/og.png", - splash: "/images/splash.png", - }, - }, - theme: { - default: { - id: "default", - name: "Default", - properties: { - font: "Inter", - fontColor: "#000000", - headingsFont: "Inter", - headingsFontColor: "#000000", - background: "#ffffff", - backgroundHTML: "", - musicURL: "", - fidgetBackground: "#ffffff", - fidgetBorderWidth: "1px", - fidgetBorderColor: "#C0C0C0", - fidgetShadow: "none", - fidgetBorderRadius: "12px", - gridSpacing: "16", - }, - }, - // ... minimal required themes - }, - community: { - type: "nounspace", - urls: { - website: "https://nounspace.com", - discord: "", - twitter: "", - github: "", - forum: "", - }, - social: { - farcaster: "", - discord: "", - twitter: "", - }, - governance: { - proposals: "", - delegates: "", - treasury: "", - }, - tokens: {}, - contracts: {}, - }, - fidgets: { - enabled: [], - disabled: [], - }, - homePage: { - defaultTab: "Home", - tabOrder: ["Home"], - tabs: { - Home: { - name: "Home", - displayName: "Home", - layoutID: "default", - layoutDetails: { - layoutConfig: { layout: [] }, - layoutFidget: "grid", - }, - theme: { - id: "default", - name: "Default", - properties: { - font: "Inter", - fontColor: "#000000", - headingsFont: "Inter", - headingsFontColor: "#000000", - background: "#ffffff", - backgroundHTML: "", - musicURL: "", - fidgetBackground: "#ffffff", - fidgetBorderWidth: "1px", - fidgetBorderColor: "#C0C0C0", - fidgetShadow: "none", - fidgetBorderRadius: "12px", - gridSpacing: "16", - }, - }, - fidgetInstanceDatums: {}, - fidgetTrayContents: [], - isEditable: false, - timestamp: new Date().toISOString(), - }, - }, - layout: { - defaultLayoutFidget: "grid", - gridSpacing: 16, - theme: { - background: "#ffffff", - fidgetBackground: "#ffffff", - font: "Inter", - fontColor: "#000000", - }, - }, - }, - explorePage: { - defaultTab: "Explore", - tabOrder: ["Explore"], - tabs: { - Explore: { - // ... minimal explore config - }, - }, - layout: { - defaultLayoutFidget: "grid", - gridSpacing: 16, - theme: { - background: "#ffffff", - fidgetBackground: "#ffffff", - font: "Inter", - fontColor: "#000000", - }, - }, - }, -}; - -export const loadSystemConfig = (): SystemConfig => { - const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - - // Try build-time config from DB - const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; - if (buildTimeConfig) { - try { - const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; - - // Validate config structure - if (dbConfig && dbConfig.brand && dbConfig.assets) { - return dbConfig; - } else { - console.error('❌ Invalid config structure from DB, using emergency fallback'); - // TODO: Alert monitoring system (Sentry, etc.) - } - } catch (error) { - console.error('❌ Failed to parse build-time config:', error); - // TODO: Alert monitoring system - } - } - - // Emergency fallback only (should rarely be used) - console.error('⚠️ Using emergency fallback config - DB config unavailable!'); - console.error('⚠️ This indicates a build configuration issue.'); - // TODO: Alert monitoring system - - return EMERGENCY_FALLBACK_CONFIG; -}; - -// Remove all static config exports -// export { nounsSystemConfig } from './nouns/index'; // ❌ Removed -// export { clankerSystemConfig } from './clanker/index'; // ❌ Removed -// export { exampleSystemConfig } from './example/index'; // ❌ Removed -``` - -**Testing:** -1. **Test with DB available:** - ```bash - npm run build - # Should use DB config, no warnings - ``` - -2. **Test without DB (simulate failure):** - ```bash - # Remove SUPABASE env vars - npm run build - # Should use emergency fallback - # Should log warnings/errors - ``` - -3. **Test app with emergency fallback:** - ```bash - npm run dev - # App should still load (with minimal config) - ``` - -**Validation:** -- ✅ App works with DB config -- ✅ App works with emergency fallback -- ✅ Errors are logged/monitored -- ✅ No references to old static configs in main code -- ✅ Build succeeds in both cases - -**Rollback:** Re-add static config imports - -**Estimated Time:** 2-3 hours - ---- - -### Phase 10B: Archive Static Config Files (Week 4, Day 3) - -**Goal:** Move static configs to archive, remove from active codebase - -**Tasks:** -1. Create archive directory -2. Move static config files to archive -3. Update any remaining references -4. Add deprecation notices - -**Files to Move:** -``` -src/config/nouns/ → src/config/_archived/nouns/ -src/config/clanker/ → src/config/_archived/clanker/ -src/config/example/ → src/config/_archived/example/ -``` - -**Implementation:** - -```bash -# Create archive directory -mkdir -p src/config/_archived - -# Move static configs -mv src/config/nouns src/config/_archived/ -mv src/config/clanker src/config/_archived/ -mv src/config/example src/config/_archived/ - -# Add README in archive -echo "# Archived Static Configs - -These configs are kept for reference only. -All active configs are now stored in the database. - -Last used: [Date] -Migrated to DB: [Date] -" > src/config/_archived/README.md -``` - -**Update Space Creators:** - -```typescript -// src/config/index.ts - -// Space creators now delegate to DB config or use minimal defaults -export const createInitialProfileSpaceConfigForFid = (fid: number, username?: string) => { - const config = loadSystemConfig(); // Always uses DB config now - - // Use config values - return { - // ... space config using config.community, config.brand, etc. - fidgetInstanceDatums: { - "feed:profile": { - config: { - editable: false, - settings: { - feedType: FeedType.Filter, - users: fid, - filterType: FilterType.Fids, - }, - data: {}, - }, - fidgetType: "feed", - id: "feed:profile", - }, - }, - // ... rest of space config - }; -}; - -// Similar updates for other space creators -``` - -**Testing:** -1. **Verify no broken imports:** - ```bash - npm run check-types - # Should pass - ``` - -2. **Search for remaining references:** - ```bash - grep -r "from './nouns\|from './clanker\|from './example" src/ --exclude-dir=_archived - # Should find no results (or only in comments) - ``` - -3. **Verify build works:** - ```bash - npm run build - # Should succeed - ``` - -**Validation:** -- ✅ No broken imports -- ✅ Build succeeds -- ✅ No references to old configs (except archived) -- ✅ Type checking passes -- ✅ Space creators work - -**Rollback:** Restore files from archive - -**Estimated Time:** 1-2 hours - ---- - -### Phase 10C: Update Space Creators (Week 4, Day 4) - -**Goal:** Ensure space creators use DB configs, not static - -**Tasks:** -1. Review all space creator functions -2. Update to use `loadSystemConfig()` (which uses DB) -3. Remove any direct static config references -4. Test all space creation flows - -**Files to Update:** -``` -src/config/index.ts # Space creator functions -``` - -**Implementation:** - -```typescript -// src/config/index.ts - -// All space creators now use loadSystemConfig() which gets from DB -export const createInitialProfileSpaceConfigForFid = (fid: number, username?: string) => { - const config = loadSystemConfig(); // Uses DB config - - // Use config values - return { - // ... space config - theme: config.theme.default, // Use theme from DB config - // ... rest uses config values - }; -}; - -// Similar pattern for all creators -export const createInitialChannelSpaceConfig = (channelId: string) => { - const config = loadSystemConfig(); - // ... use config values -}; - -export const createInitialTokenSpaceConfigForAddress = (...args: any[]) => { - const config = loadSystemConfig(); - // ... use config values -}; - -export const createInitalProposalSpaceConfigForProposalId = (...args: any[]) => { - const config = loadSystemConfig(); - // ... use config values -}; - -export const INITIAL_HOMEBASE_CONFIG = (() => { - const config = loadSystemConfig(); - return { - // ... use config.homePage values - }; -})(); -``` - -**Testing:** -1. Create profile space → Verify uses DB config -2. Create channel space → Verify uses DB config -3. Create token space → Verify uses DB config -4. Create proposal space → Verify uses DB config -5. Create homebase → Verify uses DB config - -**Validation:** -- ✅ All space creators work -- ✅ Spaces use DB config values -- ✅ No references to static configs -- ✅ Themes/styles match DB config - -**Rollback:** Restore static space creators - -**Estimated Time:** 3-4 hours - ---- - -### Phase 10D: Final Cleanup & Documentation (Week 4, Day 5) - -**Goal:** Complete cleanup and update all documentation - -**Tasks:** -1. Remove deprecated code comments -2. Update all documentation -3. Update README -4. Create migration guide -5. Update contributing guide -6. Clean up archived files (optional) - -**Files to Update:** -``` -docs/CONFIGURATION.md # Update for DB system -docs/COMMUNITY_CONFIG_SYSTEM.md # Mark as legacy, add DB section -docs/GETTING_STARTED.md # Update setup steps -README.md # Update config section -docs/CONTRIBUTING.MD # Update for DB configs -``` - -**Files to Create:** -``` -docs/MIGRATION_GUIDE.md # Static → DB migration guide -docs/ADMIN_GUIDE.md # Admin user guide -docs/LEGACY_STATIC_CONFIGS.md # Reference for archived configs -``` - -**Cleanup Tasks:** -1. Remove unused imports -2. Remove commented code -3. Remove deprecated functions -4. Update `.gitignore` if needed -5. Clean up test files - -**Documentation Updates:** - -```markdown -# docs/CONFIGURATION.md (updated) - -## Configuration System - -Nounspace now uses a **database-backed configuration system**. Configurations are stored in Supabase and loaded at build time. - -### For Developers - -Configs are automatically loaded from the database during build. No manual configuration needed. - -### For Admins - -Use the admin interface at `/admin/config/[communityId]` to update configurations. - -### Legacy Static Configs - -Static configs have been archived to `src/config/_archived/`. They are kept for reference only and are no longer used. -``` - -**Testing:** -1. **Verify documentation:** - - All examples work - - All links valid - - All instructions correct - -2. **Verify code cleanliness:** - ```bash - npm run lint - npm run check-types - # Should pass with no errors - ``` - -3. **Verify no dead code:** - ```bash - # Search for unused exports - # Check for orphaned files - ``` - -**Validation:** -- ✅ Documentation updated and accurate -- ✅ No dead code -- ✅ No lint errors -- ✅ All examples work -- ✅ Migration guide complete -- ✅ Admin guide complete - -**Rollback:** Restore old documentation - -**Estimated Time:** 2-3 hours - ---- - -## Phase 10 Summary - -**Total Time:** 8-12 hours over 1 week - -**Sub-Phases:** -- **10A:** Remove static fallbacks (2-3h) -- **10B:** Archive static files (1-2h) -- **10C:** Update space creators (3-4h) -- **10D:** Cleanup & docs (2-3h) - -**Final State:** -- ✅ DB is single source of truth -- ✅ No static config dependencies in active code -- ✅ Minimal emergency fallback only -- ✅ Static configs archived for reference -- ✅ Clean codebase -- ✅ Complete documentation -- ✅ Migration guide available - -**Success Criteria:** -- ✅ App works entirely from DB configs -- ✅ No static config imports in active code -- ✅ Emergency fallback works if DB unavailable -- ✅ All documentation updated -- ✅ Codebase is clean - -**Rollback Plan:** -- Can restore static configs from archive -- Can re-add static imports -- Can revert to Phase 9 state (DB + static fallback) - ---- - -## Testing Strategy Per Phase - -### Unit Tests -```typescript -// tests/config-loader.test.ts -describe('Config Loader', () => { - it('should load from DB when available', () => { - process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(testConfig); - const config = loadSystemConfig(); - expect(config.brand.name).toBe('Test'); - }); - - it('should fallback to static when DB unavailable', () => { - delete process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; - const config = loadSystemConfig(); - expect(config.brand.name).toBe('Nouns'); - }); -}); -``` - -### Integration Tests -```typescript -// tests/admin-api.test.ts -describe('Admin API', () => { - it('should require admin authentication', async () => { - const res = await fetch('/api/admin/config/nouns'); - expect(res.status).toBe(401); - }); - - it('should update config', async () => { - const res = await fetch('/api/admin/config/nouns', { - method: 'PUT', - headers: { - 'x-admin-identity': 'test-admin', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ config: testConfig }), - }); - expect(res.status).toBe(200); - }); -}); -``` - -### E2E Tests -```typescript -// tests/admin-ui.e2e.test.ts -describe('Admin UI', () => { - it('should load and display config', async () => { - await page.goto('/admin/config/nouns'); - await expect(page.locator('h1')).toContainText('Edit Config: nouns'); - }); - - it('should save config changes', async () => { - await page.fill('[name="brand.name"]', 'New Name'); - await page.click('button[type="submit"]'); - await expect(page.locator('.success')).toBeVisible(); - }); -}); -``` - ---- - -## Rollback Plan Per Phase - -Each phase can be rolled back independently: - -1. **Phase 1 (DB Schema)** - Drop migrations -2. **Phase 2 (Config Loading)** - Remove env var reading -3. **Phase 3 (Admin API)** - Remove API routes -4. **Phase 4 (Admin UI)** - Remove admin pages -5. **Phase 5 (Asset Upload)** - Remove upload functionality -6. **Phase 6 (Asset Download)** - Remove download code -7. **Phase 7 (Optimization)** - Remove optimization -8. **Phase 8 (Rebuild Trigger)** - Remove trigger -9. **Phase 9 (Production)** - Revert to static configs - ---- - -## Success Criteria Per Phase - -### Phase 1: Database Schema -- ✅ Tables created successfully (without page/theme columns) -- ✅ Functions work correctly -- ✅ RLS policies enforce access -- ✅ Can seed initial data (without themes/pages) -- ✅ navPage spaceType added - -### Phase 2: Config Loading -- ✅ Shared themes file created -- ✅ Build succeeds with/without DB -- ✅ App uses DB config when available -- ✅ Themes loaded from shared file -- ✅ Pages loaded from Spaces (via nav items) -- ✅ Config file generated (not env var) -- ✅ Config size reduced (~2.8 KB) -- ✅ App falls back to static when unavailable -- ✅ No breaking changes - -### Phase 3: Admin API -- ✅ Can fetch config (without themes/pages) -- ✅ Can update config -- ✅ Can fetch/update nav page Spaces -- ✅ Permission checks work - -### Phase 4: Admin UI -- ✅ Can view config (without themes/pages) -- ✅ Can edit config -- ✅ Can edit nav page Spaces -- ✅ Can save changes -- ✅ Changes persist - -### Phase 5: Asset Upload -- ✅ Can upload assets -- ✅ Assets stored correctly -- ✅ Config updated with paths - -### Phase 6: Asset Download -- ✅ Assets download at build time -- ✅ Assets in correct location -- ✅ Config paths updated -- ✅ Assets load correctly - -### Phase 7: Asset Optimization -- ✅ Images optimized -- ✅ File sizes reduced -- ✅ Quality maintained -- ✅ Performance improved - -### Phase 8: Rebuild Trigger -- ✅ Rebuild triggers correctly -- ✅ Build completes successfully -- ✅ Changes deployed - -### Phase 9: Production -- ✅ All communities work -- ✅ Performance acceptable -- ✅ No regressions - -### Phase 10: Phase Out Static Configs -- ✅ No static config dependencies -- ✅ DB is single source of truth -- ✅ Emergency fallback works -- ✅ Documentation updated -- ✅ Codebase cleaned - ---- - -## Timeline Summary - -| Phase | Duration | Dependencies | Risk Level | -|-------|----------|--------------|------------| -| Phase 0 | 1-2h | None | Low | -| Phase 1 | 4-6h | Phase 0 | Low | -| Phase 2 | 2-3h | Phase 1 | Medium | -| Phase 3 | 3-4h | Phase 1 | Low | -| Phase 4 | 4-6h | Phase 3 | Medium | -| Phase 5 | 4-6h | Phase 1 | Medium | -| Phase 6 | 2-3h | Phase 5 | Medium | -| Phase 7 | 2-3h | Phase 6 | Low | -| Phase 8 | 3-4h | Phase 4 | Medium | -| Phase 9 | 4-8h | All phases | High | -| **Phase 10** | **8-12h** | **Phase 9** | **Medium** | - -**Total Estimated Time:** 4-5 weeks - ---- - -## Quick Start: Minimal Viable Implementation - -If you want to test the concept quickly: - -1. **Phase 1** - Create DB schema (2h) -2. **Phase 2** - Basic config loading (2h) -3. **Manual testing** - Update DB directly, rebuild, verify - -This gives you a working proof-of-concept in ~4 hours! - ---- - -## Next Steps - -1. **Start with Phase 0** - Set up test environment -2. **Complete Phase 1** - Database foundation -3. **Test Phase 2** - Verify config loading works -4. **Iterate** - Add phases incrementally - -Each phase is independently testable and can be rolled back if issues arise. - diff --git a/docs/DATABASE_CONFIG/QUICK_START_IMPLEMENTATION.md b/docs/DATABASE_CONFIG/QUICK_START_IMPLEMENTATION.md deleted file mode 100644 index 4c58d0ba0..000000000 --- a/docs/DATABASE_CONFIG/QUICK_START_IMPLEMENTATION.md +++ /dev/null @@ -1,714 +0,0 @@ -# Quick Start: Incremental Implementation Guide - -> **See also:** -> - `DATABASE_CONFIG_GUIDE.md` - Architecture and overview -> - `DATABASE_CONFIG_IMPLEMENTATION.md` - Detailed implementation plan -> - `QUICK_START_TESTING.md` - Testing guide -> - `README.md` - Documentation index - -## 🎯 Goal - -Implement database-backed configs incrementally, testing each piece before moving forward. - -**Current Architecture:** -- ✅ Configs stored in DB (without themes/pages) - ~2.8 KB -- ✅ Configs loaded at build time, stored in `NEXT_PUBLIC_BUILD_TIME_CONFIG` env var -- ✅ Themes in shared file (`src/config/shared/themes.ts`) -- ✅ Pages stored as Spaces (referenced by navigation `spaceId`) -- ✅ Config size reduced by ~90% (from ~29 KB to ~2.8 KB) - -## 📋 Phase Overview - -``` -Phase 0: Setup → Phase 1: DB Schema - ↓ -Phase 2: Config Loading ← Phase 3: Admin API - ↓ -Phase 4: Admin UI → Phase 5: Asset Upload - ↓ -Phase 6: Asset Download → Phase 7: Optimization - ↓ -Phase 8: Rebuild Trigger → Phase 9: Production → Phase 10: Phase Out Static -``` - -## 🚀 Quick Proof of Concept (4 hours) - -Want to test the concept quickly? Do these 3 phases: - -### Step 1: Database Setup (1-2 hours) - -**Reset database from scratch:** -```bash -# Reset Supabase database (applies all migrations + seed data) -supabase db reset -``` - -This will: -1. ✅ Apply migration: `create_community_configs.sql` (creates table without themes/pages) -2. ✅ Apply migration: `add_navpage_space_type.sql` (adds navPage spaceType) -3. ✅ Run `seed.sql`: - - Seeds configs for nouns, example, clanker - - Creates navPage space registrations (nouns-home, nouns-explore, clanker-home) - - Links navigation items to spaces via spaceId - -**Verify:** -```sql --- Check table exists and has correct columns -SELECT column_name, data_type -FROM information_schema.columns -WHERE table_name = 'community_configs'; - --- Should see: brand_config, assets_config, community_config, fidgets_config, navigation_config, ui_config --- Should NOT see: theme_config, home_page_config, explore_page_config - --- Test function -SELECT get_active_community_config('nouns'); - --- Verify navPage spaceType exists -SELECT DISTINCT "spaceType" FROM "spaceRegistrations"; --- Should include: 'navPage' - --- Verify navPage spaces were created -SELECT "spaceId", "spaceName", "spaceType" -FROM "spaceRegistrations" -WHERE "spaceType" = 'navPage'; --- Should see: nouns-home, nouns-explore, clanker-home - --- Verify navigation configs reference spaces -SELECT "community_id", "navigation_config"->'items' as nav_items -FROM "community_configs" -WHERE "community_id" = 'nouns'; --- Should see spaceId references in navigation items -``` - -**Expected output:** -- ✅ Table created with correct columns -- ✅ Function returns config (without themes/pages) -- ✅ Seed data inserted for all 3 communities -- ✅ navPage spaceType available -- ✅ navPage space registrations created -- ✅ Navigation items reference spaceIds - -### Step 2: Build-Time Loading (1 hour) - -```javascript -// next.config.mjs (add at top) - -import { createClient } from '@supabase/supabase-js'; -import { writeFile, mkdir } from 'fs/promises'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -async function fetchSpaceBySpaceId(supabase, spaceId) { - // Fetch tab order first - const { data: tabOrderData } = await supabase.storage - .from('spaces') - .download(`${spaceId}/tabOrder`); - - if (!tabOrderData) return null; - - const tabOrderFile = JSON.parse(await tabOrderData.text()); - const tabOrder = tabOrderFile.tabOrder || []; - - // Fetch each tab config - const tabs = {}; - for (const tabName of tabOrder) { - try { - const { data: tabData } = await supabase.storage - .from('spaces') - .download(`${spaceId}/tabs/${tabName}`); - - if (tabData) { - const tabFile = JSON.parse(await tabData.text()); - const tabConfig = JSON.parse(tabFile.fileData); // Unencrypted SignedFile - tabs[tabName] = tabConfig; - } - } catch (error) { - console.warn(`Failed to fetch tab ${tabName}:`, error.message); - } - } - - if (Object.keys(tabs).length === 0) return null; - - // Reconstruct HomePageConfig/ExplorePageConfig format - return { - defaultTab: tabOrder[0] || 'Home', - tabOrder, - tabs, - layout: { - defaultLayoutFidget: 'grid', - gridSpacing: 16, - theme: {}, - }, - }; -} - -function convertSpaceToPageConfig(spacePageConfig) { - // Space is already in HomePageConfig/ExplorePageConfig format - // (reconstructed from tabs in fetchSpaceBySpaceId) - return spacePageConfig; -} - -async function loadConfigFromDB() { - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; - - if (!supabaseUrl || !supabaseKey) { - console.log('ℹ️ Using static configs (no DB credentials)'); - return; - } - - const supabase = createClient(supabaseUrl, supabaseKey); - const community = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - - try { - // Fetch main config (now much smaller - no themes/pages!) - const { data: config, error } = await supabase - .rpc('get_active_community_config', { p_community_id: community }) - .single(); - - if (error || !data) { - console.log('ℹ️ No DB config found, using static configs'); - if (error) { - console.log(` Error: ${error.message}`); - } - return; - } - - // Extract spaceIds from navigation items - const navItems = config.navigation?.items || []; - const spaceIds = navItems - .filter(item => item.spaceId) - .map(item => ({ navId: item.id, spaceId: item.spaceId })); - - // Fetch Spaces for nav items - const pageConfigs = {}; - for (const { navId, spaceId } of spaceIds) { - try { - const space = await fetchSpaceBySpaceId(supabase, spaceId); - if (space) { - pageConfigs[navId] = convertSpaceToPageConfig(space); - } - } catch (error) { - console.warn(`⚠️ Failed to fetch Space ${spaceId}:`, error.message); - } - } - - // Import shared themes (will be created in Step 3) - // For now, we'll add a placeholder - themes will be imported from shared file - const themesPlaceholder = {}; // Will be replaced with import from shared/themes.ts - - // Combine: config + themes + pages - const fullConfig = { - ...config, - theme: themesPlaceholder, // From shared file (Step 3) - pages: pageConfigs, // From Spaces - }; - - // Store config in environment variable (now small enough at ~2.8 KB) - process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(fullConfig); - console.log('✅ Loaded config from database'); - } catch (error) { - console.warn('⚠️ Error loading config from DB:', error.message); - } -} - -// Load config before Next.js config is created -await loadConfigFromDB(); - -// Continue with existing Next.js config... -``` - -```typescript -// src/config/index.ts (modify loadSystemConfig) - -import { SystemConfig } from './systemConfig'; -import { nounsSystemConfig } from './nouns/index'; -import { exampleSystemConfig } from './example/index'; -import { clankerSystemConfig } from './clanker/index'; -// Import shared themes (will be created in Step 3) -// import { themes } from './shared/themes'; - -export const loadSystemConfig = (): SystemConfig => { - const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - - // Try build-time config from database (stored in env var at build time) - const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; - if (buildTimeConfig) { - try { - const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; - - // Map page configs from pages object to homePage/explorePage - const homePage = dbConfig.pages?.['home'] || null; - const explorePage = dbConfig.pages?.['explore'] || null; - - // Get themes from shared file (Step 3) - // const themes = require('./shared/themes').themes; - - // Validate config structure - if (dbConfig && dbConfig.brand && dbConfig.assets) { - console.log('✅ Using config from database'); - return { - ...dbConfig, - homePage, - explorePage, - // theme: themes, // From shared file (Step 3) - } as SystemConfig; - } - } catch (error) { - console.warn('⚠️ Failed to parse build-time config, falling back to static:', error); - } - } - - // Fall back to static configs (existing behavior) - console.log('ℹ️ Using static configs'); - switch (communityConfig.toLowerCase()) { - case 'nouns': - return nounsSystemConfig; - case 'example': - return exampleSystemConfig; - case 'clanker': - return clankerSystemConfig as unknown as SystemConfig; - default: - return nounsSystemConfig; - } -}; -``` - -**Test:** -```bash -# Test with DB -NEXT_PUBLIC_SUPABASE_URL=... SUPABASE_SERVICE_ROLE_KEY=... npm run build -# Should see: "✅ Loaded config from database" - -# Test without DB -npm run build -# Should see: "ℹ️ Using static configs" -# Should fall back to static configs - -# Verify app works -npm run dev -# App should load correctly -``` - -**Note:** Build-time loading requires space configs to be uploaded (Step 1.5). Without them, page configs won't be available. - -### Step 3: Create Shared Themes (30 min) - -```typescript -// src/config/shared/themes.ts - -// Extract themes from nouns.theme.ts (or any community - they're all the same) -import { nounsTheme } from '../nouns/nouns.theme'; - -// Export shared themes (all communities use the same themes) -export const themes = nounsTheme; - -// Update community configs to import from shared -// src/config/nouns/nouns.theme.ts -export { themes as nounsTheme } from '../shared/themes'; - -// src/config/clanker/clanker.theme.ts -export { themes as clankerTheme } from '../shared/themes'; - -// src/config/example/example.theme.ts -export { themes as exampleTheme } from '../shared/themes'; -``` - -**Update next.config.mjs to import themes:** -```javascript -// In loadConfigFromDB function, replace themesPlaceholder: -import { themes } from './src/config/shared/themes'; - -// Then use: -const fullConfig = { - ...config, - theme: themes, // From shared file - pages: pageConfigs, // From Spaces -}; - -// Store in env var -process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(fullConfig); -``` - -**Update src/config/index.ts to use shared themes:** -```typescript -import { themes } from './shared/themes'; - -// In loadSystemConfig, when using dbConfig: -return { - ...dbConfig, - theme: themes, // Always use shared themes - homePage, - explorePage, -} as SystemConfig; -``` - -**Test:** -```bash -npm run build -# Should see: "✅ Loaded config from database" -# Config is now ~2.8 KB (much smaller!), stored in env var -``` - -**✅ Proof of Concept Complete!** - -If this works, you've validated: -- ✅ Database storage works (without themes/pages) -- ✅ Space registrations created in database -- ✅ Space configs uploaded to storage -- ✅ Build-time loading works -- ✅ Shared themes work -- ✅ Space-based pages work (loaded from storage) -- ✅ Fallback works -- ✅ Config size reduced by ~90% - ---- - -## 📊 Full Implementation Phases - -### Phase 1: Database Foundation (Week 1, Days 1-2) - -**What:** Create tables, functions, seed data - -**Files:** -- `supabase/migrations/20251129172847_create_community_configs.sql` - Creates table (no themes/pages) -- `supabase/migrations/20251129172848_add_navpage_space_type.sql` - Adds navPage spaceType -- `supabase/seed.sql` - Seeds configs and creates navPage space registrations -- `scripts/seed-navpage-spaces.ts` - Uploads space config files to storage - -**Test:** -- ✅ Can query configs from DB -- ✅ Functions work correctly -- ✅ Config excludes themes/pages -- ✅ navPage spaceType exists -- ✅ Seed data loaded -- ✅ navPage space registrations created -- ✅ Space config files uploaded to storage - -**Rollback:** `supabase db reset` (resets to clean state) - ---- - -### Phase 2: Config Loading (Week 1, Days 3-4) - -**What:** Load config from DB at build time, fetch Spaces for nav items - -**Files:** -- `next.config.mjs` - Build-time config generation -- `src/config/index.ts` - Config loader -- `src/config/shared/themes.ts` - Shared themes - -**Test:** -- ✅ Build succeeds with/without DB -- ✅ App uses DB config when available -- ✅ Themes loaded from shared file -- ✅ Pages loaded from Spaces (when space configs uploaded) -- ✅ App falls back to static -- ✅ Config size ~2.8 KB - -**Note:** Pages require space configs to be uploaded (via `scripts/seed-navpage-spaces.ts`) before they can be loaded at build time. - -**Rollback:** Remove env var reading - ---- - -### Phase 3: Admin API (Week 1, Day 5) - -**What:** Create API endpoints for updates - -**Test:** -- ✅ Can fetch config via API (without themes/pages) -- ✅ Can update config via API -- ✅ Can fetch/update nav page Spaces -- ✅ Permission checks work - -**Rollback:** Remove API routes - ---- - -### Phase 4: Admin UI (Week 2, Days 1-2) - -**What:** Create admin interface - -**Test:** -- ✅ Can view config (without themes/pages) -- ✅ Can edit config -- ✅ Can edit nav page Spaces -- ✅ Can save changes - -**Rollback:** Remove admin pages - ---- - -### Phase 5: Asset Upload (Week 2, Days 3-4) - -**What:** Upload assets to Supabase Storage - -**Test:** -- ✅ Can upload assets -- ✅ Assets stored correctly -- ✅ Config updated with paths - -**Rollback:** Remove upload functionality - ---- - -### Phase 6: Asset Download (Week 2, Day 5) - -**What:** Download assets during build - -**Test:** -- ✅ Assets download to public/ -- ✅ Config paths updated -- ✅ Assets load correctly - -**Rollback:** Remove download code - ---- - -### Phase 7: Optimization (Week 3, Day 1) - -**What:** Optimize images with Sharp - -**Test:** -- ✅ Images optimized -- ✅ File sizes reduced -- ✅ Quality maintained - -**Rollback:** Remove optimization - ---- - -### Phase 8: Rebuild Trigger (Week 3, Day 2) - -**What:** Trigger rebuilds after updates - -**Test:** -- ✅ Rebuild triggers correctly -- ✅ Build completes successfully -- ✅ Changes deployed - -**Rollback:** Remove trigger - ---- - -### Phase 9: Production (Week 3, Days 3-5) - -**What:** Roll out to production - -**Test:** -- ✅ All communities work -- ✅ Performance acceptable -- ✅ No regressions - -**Rollback:** Revert to static configs - ---- - -## 🧪 Testing Checklist Per Phase - -### Phase 1: Database -- [ ] Tables created (without theme/home/explore columns) -- [ ] Functions work (exclude themes/pages) -- [ ] Seed data loaded -- [ ] navPage spaceType exists -- [ ] navPage space registrations created -- [ ] Space config files uploaded to storage -- [ ] RLS policies work - -### Phase 2: Config Loading -- [ ] Build succeeds with DB -- [ ] Build succeeds without DB -- [ ] App uses DB config -- [ ] Themes loaded from shared file -- [ ] Pages loaded from Spaces (when available) -- [ ] App falls back to static -- [ ] Config size ~2.8 KB -- [ ] No breaking changes - -### Phase 3: Admin API -- [ ] GET returns config (without themes/pages) -- [ ] PUT updates config -- [ ] GET/PUT for nav page Spaces -- [ ] Permission checks work -- [ ] Invalid requests rejected - -### Phase 4: Admin UI -- [ ] Can view config (without themes/pages) -- [ ] Can edit config -- [ ] Can edit nav page Spaces -- [ ] Can save changes -- [ ] Changes persist - -### Phase 5: Asset Upload -- [ ] Can upload assets -- [ ] Assets in storage -- [ ] Config updated - -### Phase 6: Asset Download -- [ ] Assets download -- [ ] Assets in public/ -- [ ] Config paths updated -- [ ] Assets load correctly - -### Phase 7: Optimization -- [ ] Images optimized -- [ ] File sizes reduced -- [ ] Quality maintained - -### Phase 8: Rebuild Trigger -- [ ] Rebuild triggers -- [ ] Build completes -- [ ] Changes deployed - -### Phase 9: Production -- [ ] All communities work -- [ ] Performance good -- [ ] No regressions - ---- - -## 🎯 Success Metrics - -### Phase 2 (Config Loading) -- Build time: < 30s -- App loads: < 2s -- Config size: ~2.8 KB (down from ~29 KB) -- Config matches: DB or static - -### Phase 6 (Asset Download) -- Asset download: < 5s per community -- Asset sizes: < 500KB each -- Assets load: < 500ms - -### Phase 7 (Optimization) -- File size reduction: > 50% -- Build time increase: < 5s -- Quality: Acceptable - -### Phase 9 (Production) -- All communities: Working -- Performance: No degradation -- Errors: < 0.1% - ---- - -## 🚨 Risk Mitigation - -### High Risk Phases -- **Phase 2** - Could break builds - - **Mitigation:** Extensive fallback testing - - **Rollback:** Remove env var reading - -- **Phase 6** - Could break asset loading - - **Mitigation:** Fallback to static assets - - **Rollback:** Remove download code - -- **Phase 9** - Production rollout - - **Mitigation:** Gradual rollout, monitor closely - - **Rollback:** Revert to static configs - -### Low Risk Phases -- **Phase 1** - Database only, no app changes -- **Phase 3** - API only, no app changes -- **Phase 4** - Admin UI only, no app changes -- **Phase 7** - Optimization only, improves performance - ---- - -## 📝 Implementation Notes - -### Environment Variables Needed - -```bash -# Required for all phases -NEXT_PUBLIC_SUPABASE_URL=... -NEXT_PUBLIC_SUPABASE_ANON_KEY=... - -# Required for build-time loading (Phase 2+) -SUPABASE_SERVICE_ROLE_KEY=... - -# Required for admin features (Phase 3+) -# (Admin identity from your auth system) -``` - -### Database Reset - -To reset database from scratch: -```bash -supabase db reset -``` - -This will: -1. Drop all tables -2. Apply all migrations (including community_configs) -3. Run seed.sql (seeds configs and creates navPage space registrations) - -**After reset, upload space configs:** -```bash -tsx scripts/seed-navpage-spaces.ts -``` - -This uploads the actual space config files (tabs and tabOrder) to Supabase Storage. - -### Git Workflow - -```bash -# Create feature branch -git checkout -b feature/database-configs - -# Work on Phase 1 -git commit -m "Phase 1: Database schema" - -# Work on Phase 2 -git commit -m "Phase 2: Config loading" - -# Test each phase before moving on -# Merge when all phases complete -``` - -### Testing Strategy - -1. **Local testing** - Test each phase locally first -2. **Staging testing** - Deploy to staging after each phase -3. **Production testing** - Test with one community first -4. **Full rollout** - Roll out to all communities - ---- - -## 🎓 Learning Path - -If you're new to any part: - -1. **Supabase** - Start with local Supabase, test queries -2. **Next.js build** - Understand `next.config.mjs` execution -3. **Environment variables** - Learn Next.js env var system -4. **Sharp** - Test image optimization separately first - ---- - -## 📞 Support - -If you get stuck on any phase: - -1. Check the detailed plan in `INCREMENTAL_IMPLEMENTATION_PLAN.md` -2. Review the specific phase documentation -3. Test the rollback procedure -4. Ask for help before proceeding - ---- - -## ✅ Quick Validation - -After each phase, verify: - -1. **Build succeeds** - `npm run build` works -2. **App works** - `npm run dev` loads correctly -3. **No regressions** - Existing features still work -4. **Can rollback** - Know how to revert if needed - -If all ✅, proceed to next phase! diff --git a/docs/DATABASE_CONFIG/QUICK_START_TESTING.md b/docs/DATABASE_CONFIG/QUICK_START_TESTING.md deleted file mode 100644 index efa9fd17a..000000000 --- a/docs/DATABASE_CONFIG/QUICK_START_TESTING.md +++ /dev/null @@ -1,373 +0,0 @@ -# Quick Start Testing Guide - -> **See also:** -> - `DATABASE_CONFIG_GUIDE.md` - Architecture and overview -> - `DATABASE_CONFIG_IMPLEMENTATION.md` - Detailed implementation plan -> - `QUICK_START_IMPLEMENTATION.md` - Quick start guide -> - `README.md` - Documentation index - -## Overview - -This guide walks you through testing the database-backed configuration system. - -## Prerequisites - -- ✅ Supabase project set up -- ✅ Environment variables configured -- ✅ Node.js and npm installed - -## Step 1: Reset Database (Applies Migrations + Seed Data) - -**Updated:** Database is now seeded via SQL in `supabase/seed.sql`, which runs automatically on reset. - -```bash -# Reset database from scratch (applies all migrations + seed data) -supabase db reset -``` - -This will: -1. ✅ Apply migration: `create_community_configs.sql` (creates table without themes/pages) -2. ✅ Apply migration: `add_navpage_space_type.sql` (adds navPage spaceType) -3. ✅ Run `seed.sql`: - - Seeds configs for nouns, example, clanker - - Creates navPage space registrations (nouns-home, nouns-explore, clanker-home) - - Links navigation items to spaces via spaceId - -**Verify:** -```sql --- Check table exists with correct columns (should NOT have theme_config, home_page_config, explore_page_config) -SELECT column_name, data_type -FROM information_schema.columns -WHERE table_name = 'community_configs'; - --- Should see: brand_config, assets_config, community_config, fidgets_config, navigation_config, ui_config --- Should NOT see: theme_config, home_page_config, explore_page_config - --- Test function (should return config without themes/pages) -SELECT get_active_community_config('nouns'); - --- Verify seed data loaded -SELECT community_id FROM community_configs; --- Should see: nouns, example, clanker - --- Verify navPage spaceType exists -SELECT DISTINCT "spaceType" FROM "spaceRegistrations"; --- Should include: 'navPage' - --- Verify navPage spaces were created -SELECT "spaceId", "spaceName", "spaceType" -FROM "spaceRegistrations" -WHERE "spaceType" = 'navPage'; --- Should see: nouns-home, nouns-explore, clanker-home - --- Verify navigation configs reference spaces -SELECT "community_id", "navigation_config"->'items' as nav_items -FROM "community_configs" -WHERE "community_id" = 'nouns'; --- Should see spaceId references in navigation items -``` - -**Expected output:** -- ✅ Table created with correct columns (no themes/pages) -- ✅ Function returns config (without themes/pages) -- ✅ Seed data inserted for all 3 communities -- ✅ navPage spaceType available -- ✅ navPage space registrations created -- ✅ Navigation items reference spaceIds - -## Step 2: Upload Space Configs to Storage - -**After database reset, upload the actual space config files:** - -```bash -# Set environment variables -export NEXT_PUBLIC_SUPABASE_URL="your-supabase-url" -export SUPABASE_SERVICE_ROLE_KEY="your-service-role-key" - -# Upload space configs to Supabase Storage -tsx scripts/seed-navpage-spaces.ts -``` - -This script will: -1. ✅ Read space registrations from database (created in seed.sql) -2. ✅ Import page configs from TypeScript (nounsHomePage, nounsExplorePage, etc.) -3. ✅ Upload each tab as `{spaceId}/tabs/{tabName}` to Supabase Storage as SignedFile -4. ✅ Upload tab order as `{spaceId}/tabOrder` to Supabase Storage as SignedFile - -**Expected output:** -``` -🚀 Starting navPage space config seeding... - -📦 Uploading space: nouns-home - 📍 Space ID: - 📄 Uploading 6 tabs... - ✅ Uploaded tab: Nouns - ✅ Uploaded tab: Social - ✅ Uploaded tab: Governance - ✅ Uploaded tab: Resources - ✅ Uploaded tab: Funded Works - ✅ Uploaded tab: Places - ✅ Uploaded tab order: [Nouns, Social, Governance, Resources, Funded Works, Places] - ✅ Successfully uploaded nouns-home - -📦 Uploading space: nouns-explore - ... - -✅ All navPage spaces seeded successfully! -``` - -**Verify space configs uploaded:** -- Check Supabase Storage dashboard: `spaces` bucket should contain files -- Or verify via API/CLI that files exist at `{spaceId}/tabs/{tabName}` paths - -**Note:** Space configs are stored as unencrypted SignedFile objects (`isEncrypted: false`). They can be read by anyone, and the `decryptEncryptedSignedFile` function handles unencrypted files correctly. - -## Step 3: Optional - Manual Seed Script - -**Note:** The TypeScript seed script (`scripts/seed-community-configs.ts`) is now **OPTIONAL**. -Use it only if you need to update configs without resetting the database. - -If you need to use it: - -```bash -# Make sure you have these env vars set: -export NEXT_PUBLIC_SUPABASE_URL="your-supabase-url" -export SUPABASE_SERVICE_ROLE_KEY="your-service-role-key" - -# Run seed script (optional - seed.sql already did this) -npx tsx scripts/seed-community-configs.ts -``` - -**Note:** The TypeScript script still includes themes/pages for backward compatibility. -The new architecture stores themes in shared file and pages as Spaces. - -## Step 4: Create Shared Themes (If Not Done) - -Before testing build-time loading, ensure shared themes file exists: - -```typescript -// src/config/shared/themes.ts -import { nounsTheme } from '../nouns/nouns.theme'; -export const themes = nounsTheme; -``` - -Update community configs to import from shared: -- `src/config/nouns/nouns.theme.ts` → `export { themes as nounsTheme } from '../shared/themes';` -- `src/config/clanker/clanker.theme.ts` → `export { themes as clankerTheme } from '../shared/themes';` -- `src/config/example/example.theme.ts` → `export { themes as exampleTheme } from '../shared/themes';` - -## Step 5: Test Build-Time Loading - -### Test with Database Available - -```bash -# Set environment variables -export NEXT_PUBLIC_SUPABASE_URL="your-supabase-url" -export SUPABASE_SERVICE_ROLE_KEY="your-service-role-key" -export NEXT_PUBLIC_COMMUNITY="nouns" - -# Run build -npm run build -``` - -**Expected output:** -``` -✅ Loaded config from database -``` - -**Verify in build output:** -- Should see "✅ Loaded config from database" message -- Build should complete successfully -- No errors about missing config -- Config is stored in `NEXT_PUBLIC_BUILD_TIME_CONFIG` env var (~2.8 KB) - -### Test without Database (Fallback) - -```bash -# Remove Supabase env vars -unset NEXT_PUBLIC_SUPABASE_URL -unset SUPABASE_SERVICE_ROLE_KEY - -# Run build (should fall back to static configs) -npm run build -``` - -**Expected output:** -``` -ℹ️ Using static configs (no DB credentials) -``` - -**Verify:** -- Should see "ℹ️ Using static configs" message -- Build should complete successfully -- App should work with static configs - -## Step 6: Test Runtime - -### Start Dev Server - -```bash -# With DB config -export NEXT_PUBLIC_SUPABASE_URL="your-supabase-url" -export SUPABASE_SERVICE_ROLE_KEY="your-service-role-key" -export NEXT_PUBLIC_COMMUNITY="nouns" -npm run dev -``` - -**Check browser console:** -- Should see "✅ Using config from database" message -- App should load correctly -- Brand name should match database config - -### Test Different Communities - -```bash -# Test with 'example' community -export NEXT_PUBLIC_COMMUNITY="example" -npm run build -npm run dev - -# Test with 'clanker' community -export NEXT_PUBLIC_COMMUNITY="clanker" -npm run build -npm run dev -``` - -## Step 7: Manual Database Update Test - -Update config directly in database: - -```sql --- Update brand display name -UPDATE community_configs -SET brand_config = jsonb_set( - brand_config, - '{displayName}', - '"Nouns Updated"' -) -WHERE community_id = 'nouns'; -``` - -Then rebuild and verify: - -```bash -npm run build -npm run dev -``` - -**Verify:** -- Brand name should be "Nouns Updated" in the app -- Changes should be reflected after rebuild - -## Troubleshooting - -### Migration Fails - -**Error:** `relation "community_configs" already exists` - -**Solution:** -```sql -DROP TABLE IF EXISTS community_configs CASCADE; -DROP FUNCTION IF EXISTS get_active_community_config; -``` -Then re-run migration. - -### Seed Script Fails - -**Error:** `Missing required environment variables` - -**Solution:** -```bash -# Check env vars are set -echo $NEXT_PUBLIC_SUPABASE_URL -echo $SUPABASE_SERVICE_ROLE_KEY - -# Set them if missing -export NEXT_PUBLIC_SUPABASE_URL="your-url" -export SUPABASE_SERVICE_ROLE_KEY="your-key" -``` - -**Error:** `Error seeding nouns: ...` - -**Solution:** -- Check Supabase connection -- Verify service role key has permissions -- Check table exists: `SELECT * FROM community_configs LIMIT 1;` - -### Build Fails - -**Error:** `Cannot find module '@supabase/supabase-js'` - -**Solution:** -```bash -npm install @supabase/supabase-js -``` - -**Error:** `Function get_active_community_config does not exist` - -**Solution:** -- Re-run migration: `supabase migration up` -- Verify function exists: `SELECT * FROM pg_proc WHERE proname = 'get_active_community_config';` - -### Runtime Issues - -**App shows static config instead of DB config** - -**Check:** -1. Build-time env vars are set -2. Database has config for community -3. Check build logs for "✅ Loaded config from database" -4. Check browser console for config source - -**Config not updating after DB change** - -**Solution:** -- Rebuild required: `npm run build` -- Config is loaded at build time, not runtime -- Changes require rebuild to take effect - -## Success Criteria - -✅ **Migration succeeds** - Table and function created -✅ **Seed succeeds** - All configs inserted -✅ **Build with DB** - Config loaded from database -✅ **Build without DB** - Falls back to static configs -✅ **Runtime works** - App loads correctly -✅ **DB updates work** - Changes reflected after rebuild - -## Next Steps - -Once testing is successful: - -1. ✅ Phase 1 complete - Database schema working -2. ✅ Phase 2 complete - Build-time loading working -3. ➡️ Phase 3 - Create admin API endpoints -4. ➡️ Phase 4 - Create admin UI - -## Quick Commands Reference - -# Reset database (applies migrations + seed data) -supabase db reset - -# Upload space configs to storage (required after reset) -tsx scripts/seed-navpage-spaces.ts - -# Optional: Seed configs manually (seed.sql already does this) -npx tsx scripts/seed-community-configs.ts - -# Build with DB -NEXT_PUBLIC_SUPABASE_URL=... SUPABASE_SERVICE_ROLE_KEY=... npm run build - -# Build without DB (fallback) -npm run build - -# Dev server -npm run dev - -# Test function -psql -c "SELECT get_active_community_config('nouns');" - -# Verify config is loaded (check env var in build output) -# Config is stored in NEXT_PUBLIC_BUILD_TIME_CONFIG env var (~2.8 KB) -``` - diff --git a/docs/DATABASE_CONFIG/README.md b/docs/DATABASE_CONFIG/README.md deleted file mode 100644 index bf571321e..000000000 --- a/docs/DATABASE_CONFIG/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# Database-Backed Configuration System - -This directory contains all documentation for the database-backed configuration system. - -## Documentation Files - -### Getting Started -- **`QUICK_START_IMPLEMENTATION.md`** - Quick start guide for a 4-hour proof of concept -- **`QUICK_START_TESTING.md`** - Step-by-step testing guide - -### Architecture & Implementation -- **`DATABASE_CONFIG_GUIDE.md`** - Architecture overview, database schema, and key design decisions -- **`DATABASE_CONFIG_IMPLEMENTATION.md`** - Detailed phase-by-phase implementation plan (Phases 0-4) -- **`INCREMENTAL_IMPLEMENTATION_PLAN.md`** - Complete implementation plan with all 10 phases - -### Reference -- **`DATABASE_CONFIG_CONSOLIDATION.md`** - Summary of documentation consolidation and what changed - -## Quick Navigation - -**New to the system?** -1. Start with `DATABASE_CONFIG_GUIDE.md` - Understand the architecture -2. Then `QUICK_START_IMPLEMENTATION.md` - Try the quick POC -3. Then `QUICK_START_TESTING.md` - Test it - -**Ready to implement?** -1. Read `DATABASE_CONFIG_IMPLEMENTATION.md` - Phases 0-4 -2. Or `INCREMENTAL_IMPLEMENTATION_PLAN.md` - All 10 phases - -## Current Architecture - -- **Config Storage**: Supabase `community_configs` table (~2.8 KB per config) -- **Build-Time Loading**: Configs fetched during build, stored in `NEXT_PUBLIC_BUILD_TIME_CONFIG` env var -- **Shared Themes**: Stored in `src/config/shared/themes.ts` (not in DB) -- **Pages as Spaces**: homePage/explorePage stored as Spaces, referenced by navigation items -- **Space Seeding**: Space registrations created in `seed.sql`, config files uploaded via `scripts/seed-navpage-spaces.ts` -- **Zero Runtime Queries**: All configs loaded at build time - -## Related Files - -- **Migrations**: - - `supabase/migrations/20251129172847_create_community_configs.sql` - Creates config table - - `supabase/migrations/20251129172848_add_navpage_space_type.sql` - Adds navPage spaceType -- **Seed Data**: - - `supabase/seed.sql` - Seeds configs and creates navPage space registrations - - `scripts/seed-navpage-spaces.ts` - Uploads space config files to storage (run after seed.sql) -- **Build Config**: `next.config.mjs` (loads config at build time) -- **Config Loader**: `src/config/index.ts` (reads from env var) - diff --git a/docs/DOCUMENTATION_OVERVIEW.md b/docs/DOCUMENTATION_OVERVIEW.md index 34460a01a..947993e3a 100644 --- a/docs/DOCUMENTATION_OVERVIEW.md +++ b/docs/DOCUMENTATION_OVERVIEW.md @@ -26,6 +26,8 @@ docs/ │ │ └── OVERVIEW.md # Fidget architecture │ ├── THEMES/ # Theme system │ │ └── OVERVIEW.md # Theme architecture +│ ├── CONFIGURATION/ # Configuration system +│ │ └── OVERVIEW.md # Database-backed configuration system │ └── DISCOVERY/ # Discovery system │ └── MINI_APP_DISCOVERY_SYSTEM.md # Mini-app discovery system │ @@ -67,6 +69,7 @@ docs/ - **SYSTEMS/SPACES/LAYOUT_MIGRATION_GUIDE.md** - Layout migration guide - **SYSTEMS/FIDGETS/OVERVIEW.md** - Fidget system, types, development patterns - **SYSTEMS/THEMES/OVERVIEW.md** - Theme system, customization, CSS variables +- **SYSTEMS/CONFIGURATION/OVERVIEW.md** - Database-backed configuration system - **SYSTEMS/DISCOVERY/MINI_APP_DISCOVERY_SYSTEM.md** - Mini-app discovery system ### Integrations diff --git a/docs/INTEGRATIONS/SUPABASE.md b/docs/INTEGRATIONS/SUPABASE.md index 1c1fda470..014754b37 100644 --- a/docs/INTEGRATIONS/SUPABASE.md +++ b/docs/INTEGRATIONS/SUPABASE.md @@ -14,6 +14,31 @@ Supabase integration provides: ### 1. Core Tables ```sql +-- Community configurations +CREATE TABLE community_configs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + community_id VARCHAR(50) NOT NULL UNIQUE, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT now(), + brand_config JSONB NOT NULL, + assets_config JSONB NOT NULL, + community_config JSONB NOT NULL, + fidgets_config JSONB NOT NULL, + navigation_config JSONB, + ui_config JSONB, + is_published BOOLEAN DEFAULT true +); + +-- Space registrations (includes navPage type) +CREATE TABLE spaceRegistrations ( + spaceId UUID PRIMARY KEY, + fid INTEGER, + spaceName TEXT NOT NULL, + spaceType TEXT NOT NULL CHECK (spaceType IN ('profile', 'token', 'proposal', 'channel', 'navPage')), + identityPublicKey TEXT, + signature TEXT, + timestamp TIMESTAMP WITH TIME ZONE DEFAULT now() +); + -- Users table CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), @@ -56,20 +81,56 @@ ALTER TABLE fidgets ADD CONSTRAINT fk_fidgets_space FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE; -- Indexes for performance +CREATE INDEX idx_community_configs_community_id ON community_configs(community_id); +CREATE INDEX idx_community_configs_published ON community_configs(is_published) WHERE is_published = true; CREATE INDEX idx_spaces_owner_id ON spaces(owner_id); CREATE INDEX idx_fidgets_space_id ON fidgets(space_id); CREATE INDEX idx_spaces_public ON spaces(is_public) WHERE is_public = TRUE; ``` +### 2. Database Functions + +```sql +-- Get active community configuration +CREATE OR REPLACE FUNCTION get_active_community_config( + p_community_id VARCHAR(50) +) +RETURNS JSONB +LANGUAGE plpgsql +SECURITY DEFINER +AS $$ +DECLARE + v_config JSONB; +BEGIN + SELECT jsonb_build_object( + 'brand', "brand_config", + 'assets', "assets_config", + 'community', "community_config", + 'fidgets', "fidgets_config", + 'navigation', "navigation_config", + 'ui', "ui_config" + ) + INTO v_config + FROM "public"."community_configs" + WHERE "community_id" = p_community_id + AND "is_published" = true + LIMIT 1; + + RETURN v_config; +END; +$$; +``` + ## File Storage ### 1. Storage Structure ``` supabase/storage/ -├── spaces/ # Public space files +├── spaces/ # Public space files (including navPage spaces) │ ├── {spaceId}/ +│ │ ├── tabOrder # Tab order for navigation pages │ │ ├── tabs/ -│ │ │ ├── {tabName} +│ │ │ ├── {tabName} # Individual tab configs (SignedFile format) │ │ │ └── ... │ │ └── assets/ │ └── ... diff --git a/docs/NAVIGATION_SPACE_REFERENCE_APPROACH.md b/docs/NAVIGATION_SPACE_REFERENCE_APPROACH.md deleted file mode 100644 index 172b1b2b3..000000000 --- a/docs/NAVIGATION_SPACE_REFERENCE_APPROACH.md +++ /dev/null @@ -1,313 +0,0 @@ -# Navigation-Space Reference Approach - -## Overview - -Instead of storing `homePage` and `explorePage` configs directly in `community_configs`, navigation items reference Spaces in the database. Pages are fetched at build time based on navigation entries. - -## Architecture - -``` -Navigation Config - ├── items: [ - │ { id: 'home', spaceId: 'uuid-1', ... }, - │ { id: 'explore', spaceId: 'uuid-2', ... }, - │ ... - │ ] - │ - └── Build Time: - ├── Fetch nav config from community_configs - ├── For each nav item with spaceId: - │ └── Fetch Space from database/storage - └── Build page configs from fetched Spaces -``` - -## Benefits - -1. **Dramatically reduces config size** - Removes 71% (20.6 KB) of config -2. **Unified architecture** - Everything is Spaces -3. **Navigation as source of truth** - Nav defines what pages exist -4. **Flexible** - Any nav item can reference a Space -5. **Reuses existing infrastructure** - Uses Space storage/retrieval - -## Implementation - -### 1. Update NavigationItem Interface - -```typescript -// src/config/systemConfig.ts - -export interface NavigationItem { - id: string; - label: string; - href: string; - icon?: 'home' | 'explore' | 'notifications' | 'search' | 'space' | 'robot' | 'custom'; - openInNewTab?: boolean; - requiresAuth?: boolean; - spaceId?: string; // ← NEW: Reference to Space in database -} -``` - -### 2. Add 'navPage' Space Type - -```sql --- Migration: Add navPage to spaceType enum -ALTER TABLE "public"."spaceRegistrations" - DROP CONSTRAINT IF EXISTS valid_space_type; - -ALTER TABLE "public"."spaceRegistrations" - ADD CONSTRAINT valid_space_type CHECK ( - "spaceType" IN ('profile', 'token', 'proposal', 'channel', 'navPage') - ); -``` - -### 3. Update Navigation Config Structure - -```typescript -// src/config/nouns/nouns.navigation.ts - -export const nounsNavigation: NavigationConfig = { - logoTooltip: { - text: "wtf is nouns?", - href: "https://nouns.wtf", - }, - items: [ - { - id: 'home', - label: 'Home', - href: '/home', - icon: 'home', - spaceId: '550e8400-e29b-41d4-a716-446655440000' // ← Reference to Space - }, - { - id: 'explore', - label: 'Explore', - href: '/explore', - icon: 'explore', - spaceId: '550e8400-e29b-41d4-a716-446655440001' // ← Reference to Space - }, - { - id: 'notifications', - label: 'Notifications', - href: '/notifications', - icon: 'notifications', - requiresAuth: true - // No spaceId - not a Space-based page - }, - ], - showMusicPlayer: true, - showSocials: true, -}; -``` - -### 4. Update Database Schema - -```sql --- Remove home_page_config and explore_page_config from community_configs -ALTER TABLE "public"."community_configs" - DROP COLUMN IF EXISTS "home_page_config", - DROP COLUMN IF EXISTS "explore_page_config"; - --- Navigation config now contains spaceId references --- No schema changes needed - navigation_config JSONB column handles it -``` - -### 5. Update Build-Time Config Generation - -```javascript -// next.config.mjs - -async function generateConfigFile() { - // Fetch main config (now much smaller - no homePage/explorePage!) - const { data: config } = await supabase - .rpc('get_active_community_config', { p_community_id: community }) - .single(); - - // Fetch Spaces for nav items that have spaceId - const navItems = config.navigation?.items || []; - const spaceIds = navItems - .filter(item => item.spaceId) - .map(item => item.spaceId); - - // Fetch Spaces from database/storage - const spaces = {}; - for (const spaceId of spaceIds) { - // Fetch Space based on how Spaces are stored - // (Could be from spaceRegistrations + Storage, or new table) - const space = await fetchSpace(spaceId); - if (space) { - spaces[spaceId] = space; - } - } - - // Build page configs from Spaces - const pageConfigs = {}; - navItems.forEach(item => { - if (item.spaceId && spaces[item.spaceId]) { - // Map Space to page config format - pageConfigs[item.id] = convertSpaceToPageConfig(spaces[item.spaceId]); - } - }); - - // Combine configs - const fullConfig = { - ...config, - // Add page configs based on nav items - pages: pageConfigs, - }; - - // Generate file - await writeFile('src/config/db-config.ts', ...); -} -``` - -### 6. Update Config Loader - -```typescript -// src/config/index.ts - -export const loadSystemConfig = (): SystemConfig => { - const config = dbConfig || staticConfig; - - // Get page configs from nav items - const navItems = config.navigation?.items || []; - const pages = {}; - - navItems.forEach(item => { - if (item.spaceId && config.pages?.[item.id]) { - pages[item.id] = config.pages[item.id]; - } - }); - - // Map to legacy structure for backward compatibility - return { - ...config, - homePage: pages['home'] || staticConfig.homePage, - explorePage: pages['explore'] || staticConfig.explorePage, - }; -}; -``` - -## Space Storage Options - -### Option A: Use Existing Space Storage System - -Spaces stored in Supabase Storage: -- Path: `spaces/{spaceId}/tabs/{tabName}` -- Encrypted/signed files -- Requires decryption at build time - -**Pros:** -- Uses existing infrastructure -- No schema changes needed - -**Cons:** -- Requires encryption/decryption logic at build time -- More complex fetching - -### Option B: New Database Table for Nav Pages - -```sql -CREATE TABLE community_nav_pages ( - id UUID PRIMARY KEY, - community_id VARCHAR(50), - nav_item_id VARCHAR(50), -- 'home', 'explore', etc. - space_config JSONB NOT NULL, - version INTEGER DEFAULT 1, - created_at TIMESTAMP DEFAULT NOW() -); -``` - -**Pros:** -- Simple database queries -- No encryption needed (public pages) -- Easy to version - -**Cons:** -- New table needed -- Duplicates Space structure - -### Option C: Use spaceRegistrations + Storage - -Store nav pages as Spaces in `spaceRegistrations` with `spaceType = 'navPage'`: -- Register in `spaceRegistrations` table -- Store config in Supabase Storage -- Fetch at build time - -**Pros:** -- Uses existing Space infrastructure -- Consistent with other Spaces -- Can reuse Space APIs - -**Cons:** -- Requires spaceRegistrations entry -- Need to handle Storage fetching - -## Recommended: Option C (spaceRegistrations + Storage) - -**Why:** -- ✅ Uses existing Space system -- ✅ Consistent architecture -- ✅ Can reuse Space loading logic -- ✅ No new tables needed - -## Migration Steps - -1. **Add 'navPage' to spaceType enum** -2. **Create Spaces for homePage/explorePage** - - Register in `spaceRegistrations` with `spaceType = 'navPage'` - - Store configs in Supabase Storage -3. **Update navigation configs** - - Add `spaceId` to home/explore nav items -4. **Update build-time fetching** - - Fetch Spaces based on nav items - - Build page configs from Spaces -5. **Remove homePage/explorePage from community_configs** - - Update schema - - Update seed script - -## Size Reduction - -**Before:** -- Config: ~29 KB -- homePage: 19.2 KB -- explorePage: 1.4 KB - -**After:** -- Config: ~8.4 KB (71% reduction!) -- Navigation: ~500 bytes (includes spaceId references) -- Spaces: Fetched separately, no size limit - -**Result:** Config easily fits in env vars or generated file! - -## Example: Updated Navigation Config - -```typescript -export const nounsNavigation: NavigationConfig = { - items: [ - { - id: 'home', - label: 'Home', - href: '/home', - icon: 'home', - spaceId: 'nouns-home-space-uuid' // ← References Space - }, - { - id: 'explore', - label: 'Explore', - href: '/explore', - icon: 'explore', - spaceId: 'nouns-explore-space-uuid' // ← References Space - }, - ], -}; -``` - -## Benefits Summary - -✅ **Solves E2BIG** - Config size reduced by 71% -✅ **Unified architecture** - Everything is Spaces -✅ **Navigation as source of truth** - Nav defines pages -✅ **Flexible** - Any nav item can be a Space -✅ **Reuses infrastructure** - Uses existing Space system -✅ **No breaking changes** - Can maintain backward compatibility - diff --git a/docs/NAVIGATION_SPACE_REFERENCE_IMPLEMENTATION.md b/docs/NAVIGATION_SPACE_REFERENCE_IMPLEMENTATION.md deleted file mode 100644 index cdb95e1c1..000000000 --- a/docs/NAVIGATION_SPACE_REFERENCE_IMPLEMENTATION.md +++ /dev/null @@ -1,357 +0,0 @@ -# Navigation-Space Reference Implementation Plan - -## Architecture Overview - -**Key Insight:** Navigation items reference Spaces. Pages are fetched from Spaces at build time. - -``` -Navigation Config (in community_configs) - └── items: [ - { id: 'home', spaceId: 'uuid', ... }, - { id: 'explore', spaceId: 'uuid', ... } - ] - ↓ - Build Time: - ↓ - Fetch Spaces by spaceId - ↓ - Build page configs from Spaces -``` - -## Changes Required - -### 1. Add 'navPage' Space Type - -**File:** `src/common/types/spaceData.ts` - -```typescript -export const SPACE_TYPES = { - PROFILE: 'profile', - TOKEN: 'token', - PROPOSAL: 'proposal', - CHANNEL: 'channel', - NAV_PAGE: 'navPage', // ← NEW -} as const; -``` - -**Migration:** `supabase/migrations/YYYYMMDDHHMMSS_add_navpage_space_type.sql` - -```sql --- Add navPage to spaceType constraint -ALTER TABLE "public"."spaceRegistrations" - DROP CONSTRAINT IF EXISTS valid_space_type; - -ALTER TABLE "public"."spaceRegistrations" - ADD CONSTRAINT valid_space_type CHECK ( - "spaceType" IN ('profile', 'token', 'proposal', 'channel', 'navPage') - ); -``` - -### 2. Update NavigationItem Interface - -**File:** `src/config/systemConfig.ts` - -```typescript -export interface NavigationItem { - id: string; - label: string; - href: string; - icon?: 'home' | 'explore' | 'notifications' | 'search' | 'space' | 'robot' | 'custom'; - openInNewTab?: boolean; - requiresAuth?: boolean; - spaceId?: string; // ← NEW: Optional reference to Space -} -``` - -### 3. Update Navigation Configs - -**File:** `src/config/nouns/nouns.navigation.ts` - -```typescript -export const nounsNavigation: NavigationConfig = { - logoTooltip: { - text: "wtf is nouns?", - href: "https://nouns.wtf", - }, - items: [ - { - id: 'home', - label: 'Home', - href: '/home', - icon: 'home', - spaceId: 'nouns-home-space-id' // ← Reference to Space - }, - { - id: 'explore', - label: 'Explore', - href: '/explore', - icon: 'explore', - spaceId: 'nouns-explore-space-id' // ← Reference to Space - }, - { - id: 'notifications', - label: 'Notifications', - href: '/notifications', - icon: 'notifications', - requiresAuth: true - // No spaceId - not a Space-based page - }, - ], - showMusicPlayer: true, - showSocials: true, -}; -``` - -### 4. Update Database Schema - -**Migration:** Remove homePage/explorePage columns - -```sql --- Remove large page config columns -ALTER TABLE "public"."community_configs" - DROP COLUMN IF EXISTS "home_page_config", - DROP COLUMN IF EXISTS "explore_page_config"; - --- Update function to exclude page configs -CREATE OR REPLACE FUNCTION "public"."get_active_community_config"( - p_community_id VARCHAR(50) -) -RETURNS JSONB -LANGUAGE plpgsql -SECURITY DEFINER -AS $$ -DECLARE - v_config JSONB; -BEGIN - SELECT jsonb_build_object( - 'brand', "brand_config", - 'assets', "assets_config", - 'theme', "theme_config", - 'community', "community_config", - 'fidgets', "fidgets_config", - 'navigation', "navigation_config", -- Contains spaceId references - 'ui', "ui_config" - ) - INTO v_config - FROM "public"."community_configs" - WHERE "community_id" = p_community_id - AND "is_active" = true - AND "is_published" = true - ORDER BY "version" DESC - LIMIT 1; - - RETURN v_config; -END; -$$; -``` - -### 5. Update Build-Time Config Generation - -**File:** `next.config.mjs` - -```javascript -async function generateConfigFile() { - // Fetch main config (now much smaller!) - const { data: config } = await supabase - .rpc('get_active_community_config', { p_community_id: community }) - .single(); - - if (!config) return; - - // Extract spaceIds from navigation items - const navItems = config.navigation?.items || []; - const spaceIds = navItems - .filter(item => item.spaceId) - .map(item => ({ navId: item.id, spaceId: item.spaceId })); - - // Fetch Spaces for nav items - const pageConfigs = {}; - for (const { navId, spaceId } of spaceIds) { - try { - // Fetch Space from spaceRegistrations + Storage - const space = await fetchSpaceBySpaceId(spaceId); - if (space) { - // Convert Space config to page config format - pageConfigs[navId] = convertSpaceToPageConfig(space); - } - } catch (error) { - console.warn(`⚠️ Failed to fetch Space ${spaceId} for nav item ${navId}:`, error.message); - } - } - - // Combine configs - const fullConfig = { - ...config, - pages: pageConfigs, // Add page configs - }; - - // Generate TypeScript file - const configFile = `// Auto-generated at build time -import { SystemConfig } from './systemConfig'; - -export const dbConfig: SystemConfig | null = ${JSON.stringify(fullConfig, null, 2)} as SystemConfig; -`; - - await writeFile('src/config/db-config.ts', configFile, 'utf-8'); -} - -async function fetchSpaceBySpaceId(spaceId: string) { - // Option 1: Fetch from spaceRegistrations + Storage - const { data: registration } = await supabase - .from('spaceRegistrations') - .select('*') - .eq('spaceId', spaceId) - .eq('spaceType', 'navPage') - .single(); - - if (!registration) return null; - - // Fetch Space config from Storage - const { data } = await supabase.storage - .from('spaces') - .download(`${spaceId}/tabs/default`); // Or fetch all tabs - - if (!data) return null; - - // Parse and return Space config - const fileData = JSON.parse(await data.text()); - return fileData; // Return SpaceConfig -} - -function convertSpaceToPageConfig(space: SpaceConfig): HomePageConfig { - // Convert Space config to HomePageConfig/ExplorePageConfig format - // This maps Space tabs to page tabs - return { - defaultTab: space.defaultTab || 'Home', - tabOrder: Object.keys(space.tabs || {}), - tabs: space.tabs || {}, - layout: { - defaultLayoutFidget: space.layout?.defaultLayoutFidget || 'grid', - gridSpacing: space.layout?.gridSpacing || 16, - theme: space.theme || {}, - }, - }; -} -``` - -### 6. Update Config Loader - -**File:** `src/config/index.ts` - -```typescript -export const loadSystemConfig = (): SystemConfig => { - const config = dbConfig || staticConfig; - - // Extract page configs from pages object (built from nav items) - const homePage = config.pages?.['home'] || staticConfig.homePage; - const explorePage = config.pages?.['explore'] || staticConfig.explorePage; - - return { - ...config, - homePage, // Map from pages['home'] - explorePage, // Map from pages['explore'] - }; -}; -``` - -## Space Storage Strategy - -### How Nav Pages Are Stored - -1. **Register in spaceRegistrations:** - ```sql - INSERT INTO spaceRegistrations ( - "spaceId", - "spaceName", - "spaceType", - "identityPublicKey", - "signature", - "timestamp" - ) VALUES ( - 'nouns-home-space-id', - 'nouns-home', - 'navPage', - 'system-identity-key', - 'signature', - NOW() - ); - ``` - -2. **Store config in Supabase Storage:** - - Path: `spaces/{spaceId}/tabs/{tabName}` - - Format: Same as other Spaces (SpaceConfig JSON) - -3. **Fetch at build time:** - - Query `spaceRegistrations` for `spaceType = 'navPage'` - - Download from Storage - - Parse and convert to page config format - -## Size Reduction - -**Before:** -- Config: ~29 KB -- homePage: 19.2 KB (66.5%) -- explorePage: 1.4 KB (4.8%) - -**After:** -- Config: ~8.4 KB (71% reduction!) -- Navigation: ~500 bytes (includes spaceId strings) -- Spaces: Fetched separately, no size limit - -**Result:** Config easily fits in env vars or generated file! - -## Migration Path - -1. **Add navPage spaceType** - Migration + TypeScript constants -2. **Create Spaces for existing pages** - Register homePage/explorePage as Spaces -3. **Update navigation configs** - Add spaceId to nav items -4. **Update build-time fetching** - Fetch Spaces based on nav items -5. **Remove page configs from schema** - Drop home_page_config/explore_page_config columns -6. **Update seed script** - Don't seed page configs, seed Spaces instead - -## Benefits - -✅ **Solves E2BIG** - Config size reduced by 71% -✅ **Unified architecture** - Everything is Spaces -✅ **Navigation as source of truth** - Nav defines what pages exist -✅ **Flexible** - Any nav item can reference a Space -✅ **Reuses infrastructure** - Uses existing Space system -✅ **No breaking changes** - Can maintain backward compatibility during migration - -## Example: Complete Flow - -### 1. Navigation Config (in DB) -```json -{ - "navigation": { - "items": [ - { "id": "home", "label": "Home", "href": "/home", "spaceId": "uuid-1" }, - { "id": "explore", "label": "Explore", "href": "/explore", "spaceId": "uuid-2" } - ] - } -} -``` - -### 2. Build Time -```javascript -// Fetch nav config → see spaceIds -// Fetch Spaces: uuid-1, uuid-2 -// Convert Spaces to page configs -// Generate: -{ - ...config, - pages: { - 'home': { /* Space config converted */ }, - 'explore': { /* Space config converted */ } - } -} -``` - -### 3. Runtime -```typescript -// Load config -const config = loadSystemConfig(); -// config.homePage comes from config.pages['home'] -// config.explorePage comes from config.pages['explore'] -``` - diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md index 454f3a40d..937458a69 100644 --- a/docs/PROJECT_STRUCTURE.md +++ b/docs/PROJECT_STRUCTURE.md @@ -131,30 +131,36 @@ src/constants/ ### 5. System Configuration (`src/config/`) -Whitelabeling and system configuration for community customization: +Whitelabeling and system configuration for community customization. Configurations are stored in Supabase and loaded at build time. See [Configuration System](SYSTEMS/CONFIGURATION/OVERVIEW.md) for details. ``` src/config/ # System configuration -├── brand/ # Brand identity configuration -│ └── nouns.brand.ts # Nouns brand config (name, tagline, etc.) -├── assets/ # Asset configuration -│ └── nouns.assets.ts # Nouns asset config (logos, icons) -├── theme/ # Theme configuration -│ └── nouns.theme.ts # Nouns theme config (colors, fonts) -├── community/ # Community-specific configuration -│ └── nouns.community.ts # Community URLs, contracts, tokens -├── fidgets/ # Fidget configuration -│ └── nouns.fidgets.ts # Enabled/disabled fidgets -├── spaces/ # Initial space configurations -│ ├── nouns.home.ts # Home page configuration -│ └── initial*.ts # Initial space templates -└── systemConfig.ts # Main system configuration interface +├── nouns/ # Nouns community configuration +│ ├── nouns.brand.ts # Brand identity +│ ├── nouns.assets.ts # Visual assets +│ ├── nouns.community.ts # Community integration +│ ├── nouns.fidgets.ts # Enabled/disabled fidgets +│ ├── nouns.home.ts # Home page config (legacy) +│ ├── nouns.explore.ts # Explore page config (legacy) +│ ├── nouns.navigation.ts # Navigation config +│ ├── nouns.theme.ts # Theme config (references shared) +│ ├── nouns.ui.ts # UI colors +│ └── initialSpaces/ # Initial space templates +├── clanker/ # Clanker community configuration +├── example/ # Example community configuration +├── shared/ # Shared configuration +│ └── themes.ts # Shared theme definitions +├── systemConfig.ts # System configuration interface +├── index.ts # Configuration loader (reads from DB or static) +└── initialSpaceConfig.ts # Base space configuration ``` **Key Features:** +- **Database-Backed**: Configs stored in Supabase, loaded at build time - **Whitelabeling Support**: Complete brand customization through configuration - **Community Integration**: URLs, social handles, governance links, contract addresses -- **Theme System**: Customizable colors, fonts, and visual styling +- **Shared Themes**: Themes stored in shared file, not per-community +- **Pages as Spaces**: Navigation pages stored as Spaces in Storage - **Fidget Management**: Enable/disable specific fidgets per community - **Space Templates**: Configurable initial space configurations diff --git a/docs/README.md b/docs/README.md index e176a475b..c3c610951 100644 --- a/docs/README.md +++ b/docs/README.md @@ -41,6 +41,9 @@ Nounspace is a highly customizable Farcaster client funded by Nouns DAO. This do - [Theme System](SYSTEMS/THEMES/OVERVIEW.md) - Theme architecture - [Customization](SYSTEMS/THEMES/CUSTOMIZATION.md) - Theme customization +### Configuration +- [Configuration System](SYSTEMS/CONFIGURATION/OVERVIEW.md) - Database-backed configuration system + ### Discovery - [Mini App Discovery System](SYSTEMS/DISCOVERY/MINI_APP_DISCOVERY_SYSTEM.md) - Mini-app discovery system - [Mini App Discovery](SYSTEMS/DISCOVERY/MINI_APP_DISCOVERY.md) - Mini-app discovery system (to be created) @@ -52,14 +55,6 @@ Nounspace is a highly customizable Farcaster client funded by Nouns DAO. This do - [Supabase](INTEGRATIONS/SUPABASE.md) - Supabase integration - [Neynar](INTEGRATIONS/NEYNAR.md) - Neynar API integration -## Database-Backed Configuration - -- [Database Config Guide](DATABASE_CONFIG/DATABASE_CONFIG_GUIDE.md) - Architecture and overview -- [Quick Start](DATABASE_CONFIG/QUICK_START_IMPLEMENTATION.md) - 4-hour proof of concept guide -- [Testing Guide](DATABASE_CONFIG/QUICK_START_TESTING.md) - Step-by-step testing -- [Implementation Plan](DATABASE_CONFIG/DATABASE_CONFIG_IMPLEMENTATION.md) - Detailed implementation (Phases 0-4) -- [Complete Plan](DATABASE_CONFIG/INCREMENTAL_IMPLEMENTATION_PLAN.md) - Full phase-by-phase plan (all 10 phases) - ## Development - [Development Guide](DEVELOPMENT/DEVELOPMENT_GUIDE.md) - Comprehensive development guide diff --git a/docs/SHARED_THEMES_APPROACH.md b/docs/SHARED_THEMES_APPROACH.md deleted file mode 100644 index 5b52cbe84..000000000 --- a/docs/SHARED_THEMES_APPROACH.md +++ /dev/null @@ -1,255 +0,0 @@ -# Shared Themes Approach - -## Overview - -Move themes out of individual community configs into a shared file, since themes are reusable across communities with only minor customizations. - -## Current State - -- Each community has its own `{community}.theme.ts` file -- Themes are mostly identical (same structure, different values) -- Themes take up 5.5 KB (66.7% of remaining config) - -## Proposed Location Options - -### Option 1: `src/config/shared/themes.ts` (Recommended) - -**Structure:** -``` -src/config/ -├── shared/ -│ └── themes.ts # Shared theme definitions -├── nouns/ -│ └── nouns.theme.ts # Community-specific overrides (optional) -└── ... -``` - -**Pros:** -- ✅ Clear organization - shared configs in `shared/` folder -- ✅ Easy to find - obvious location -- ✅ Extensible - can add other shared configs later -- ✅ Separates shared from community-specific - -**Cons:** -- ⚠️ New directory structure - -### Option 2: `src/config/themes.ts` - -**Structure:** -``` -src/config/ -├── themes.ts # Shared themes at root -├── nouns/ -└── ... -``` - -**Pros:** -- ✅ Simple - no new directory -- ✅ Easy to import - -**Cons:** -- ⚠️ Mixes shared with community configs -- ⚠️ Less clear organization - -### Option 3: `src/common/config/themes.ts` - -**Structure:** -``` -src/common/ -├── config/ -│ └── themes.ts # Shared themes -└── ... -``` - -**Pros:** -- ✅ In `common/` (shared code) -- ✅ Separated from community configs - -**Cons:** -- ⚠️ New directory structure -- ⚠️ Mixes config with common utilities - -## Recommendation: `src/config/shared/themes.ts` - -**Why:** -1. **Clear organization** - `shared/` folder makes intent obvious -2. **Extensible** - Can add other shared configs (e.g., `shared/defaultFidgets.ts`) -3. **Consistent** - Keeps configs in config directory -4. **Easy imports** - `import { themes } from '@/config/shared/themes'` - -## Implementation Approach - -### Option A: Single Shared File (All Themes) - -Store all theme variants in one shared file: - -```typescript -// src/config/shared/themes.ts - -export const sharedThemes = { - default: { /* ... */ }, - nounish: { /* ... */ }, - gradientAndWave: { /* ... */ }, - colorBlobs: { /* ... */ }, - floatingShapes: { /* ... */ }, - imageParallax: { /* ... */ }, - shootingStar: { /* ... */ }, - squareGrid: { /* ... */ }, - tesseractPattern: { /* ... */ }, - retro: { /* ... */ }, -}; -``` - -**Pros:** -- ✅ Single source of truth -- ✅ Easy to maintain -- ✅ All communities use same themes - -**Cons:** -- ⚠️ No community customization -- ⚠️ Can't override specific themes per community - -### Option B: Shared Base + Community Overrides - -Store base themes in shared file, allow community-specific overrides: - -```typescript -// src/config/shared/themes.ts - -export const baseThemes = { - default: { /* ... */ }, - nounish: { /* ... */ }, - // ... all themes -}; - -// src/config/nouns/nouns.theme.ts - -import { baseThemes } from '../../shared/themes'; - -export const nounsTheme = { - ...baseThemes, - // Override specific themes if needed - default: { - ...baseThemes.default, - properties: { - ...baseThemes.default.properties, - musicURL: "https://...", // Nouns-specific music - }, - }, -}; -``` - -**Pros:** -- ✅ Shared base themes -- ✅ Allows community customization -- ✅ Best of both worlds - -**Cons:** -- ⚠️ More complex -- ⚠️ Need merge logic - -### Option C: Shared Themes + Community Theme Config - -Store themes in shared file, reference from community config: - -```typescript -// src/config/shared/themes.ts - -export const themes = { - default: { /* ... */ }, - nounish: { /* ... */ }, - // ... all themes -}; - -// src/config/nouns/nouns.theme.ts - -import { themes } from '../../shared/themes'; - -// Just export shared themes (or override if needed) -export const nounsTheme = themes; -``` - -**Pros:** -- ✅ Simplest approach -- ✅ Single source of truth -- ✅ Easy to use - -**Cons:** -- ⚠️ No community customization (but maybe that's fine?) - -## Recommended: Option C (Simple Shared Reference) - -**Why:** -- Themes are visual templates, not community-specific -- Communities can customize via theme editor at runtime -- Keeps config simple and maintainable - -## Database Storage - -**Option 1: Store in Database as Shared Resource** - -```sql -CREATE TABLE shared_themes ( - id UUID PRIMARY KEY, - theme_id VARCHAR(50) UNIQUE, -- 'default', 'nounish', etc. - theme_config JSONB NOT NULL, - version INTEGER DEFAULT 1, - created_at TIMESTAMP DEFAULT NOW() -); -``` - -**Option 2: Store in Code (Recommended for Now)** - -Keep themes in code, reference from config: -- Themes are code/templates, not data -- Easier to version control -- Can move to DB later if needed - -## Size Impact - -**Before:** -- theme: 5.5 KB (66.7% of config) - -**After:** -- theme: Removed from config (0 KB) -- Config size: ~2.8 KB (down from 8.3 KB) -- **Total reduction: 90%** (from 29 KB to 2.8 KB) - -## Migration Steps - -1. **Create `src/config/shared/themes.ts`** - - Move theme definitions from nouns.theme.ts - - Export as `themes` object - -2. **Update community configs** - - Change `nouns.theme.ts` to import from shared - - Update other communities similarly - -3. **Update SystemConfig interface** - - Keep `theme: ThemeConfig` in interface - - But it now references shared themes - -4. **Update database schema** - - Remove `theme_config` column (or keep as reference) - - Or store theme reference IDs - -5. **Update build-time config** - - Themes loaded from shared file, not DB - - Or fetch from DB if storing there - -## Final Config Size - -After removing homePage, explorePage, and theme: - -| Section | Size | Percentage | -|---------|------|------------| -| community | 1.4 KB | 50% | -| navigation | 0.5 KB | 18% | -| assets | 0.3 KB | 11% | -| brand | 0.2 KB | 7% | -| fidgets | 0.2 KB | 7% | -| ui | 0.2 KB | 7% | -| **TOTAL** | **~2.8 KB** | **100%** | - -**Result:** Config is now tiny! Easily fits in env vars or generated file. - diff --git a/docs/SYSTEMS/CONFIGURATION/OVERVIEW.md b/docs/SYSTEMS/CONFIGURATION/OVERVIEW.md new file mode 100644 index 000000000..fa3838866 --- /dev/null +++ b/docs/SYSTEMS/CONFIGURATION/OVERVIEW.md @@ -0,0 +1,312 @@ +# Database-Backed Configuration System + +## Overview + +Nounspace uses a database-backed configuration system that allows community configurations to be stored in Supabase and loaded at build time. This provides admin-editable configs with zero runtime database queries. + +## Architecture + +``` +┌─────────────────┐ +│ Database │ +│ (Stores Config)│ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ ┌──────────────────┐ +│ Build Process │─────▶│ Set Env Var │ +│ (next.config.mjs)│ │ NEXT_PUBLIC_ │ +│ │ │ BUILD_TIME_ │ +│ │ │ CONFIG │ +└─────────────────┘ └─────────┬──────────┘ + │ + ▼ + ┌──────────────────┐ + │ Runtime App │ + │ (Reads Env Var │ + │ Zero DB Queries)│ + └──────────────────┘ +``` + +## Configuration Storage + +### Database Table + +Configurations are stored in the `community_configs` table: + +```sql +CREATE TABLE "public"."community_configs" ( + "id" UUID PRIMARY KEY, + "community_id" VARCHAR(50) NOT NULL UNIQUE, + "updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), + "brand_config" JSONB NOT NULL, -- Brand identity + "assets_config" JSONB NOT NULL, -- Asset paths + "community_config" JSONB NOT NULL, -- Community integration data + "fidgets_config" JSONB NOT NULL, -- Enabled/disabled fidgets + "navigation_config" JSONB, -- Navigation items (with spaceId refs) + "ui_config" JSONB, -- UI colors + "is_published" BOOLEAN DEFAULT true +); +``` + +### Config Sections + +- **`brand_config`**: Display name, tagline, description, mini app tags +- **`assets_config`**: Logo paths (main, icon, favicon, og image, splash) +- **`community_config`**: URLs, social handles, governance links, tokens, contracts +- **`fidgets_config`**: Enabled and disabled fidget IDs +- **`navigation_config`**: Navigation items with optional `spaceId` references +- **`ui_config`**: Primary colors, hover states, cast button colors + +### What's Not in the Database + +- **Themes**: Stored in `src/config/shared/themes.ts` (shared across communities) +- **Pages** (homePage/explorePage): Stored as Spaces in Supabase Storage, referenced by navigation items + +## Build-Time Loading + +### Process + +1. **Fetch Config from Database** + ```javascript + // next.config.mjs runs during build + const { data } = await supabase + .rpc('get_active_community_config', { p_community_id: community }) + .single(); + ``` + +2. **Store in Environment Variable** + ```javascript + process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(data); + ``` + +3. **Runtime Access** + ```typescript + // src/config/index.ts + const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; + if (buildTimeConfig) { + const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; + return dbConfig; + } + // Falls back to static configs if unavailable + ``` + +### Database Function + +The `get_active_community_config(community_id)` function returns a combined JSON object: + +```json +{ + "brand": { /* brand_config */ }, + "assets": { /* assets_config */ }, + "community": { /* community_config */ }, + "fidgets": { /* fidgets_config */ }, + "navigation": { /* navigation_config */ }, + "ui": { /* ui_config */ } +} +``` + +## Navigation Pages as Spaces + +### Concept + +Navigation pages (like `/home` and `/explore`) are stored as Spaces in Supabase Storage, not in the database config. Navigation items reference these Spaces via `spaceId`. + +### Storage Structure + +**Space Registration** (in `spaceRegistrations` table): +- `spaceType = 'navPage'` +- `fid = NULL` (system-owned) +- `identityPublicKey = 'system'` +- `signature = 'system-seed'` + +**Space Config Files** (in Supabase Storage bucket `spaces`): +``` +spaces/ + {spaceId}/ + tabOrder ← JSON: { tabOrder: ["Nouns", "Socials", ...] } + tabs/ + Nouns ← SpaceConfig JSON (fidgets, layout, etc.) + Socials ← SpaceConfig JSON + ... +``` + +**File Format** (SignedFile wrapper): +```json +{ + "fileData": "{...SpaceConfig JSON as string...}", + "fileType": "json", + "isEncrypted": false, + "timestamp": "2024-01-01T00:00:00Z", + "publicKey": "nounspace", + "signature": "not applicable, machine generated file" +} +``` + +### Navigation Reference + +Navigation items reference Spaces: + +```typescript +{ + id: 'home', + label: 'Home', + href: '/home', + icon: 'home', + spaceId: 'uuid-of-home-space' // ← References Space +} +``` + +## Dynamic Routing + +### Route Handler + +The `/[navSlug]/[[...tabName]]/page.tsx` route handles all navigation-backed pages: + +- `/home` → redirects to `/home/{defaultTab}` +- `/home/Nouns` → renders Nouns tab +- `/explore` → redirects to `/explore/{defaultTab}` +- `/explore/Featured` → renders Featured tab + +### Request Flow + +1. User visits `/home` +2. Route handler finds navigation item by slug +3. If `spaceId` exists, loads Space from Storage: + - Fetches `{spaceId}/tabOrder` to get tab order + - Fetches each `{spaceId}/tabs/{tabName}` for tab configs + - Reconstructs `PageConfig` format +4. If no tab specified, redirects to default tab +5. Renders `NavPageClient` with tabs + +### Space Loading Function + +```typescript +async function loadSpaceAsPageConfig(spaceId: string): Promise { + // 1. Fetch tab order + const { data: tabOrderData } = await supabase.storage + .from('spaces') + .download(`${spaceId}/tabOrder`); + + const tabOrderFile = JSON.parse(await tabOrderData.text()) as SignedFile; + const tabOrderObj = JSON.parse(tabOrderFile.fileData); + const tabOrder = tabOrderObj.tabOrder; + + // 2. Fetch each tab config + const tabs = {}; + for (const tabName of tabOrder) { + const { data: tabData } = await supabase.storage + .from('spaces') + .download(`${spaceId}/tabs/${tabName}`); + + const tabFile = JSON.parse(await tabData.text()) as SignedFile; + const tabConfig = JSON.parse(tabFile.fileData); + tabs[tabName] = tabConfig; + } + + // 3. Reconstruct PageConfig + return { + defaultTab: tabOrder[0], + tabOrder, + tabs, + layout: { /* defaults */ } + }; +} +``` + +## Shared Themes + +Themes are stored in `src/config/shared/themes.ts` and shared across all communities: + +```typescript +export const themes = { + default: { /* ... */ }, + nounish: { /* ... */ }, + gradientAndWave: { /* ... */ }, + // ... all 10 themes +}; +``` + +All communities import from this shared file, reducing duplication and config size. + +## Configuration Size + +The database config is ~2.8 KB (down from ~29 KB) by: +- Moving themes to shared file (5.5 KB saved) +- Moving pages to Spaces (20.6 KB saved) + +This size reduction makes the environment variable approach viable. + +## Runtime Access + +### Config Loader + +```typescript +// src/config/index.ts +export const loadSystemConfig = (): SystemConfig => { + const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; + if (buildTimeConfig) { + const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; + // Map pages object to homePage/explorePage for backward compatibility + return { + ...dbConfig, + homePage: dbConfig.pages?.['home'] || dbConfig.homePage || null, + explorePage: dbConfig.pages?.['explore'] || dbConfig.explorePage || null, + }; + } + // Fall back to static configs +}; +``` + +### Component Usage + +```typescript +// In components +import { loadSystemConfig } from '@/config'; + +const config = loadSystemConfig(); +const brandName = config.brand.displayName; +const navItems = config.navigation?.items || []; +``` + +```typescript +// React hook +import { useSystemConfig } from '@/common/lib/hooks/useSystemConfig'; + +function Navigation() { + const config = useSystemConfig(); + // Use config.brand, config.navigation, etc. +} +``` + +## Environment Variables + +**Required for Build:** +- `NEXT_PUBLIC_SUPABASE_URL` - Supabase project URL +- `SUPABASE_SERVICE_ROLE_KEY` - Service role key for build-time access +- `NEXT_PUBLIC_COMMUNITY` - Community ID (defaults to 'nouns') + +**Fallback Behavior:** +- If DB credentials missing → Falls back to static configs +- If DB config not found → Falls back to static configs +- App continues to work in all cases + +## Benefits + +- **Zero Runtime Overhead** - No database queries in production +- **Admin Updates** - Configs can be updated via database +- **Fast Runtime** - Config loaded from env var (instant) +- **Safe Fallback** - Static configs always available +- **Small Config** - Only ~2.8 KB (fits in env vars) +- **Unified Architecture** - Pages are Spaces, consistent with existing system +- **Shared Themes** - Single source of truth, no duplication + +## Related Files + +- **Database**: `supabase/migrations/20251129172847_create_community_configs.sql` +- **Build Config**: `next.config.mjs` - Loads config at build time +- **Config Loader**: `src/config/index.ts` - Reads from env var +- **Route Handler**: `src/app/[navSlug]/[[...tabName]]/page.tsx` - Dynamic navigation +- **Space Seeding**: `scripts/seed-navpage-spaces.ts` - Uploads space configs to Storage +- **Shared Themes**: `src/config/shared/themes.ts` - Theme definitions + diff --git a/docs/SYSTEM_WALKTHROUGH.md b/docs/SYSTEM_WALKTHROUGH.md deleted file mode 100644 index cac220e58..000000000 --- a/docs/SYSTEM_WALKTHROUGH.md +++ /dev/null @@ -1,578 +0,0 @@ -# System Walkthrough: How Nounspace Works Now - -This document provides a comprehensive walkthrough of how the Nounspace system currently works, focusing on the database-backed configuration system and dynamic navigation routing. - -## Table of Contents - -1. [Overview](#overview) -2. [Architecture Flow](#architecture-flow) -3. [Build-Time Configuration Loading](#build-time-configuration-loading) -4. [Runtime Configuration Access](#runtime-configuration-access) -5. [Navigation Pages as Spaces](#navigation-pages-as-spaces) -6. [Dynamic Routing](#dynamic-routing) -7. [Data Flow Examples](#data-flow-examples) -8. [Key Components](#key-components) - ---- - -## Overview - -Nounspace is a **customizable Farcaster client** that can be whitelabeled for different communities (Nouns, Clanker, etc.). The system uses a **database-backed configuration** approach where: - -- **Configurations are stored in Supabase** (PostgreSQL database) -- **Configs are loaded at build time** (not runtime), ensuring zero database queries in production -- **Navigation pages are stored as Spaces** in Supabase Storage, referenced by navigation items -- **Themes are shared** across communities in a TypeScript file -- **Zero runtime queries** - everything is baked into the build - -### Key Benefits - -- ✅ **Admin-editable**: Admins can update configs via database without code changes -- ✅ **Performance**: No runtime database queries -- ✅ **Flexible**: Navigation items can reference Spaces for page content -- ✅ **Maintainable**: Shared themes reduce duplication - ---- - -## Architecture Flow - -``` -┌─────────────────────────────────────────────────────────────┐ -│ BUILD TIME │ -├─────────────────────────────────────────────────────────────┤ -│ │ -│ 1. next.config.mjs runs │ -│ ├─> Fetches config from community_configs table │ -│ ├─> Extracts spaceIds from navigation items │ -│ ├─> Fetches Spaces from Supabase Storage │ -│ └─> Stores everything in NEXT_PUBLIC_BUILD_TIME_CONFIG │ -│ │ -│ 2. Next.js builds static pages │ -│ ├─> generateStaticParams() runs │ -│ ├─> Generates static paths for navigation pages │ -│ └─> Creates static HTML/JS bundles │ -│ │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ RUNTIME │ -├─────────────────────────────────────────────────────────────┤ -│ │ -│ 1. User visits /home or /explore │ -│ ├─> Next.js routes to [navSlug]/[[...tabName]]/page.tsx │ -│ ├─> loadSystemConfig() reads from env var │ -│ └─> No database queries! │ -│ │ -│ 2. Page component loads │ -│ ├─> Finds navigation item by slug │ -│ ├─> If spaceId exists, loads Space from Storage │ -│ ├─> Converts Space config to PageConfig │ -│ └─> Renders NavPageClient with tabs │ -│ │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## Build-Time Configuration Loading - -### Step 1: Database Query - -When you run `npm run build`, `next.config.mjs` runs first: - -```javascript -// next.config.mjs - -async function loadConfigFromDB() { - // 1. Get credentials - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; - - if (!supabaseUrl || !supabaseKey) { - console.log('ℹ️ Using static configs (no DB credentials)'); - return; // Falls back to static configs - } - - // 2. Create Supabase client - const supabase = createClient(supabaseUrl, supabaseKey); - const community = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - - // 3. Fetch config from database - const { data, error } = await supabase - .rpc('get_active_community_config', { p_community_id: community }) - .single(); - - if (error || !data) { - console.log('ℹ️ Using static configs (no DB config found)'); - return; // Falls back to static configs - } - - // 4. Store in environment variable - process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(data); - console.log('✅ Loaded config from database'); -} -``` - -### Step 2: What Gets Fetched - -The `get_active_community_config` function returns: - -```json -{ - "brand": { /* Brand identity */ }, - "assets": { /* Asset paths */ }, - "community": { /* Community integration data */ }, - "fidgets": { /* Enabled/disabled fidgets */ }, - "navigation": { - "items": [ - { - "id": "home", - "label": "Home", - "href": "/home", - "icon": "home", - "spaceId": "uuid-123" // ← References a Space! - }, - { - "id": "explore", - "label": "Explore", - "href": "/explore", - "icon": "explore", - "spaceId": "uuid-456" // ← References a Space! - } - ] - }, - "ui": { /* UI colors */ } -} -``` - -**Note:** -- No `theme` (stored in shared file) -- No `homePage` or `explorePage` (stored as Spaces) -- Navigation items have `spaceId` references - -### Step 3: Space Fetching (Future Enhancement) - -Currently, Spaces are fetched at **runtime** when pages load. In the future, we could fetch them at build time too: - -```javascript -// For each navigation item with spaceId: -const spaceIds = navigation.items - .filter(item => item.spaceId) - .map(item => item.spaceId); - -// Fetch each Space from Storage -for (const spaceId of spaceIds) { - const tabOrder = await fetchTabOrder(spaceId); - const tabs = await fetchAllTabs(spaceId, tabOrder); - // Store in config... -} -``` - ---- - -## Runtime Configuration Access - -### Step 1: Config Loader - -When the app runs, `loadSystemConfig()` is called: - -```typescript -// src/config/index.ts - -export const loadSystemConfig = (): SystemConfig => { - // 1. Try build-time config from database - const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; - if (buildTimeConfig) { - try { - const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; - if (dbConfig && dbConfig.brand && dbConfig.assets) { - console.log('✅ Using config from database'); - return dbConfig; // ← Returns DB config - } - } catch (error) { - console.warn('⚠️ Failed to parse build-time config'); - } - } - - // 2. Fall back to static configs - console.log('ℹ️ Using static configs'); - const community = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - - switch (community) { - case 'nouns': - return nounsSystemConfig; - case 'clanker': - return clankerSystemConfig; - default: - return nounsSystemConfig; - } -}; -``` - -### Step 2: Component Usage - -Components use `loadSystemConfig()` or the `useSystemConfig()` hook: - -```typescript -// In a component -import { loadSystemConfig } from '@/config'; - -const config = loadSystemConfig(); -const brandName = config.brand.displayName; -const logo = config.assets.logos.main; -const navItems = config.navigation?.items || []; -``` - -```typescript -// In a React component -import { useSystemConfig } from '@/common/lib/hooks/useSystemConfig'; - -function Navigation() { - const config = useSystemConfig(); - // Use config.brand, config.navigation, etc. -} -``` - -**Important:** This happens at runtime, but **no database queries occur** - the config is already in the environment variable! - ---- - -## Navigation Pages as Spaces - -### Concept - -Navigation pages (like `/home` and `/explore`) are stored as **Spaces** in Supabase Storage, not directly in the config. This allows: - -- ✅ Pages to be updated independently -- ✅ Reusing existing Space infrastructure -- ✅ Dramatically reducing config size (from ~29 KB to ~2.8 KB) - -### Storage Structure - -**1. Space Registration** (in `spaceRegistrations` table): - -```sql -INSERT INTO spaceRegistrations ( - spaceId, - fid, -- NULL for system-owned pages - spaceName, -- 'nouns-home', 'nouns-explore' - spaceType, -- 'navPage' - identityPublicKey,-- 'system' - signature, -- 'system-seed' - timestamp -) VALUES (...); -``` - -**2. Space Config Files** (in Supabase Storage bucket `spaces`): - -``` -spaces/ - {spaceId}/ - tabOrder ← JSON: { tabOrder: ["Nouns", "Socials", ...] } - tabs/ - Nouns ← SpaceConfig JSON (fidgets, layout, etc.) - Socials ← SpaceConfig JSON - ... -``` - -**3. File Format** (SignedFile wrapper): - -```json -{ - "fileData": "{...SpaceConfig JSON as string...}", - "fileType": "json", - "isEncrypted": false, - "timestamp": "2024-01-01T00:00:00Z", - "publicKey": "nounspace", - "signature": "not applicable, machine generated file" -} -``` - -### How It Works - -1. **Navigation Config** references Spaces: - ```typescript - { - id: 'home', - href: '/home', - spaceId: 'uuid-123' // ← References Space - } - ``` - -2. **At Runtime**, when user visits `/home`: - - Route handler finds navigation item - - Extracts `spaceId` - - Fetches Space from Storage - - Converts Space config to PageConfig - - Renders page with tabs - ---- - -## Dynamic Routing - -### Route Structure - -``` -src/app/[navSlug]/[[...tabName]]/page.tsx -``` - -This handles: -- `/home` → redirects to `/home/{defaultTab}` -- `/home/Nouns` → renders Nouns tab -- `/explore` → redirects to `/explore/{defaultTab}` -- `/explore/Featured` → renders Featured tab -- Any custom nav item with a Space - -### Request Flow - -``` -User visits /home - │ - ▼ -┌─────────────────────────────────────────┐ -│ [navSlug]/[[...tabName]]/page.tsx │ -├─────────────────────────────────────────┤ -│ │ -│ 1. Extract navSlug = "home" │ -│ │ -│ 2. Load system config │ -│ const config = loadSystemConfig(); │ -│ │ -│ 3. Find navigation item │ -│ const navItem = config.navigation │ -│ .items.find(i => i.href === "/home")│ -│ │ -│ 4. Check if spaceId exists │ -│ if (navItem.spaceId) { │ -│ // Load Space from Storage │ -│ } │ -│ │ -└─────────────────────────────────────────┘ -``` - -### Space Loading - -When a navigation item has a `spaceId`, the system loads it: - -```typescript -async function loadSpaceAsPageConfig(spaceId: string): Promise { - // 1. Create Supabase client (with credentials check) - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const supabaseKey = process.env.SUPABASE_SERVICE_KEY || ...; - - if (!supabaseUrl || !supabaseKey) { - return null; // Will render at runtime - } - - const supabase = createClient(supabaseUrl, supabaseKey); - - // 2. Fetch tab order - const { data: tabOrderData } = await supabase.storage - .from('spaces') - .download(`${spaceId}/tabOrder`); - - const tabOrderFile = JSON.parse(await tabOrderData.text()) as SignedFile; - const tabOrderObj = JSON.parse(tabOrderFile.fileData); - const tabOrder = tabOrderObj.tabOrder; - - // 3. Fetch each tab config - const tabs = {}; - for (const tabName of tabOrder) { - const { data: tabData } = await supabase.storage - .from('spaces') - .download(`${spaceId}/tabs/${tabName}`); - - const tabFile = JSON.parse(await tabData.text()) as SignedFile; - const tabConfig = JSON.parse(tabFile.fileData); - tabs[tabName] = tabConfig; - } - - // 4. Reconstruct PageConfig - return { - defaultTab: tabOrder[0], - tabOrder, - tabs, - layout: { /* defaults */ } - }; -} -``` - -### Redirect Logic - -If no tab is specified, redirect to default: - -```typescript -// If no tab name provided, redirect to default tab -if (!tabName || tabName.length === 0) { - const pageConfig = await loadSpaceAsPageConfig(navItem.spaceId); - if (pageConfig) { - const defaultTab = encodeURIComponent(pageConfig.defaultTab); - redirect(`/${navSlug}/${defaultTab}`); // e.g., /home/Nouns - return null; - } -} -``` - ---- - -## Data Flow Examples - -### Example 1: Building the App - -``` -1. Developer runs: npm run build - │ - ▼ -2. next.config.mjs executes - ├─> loadConfigFromDB() runs - ├─> Queries: SELECT get_active_community_config('nouns') - ├─> Gets JSON config from database - └─> Sets: process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = "{...}" - │ - ▼ -3. Next.js build process starts - ├─> generateStaticParams() runs for [navSlug] route - ├─> loadSystemConfig() reads from env var - ├─> Extracts navigation items - ├─> (Optionally) Generates static paths - └─> Builds static pages - │ - ▼ -4. Build completes - └─> App is ready to deploy (no DB needed!) -``` - -### Example 2: User Visits /home - -``` -1. User navigates to: https://nounspace.com/home - │ - ▼ -2. Next.js routes to: [navSlug]/[[...tabName]]/page.tsx - ├─> navSlug = "home" - └─> tabName = undefined (no tab specified) - │ - ▼ -3. Page component executes: - ├─> const config = loadSystemConfig(); - │ └─> Reads from NEXT_PUBLIC_BUILD_TIME_CONFIG (no DB query!) - │ - ├─> const navItem = config.navigation.items.find(...) - │ └─> Finds: { id: 'home', spaceId: 'uuid-123', ... } - │ - ├─> if (!tabName) { redirect to default tab } - │ └─> loadSpaceAsPageConfig('uuid-123') - │ ├─> Fetches: spaces/uuid-123/tabOrder - │ ├─> Fetches: spaces/uuid-123/tabs/Nouns - │ ├─> Fetches: spaces/uuid-123/tabs/Socials - │ └─> Returns PageConfig - │ - └─> redirect(`/home/${defaultTab}`) // e.g., /home/Nouns - │ - ▼ -4. User redirected to: /home/Nouns - └─> Same component, but now tabName = ["Nouns"] - │ - ▼ -5. Page renders: - ├─> Loads Space config (same as above) - ├─> Renders NavPageClient with: - │ ├─> pageConfig: { tabs: {...}, tabOrder: [...] } - │ ├─> activeTabName: "Nouns" - │ └─> navSlug: "home" - └─> User sees Home page with Nouns tab active -``` - -### Example 3: Admin Updates Config - -``` -1. Admin opens admin UI (future feature) - │ - ▼ -2. Admin updates brand name in UI - ├─> UI calls: UPDATE community_configs - │ SET brand_config = '{...new config...}' - │ WHERE community_id = 'nouns' - │ - ▼ -3. Admin triggers rebuild - ├─> CI/CD system runs: npm run build - ├─> Build process fetches new config from DB - └─> New config is baked into build - │ - ▼ -4. New build deployed - └─> Users see updated brand name (no code changes!) -``` - ---- - -## Key Components - -### 1. Configuration System - -**Files:** -- `src/config/index.ts` - Main config loader -- `src/config/systemConfig.ts` - TypeScript interfaces -- `src/config/shared/themes.ts` - Shared theme definitions -- `src/config/nouns/` - Static fallback configs - -**Key Functions:** -- `loadSystemConfig()` - Returns SystemConfig (DB or static) -- `useSystemConfig()` - React hook for components - -### 2. Build-Time Loading - -**Files:** -- `next.config.mjs` - Fetches config from DB at build time - -**Key Functions:** -- `loadConfigFromDB()` - Queries database, stores in env var - -### 3. Database Schema - -**Tables:** -- `community_configs` - Stores brand, assets, navigation, etc. -- `spaceRegistrations` - Registers navPage Spaces -- `storage.buckets.spaces` - Stores Space config files - -**Functions:** -- `get_active_community_config(community_id)` - Returns combined config - -### 4. Dynamic Routing - -**Files:** -- `src/app/[navSlug]/[[...tabName]]/page.tsx` - Main route handler -- `src/app/[navSlug]/[[...tabName]]/NavPageClient.tsx` - Client component - -**Key Functions:** -- `loadSpaceAsPageConfig(spaceId)` - Fetches Space from Storage -- `generateStaticParams()` - Generates static paths (optional) - -### 5. Space Storage - -**Structure:** -``` -Supabase Storage: spaces/ - {spaceId}/ - tabOrder ← Tab order JSON - tabs/ - {tabName} ← SpaceConfig JSON (fidgets, layout, etc.) -``` - -**Format:** SignedFile wrapper (unencrypted for system files) - ---- - -## Summary - -1. **Configs are stored in Supabase** but loaded at build time, not runtime -2. **Navigation pages are Spaces** stored in Supabase Storage, referenced by navigation items -3. **Themes are shared** across communities in a TypeScript file -4. **Zero runtime queries** - everything is in environment variables -5. **Dynamic routing** handles any navigation item that references a Space -6. **Graceful fallbacks** - if DB/config unavailable, falls back to static configs - -This architecture provides the flexibility of database-backed configs with the performance of static builds! - diff --git a/docs/UPDATED_IMPLEMENTATION_SUMMARY.md b/docs/UPDATED_IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index c62f9d0fd..000000000 --- a/docs/UPDATED_IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,147 +0,0 @@ -# Updated Implementation Plan Summary - -## Key Architectural Changes - -### 1. **Navigation-Space Reference** -- `homePage` and `explorePage` removed from `community_configs` -- Navigation items reference Spaces via `spaceId` -- Pages fetched from Spaces at build time -- **Size reduction: 71%** (removes 20.6 KB) - -### 2. **Shared Themes** -- Themes moved to `src/config/shared/themes.ts` -- All communities use same shared themes -- Themes removed from `community_configs` -- **Size reduction: 66%** (removes 5.5 KB) - -### 3. **File-Based Config (Not Env Var)** -- Config generated as TypeScript file (`src/config/db-config.ts`) -- Avoids E2BIG error (env var size limits) -- No size restrictions - -## Final Config Structure - -### In Database (`community_configs`): -```json -{ - "brand_config": { /* 0.2 KB */ }, - "assets_config": { /* 0.3 KB */ }, - "community_config": { /* 1.4 KB */ }, - "fidgets_config": { /* 0.2 KB */ }, - "navigation_config": { /* 0.5 KB - includes spaceId references */ }, - "ui_config": { /* 0.2 KB */ } - // NO theme_config (in shared file) - // NO home_page_config (in Spaces) - // NO explore_page_config (in Spaces) -} -``` - -**Total: ~2.8 KB** (down from ~29 KB - **90% reduction!**) - -### In Code (`src/config/shared/themes.ts`): -```typescript -export const themes = { - default: { /* ... */ }, - nounish: { /* ... */ }, - // ... all 10 themes -}; -``` - -### In Spaces (via navigation): -- `homePage` → Space referenced by nav item `spaceId` -- `explorePage` → Space referenced by nav item `spaceId` -- Stored in `spaceRegistrations` with `spaceType = 'navPage'` -- Config stored in Supabase Storage - -## Updated Database Schema - -### `community_configs` Table: -```sql -CREATE TABLE community_configs ( - id UUID PRIMARY KEY, - community_id VARCHAR(50) UNIQUE, - brand_config JSONB, -- ✅ Kept - assets_config JSONB, -- ✅ Kept - community_config JSONB, -- ✅ Kept - fidgets_config JSONB, -- ✅ Kept - navigation_config JSONB, -- ✅ Kept (now includes spaceId) - ui_config JSONB, -- ✅ Kept - -- ❌ REMOVED: theme_config (in shared file) - -- ❌ REMOVED: home_page_config (in Spaces) - -- ❌ REMOVED: explore_page_config (in Spaces) -); -``` - -### `spaceRegistrations` Table: -```sql --- Add navPage spaceType -ALTER TABLE spaceRegistrations - ADD CONSTRAINT valid_space_type CHECK ( - "spaceType" IN ('profile', 'token', 'proposal', 'channel', 'navPage') - ); -``` - -## Updated Build Process - -```javascript -// next.config.mjs - -1. Fetch main config from DB (small - ~2.8 KB) -2. Import shared themes from code -3. Extract spaceIds from navigation items -4. Fetch Spaces for nav items -5. Convert Spaces to page configs -6. Combine: config + themes + pages -7. Generate TypeScript file -``` - -## Updated Config Loader - -```typescript -// src/config/index.ts - -1. Try to import db-config.ts (generated file) -2. If exists: - - Use DB config - - Add shared themes - - Map pages['home'] → homePage - - Map pages['explore'] → explorePage -3. If not exists: - - Fall back to static configs -``` - -## Size Comparison - -| Stage | Config Size | Reduction | -|-------|-------------|-----------| -| **Original** | ~29 KB | - | -| **After removing pages** | ~8.3 KB | 71% | -| **After removing themes** | **~2.8 KB** | **90%** | - -## Migration Impact - -### Phase 1 (Database Schema): -- ✅ Create `community_configs` (without page/theme columns) -- ✅ Add `navPage` spaceType -- ✅ Seed configs (without themes/pages) - -### Phase 2 (Config Loading): -- ✅ Create `src/config/shared/themes.ts` -- ✅ Update community configs to import shared themes -- ✅ Fetch Spaces for nav items at build time -- ✅ Generate TypeScript file (not env var) - -### Phase 3+ (Admin/UI): -- ✅ Admin can edit config (smaller now) -- ✅ Admin can edit nav page Spaces -- ✅ Themes edited in code (shared file) - -## Benefits Summary - -✅ **Solves E2BIG** - Config now ~2.8 KB (well under limits) -✅ **Unified architecture** - Pages are Spaces -✅ **Shared themes** - Single source of truth -✅ **Navigation as source of truth** - Nav defines pages -✅ **Flexible** - Any nav item can reference a Space -✅ **Maintainable** - Clear separation of concerns - diff --git a/src/app/page.tsx b/src/app/page.tsx index 3805e83d6..dfc785545 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -6,8 +6,8 @@ export default function RootRedirect() { // If homePage exists (legacy config), redirect to its default tab if (config.homePage?.defaultTab) { - const tab = encodeURIComponent(config.homePage.defaultTab); - redirect(`/home/${tab}`); + const tab = encodeURIComponent(config.homePage.defaultTab); + redirect(`/home/${tab}`); return null; } From 77e49b6b361d5f1279243d61cad665fb26941350 Mon Sep 17 00:00:00 2001 From: Jesse Paterson Date: Sun, 30 Nov 2025 15:45:13 -0600 Subject: [PATCH 111/155] removed docs archive --- .../ADMIN_ASSET_UPLOAD_STRATEGY.md | 509 -------- .../ASSETS_CONFIG_STORAGE_RELATIONSHIP.md | 386 ------- .../ASSET_HANDLING_STRATEGY.md | 337 ------ .../ASSET_PERFORMANCE_OPTIMIZATION.md | 329 ------ .../BUILD_TIME_CONFIG_SUMMARY.md | 152 --- .../CONFIG_DATABASE_SCHEMA_DETAILED.md | 1027 ----------------- .../DATABASE_CONFIG_MIGRATION_PLAN.md | 1000 ---------------- .../database-config/E2BIG_SOLUTION.md | 99 -- .../NAVIGATION_SPACE_REFERENCE_APPROACH.md | 313 ----- ...VIGATION_SPACE_REFERENCE_IMPLEMENTATION.md | 357 ------ .../NEXTJS_CONFIG_APPROACHES.md | 347 ------ docs/_archived/database-config/README.md | 43 - .../database-config/SHARED_THEMES_APPROACH.md | 255 ---- .../SIMPLE_BUILD_TIME_CONFIG.md | 424 ------- .../SPACE_REFERENCE_APPROACH.md | 202 ---- .../UPDATED_IMPLEMENTATION_SUMMARY.md | 147 --- 16 files changed, 5927 deletions(-) delete mode 100644 docs/_archived/database-config/ADMIN_ASSET_UPLOAD_STRATEGY.md delete mode 100644 docs/_archived/database-config/ASSETS_CONFIG_STORAGE_RELATIONSHIP.md delete mode 100644 docs/_archived/database-config/ASSET_HANDLING_STRATEGY.md delete mode 100644 docs/_archived/database-config/ASSET_PERFORMANCE_OPTIMIZATION.md delete mode 100644 docs/_archived/database-config/BUILD_TIME_CONFIG_SUMMARY.md delete mode 100644 docs/_archived/database-config/CONFIG_DATABASE_SCHEMA_DETAILED.md delete mode 100644 docs/_archived/database-config/DATABASE_CONFIG_MIGRATION_PLAN.md delete mode 100644 docs/_archived/database-config/E2BIG_SOLUTION.md delete mode 100644 docs/_archived/database-config/NAVIGATION_SPACE_REFERENCE_APPROACH.md delete mode 100644 docs/_archived/database-config/NAVIGATION_SPACE_REFERENCE_IMPLEMENTATION.md delete mode 100644 docs/_archived/database-config/NEXTJS_CONFIG_APPROACHES.md delete mode 100644 docs/_archived/database-config/README.md delete mode 100644 docs/_archived/database-config/SHARED_THEMES_APPROACH.md delete mode 100644 docs/_archived/database-config/SIMPLE_BUILD_TIME_CONFIG.md delete mode 100644 docs/_archived/database-config/SPACE_REFERENCE_APPROACH.md delete mode 100644 docs/_archived/database-config/UPDATED_IMPLEMENTATION_SUMMARY.md diff --git a/docs/_archived/database-config/ADMIN_ASSET_UPLOAD_STRATEGY.md b/docs/_archived/database-config/ADMIN_ASSET_UPLOAD_STRATEGY.md deleted file mode 100644 index e37049e9c..000000000 --- a/docs/_archived/database-config/ADMIN_ASSET_UPLOAD_STRATEGY.md +++ /dev/null @@ -1,509 +0,0 @@ -# Admin Asset Upload Strategy - -## Overview - -Admins need to upload assets through the admin UI (no repo access), and those assets must be available at build time. This requires: - -1. **Asset upload** - Admins upload to Supabase Storage -2. **Asset storage** - Store asset URLs/paths in database -3. **Build-time download** - Download assets during build -4. **Asset resolution** - Make assets available to Next.js - -## Architecture - -``` -Admin UI → Upload to Supabase Storage → Store URLs in DB - ↓ - Build Time: - Download Assets - → Copy to public/ - → Reference in Config -``` - -## Implementation - -### 1. Database Schema - -Add asset storage information to the config: - -```sql --- In community_configs table, assets_config JSONB column: -{ - "logos": { - "main": { - "storagePath": "community-assets/nouns/logo.svg", - "publicUrl": "https://{supabase-url}/storage/v1/object/public/community-assets/nouns/logo.svg", - "localPath": "/images/nouns/logo.svg" // Where it will be in public/ after build - }, - "icon": { - "storagePath": "community-assets/nouns/noggles.svg", - "publicUrl": "https://...", - "localPath": "/images/nouns/noggles.svg" - }, - // ... etc - } -} -``` - -Or simpler - just store storage paths, generate URLs at build time: - -```json -{ - "assets": { - "logos": { - "main": "community-assets/nouns/logo.svg", - "icon": "community-assets/nouns/noggles.svg", - "favicon": "community-assets/nouns/favicon.ico", - "appleTouch": "community-assets/nouns/apple-touch.png", - "og": "community-assets/nouns/og.png", - "splash": "community-assets/nouns/splash.png" - } - } -} -``` - -### 2. Supabase Storage Setup - -Create a storage bucket for community assets: - -```sql --- Create storage bucket -INSERT INTO storage.buckets (id, name, public) -VALUES ('community-assets', 'community-assets', true); - --- Set up RLS policies -CREATE POLICY "Admins can upload assets" -ON storage.objects FOR INSERT -WITH CHECK ( - bucket_id = 'community-assets' AND - EXISTS ( - SELECT 1 FROM community_config_admins cca - WHERE cca.admin_identity_public_key = auth.uid()::text - AND cca.is_active = true - ) -); - -CREATE POLICY "Public can read assets" -ON storage.objects FOR SELECT -USING (bucket_id = 'community-assets'); -``` - -### 3. Build-Time Asset Download - -```javascript -// next.config.mjs - -import { createClient } from '@supabase/supabase-js'; -import { writeFile, mkdir } from 'fs/promises'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; -import { createWriteStream } from 'fs'; -import { pipeline } from 'stream/promises'; -import { Readable } from 'stream'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -async function downloadAsset(supabase, storagePath, localPath) { - try { - const { data, error } = await supabase.storage - .from('community-assets') - .download(storagePath); - - if (error || !data) { - console.warn(`⚠️ Failed to download ${storagePath}:`, error?.message); - return false; - } - - // Ensure directory exists - const fullPath = join(__dirname, 'public', localPath); - const dir = dirname(fullPath); - await mkdir(dir, { recursive: true }); - - // Convert blob to buffer and write - const buffer = Buffer.from(await data.arrayBuffer()); - await writeFile(fullPath, buffer); - - console.log(`✅ Downloaded ${storagePath} → ${localPath}`); - return true; - } catch (error) { - console.error(`❌ Error downloading ${storagePath}:`, error.message); - return false; - } -} - -async function downloadAssets(config, communityId) { - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; - - if (!supabaseUrl || !supabaseKey) { - console.warn('⚠️ Missing Supabase credentials, skipping asset download'); - return config; - } - - const supabase = createClient(supabaseUrl, supabaseKey); - const assets = config.assets?.logos || {}; - const downloadedAssets = {}; - - // Download each asset - for (const [key, storagePath] of Object.entries(assets)) { - if (typeof storagePath === 'string' && storagePath.startsWith('community-assets/')) { - // Generate local path - const localPath = `/images/${communityId}/${storagePath.split('/').pop()}`; - - // Download asset - const success = await downloadAsset(supabase, storagePath, localPath); - - if (success) { - downloadedAssets[key] = localPath; - } else { - // Fall back to static asset if download fails - console.warn(`⚠️ Using static fallback for ${key}`); - downloadedAssets[key] = null; // Will be replaced with static in loader - } - } else if (typeof storagePath === 'string') { - // Already a public path, use as-is - downloadedAssets[key] = storagePath; - } - } - - // Update config with downloaded asset paths - return { - ...config, - assets: { - logos: downloadedAssets - } - }; -} - -async function generateConfig() { - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; - - if (!supabaseUrl || !supabaseKey) { - console.warn('⚠️ Missing Supabase credentials, using static config'); - return; - } - - const supabase = createClient(supabaseUrl, supabaseKey); - const community = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - - try { - const { data, error } = await supabase - .rpc('get_active_community_config', { p_community_id: community }) - .single(); - - if (error || !data) { - console.warn('⚠️ No config in DB, using static'); - return; - } - - // Download assets from Supabase Storage - const configWithAssets = await downloadAssets(data, community); - - // Set as env var - process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(configWithAssets); - console.log('✅ Loaded config with downloaded assets from database'); - } catch (error) { - console.warn('⚠️ Error loading config:', error.message); - } -} - -await generateConfig(); - -// Continue with Next.js config... -``` - -### 4. Config Loader with Asset Fallback - -```typescript -// src/config/index.ts - -import { SystemConfig } from './systemConfig'; -import { nounsSystemConfig } from './nouns/index'; -import { clankerSystemConfig } from './clanker/index'; -import { exampleSystemConfig } from './example/index'; - -// Import static assets for fallback -import { nounsAssets } from './nouns/nouns.assets'; -import { clankerAssets } from './clanker/clanker.assets'; -import { exampleAssets } from './example/example.assets'; - -const STATIC_CONFIGS: Record = { - nouns: nounsSystemConfig, - clanker: clankerSystemConfig as unknown as SystemConfig, - example: exampleSystemConfig, -}; - -const STATIC_ASSETS: Record = { - nouns: nounsAssets, - clanker: clankerAssets, - example: exampleAssets, -}; - -export const loadSystemConfig = (): SystemConfig => { - const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - const community = communityConfig.toLowerCase(); - - const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; - - if (buildTimeConfig) { - try { - const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; - const staticAssets = STATIC_ASSETS[community]; - - // Merge assets: use downloaded assets, fall back to static - const mergedAssets = { - logos: { - main: dbConfig.assets?.logos?.main || staticAssets?.logos.main, - icon: dbConfig.assets?.logos?.icon || staticAssets?.logos.icon, - favicon: dbConfig.assets?.logos?.favicon || staticAssets?.logos.favicon, - appleTouch: dbConfig.assets?.logos?.appleTouch || staticAssets?.logos.appleTouch, - og: dbConfig.assets?.logos?.og || staticAssets?.logos.og, - splash: dbConfig.assets?.logos?.splash || staticAssets?.logos.splash, - } - }; - - return { - ...dbConfig, - assets: mergedAssets, - }; - } catch (error) { - console.warn('Failed to parse build-time config:', error); - } - } - - return STATIC_CONFIGS[community] || STATIC_CONFIGS.nouns; -}; -``` - -### 5. Admin Upload API - -```typescript -// src/app/api/admin/assets/upload/route.ts - -import { createClient } from '@supabase/supabase-js'; -import { NextRequest, NextResponse } from 'next/server'; - -export async function POST(request: NextRequest) { - const formData = await request.formData(); - const file = formData.get('file') as File; - const communityId = formData.get('communityId') as string; - const assetType = formData.get('assetType') as string; // 'main', 'icon', etc. - - // Verify admin permissions (use your auth system) - const adminIdentity = request.headers.get('x-admin-identity'); - if (!adminIdentity) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - - const supabase = createClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.SUPABASE_SERVICE_ROLE_KEY! - ); - - // Check admin permissions - const { data: admin } = await supabase - .from('community_config_admins') - .select('id') - .eq('community_id', communityId) - .eq('admin_identity_public_key', adminIdentity) - .eq('is_active', true) - .single(); - - if (!admin) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } - - // Generate storage path - const fileExt = file.name.split('.').pop(); - const storagePath = `community-assets/${communityId}/${assetType}.${fileExt}`; - - // Upload to Supabase Storage - const { data, error } = await supabase.storage - .from('community-assets') - .upload(storagePath, file, { - upsert: true, - contentType: file.type, - }); - - if (error) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } - - // Get public URL - const { data: { publicUrl } } = supabase.storage - .from('community-assets') - .getPublicUrl(storagePath); - - // Update config in database - // (You'll need to update the assets_config JSONB column) - - return NextResponse.json({ - success: true, - storagePath, - publicUrl, - }); -} -``` - -### 6. Admin UI Component - -```typescript -// src/app/admin/config/[communityId]/assets/page.tsx - -'use client'; - -import { useState } from 'react'; -import { useDropzone } from 'react-dropzone'; - -export default function AssetUploadPage({ params }: { params: { communityId: string } }) { - const [uploading, setUploading] = useState(false); - - const onDrop = async (acceptedFiles: File[]) => { - const file = acceptedFiles[0]; - if (!file) return; - - setUploading(true); - - const formData = new FormData(); - formData.append('file', file); - formData.append('communityId', params.communityId); - formData.append('assetType', 'main'); // or from UI selection - - try { - const response = await fetch('/api/admin/assets/upload', { - method: 'POST', - body: formData, - headers: { - 'x-admin-identity': getAdminIdentity(), // Your auth - }, - }); - - const result = await response.json(); - - if (result.success) { - // Update config in DB - await updateConfigAsset(params.communityId, 'main', result.storagePath); - alert('Asset uploaded! Trigger rebuild to see changes.'); - } - } catch (error) { - console.error('Upload failed:', error); - } finally { - setUploading(false); - } - }; - - const { getRootProps, getInputProps } = useDropzone({ - onDrop, - accept: { - 'image/*': ['.svg', '.png', '.jpg', '.jpeg', '.webp'], - }, - }); - - return ( -
-

Upload Assets

-
- -

Drag & drop assets here, or click to select

-
- {uploading &&

Uploading...

} -
- ); -} -``` - -## Asset Path Structure - -### Storage Paths (in Supabase) -``` -community-assets/ - ├── nouns/ - │ ├── logo.svg - │ ├── noggles.svg - │ ├── favicon.ico - │ ├── apple-touch.png - │ ├── og.png - │ └── splash.png - ├── clanker/ - │ └── ... - └── example/ - └── ... -``` - -### Public Paths (after build) -``` -public/ - └── images/ - ├── nouns/ - │ ├── logo.svg - │ ├── noggles.svg - │ └── ... - ├── clanker/ - │ └── ... - └── example/ - └── ... -``` - -## Build Process Flow - -1. **Build starts** → `next.config.mjs` runs -2. **Fetch config** → Get config from database -3. **Download assets** → For each asset in config: - - Download from Supabase Storage - - Save to `public/images/{community}/{filename}` -4. **Update config** → Replace storage paths with public paths -5. **Set env var** → Store config with public paths -6. **Next.js build** → Uses config with public paths -7. **Assets available** → Served from `public/` folder - -## Benefits - -✅ **Admin uploads** - No repo access needed -✅ **Build-time availability** - Assets downloaded before build -✅ **Public paths** - Assets served from `public/` folder -✅ **Fallback safety** - Static assets if download fails -✅ **Version control** - Assets stored in Supabase Storage -✅ **CDN ready** - Can be served via CDN if needed - -## Considerations - -1. **Build time** - Asset downloads add ~1-2 seconds per community -2. **Storage costs** - Supabase Storage usage -3. **Cache invalidation** - May need to clear Next.js cache after asset updates -4. **File size limits** - Set reasonable limits for uploads -5. **File types** - Validate file types (SVG, PNG, etc.) - -## Alternative: CDN URLs - -If you want to skip downloads entirely, you could: - -1. Upload to Supabase Storage -2. Store public URLs directly in config -3. Use URLs directly (no download needed) - -```json -{ - "assets": { - "logos": { - "main": "https://{supabase-url}/storage/v1/object/public/community-assets/nouns/logo.svg" - } - } -} -``` - -**Pros:** Faster builds, no local storage -**Cons:** External dependency, no bundling, CDN costs - -## Recommended Approach - -**Download to public/ folder** because: -- ✅ Assets are part of the build -- ✅ No external dependencies at runtime -- ✅ Can be cached/CDN'd with the app -- ✅ Works offline -- ✅ Predictable paths - -This gives you the best balance of admin flexibility and build reliability. - diff --git a/docs/_archived/database-config/ASSETS_CONFIG_STORAGE_RELATIONSHIP.md b/docs/_archived/database-config/ASSETS_CONFIG_STORAGE_RELATIONSHIP.md deleted file mode 100644 index cee4e00b6..000000000 --- a/docs/_archived/database-config/ASSETS_CONFIG_STORAGE_RELATIONSHIP.md +++ /dev/null @@ -1,386 +0,0 @@ -# Assets Config and Storage Relationship - -## Overview - -Yes, `assets_config` is directly tied to assets uploaded to Supabase Storage. The paths stored in `assets_config` reference assets in the `community-assets` storage bucket, which are then downloaded at build time. - -## The Relationship - -### 1. Admin Uploads Asset - -When an admin uploads an asset via the admin UI: - -``` -Admin UI → Upload to Supabase Storage - → Store path in assets_config -``` - -**Example:** -- Admin uploads `logo.svg` for Nouns community -- Asset stored at: `community-assets/nouns/logo.svg` in Supabase Storage -- `assets_config` updated to: `"main": "community-assets/nouns/logo.svg"` - -### 2. Database Storage - -The `assets_config` JSONB column stores the **storage path**: - -```json -{ - "logos": { - "main": "community-assets/nouns/logo.svg", // ← Supabase Storage path - "icon": "community-assets/nouns/noggles.svg", // ← Supabase Storage path - "favicon": "community-assets/nouns/favicon.ico", - "appleTouch": "community-assets/nouns/apple-touch.png", - "og": "community-assets/nouns/og.png", - "splash": "community-assets/nouns/splash.png" - } -} -``` - -### 3. Build-Time Download - -During build (`next.config.mjs`), assets are downloaded: - -```javascript -// next.config.mjs - -async function downloadAssets(config, communityId) { - const assets = config.assets?.logos || {}; - - for (const [key, storagePath] of Object.entries(assets)) { - if (storagePath.startsWith('community-assets/')) { - // Download from Supabase Storage - const { data } = await supabase.storage - .from('community-assets') - .download(storagePath); - - // Save to public/images/{community}/ - const localPath = `/images/${communityId}/${filename}`; - await writeFile(`public${localPath}`, buffer); - - // Update config with public path - assets[key] = localPath; // ← Changed to public path - } - } - - return { ...config, assets: { logos: assets } }; -} -``` - -### 4. Runtime Usage - -After build, the app uses **public paths**: - -```typescript -// src/config/index.ts - -const config = loadSystemConfig(); -const logoSrc = config.assets.logos.main; // "/images/nouns/logo.svg" -``` - -## Complete Flow Diagram - -``` -┌─────────────────┐ -│ Admin Uploads │ -│ logo.svg │ -└────────┬────────┘ - │ - ▼ -┌─────────────────────────┐ -│ Supabase Storage │ -│ community-assets/ │ -│ └─ nouns/ │ -│ └─ logo.svg │ -└────────┬────────────────┘ - │ - ▼ -┌─────────────────────────┐ -│ Database │ -│ assets_config JSONB │ -│ "main": "community- │ -│ assets/nouns/ │ -│ logo.svg" │ -└────────┬────────────────┘ - │ - ▼ -┌─────────────────────────┐ -│ Build Time │ -│ next.config.mjs │ -│ 1. Read storage path │ -│ 2. Download from │ -│ Supabase Storage │ -│ 3. Save to public/ │ -│ 4. Update config to │ -│ public path │ -└────────┬────────────────┘ - │ - ▼ -┌─────────────────────────┐ -│ public/images/ │ -│ └─ nouns/ │ -│ └─ logo.svg │ -└────────┬────────────────┘ - │ - ▼ -┌─────────────────────────┐ -│ Runtime │ -│ App uses: │ -│ "/images/nouns/ │ -│ logo.svg" │ -└─────────────────────────┘ -``` - -## Database Schema - -### Storage Path Format (in database) - -```json -{ - "logos": { - "main": "community-assets/nouns/logo.svg" - } -} -``` - -**Format:** `community-assets/{communityId}/{filename}` - -### Public Path Format (after build) - -```json -{ - "logos": { - "main": "/images/nouns/logo.svg" - } -} -``` - -**Format:** `/images/{communityId}/{filename}` - -## Implementation Details - -### Admin Upload API - -```typescript -// src/app/api/admin/assets/upload/route.ts - -export async function POST(request: NextRequest) { - const file = formData.get('file') as File; - const communityId = formData.get('communityId') as string; - const assetType = formData.get('assetType') as string; // 'main', 'icon', etc. - - // Upload to Supabase Storage - const storagePath = `community-assets/${communityId}/${assetType}.${fileExt}`; - - await supabase.storage - .from('community-assets') - .upload(storagePath, file, { upsert: true }); - - // Update assets_config in database - await supabase - .from('community_configs') - .update({ - assets_config: jsonb_set( - assets_config, - `{logos,${assetType}}`, - `"${storagePath}"`::jsonb - ) - }) - .eq('community_id', communityId); - - return { success: true, storagePath }; -} -``` - -### Build-Time Download - -```javascript -// next.config.mjs - -async function downloadAssets(config, communityId) { - const supabase = createClient(supabaseUrl, supabaseKey); - const assets = config.assets?.logos || {}; - const downloadedAssets = {}; - - for (const [key, storagePath] of Object.entries(assets)) { - if (typeof storagePath === 'string' && storagePath.startsWith('community-assets/')) { - // Download from Supabase Storage - const { data } = await supabase.storage - .from('community-assets') - .download(storagePath); - - if (data) { - // Generate local path - const filename = storagePath.split('/').pop(); - const localPath = `/images/${communityId}/${filename}`; - - // Save to public folder - await writeFile(`public${localPath}`, Buffer.from(await data.arrayBuffer())); - - // Update to public path - downloadedAssets[key] = localPath; - } - } else { - // Already a public path or external URL, use as-is - downloadedAssets[key] = storagePath; - } - } - - // Return config with updated paths - return { - ...config, - assets: { - logos: downloadedAssets - } - }; -} -``` - -## Path Resolution Logic - -### In Database (Storage Paths) - -```json -{ - "logos": { - "main": "community-assets/nouns/logo.svg", // ← Storage path - "icon": "community-assets/nouns/noggles.svg", // ← Storage path - "favicon": "/images/favicon.ico" // ← Public path (static fallback) - } -} -``` - -### After Build (Public Paths) - -```json -{ - "logos": { - "main": "/images/nouns/logo.svg", // ← Downloaded from storage - "icon": "/images/nouns/noggles.svg", // ← Downloaded from storage - "favicon": "/images/favicon.ico" // ← Static (not in storage) - } -} -``` - -## Fallback Strategy - -### Static Assets (Not in Storage) - -Some assets might not be uploaded (e.g., default favicon): - -```json -{ - "logos": { - "main": "community-assets/nouns/logo.svg", // ← From storage - "favicon": "/images/favicon.ico" // ← Static fallback - } -} -``` - -**Build-time logic:** -- If path starts with `community-assets/` → Download from storage -- If path starts with `/` → Use as-is (public path) -- If path starts with `http://` or `https://` → Use as-is (external URL) - -## Storage Bucket Structure - -``` -Supabase Storage: community-assets/ -├── nouns/ -│ ├── logo.svg -│ ├── noggles.svg -│ ├── favicon.ico -│ ├── apple-touch.png -│ ├── og.png -│ └── splash.png -├── clanker/ -│ ├── logo.svg -│ └── ... -└── example/ - └── ... -``` - -## Public Folder Structure (After Build) - -``` -public/ -└── images/ - ├── nouns/ - │ ├── logo.svg ← Downloaded from storage - │ ├── noggles.svg ← Downloaded from storage - │ └── ... - ├── clanker/ - │ └── ... - └── favicon.ico ← Static (not downloaded) -``` - -## Key Points - -1. **`assets_config` stores storage paths** - References assets in Supabase Storage -2. **Build-time transformation** - Storage paths → Public paths -3. **Runtime uses public paths** - App serves from `public/images/` -4. **Fallback support** - Can mix storage paths and static paths -5. **Admin updates storage** - Changes reflected after rebuild - -## Example: Complete Flow - -### Step 1: Admin Uploads Logo - -```typescript -// Admin uploads logo.svg via UI -POST /api/admin/assets/upload -{ - file: File, - communityId: "nouns", - assetType: "main" -} - -// Asset stored at: community-assets/nouns/logo.svg -// Database updated: -{ - "assets_config": { - "logos": { - "main": "community-assets/nouns/logo.svg" // ← Storage path - } - } -} -``` - -### Step 2: Build Time - -```javascript -// next.config.mjs runs -1. Fetch config from DB -2. See "main": "community-assets/nouns/logo.svg" -3. Download from Supabase Storage -4. Save to public/images/nouns/logo.svg -5. Update config: "main": "/images/nouns/logo.svg" -6. Store in env var: NEXT_PUBLIC_BUILD_TIME_CONFIG -``` - -### Step 3: Runtime - -```typescript -// App loads config -const config = loadSystemConfig(); -const logoSrc = config.assets.logos.main; // "/images/nouns/logo.svg" - -// Next.js Image component uses public path - // Serves from public/images/nouns/logo.svg -``` - -## Summary - -**Yes, `assets_config` is directly tied to uploaded assets:** - -1. ✅ **Admin uploads** → Stored in Supabase Storage -2. ✅ **Path stored** → `assets_config` contains storage path -3. ✅ **Build downloads** → Assets copied to `public/images/` -4. ✅ **Path updated** → Config uses public path -5. ✅ **Runtime serves** → App uses public path - -The `assets_config` paths act as the **bridge** between: -- **Storage** (where admins upload) -- **Build** (where assets are downloaded) -- **Runtime** (where assets are served) - diff --git a/docs/_archived/database-config/ASSET_HANDLING_STRATEGY.md b/docs/_archived/database-config/ASSET_HANDLING_STRATEGY.md deleted file mode 100644 index a625c0521..000000000 --- a/docs/_archived/database-config/ASSET_HANDLING_STRATEGY.md +++ /dev/null @@ -1,337 +0,0 @@ -# Asset Handling Strategy for Database Configs - -## The Challenge - -Assets in the config can be: -1. **Imported modules** (bundled by Next.js) - `import logo from './assets/logo.svg'` -2. **String paths** (public paths) - `"/images/logo.png"` - -When using environment variables, we need to handle both cases. - -## Recommended Approach: Hybrid Strategy - -**Keep assets in repo, store paths in DB, resolve at build time** - -### Strategy Overview - -1. **Assets stay in version control** - `src/config/{community}/assets/` directory -2. **DB stores relative paths** - Just the paths, not the files -3. **Build-time resolution** - Convert DB paths to actual asset references -4. **Fallback to static** - If DB path doesn't exist, use static asset config - -### Implementation - -#### 1. Database Schema - -Store asset paths as strings in the database: - -```sql --- In community_configs table, assets_config JSONB column: -{ - "logos": { - "main": "/images/nouns/logo.svg", -- Public path - "icon": "./assets/noggles.svg", -- Relative to config dir - "favicon": "/images/favicon.ico", -- Public path - "appleTouch": "/images/apple-touch-icon.png", - "og": "./assets/og.svg", -- Relative path - "splash": "./assets/splash.svg" -- Relative path - } -} -``` - -#### 2. Build-Time Resolution - -```javascript -// next.config.mjs - -import { createClient } from '@supabase/supabase-js'; -import { existsSync } from 'fs'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -async function resolveAssets(config, communityId) { - const configDir = join(__dirname, 'src', 'config', communityId); - const publicDir = join(__dirname, 'public'); - - const resolvedAssets = { ...config.assets }; - - // Resolve each asset path - for (const [key, value] of Object.entries(config.assets.logos)) { - if (typeof value === 'string') { - // Check if it's a relative path (starts with ./) - if (value.startsWith('./')) { - const assetPath = join(configDir, value.replace('./', '')); - if (existsSync(assetPath)) { - // Keep relative path - Next.js will bundle it - resolvedAssets.logos[key] = value; - } else { - console.warn(`⚠️ Asset not found: ${assetPath}, using static fallback`); - resolvedAssets.logos[key] = null; // Will use static - } - } else if (value.startsWith('/')) { - // Public path - check if exists - const publicPath = join(publicDir, value); - if (existsSync(publicPath)) { - resolvedAssets.logos[key] = value; - } else { - console.warn(`⚠️ Public asset not found: ${publicPath}`); - resolvedAssets.logos[key] = null; - } - } else { - // Assume it's a valid path - resolvedAssets.logos[key] = value; - } - } - } - - return resolvedAssets; -} - -async function generateConfig() { - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; - - if (!supabaseUrl || !supabaseKey) return; - - const supabase = createClient(supabaseUrl, supabaseKey); - const community = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - - try { - const { data, error } = await supabase - .rpc('get_active_community_config', { p_community_id: community }) - .single(); - - if (error || !data) return; - - // Resolve asset paths - const resolvedAssets = await resolveAssets(data, community); - const configWithAssets = { - ...data, - assets: resolvedAssets - }; - - // Set as env var - process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(configWithAssets); - console.log('✅ Loaded config with resolved assets from database'); - } catch (error) { - console.warn('⚠️ Error loading config:', error.message); - } -} - -await generateConfig(); -``` - -#### 3. Config Loader with Asset Fallback - -```typescript -// src/config/index.ts - -import { SystemConfig } from './systemConfig'; -import { nounsSystemConfig } from './nouns/index'; -import { exampleSystemConfig } from './example/index'; -import { clankerSystemConfig } from './clanker/index'; - -// Import static asset configs for fallback -import { nounsAssets } from './nouns/nouns.assets'; -import { clankerAssets } from './clanker/clanker.assets'; -import { exampleAssets } from './example/example.assets'; - -const STATIC_CONFIGS: Record = { - nouns: nounsSystemConfig, - example: exampleSystemConfig, - clanker: clankerSystemConfig as unknown as SystemConfig, -}; - -const STATIC_ASSETS: Record = { - nouns: nounsAssets, - clanker: clankerAssets, - example: exampleAssets, -}; - -export const loadSystemConfig = (): SystemConfig => { - const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - const community = communityConfig.toLowerCase(); - - // Try build-time config from env var - const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; - if (buildTimeConfig) { - try { - const config = JSON.parse(buildTimeConfig) as SystemConfig; - - // Merge assets: use DB assets, fall back to static for missing ones - const staticAssets = STATIC_ASSETS[community]; - if (staticAssets) { - config.assets = { - logos: { - main: config.assets?.logos?.main || staticAssets.logos.main, - icon: config.assets?.logos?.icon || staticAssets.logos.icon, - favicon: config.assets?.logos?.favicon || staticAssets.logos.favicon, - appleTouch: config.assets?.logos?.appleTouch || staticAssets.logos.appleTouch, - og: config.assets?.logos?.og || staticAssets.logos.og, - splash: config.assets?.logos?.splash || staticAssets.logos.splash, - } - }; - } - - return config; - } catch { - // Fall through to static - } - } - - // Fall back to static configs - return STATIC_CONFIGS[community] || STATIC_CONFIGS.nouns; -}; -``` - ---- - -## Alternative Approach: Keep Assets Static - -**Simpler**: Only store non-asset config in DB, keep assets as static imports. - -### Implementation - -```typescript -// src/config/index.ts - -export const loadSystemConfig = (): SystemConfig => { - const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - const community = communityConfig.toLowerCase(); - - // Try build-time config from env var - const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; - - if (buildTimeConfig) { - try { - const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; - const staticConfig = STATIC_CONFIGS[community]; - - // Merge: DB config + static assets - return { - ...dbConfig, - assets: staticConfig.assets, // Always use static assets - }; - } catch { - // Fall through - } - } - - return STATIC_CONFIGS[community] || STATIC_CONFIGS.nouns; -}; -``` - -**Pros:** -- ✅ Simplest approach -- ✅ Assets always bundled by Next.js -- ✅ No path resolution needed -- ✅ Type-safe asset imports - -**Cons:** -- ⚠️ Assets can't be updated via admin UI -- ⚠️ Assets require code deployment - ---- - -## Recommended: Hybrid with Smart Fallback - -**Best of both worlds**: Store asset paths in DB, but fall back to static imports if paths don't resolve. - -### Final Implementation - -```typescript -// src/config/index.ts - -import { SystemConfig } from './systemConfig'; -import { nounsSystemConfig } from './nouns/index'; -// ... other imports - -const STATIC_CONFIGS: Record = { - nouns: nounsSystemConfig, - // ... -}; - -export const loadSystemConfig = (): SystemConfig => { - const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - const community = communityConfig.toLowerCase(); - - const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; - - if (buildTimeConfig) { - try { - const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; - const staticConfig = STATIC_CONFIGS[community]; - - // Smart asset merging: use DB assets if valid, otherwise static - const mergedAssets = { - logos: { - main: dbConfig.assets?.logos?.main || staticConfig.assets.logos.main, - icon: dbConfig.assets?.logos?.icon || staticConfig.assets.logos.icon, - favicon: dbConfig.assets?.logos?.favicon || staticConfig.assets.logos.favicon, - appleTouch: dbConfig.assets?.logos?.appleTouch || staticConfig.assets.logos.appleTouch, - og: dbConfig.assets?.logos?.og || staticConfig.assets.logos.og, - splash: dbConfig.assets?.logos?.splash || staticConfig.assets.logos.splash, - } - }; - - return { - ...dbConfig, - assets: mergedAssets, - }; - } catch { - // Fall through - } - } - - return STATIC_CONFIGS[community] || STATIC_CONFIGS.nouns; -}; -``` - -**Benefits:** -- ✅ Admin can update asset paths in DB -- ✅ Falls back to static assets if DB paths invalid -- ✅ Assets still bundled by Next.js (if relative paths) -- ✅ Public paths work for CDN-hosted assets -- ✅ Type-safe with fallbacks - ---- - -## Database Storage Format - -Store assets as simple string paths in the database: - -```json -{ - "assets": { - "logos": { - "main": "./assets/logo.svg", // Relative - will be bundled - "icon": "./assets/noggles.svg", // Relative - will be bundled - "favicon": "/images/favicon.ico", // Public path - "appleTouch": "/images/apple-touch.png", // Public path - "og": "./assets/og.svg", // Relative - will be bundled - "splash": "./assets/splash.svg" // Relative - will be bundled - } - } -} -``` - -**Path conventions:** -- `./assets/...` - Relative to `src/config/{community}/assets/` (bundled) -- `/images/...` - Public path in `public/images/` (not bundled) -- `https://...` - External URL (if needed) - ---- - -## Summary - -**Recommended**: Hybrid approach with smart fallback -- Store asset paths in DB -- Resolve paths at build time -- Fall back to static assets if DB paths invalid -- Supports both bundled (relative) and public (absolute) paths - -This gives admins flexibility while maintaining the benefits of Next.js asset bundling. - diff --git a/docs/_archived/database-config/ASSET_PERFORMANCE_OPTIMIZATION.md b/docs/_archived/database-config/ASSET_PERFORMANCE_OPTIMIZATION.md deleted file mode 100644 index 4fbb51319..000000000 --- a/docs/_archived/database-config/ASSET_PERFORMANCE_OPTIMIZATION.md +++ /dev/null @@ -1,329 +0,0 @@ -# Asset Performance Optimization Strategy - -## Current Performance Analysis - -### ✅ What's Already Good - -1. **Next.js Image Component** - You're using `` which provides: - - Automatic image optimization - - Lazy loading - - Responsive images - - WebP/AVIF format conversion (configured in `next.config.mjs`) - -2. **Static File Serving** - Assets in `public/` are: - - Served as static files (very fast) - - Can be cached by CDN - - No server processing needed - -3. **Build-Time Download** - Assets downloaded at build time: - - No runtime fetching - - Part of the static build - - Can be optimized during build - -### ⚠️ Potential Performance Issues - -1. **No Image Optimization** - Raw assets downloaded without optimization -2. **Large File Sizes** - Admin-uploaded assets might be unoptimized -3. **No Format Conversion** - Not leveraging WebP/AVIF automatically -4. **Build Time Impact** - Large assets slow down builds - -## Optimized Solution - -### Enhanced Build-Time Asset Processing - -Add image optimization during the build-time download process: - -```javascript -// next.config.mjs - -import { createClient } from '@supabase/supabase-js'; -import { writeFile, mkdir } from 'fs/promises'; -import { join, dirname, extname } from 'path'; -import { fileURLToPath } from 'url'; -import sharp from 'sharp'; // Already in your dependencies! - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -/** - * Optimize image using Sharp - */ -async function optimizeImage(buffer, format, maxWidth = 2000) { - let image = sharp(buffer); - - // Get metadata - const metadata = await image.metadata(); - - // Resize if too large - if (metadata.width > maxWidth) { - image = image.resize(maxWidth, null, { - withoutEnlargement: true, - fit: 'inside' - }); - } - - // Convert format based on original - switch (format.toLowerCase()) { - case '.png': - case '.jpg': - case '.jpeg': - // Convert to WebP for better compression - return await image.webp({ quality: 85 }).toBuffer(); - case '.svg': - // SVGs are already optimized, but we can validate - return buffer; - default: - return buffer; - } -} - -/** - * Download and optimize asset - */ -async function downloadAndOptimizeAsset(supabase, storagePath, localPath, assetType) { - try { - // Download from Supabase Storage - const { data, error } = await supabase.storage - .from('community-assets') - .download(storagePath); - - if (error || !data) { - console.warn(`⚠️ Failed to download ${storagePath}:`, error?.message); - return false; - } - - const buffer = Buffer.from(await data.arrayBuffer()); - const ext = extname(storagePath); - - // Optimize based on asset type - let optimizedBuffer = buffer; - let optimizedExt = ext; - - // For logos/icons, optimize aggressively - if (['main', 'icon', 'og', 'splash'].includes(assetType)) { - if (['.png', '.jpg', '.jpeg'].includes(ext)) { - optimizedBuffer = await optimizeImage(buffer, ext, 2000); - optimizedExt = '.webp'; // Convert to WebP - localPath = localPath.replace(ext, '.webp'); - } - } - - // For favicon/appleTouch, keep original format but optimize size - if (['favicon', 'appleTouch'].includes(assetType)) { - if (['.png', '.jpg', '.jpeg'].includes(ext)) { - optimizedBuffer = await optimizeImage(buffer, ext, 512); - } - } - - // Ensure directory exists - const fullPath = join(__dirname, 'public', localPath); - const dir = dirname(fullPath); - await mkdir(dir, { recursive: true }); - - // Write optimized asset - await writeFile(fullPath, optimizedBuffer); - - console.log(`✅ Downloaded & optimized ${storagePath} → ${localPath}`); - return { success: true, path: localPath }; - } catch (error) { - console.error(`❌ Error processing ${storagePath}:`, error.message); - return false; - } -} - -async function downloadAssets(config, communityId) { - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; - - if (!supabaseUrl || !supabaseKey) { - console.warn('⚠️ Missing Supabase credentials'); - return config; - } - - const supabase = createClient(supabaseUrl, supabaseKey); - const assets = config.assets?.logos || {}; - const downloadedAssets = {}; - - // Download and optimize each asset - for (const [key, storagePath] of Object.entries(assets)) { - if (typeof storagePath === 'string' && storagePath.startsWith('community-assets/')) { - const localPath = `/images/${communityId}/${storagePath.split('/').pop()}`; - const result = await downloadAndOptimizeAsset(supabase, storagePath, localPath, key); - - if (result && result.success) { - downloadedAssets[key] = result.path; - } else { - downloadedAssets[key] = null; // Will use static fallback - } - } else if (typeof storagePath === 'string') { - downloadedAssets[key] = storagePath; - } - } - - return { - ...config, - assets: { - logos: downloadedAssets - } - }; -} -``` - -## Performance Optimizations - -### 1. Image Optimization During Build - -**Benefits:** -- ✅ **Smaller file sizes** - WebP/AVIF conversion reduces size by 25-50% -- ✅ **Faster loads** - Optimized images load faster -- ✅ **Better UX** - Faster page loads, better Core Web Vitals - -**Implementation:** -- Use Sharp (already in dependencies) to optimize images -- Convert PNG/JPG to WebP for better compression -- Resize large images to reasonable dimensions -- Keep SVGs as-is (already optimized) - -### 2. Next.js Image Component Optimization - -You're already using `` which provides: -- ✅ **Automatic optimization** - Next.js optimizes on-demand -- ✅ **Lazy loading** - Images load when needed -- ✅ **Responsive images** - Serves appropriate sizes -- ✅ **Format conversion** - WebP/AVIF when supported - -**Ensure you're using it correctly:** - -```typescript -// ✅ Good - Uses Next.js Image optimization -{`${brand.displayName} - -// ❌ Bad - Regular img tag (no optimization) -Logo -``` - -### 3. CDN Integration - -**For even better performance**, serve assets via CDN: - -```javascript -// Option 1: Use Supabase Storage CDN URLs directly -const publicUrl = supabase.storage - .from('community-assets') - .getPublicUrl(storagePath); - -// Option 2: Use Vercel CDN (if deploying on Vercel) -// Assets in public/ are automatically CDN'd - -// Option 3: Custom CDN (Cloudflare, etc.) -const cdnUrl = `https://cdn.yoursite.com${localPath}`; -``` - -### 4. Caching Strategy - -**Add cache headers** for assets: - -```javascript -// next.config.mjs - -async headers() { - return [ - { - source: '/images/:path*', - headers: [ - { - key: 'Cache-Control', - value: 'public, max-age=31536000, immutable', // 1 year - }, - ], - }, - // ... other headers - ]; -} -``` - -### 5. Lazy Loading for Non-Critical Assets - -**For below-the-fold assets:** - -```typescript -// Use Next.js Image with lazy loading -Splash -``` - -## Performance Comparison - -### Without Optimization -- **Raw PNG logo**: 500KB -- **Load time**: ~2s on 3G -- **Build time**: +5s per large asset - -### With Optimization -- **Optimized WebP logo**: 50KB (90% smaller!) -- **Load time**: ~200ms on 3G -- **Build time**: +2s (optimization overhead) - -## Recommended Approach - -### Hybrid: Optimize + CDN - -1. **Build-time optimization** - Optimize images during download -2. **Public folder** - Store optimized assets in `public/` -3. **CDN serving** - Let Vercel/CDN serve from `public/` -4. **Next.js Image** - Use `` component for additional optimization - -**Why this works:** -- ✅ **Build-time optimization** - Reduces file sizes permanently -- ✅ **CDN caching** - Fast global delivery -- ✅ **Next.js optimization** - Additional on-demand optimization -- ✅ **Best of both worlds** - Pre-optimized + runtime optimization - -## Implementation Priority - -### Phase 1: Basic Optimization (MVP) -- Download assets to `public/` -- Use Next.js `` component -- Add cache headers - -### Phase 2: Build-Time Optimization -- Add Sharp optimization during download -- Convert to WebP for better compression -- Resize large images - -### Phase 3: Advanced Optimization -- Generate multiple sizes (srcset) -- Generate AVIF versions -- Implement CDN integration - -## Performance Metrics to Monitor - -1. **Build time** - Should be < 30s total -2. **Asset sizes** - Logos should be < 100KB -3. **Load times** - First Contentful Paint < 1.8s -4. **Core Web Vitals** - LCP < 2.5s - -## Summary - -**Yes, assets will be performant** with proper optimization: - -✅ **Build-time optimization** - Optimize during download -✅ **Next.js Image component** - Automatic runtime optimization -✅ **CDN serving** - Fast global delivery -✅ **Caching** - Long-term browser/CDN caching -✅ **Format conversion** - WebP/AVIF for smaller sizes - -The key is adding Sharp optimization during the build-time download process. This ensures admin-uploaded assets are optimized before being served, maintaining performance even if admins upload large, unoptimized files. - diff --git a/docs/_archived/database-config/BUILD_TIME_CONFIG_SUMMARY.md b/docs/_archived/database-config/BUILD_TIME_CONFIG_SUMMARY.md deleted file mode 100644 index e5c834aea..000000000 --- a/docs/_archived/database-config/BUILD_TIME_CONFIG_SUMMARY.md +++ /dev/null @@ -1,152 +0,0 @@ -# Build-Time Configuration System - Quick Summary - -## Overview - -Configurations are stored in the database for admin updates, but fetched at **build time** and generated as static TypeScript files. Runtime has zero database queries. - -## Architecture Flow - -``` -┌─────────────────┐ -│ Admin UI │ -│ (Updates DB) │ -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ Database │ -│ (Stores Config)│ -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ ┌──────────────────┐ -│ Build Process │─────▶│ Generate TS Files │ -│ (prebuild hook) │ │ from DB Configs │ -└─────────────────┘ └─────────┬──────────┘ - │ - ▼ - ┌──────────────────┐ - │ Static TS Files │ - │ (in src/config/) │ - └─────────┬──────────┘ - │ - ▼ - ┌──────────────────┐ - │ Runtime App │ - │ (Uses Static Files│ - │ Zero DB Queries) │ - └──────────────────┘ -``` - -## Key Components - -### 1. Database Tables -- `community_configs` - Active configs -- `community_config_history` - Version history -- `community_config_admins` - Admin permissions - -### 2. Build Script -- `scripts/generate-configs.ts` - Fetches from DB, generates TS files -- Runs before every build via `prebuild` hook -- Falls back to static configs if DB unavailable - -### 3. Generated Files -- `src/config/{community}/{community}.{section}.ts` - Generated from DB -- `src/config/{community}/index.ts` - Main export -- Structure matches existing static configs exactly - -### 4. Runtime -- Uses generated static files (same as before) -- Zero database queries -- Fallback to static configs if generation failed - -## Workflow - -### Admin Updates Config -1. Admin edits config in admin UI -2. Changes saved to database -3. Admin clicks "Trigger Rebuild" -4. CI/CD rebuilds application -5. Build process fetches latest configs from DB -6. Generates static TypeScript files -7. Next.js build uses generated files -8. Deploy with new configs - -### Build Process -```bash -npm run build - ↓ -prebuild hook runs - ↓ -scripts/generate-configs.ts - ↓ -Fetches configs from Supabase - ↓ -Generates src/config/{community}/*.ts files - ↓ -next build (uses generated files) -``` - -## Benefits - -✅ **Zero Runtime Overhead** - No database queries in production -✅ **Admin Updates** - Changes made through database UI -✅ **Type Safety** - Generated TypeScript files maintain type safety -✅ **Fast Runtime** - Static file imports are instant -✅ **Safe Fallback** - Static configs always available -✅ **Version History** - Full audit trail in database -✅ **Easy Rollback** - Remove prebuild hook to use static configs - -## Migration Steps - -1. **Create database schema** (migration files) -2. **Seed existing configs** to database -3. **Create generator script** (`scripts/generate-configs.ts`) -4. **Add prebuild hook** to package.json -5. **Test build process** with generated configs -6. **Create admin interface** for updates -7. **Set up rebuild trigger** (CI/CD integration) -8. **Roll out gradually** (one community at a time) - -## Environment Variables - -**Required for Build:** -```bash -NEXT_PUBLIC_SUPABASE_URL=... -NEXT_PUBLIC_SUPABASE_ANON_KEY=... # Or SUPABASE_SERVICE_ROLE_KEY -NEXT_PUBLIC_COMMUNITY=nouns # Which community to build -``` - -**Optional:** -```bash -GENERATE_CONFIGS=nouns,clanker,example # Which configs to generate -``` - -## File Structure - -``` -src/config/ -├── {community}/ # Generated from DB at build time -│ ├── {community}.brand.ts -│ ├── {community}.assets.ts -│ ├── {community}.theme.ts -│ ├── ... -│ └── index.ts -├── index.ts # Main loader (uses generated files) -└── systemConfig.ts # Type definitions - -scripts/ -└── generate-configs.ts # Build-time generator -``` - -## Rollback - -If issues arise: -1. Remove `prebuild` hook from package.json -2. Build uses static configs directly -3. No runtime impact - -## Next Steps - -See `DATABASE_CONFIG_MIGRATION_PLAN.md` for complete implementation details. - diff --git a/docs/_archived/database-config/CONFIG_DATABASE_SCHEMA_DETAILED.md b/docs/_archived/database-config/CONFIG_DATABASE_SCHEMA_DETAILED.md deleted file mode 100644 index b9b14ce1e..000000000 --- a/docs/_archived/database-config/CONFIG_DATABASE_SCHEMA_DETAILED.md +++ /dev/null @@ -1,1027 +0,0 @@ -# Detailed Database Config Schema Breakdown - -## Overview - -This document breaks down exactly what information is stored in each JSONB column of the `community_configs` table. Each column corresponds to a section of the `SystemConfig` interface. - -## Database Table Structure - -```sql -CREATE TABLE community_configs ( - id UUID PRIMARY KEY, - community_id VARCHAR(50) UNIQUE, - brand_config JSONB, -- Brand identity - assets_config JSONB, -- Visual assets - theme_config JSONB, -- Theme definitions - community_config JSONB, -- Community integration data - fidgets_config JSONB, -- Fidget enable/disable - home_page_config JSONB, -- Home page structure - explore_page_config JSONB, -- Explore page structure - navigation_config JSONB, -- Navigation items (optional) - ui_config JSONB -- UI colors (optional) -); -``` - ---- - -## 1. `brand_config` (BrandConfig) - -**Purpose:** Brand identity and metadata for the community - -**Structure:** -```json -{ - "name": "Nouns", // Internal identifier (lowercase, no spaces) - "displayName": "Nouns", // Display name shown to users - "tagline": "A space for Nouns", // Short tagline/slogan - "description": "The social hub for Nouns", // Full description (used in meta tags, OG tags) - "miniAppTags": [ // Tags for Farcaster Mini App discovery - "nouns", - "client", - "customizable", - "social", - "link" - ] -} -``` - -**Fields Explained:** -- **`name`**: Internal identifier, used in code/logs (e.g., "nouns", "clanker") -- **`displayName`**: User-facing name shown in UI, titles, headers -- **`tagline`**: Short marketing tagline (1-2 sentences) -- **`description`**: Full description used in: - - HTML `` - - Open Graph tags - - Social media previews - - SEO -- **`miniAppTags`**: Array of strings used for: - - Farcaster Mini App catalog discovery - - Search/filtering in Mini App stores - - Categorization - -**Example:** -```json -{ - "name": "nouns", - "displayName": "Nouns", - "tagline": "A space for Nouns", - "description": "The social hub for Nouns", - "miniAppTags": ["nouns", "client", "customizable", "social", "link"] -} -``` - ---- - -## 2. `assets_config` (AssetConfig) - -**Purpose:** Paths/URLs to visual assets (logos, icons, images) - -**Structure:** -```json -{ - "logos": { - "main": "/images/nouns/logo.svg", // Main logo (large, used in headers) - "icon": "/images/nouns/noggles.svg", // Icon (small, used in nav/favicon) - "favicon": "/images/favicon.ico", // Browser favicon - "appleTouch": "/images/apple-touch-icon.png", // iOS home screen icon - "og": "/images/nouns/og.png", // Open Graph image (social sharing) - "splash": "/images/nouns/splash.png" // Splash screen (Farcaster frames) - } -} -``` - -**Fields Explained:** -- **`main`**: Primary logo, typically: - - SVG or PNG - - Used in main header/navigation - - Larger size (200-400px width) -- **`icon`**: Small icon/logo variant: - - Used in navigation bars - - Mobile headers - - Sometimes same as main, sometimes different (e.g., "noggles" for Nouns) -- **`favicon`**: Browser tab icon: - - `.ico` format - - Multiple sizes (16x16, 32x32) -- **`appleTouch`**: iOS home screen icon: - - PNG format - - 180x180px recommended -- **`og`**: Open Graph image for social sharing: - - PNG/JPG format - - 1200x630px recommended - - Used when sharing links on Twitter, Discord, etc. -- **`splash`**: Splash screen image: - - Used in Farcaster Frame launch screens - - PNG/JPG format - - Full-screen background - -**Path Formats:** - -The `assets_config` paths can be in different formats depending on the stage: - -1. **Storage Paths (in database)**: `"community-assets/nouns/logo.svg"` - - References assets uploaded to Supabase Storage - - Used when admin uploads assets via UI - - Stored directly in `assets_config` JSONB column - -2. **Public Paths (after build)**: `"/images/nouns/logo.svg"` - - References assets in `public/images/` folder - - Generated at build time when assets are downloaded - - Used at runtime by the application - -3. **CDN URLs (optional)**: `"https://cdn.example.com/logo.svg"` - - External CDN URLs (if not using build-time download) - - Can be used directly without download - -**Flow:** -``` -Admin Uploads → Supabase Storage → assets_config stores storage path - ↓ - Build Time: - Download from Storage - → Save to public/images/ - → Update assets_config to public path - ↓ - Runtime: - App uses public path -``` - -**Example:** -```json -{ - "logos": { - "main": "/images/nouns/logo.svg", - "icon": "/images/nouns/noggles.svg", - "favicon": "/images/favicon.ico", - "appleTouch": "/images/apple-touch-icon.png", - "og": "/images/nouns/og.png", - "splash": "/images/nouns/splash.png" - } -} -``` - ---- - -## 3. `theme_config` (ThemeConfig) - -**Purpose:** Visual theme definitions (colors, fonts, backgrounds, styling) - -**Structure:** -```json -{ - "default": { - "id": "default", - "name": "Default", - "properties": { - "font": "Inter", // Primary font family - "fontColor": "#000000", // Primary text color - "headingsFont": "Inter", // Font for headings (h1, h2, etc.) - "headingsFontColor": "#000000", // Heading text color - "background": "#ffffff", // Page background color - "backgroundHTML": "", // HTML/CSS for animated backgrounds (or empty string) - "musicURL": "https://...", // YouTube URL for background music - "fidgetBackground": "#ffffff", // Background color for fidget containers - "fidgetBorderWidth": "1px", // Border width for fidgets - "fidgetBorderColor": "#C0C0C0", // Border color for fidgets - "fidgetShadow": "none", // CSS shadow for fidgets - "fidgetBorderRadius": "12px", // Border radius for fidgets - "gridSpacing": "16" // Grid spacing in pixels - } - }, - "nounish": { /* ... */ }, - "gradientAndWave": { /* ... */ }, - // ... other theme variants -} -``` - -**Fields Explained:** -- **`id`**: Unique identifier for the theme (used in code) -- **`name`**: Display name shown in theme picker -- **`properties.font`**: Primary font family (Google Fonts name or system font) -- **`properties.fontColor`**: Hex color for body text -- **`properties.headingsFont`**: Font family for headings (can differ from body) -- **`properties.headingsFontColor`**: Hex color for headings -- **`properties.background`**: Page background color (hex or rgba) -- **`properties.backgroundHTML`**: - - Empty string `""` for solid color backgrounds - - HTML string for animated backgrounds (e.g., "nounish", "gradientAndWave") - - Contains full HTML/CSS for complex animated backgrounds -- **`properties.musicURL`**: YouTube URL for background music (optional) -- **`properties.fidgetBackground`**: Background color for fidget containers -- **`properties.fidgetBorderWidth`**: CSS border width (e.g., "1px", "2px", "0") -- **`properties.fidgetBorderColor`**: Hex color for fidget borders -- **`properties.fidgetShadow`**: CSS box-shadow value (e.g., "none", "0 5px 15px rgba(0,0,0,0.55)") -- **`properties.fidgetBorderRadius`**: CSS border-radius (e.g., "12px", "0px") -- **`properties.gridSpacing`**: Spacing between grid items in pixels (as string) - -**Theme Variants:** -Each community can define multiple theme variants: -- `default`: Basic theme -- `nounish`: Community-specific theme -- `gradientAndWave`: Animated gradient theme -- `colorBlobs`: Animated color blobs -- `floatingShapes`: Floating shapes animation -- `imageParallax`: Parallax image background -- `shootingStar`: Shooting star animation -- `squareGrid`: Grid pattern background -- `tesseractPattern`: 3D tesseract pattern -- `retro`: Retro/vintage theme - -**Example:** -```json -{ - "default": { - "id": "default", - "name": "Default", - "properties": { - "font": "Inter", - "fontColor": "#000000", - "headingsFont": "Inter", - "headingsFontColor": "#000000", - "background": "#ffffff", - "backgroundHTML": "", - "musicURL": "https://www.youtube.com/watch?v=dMXlZ4y7OK4&t=1804", - "fidgetBackground": "#ffffff", - "fidgetBorderWidth": "1px", - "fidgetBorderColor": "#C0C0C0", - "fidgetShadow": "none", - "fidgetBorderRadius": "12px", - "gridSpacing": "16" - } - }, - "nounish": { - "id": "nounish", - "name": "Nounish", - "properties": { - "font": "Londrina Solid", - "fontColor": "#333333", - "headingsFont": "Work Sans", - "headingsFontColor": "#000000", - "background": "#ffffff", - "backgroundHTML": "nounish", - "musicURL": "https://www.youtube.com/watch?v=dMXlZ4y7OK4&t=1804", - "fidgetBackground": "#FFFAFA", - "fidgetBorderWidth": "2px", - "fidgetBorderColor": "#F05252", - "fidgetShadow": "0 5px 15px rgba(0,0,0,0.55)", - "fidgetBorderRadius": "12px", - "gridSpacing": "16" - } - } -} -``` - ---- - -## 4. `community_config` (CommunityConfig) - -**Purpose:** Community integration data (URLs, contracts, tokens, governance) - -**Structure:** -```json -{ - "type": "nouns", // Community type identifier - "urls": { - "website": "https://nouns.com", - "discord": "https://discord.gg/nouns", - "twitter": "https://twitter.com/nounsdao", - "github": "https://github.com/nounsDAO", - "forum": "https://discourse.nouns.wtf" - }, - "social": { - "farcaster": "nouns", // Farcaster username/handle - "discord": "nouns", // Discord server identifier - "twitter": "nounsdao" // Twitter handle (without @) - }, - "governance": { - "proposals": "https://nouns.wtf/vote", - "delegates": "https://nouns.wtf/delegates", - "treasury": "https://nouns.wtf/treasury" - }, - "tokens": { - "erc20Tokens": [ - { - "address": "0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab", - "symbol": "$SPACE", - "decimals": 18, - "network": "base" - } - ], - "nftTokens": [ - { - "address": "0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03", - "symbol": "Nouns", - "type": "erc721", - "network": "eth" - } - ] - }, - "contracts": { - "nouns": "0x9c8ff314c9bc7f6e59a9d9225fb22946427edc03", - "auctionHouse": "0x830bd73e4184cef73443c15111a1df14e495c706", - "space": "0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab", - "nogs": "0xD094D5D45c06c1581f5f429462eE7cCe72215616" - } -} -``` - -**Fields Explained:** - -### `type` -- Community type identifier (e.g., "nouns", "clanker", "example") -- Used internally for routing/logic - -### `urls` -- **`website`**: Main community website -- **`discord`**: Discord invite link -- **`twitter`**: Twitter profile URL -- **`github`**: GitHub organization URL -- **`forum`**: Forum/Discourse URL - -### `social` -- **`farcaster`**: Farcaster username/handle (without @) -- **`discord`**: Discord server identifier -- **`twitter`**: Twitter handle (without @) - -### `governance` -- **`proposals`**: URL to proposals/voting page -- **`delegates`**: URL to delegates page -- **`treasury`**: URL to treasury page - -### `tokens` -- **`erc20Tokens`**: Array of ERC20 token definitions - - **`address`**: Contract address (checksummed) - - **`symbol`**: Token symbol (e.g., "$SPACE") - - **`decimals`**: Token decimals (usually 18) - - **`network`**: Network identifier ("mainnet", "base", "polygon", "eth") -- **`nftTokens`**: Array of NFT token definitions - - **`address`**: Contract address - - **`symbol`**: Token symbol (e.g., "Nouns") - - **`type`**: Token type ("erc721", "erc1155") - - **`network`**: Network identifier - -### `contracts` -- Key-value pairs of contract names to addresses -- Common contracts: - - **`nouns`**: Main NFT contract - - **`auctionHouse`**: Auction house contract - - **`space`**: Token contract - - **`nogs`**: Additional contract -- Can include any custom contracts as key-value pairs - -**Example:** -```json -{ - "type": "nouns", - "urls": { - "website": "https://nouns.com", - "discord": "https://discord.gg/nouns", - "twitter": "https://twitter.com/nounsdao", - "github": "https://github.com/nounsDAO", - "forum": "https://discourse.nouns.wtf" - }, - "social": { - "farcaster": "nouns", - "discord": "nouns", - "twitter": "nounsdao" - }, - "governance": { - "proposals": "https://nouns.wtf/vote", - "delegates": "https://nouns.wtf/delegates", - "treasury": "https://nouns.wtf/treasury" - }, - "tokens": { - "erc20Tokens": [ - { - "address": "0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab", - "symbol": "$SPACE", - "decimals": 18, - "network": "base" - } - ], - "nftTokens": [ - { - "address": "0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03", - "symbol": "Nouns", - "type": "erc721", - "network": "eth" - } - ] - }, - "contracts": { - "nouns": "0x9c8ff314c9bc7f6e59a9d9225fb22946427edc03", - "auctionHouse": "0x830bd73e4184cef73443c15111a1df14e495c706", - "space": "0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab", - "nogs": "0xD094D5D45c06c1581f5f429462eE7cCe72215616" - } -} -``` - ---- - -## 5. `fidgets_config` (FidgetConfig) - -**Purpose:** Control which fidgets are enabled/disabled for the community - -**Structure:** -```json -{ - "enabled": [ - "nounsHome", - "governance", - "feed", - "cast", - "gallery", - "text", - "iframe", - "links", - "video", - "channel", - "profile", - "snapshot", - "swap", - "rss", - "market", - "portfolio", - "chat", - "builderScore", - "framesV2" - ], - "disabled": [ - "example" - ] -} -``` - -**Fields Explained:** -- **`enabled`**: Array of fidget type IDs that are available for this community - - Controls which fidgets appear in the fidget picker - - Controls which fidgets can be added to spaces - - Empty array means no fidgets enabled -- **`disabled`**: Array of fidget type IDs that are explicitly disabled - - Overrides enabled list - - Useful for temporarily disabling specific fidgets - - Can be empty array `[]` - -**Fidget Types (examples):** -- `nounsHome`: Community-specific home fidget -- `governance`: Governance proposals/ voting -- `feed`: Social feed (Farcaster casts) -- `cast`: Single cast display -- `gallery`: Image gallery -- `text`: Text content -- `iframe`: Embedded iframe -- `links`: Link collection -- `video`: Video player -- `channel`: Channel feed -- `profile`: User profile -- `snapshot`: Snapshot governance -- `swap`: Token swap widget -- `rss`: RSS feed -- `market`: NFT marketplace -- `portfolio`: Token portfolio -- `chat`: Chat widget -- `builderScore`: Builder score display -- `framesV2`: Farcaster frames - -**Example:** -```json -{ - "enabled": [ - "feed", - "cast", - "gallery", - "text", - "iframe", - "links", - "video" - ], - "disabled": [] -} -``` - ---- - -## 6. `home_page_config` (HomePageConfig) - -**Purpose:** Defines the structure, layout, and content of the home page - -**Structure:** -```json -{ - "defaultTab": "Nouns", // Tab shown by default - "tabOrder": [ // Order tabs appear in UI - "Nouns", - "Social", - "Governance", - "Resources", - "Funded Works", - "Places" - ], - "tabs": { - "Nouns": { - "name": "Nouns", // Internal tab identifier - "displayName": "Nouns", // Display name shown to users - "layoutID": "88b78f73-37fb-4921-9410-bc298311c0bb", // Unique layout ID - "layoutDetails": { - "layoutConfig": { - "layout": [ // Grid layout items - { - "w": 12, // Width in grid units - "h": 10, // Height in grid units - "x": 0, // X position - "y": 0, // Y position - "i": "nounsHome:3a8d7f19-...", // Fidget instance ID - "minW": 2, // Minimum width - "maxW": 36, // Maximum width - "minH": 2, // Minimum height - "maxH": 36, // Maximum height - "moved": false, // Has been moved - "static": false, // Is static (can't move) - "resizeHandles": ["s", "w", "e", "n", "sw", "nw", "se", "ne"], - "isBounded": false - } - ] - }, - "layoutFidget": "grid" // Layout type ("grid" or "mobileStack") - }, - "theme": { /* ThemeProperties */ }, // Theme for this tab - "fidgetInstanceDatums": { // Fidget instances on this tab - "nounsHome:3a8d7f19-...": { - "config": { - "data": {}, // Fidget-specific data - "editable": true, // Can user edit? - "settings": { // Fidget settings - "background": "var(--user-theme-fidget-background)", - "fidgetBorderColor": "var(--user-theme-fidget-border-color)", - "showOnMobile": true, - "isScrollable": true - } - }, - "fidgetType": "nounsHome", // Type of fidget - "id": "nounsHome:3a8d7f19-..." // Unique fidget instance ID - } - }, - "fidgetTrayContents": [], // Available fidgets in tray - "isEditable": false, // Can user edit this tab? - "timestamp": "2025-06-20T05:58:44.080Z" // Last modified timestamp - }, - "Social": { /* ... */ }, - "Governance": { /* ... */ } - // ... other tabs - }, - "layout": { - "defaultLayoutFidget": "grid", // Default layout type - "gridSpacing": 16, // Grid spacing in pixels - "theme": { // Default theme - "background": "#ffffff", - "fidgetBackground": "#ffffff", - "font": "Inter", - "fontColor": "#000000" - } - } -} -``` - -**Fields Explained:** - -### Top Level -- **`defaultTab`**: Tab ID shown by default when visiting `/home` -- **`tabOrder`**: Array of tab IDs in display order -- **`tabs`**: Object mapping tab IDs to tab configurations -- **`layout`**: Default layout settings - -### Tab Configuration (`tabs[tabId]`) -- **`name`**: Internal identifier (usually same as key) -- **`displayName`**: User-facing name -- **`layoutID`**: Unique UUID for this layout -- **`layoutDetails`**: Grid layout configuration - - **`layoutConfig.layout`**: Array of grid items (fidget positions) - - **`layoutFidget`**: Layout type ("grid" or "mobileStack") -- **`theme`**: ThemeProperties for this tab -- **`fidgetInstanceDatums`**: Object mapping fidget instance IDs to fidget data - - Each fidget instance has: - - **`config.data`**: Fidget-specific data - - **`config.editable`**: Can user edit this fidget? - - **`config.settings`**: Fidget settings (styling, behavior) - - **`fidgetType`**: Type of fidget (e.g., "feed", "nounsHome") - - **`id`**: Unique instance ID -- **`fidgetTrayContents`**: Array of fidgets available in the tray (usually empty for home page) -- **`isEditable`**: Can user edit this tab? (usually `false` for home page) -- **`timestamp`**: ISO timestamp of last modification - -### Layout Configuration -- **`defaultLayoutFidget`**: Default layout type ("grid" or "mobileStack") -- **`gridSpacing`**: Spacing between grid items in pixels -- **`theme`**: Default theme properties - -**Example:** -```json -{ - "defaultTab": "Nouns", - "tabOrder": ["Nouns", "Social", "Governance"], - "tabs": { - "Nouns": { - "name": "Nouns", - "displayName": "Nouns", - "layoutID": "88b78f73-37fb-4921-9410-bc298311c0bb", - "layoutDetails": { - "layoutConfig": { - "layout": [ - { - "w": 12, - "h": 10, - "x": 0, - "y": 0, - "i": "nounsHome:3a8d7f19-3e77-4c2b-9c7f-1a6f5f5a6f01", - "minW": 2, - "maxW": 36, - "minH": 2, - "maxH": 36, - "moved": false, - "static": false, - "resizeHandles": ["s", "w", "e", "n", "sw", "nw", "se", "ne"], - "isBounded": false - } - ] - }, - "layoutFidget": "grid" - }, - "theme": { - "id": "Homebase-Tab 4 - 1-Theme", - "name": "Homebase-Tab 4 - 1-Theme", - "properties": { - "background": "#ffffff", - "backgroundHTML": "", - "fidgetBackground": "#ffffff", - "fidgetBorderColor": "#eeeeee", - "fidgetBorderRadius": "0px", - "fidgetBorderWidth": "0px", - "fidgetShadow": "none", - "font": "Inter", - "fontColor": "#000000", - "gridSpacing": "0", - "headingsFont": "Inter", - "headingsFontColor": "#000000", - "musicURL": "https://www.youtube.com/watch?v=dMXlZ4y7OK4&t=1804" - } - }, - "fidgetInstanceDatums": { - "nounsHome:3a8d7f19-3e77-4c2b-9c7f-1a6f5f5a6f01": { - "config": { - "data": {}, - "editable": true, - "settings": { - "background": "var(--user-theme-fidget-background)", - "fidgetBorderColor": "var(--user-theme-fidget-border-color)", - "showOnMobile": true, - "isScrollable": true - } - }, - "fidgetType": "nounsHome", - "id": "nounsHome:3a8d7f19-3e77-4c2b-9c7f-1a6f5f5a6f01" - } - }, - "fidgetTrayContents": [], - "isEditable": false, - "timestamp": "2025-06-20T05:58:44.080Z" - } - }, - "layout": { - "defaultLayoutFidget": "grid", - "gridSpacing": 16, - "theme": { - "background": "#ffffff", - "fidgetBackground": "#ffffff", - "font": "Inter", - "fontColor": "#000000" - } - } -} -``` - ---- - -## 7. `explore_page_config` (ExplorePageConfig) - -**Purpose:** Defines the structure, layout, and content of the explore/discovery page - -**Structure:** Same as `home_page_config` (HomePageConfig) - -```json -{ - "defaultTab": "Explore", - "tabOrder": ["Explore", "Channels", "Tokens"], - "tabs": { - "Explore": { /* TabConfig */ }, - "Channels": { /* TabConfig */ }, - "Tokens": { /* TabConfig */ } - }, - "layout": { - "defaultLayoutFidget": "grid", - "gridSpacing": 16, - "theme": { /* ThemeProperties */ } - } -} -``` - -**Fields Explained:** Same as `home_page_config` above - -**Difference from Home Page:** -- Typically has different tabs (Explore, Channels, Tokens vs. Nouns, Social, Governance) -- Usually more discovery-focused content -- May have different default fidgets - -**Example:** -```json -{ - "defaultTab": "Explore", - "tabOrder": ["Explore"], - "tabs": { - "Explore": { - "name": "Explore", - "displayName": "Explore", - "layoutID": "explore-layout-id", - "layoutDetails": { - "layoutConfig": { - "layout": [] - }, - "layoutFidget": "grid" - }, - "theme": { /* ThemeProperties */ }, - "fidgetInstanceDatums": {}, - "fidgetTrayContents": [], - "isEditable": false, - "timestamp": "2025-06-20T05:58:44.080Z" - } - }, - "layout": { - "defaultLayoutFidget": "grid", - "gridSpacing": 16, - "theme": { - "background": "#ffffff", - "fidgetBackground": "#ffffff", - "font": "Inter", - "fontColor": "#000000" - } - } -} -``` - ---- - -## 8. `navigation_config` (NavigationConfig) - Optional - -**Purpose:** Navigation bar items and settings - -**Structure:** -```json -{ - "items": [ - { - "id": "home", - "label": "Home", - "href": "/home", - "icon": "home", - "openInNewTab": false, - "requiresAuth": false - }, - { - "id": "explore", - "label": "Explore", - "href": "/explore", - "icon": "explore", - "openInNewTab": false, - "requiresAuth": false - }, - { - "id": "notifications", - "label": "Notifications", - "href": "/notifications", - "icon": "notifications", - "openInNewTab": false, - "requiresAuth": true - }, - { - "id": "space-token", - "label": "$SPACE", - "href": "/t/base/0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab/Profile", - "icon": "space", - "openInNewTab": false, - "requiresAuth": false - } - ], - "logoTooltip": { - "text": "wtf is nouns?", - "href": "https://nouns.wtf" - }, - "showMusicPlayer": true, - "showSocials": true -} -``` - -**Fields Explained:** - -### `items` (Array of NavigationItem) -- **`id`**: Unique identifier for navigation item -- **`label`**: Display text -- **`href`**: URL/path to navigate to -- **`icon`**: Icon type ("home", "explore", "notifications", "search", "space", "robot", "custom") -- **`openInNewTab`**: Open link in new tab? (boolean) -- **`requiresAuth`**: Require authentication to see this item? (boolean) - -### `logoTooltip` (Optional) -- **`text`**: Tooltip text shown on logo hover -- **`href`**: Optional link when clicking tooltip - -### `showMusicPlayer` (Optional) -- Show music player in navigation? (boolean) - -### `showSocials` (Optional) -- Show social links in navigation? (boolean) - -**Example:** -```json -{ - "items": [ - { - "id": "home", - "label": "Home", - "href": "/home", - "icon": "home" - }, - { - "id": "explore", - "label": "Explore", - "href": "/explore", - "icon": "explore" - } - ], - "logoTooltip": { - "text": "wtf is nouns?", - "href": "https://nouns.wtf" - }, - "showMusicPlayer": true, - "showSocials": true -} -``` - ---- - -## 9. `ui_config` (UIConfig) - Optional - -**Purpose:** UI color scheme and styling - -**Structure:** -```json -{ - "primaryColor": "rgb(37, 99, 235)", // Primary brand color - "primaryHoverColor": "rgb(29, 78, 216)", // Primary color on hover - "primaryActiveColor": "rgb(30, 64, 175)", // Primary color when active - "castButton": { - "backgroundColor": "rgb(37, 99, 235)", // Cast button background - "hoverColor": "rgb(29, 78, 216)", // Cast button hover color - "activeColor": "rgb(30, 64, 175)" // Cast button active color - } -} -``` - -**Fields Explained:** -- **`primaryColor`**: Main brand color (RGB, hex, or CSS color) -- **`primaryHoverColor`**: Color when hovering over primary elements -- **`primaryActiveColor`**: Color when primary elements are active/pressed -- **`castButton`**: Cast button specific colors - - **`backgroundColor`**: Default background - - **`hoverColor`**: Hover state color - - **`activeColor`**: Active/pressed state color - -**Usage:** -- Applied to buttons, links, and interactive elements -- Used for consistent color theming across UI -- Can override theme colors for specific UI elements - -**Example:** -```json -{ - "primaryColor": "rgb(37, 99, 235)", - "primaryHoverColor": "rgb(29, 78, 216)", - "primaryActiveColor": "rgb(30, 64, 175)", - "castButton": { - "backgroundColor": "rgb(37, 99, 235)", - "hoverColor": "rgb(29, 78, 216)", - "activeColor": "rgb(30, 64, 175)" - } -} -``` - ---- - -## Database Schema Summary - -| Column | Type | Required | Purpose | -|--------|------|----------|---------| -| `brand_config` | JSONB | ✅ Yes | Brand identity and metadata | -| `assets_config` | JSONB | ✅ Yes | Visual assets (logos, icons) | -| `theme_config` | JSONB | ✅ Yes | Theme definitions | -| `community_config` | JSONB | ✅ Yes | Community integration data | -| `fidgets_config` | JSONB | ✅ Yes | Enabled/disabled fidgets | -| `home_page_config` | JSONB | ✅ Yes | Home page structure | -| `explore_page_config` | JSONB | ✅ Yes | Explore page structure | -| `navigation_config` | JSONB | ❌ Optional | Navigation items | -| `ui_config` | JSONB | ❌ Optional | UI colors | - ---- - -## Complete Example Config - -Here's a complete example of what a full config would look like in the database: - -```json -{ - "brand_config": { - "name": "nouns", - "displayName": "Nouns", - "tagline": "A space for Nouns", - "description": "The social hub for Nouns", - "miniAppTags": ["nouns", "client", "customizable"] - }, - "assets_config": { - "logos": { - "main": "/images/nouns/logo.svg", - "icon": "/images/nouns/noggles.svg", - "favicon": "/images/favicon.ico", - "appleTouch": "/images/apple-touch-icon.png", - "og": "/images/nouns/og.png", - "splash": "/images/nouns/splash.png" - } - }, - "theme_config": { - "default": { /* ThemeProperties */ }, - "nounish": { /* ThemeProperties */ } - }, - "community_config": { - "type": "nouns", - "urls": { /* URLs */ }, - "social": { /* Social handles */ }, - "governance": { /* Governance URLs */ }, - "tokens": { /* Token definitions */ }, - "contracts": { /* Contract addresses */ } - }, - "fidgets_config": { - "enabled": ["feed", "cast", "gallery"], - "disabled": [] - }, - "home_page_config": { - "defaultTab": "Nouns", - "tabOrder": ["Nouns", "Social"], - "tabs": { /* Tab configurations */ }, - "layout": { /* Layout settings */ } - }, - "explore_page_config": { - "defaultTab": "Explore", - "tabOrder": ["Explore"], - "tabs": { /* Tab configurations */ }, - "layout": { /* Layout settings */ } - }, - "navigation_config": { - "items": [ /* Navigation items */ ], - "logoTooltip": { /* Tooltip config */ }, - "showMusicPlayer": true, - "showSocials": true - }, - "ui_config": { - "primaryColor": "rgb(37, 99, 235)", - "primaryHoverColor": "rgb(29, 78, 216)", - "primaryActiveColor": "rgb(30, 64, 175)", - "castButton": { /* Cast button colors */ } - } -} -``` - ---- - -## Notes - -1. **All configs are JSONB** - PostgreSQL JSONB type allows: - - Efficient storage - - Indexing on specific keys - - Querying with JSON operators - - Validation with JSON schemas - -2. **Optional fields** - `navigation_config` and `ui_config` are optional: - - Can be `NULL` in database - - Will use defaults if not provided - -3. **Nested structures** - Some configs have deeply nested structures: - - `home_page_config.tabs[tabId].fidgetInstanceDatums[fidgetId]` - Very deep nesting - - Consider flattening if querying becomes complex - -4. **Size considerations** - Large configs (especially `home_page_config` and `explore_page_config`) can be large: - - Monitor JSONB size - - Consider compression if needed - - Index frequently queried fields - -5. **Validation** - Consider adding JSON schema validation: - - At application level (TypeScript types) - - At database level (PostgreSQL CHECK constraints) - - At API level (request validation) - diff --git a/docs/_archived/database-config/DATABASE_CONFIG_MIGRATION_PLAN.md b/docs/_archived/database-config/DATABASE_CONFIG_MIGRATION_PLAN.md deleted file mode 100644 index 231e3e958..000000000 --- a/docs/_archived/database-config/DATABASE_CONFIG_MIGRATION_PLAN.md +++ /dev/null @@ -1,1000 +0,0 @@ -# Database-Backed Configuration System Migration Plan - -## Overview - -This document outlines the approach for migrating the community configuration system from build-time static files to a database-backed system that can be updated by admins in real-time. - -## Current State - -- **Build-time configuration** via `NEXT_PUBLIC_COMMUNITY` environment variable -- **Static TypeScript files** in `src/config/{community}/` -- **No runtime updates** - requires rebuild to change configuration -- **No admin interface** - changes require code deployment - -## Target State - -- **Database-backed configuration** stored in Supabase -- **Admin interface** for updating configurations -- **Versioning/history** for configuration changes -- **Caching layer** for performance -- **Backward compatibility** with existing code -- **Multi-community support** with active/inactive states - -## Database Schema Design - -### 1. Core Tables - -#### `community_configs` - Main Configuration Table - -```sql -CREATE TABLE "public"."community_configs" ( - "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), - "community_id" VARCHAR(50) NOT NULL UNIQUE, -- 'nouns', 'clanker', etc. - "is_active" BOOLEAN DEFAULT true, - "version" INTEGER DEFAULT 1, - "created_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), - "updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), - "created_by" VARCHAR(255), -- Admin identity/public key - "updated_by" VARCHAR(255), -- Admin identity/public key - - -- Configuration sections as JSONB columns - "brand_config" JSONB NOT NULL, - "assets_config" JSONB NOT NULL, - "theme_config" JSONB NOT NULL, - "community_config" JSONB NOT NULL, - "fidgets_config" JSONB NOT NULL, - "home_page_config" JSONB NOT NULL, - "explore_page_config" JSONB NOT NULL, - "navigation_config" JSONB, - "ui_config" JSONB, - - -- Metadata - "notes" TEXT, -- Admin notes about this version - "is_published" BOOLEAN DEFAULT false -- Draft vs published -); - -CREATE INDEX "idx_community_configs_community_id" ON "public"."community_configs"("community_id"); -CREATE INDEX "idx_community_configs_active" ON "public"."community_configs"("is_active") WHERE "is_active" = true; -CREATE INDEX "idx_community_configs_published" ON "public"."community_configs"("is_published") WHERE "is_published" = true; -``` - -#### `community_config_history` - Version History - -```sql -CREATE TABLE "public"."community_config_history" ( - "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), - "community_config_id" UUID NOT NULL REFERENCES "public"."community_configs"("id") ON DELETE CASCADE, - "version" INTEGER NOT NULL, - "config_snapshot" JSONB NOT NULL, -- Full config snapshot - "changed_sections" TEXT[], -- Array of section names that changed - "created_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), - "created_by" VARCHAR(255), - "change_notes" TEXT -); - -CREATE INDEX "idx_config_history_community_config_id" ON "public"."community_config_history"("community_config_id"); -CREATE INDEX "idx_config_history_version" ON "public"."community_config_history"("community_config_id", "version"); -``` - -#### `community_config_admins` - Admin Permissions - -```sql -CREATE TABLE "public"."community_config_admins" ( - "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), - "community_id" VARCHAR(50) NOT NULL, - "admin_identity_public_key" VARCHAR(255) NOT NULL, -- Cryptographic identity - "admin_fid" BIGINT, -- Optional Farcaster ID - "permissions" TEXT[] DEFAULT ARRAY['read', 'write'], -- 'read', 'write', 'publish', 'delete' - "created_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), - "created_by" VARCHAR(255), - "is_active" BOOLEAN DEFAULT true, - - UNIQUE("community_id", "admin_identity_public_key") -); - -CREATE INDEX "idx_config_admins_community" ON "public"."community_config_admins"("community_id"); -CREATE INDEX "idx_config_admins_identity" ON "public"."community_config_admins"("admin_identity_public_key"); -``` - -#### `community_config_cache` - Cache Table (Optional) - -```sql -CREATE TABLE "public"."community_config_cache" ( - "community_id" VARCHAR(50) PRIMARY KEY, - "config_data" JSONB NOT NULL, - "version" INTEGER NOT NULL, - "cached_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), - "expires_at" TIMESTAMP WITH TIME ZONE NOT NULL -); - -CREATE INDEX "idx_config_cache_expires" ON "public"."community_config_cache"("expires_at"); -``` - -### 2. Row Level Security (RLS) Policies - -```sql --- Enable RLS on all tables -ALTER TABLE "public"."community_configs" ENABLE ROW LEVEL SECURITY; -ALTER TABLE "public"."community_config_history" ENABLE ROW LEVEL SECURITY; -ALTER TABLE "public"."community_config_admins" ENABLE ROW LEVEL SECURITY; -ALTER TABLE "public"."community_config_cache" ENABLE ROW LEVEL SECURITY; - --- Public read access for active, published configs -CREATE POLICY "public_read_active_configs" ON "public"."community_configs" - FOR SELECT - USING ("is_active" = true AND "is_published" = true); - --- Admin read access (can see drafts) -CREATE POLICY "admin_read_all_configs" ON "public"."community_configs" - FOR SELECT - USING ( - EXISTS ( - SELECT 1 FROM "public"."community_config_admins" cca - WHERE cca."community_id" = "community_configs"."community_id" - AND cca."admin_identity_public_key" = current_setting('app.current_identity_public_key', true) - AND cca."is_active" = true - AND 'read' = ANY(cca."permissions") - ) - ); - --- Admin write access -CREATE POLICY "admin_write_configs" ON "public"."community_configs" - FOR INSERT - WITH CHECK ( - EXISTS ( - SELECT 1 FROM "public"."community_config_admins" cca - WHERE cca."community_id" = "community_configs"."community_id" - AND cca."admin_identity_public_key" = current_setting('app.current_identity_public_key', true) - AND cca."is_active" = true - AND 'write' = ANY(cca."permissions") - ) - ); - -CREATE POLICY "admin_update_configs" ON "public"."community_configs" - FOR UPDATE - USING ( - EXISTS ( - SELECT 1 FROM "public"."community_config_admins" cca - WHERE cca."community_id" = "community_configs"."community_id" - AND cca."admin_identity_public_key" = current_setting('app.current_identity_public_key', true) - AND cca."is_active" = true - AND 'write' = ANY(cca."permissions") - ) - ); - --- History is readable by admins -CREATE POLICY "admin_read_history" ON "public"."community_config_history" - FOR SELECT - USING ( - EXISTS ( - SELECT 1 FROM "public"."community_config_admins" cca - JOIN "public"."community_configs" cc ON cc."id" = "community_config_history"."community_config_id" - WHERE cca."community_id" = cc."community_id" - AND cca."admin_identity_public_key" = current_setting('app.current_identity_public_key', true) - AND cca."is_active" = true - AND 'read' = ANY(cca."permissions") - ) - ); - --- Cache is publicly readable -CREATE POLICY "public_read_cache" ON "public"."community_config_cache" - FOR SELECT - USING ("expires_at" > now()); -``` - -### 3. Database Functions - -#### Function to Get Active Config - -```sql -CREATE OR REPLACE FUNCTION "public"."get_active_community_config"( - p_community_id VARCHAR(50) -) -RETURNS JSONB -LANGUAGE plpgsql -SECURITY DEFINER -AS $$ -DECLARE - v_config JSONB; -BEGIN - SELECT jsonb_build_object( - 'brand', "brand_config", - 'assets', "assets_config", - 'theme', "theme_config", - 'community', "community_config", - 'fidgets', "fidgets_config", - 'homePage', "home_page_config", - 'explorePage', "explore_page_config", - 'navigation', "navigation_config", - 'ui', "ui_config" - ) - INTO v_config - FROM "public"."community_configs" - WHERE "community_id" = p_community_id - AND "is_active" = true - AND "is_published" = true - ORDER BY "version" DESC - LIMIT 1; - - RETURN v_config; -END; -$$; -``` - -#### Function to Create Config Version - -```sql -CREATE OR REPLACE FUNCTION "public"."create_config_version"( - p_community_id VARCHAR(50), - p_config_data JSONB, - p_change_notes TEXT DEFAULT NULL, - p_admin_identity VARCHAR(255) DEFAULT NULL -) -RETURNS UUID -LANGUAGE plpgsql -SECURITY DEFINER -AS $$ -DECLARE - v_config_id UUID; - v_new_version INTEGER; - v_old_config JSONB; -BEGIN - -- Get current version - SELECT "id", "version", jsonb_build_object( - 'brand', "brand_config", - 'assets', "assets_config", - 'theme', "theme_config", - 'community', "community_config", - 'fidgets', "fidgets_config", - 'homePage', "home_page_config", - 'explorePage', "explore_page_config", - 'navigation', "navigation_config", - 'ui', "ui_config" - ) - INTO v_config_id, v_new_version, v_old_config - FROM "public"."community_configs" - WHERE "community_id" = p_community_id - AND "is_active" = true - ORDER BY "version" DESC - LIMIT 1; - - -- Increment version - v_new_version := COALESCE(v_new_version, 0) + 1; - - -- If config exists, archive old version - IF v_config_id IS NOT NULL THEN - INSERT INTO "public"."community_config_history" ( - "community_config_id", - "version", - "config_snapshot", - "created_by", - "change_notes" - ) VALUES ( - v_config_id, - v_new_version - 1, - v_old_config, - p_admin_identity, - p_change_notes - ); - - -- Deactivate old version - UPDATE "public"."community_configs" - SET "is_active" = false - WHERE "id" = v_config_id; - END IF; - - -- Create new version - INSERT INTO "public"."community_configs" ( - "community_id", - "version", - "brand_config", - "assets_config", - "theme_config", - "community_config", - "fidgets_config", - "home_page_config", - "explore_page_config", - "navigation_config", - "ui_config", - "created_by", - "updated_by", - "notes", - "is_published" - ) VALUES ( - p_community_id, - v_new_version, - p_config_data->'brand', - p_config_data->'assets', - p_config_data->'theme', - p_config_data->'community', - p_config_data->'fidgets', - p_config_data->'homePage', - p_config_data->'explorePage', - p_config_data->'navigation', - p_config_data->'ui', - p_admin_identity, - p_admin_identity, - p_change_notes, - true -- Auto-publish for now, can be made configurable - ) - RETURNING "id" INTO v_config_id; - - -- Update cache - INSERT INTO "public"."community_config_cache" ( - "community_id", - "config_data", - "version", - "expires_at" - ) VALUES ( - p_community_id, - p_config_data, - v_new_version, - now() + INTERVAL '1 hour' - ) - ON CONFLICT ("community_id") DO UPDATE SET - "config_data" = EXCLUDED."config_data", - "version" = EXCLUDED."version", - "cached_at" = now(), - "expires_at" = now() + INTERVAL '1 hour'; - - RETURN v_config_id; -END; -$$; -``` - -## Application Architecture - -### Build-Time Configuration Generation - -**Key Principle**: Configs are fetched from the database at build time and generated as static TypeScript files. Runtime uses these static files with zero database queries. - -### 1. Build-Time Config Generator Script - -Create a script that runs before the Next.js build to fetch configs from the database and generate static TypeScript files: - -```typescript -// scripts/generate-configs.ts - -import { createClient } from '@supabase/supabase-js'; -import { writeFile, mkdir } from 'fs/promises'; -import { join } from 'path'; -import { SystemConfig } from '../src/config/systemConfig'; - -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; -const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; - -if (!supabaseUrl || !supabaseKey) { - console.error('Missing Supabase environment variables'); - process.exit(1); -} - -const supabase = createClient(supabaseUrl, supabaseKey); - -/** - * Fetch active config from database - */ -async function fetchConfigFromDB(communityId: string): Promise { - try { - const { data, error } = await supabase - .rpc('get_active_community_config', { p_community_id: communityId }) - .single(); - - if (error) { - console.error(`Failed to fetch config for ${communityId}:`, error); - return null; - } - - return data as SystemConfig; - } catch (error) { - console.error(`Error fetching config for ${communityId}:`, error); - return null; - } -} - -/** - * Generate TypeScript file for a config section - */ -function generateConfigFile( - sectionName: string, - data: any, - communityId: string -): string { - const exportName = `${communityId}${sectionName.charAt(0).toUpperCase() + sectionName.slice(1)}`; - return `export const ${exportName} = ${JSON.stringify(data, null, 2)} as const;\n`; -} - -/** - * Generate the main index file for a community - */ -function generateIndexFile(communityId: string, sections: string[]): string { - const imports = sections.map(section => { - const importName = `${communityId}${section.charAt(0).toUpperCase() + section.slice(1)}`; - return `import { ${importName} } from './${communityId}.${section}';`; - }).join('\n'); - - const exports = sections.map(section => { - const varName = `${communityId}${section.charAt(0).toUpperCase() + section.slice(1)}`; - return ` ${section}: ${varName},`; - }).join('\n'); - - const exportName = `${communityId}SystemConfig`; - - return `${imports} - -export const ${exportName} = { -${exports} -}; - -// Re-export individual sections -${sections.map(section => { - const varName = `${communityId}${section.charAt(0).toUpperCase() + section.slice(1)}`; - return `export { ${varName} } from './${communityId}.${section}';`; -}).join('\n')} -`; -} - -/** - * Generate initial space creator files (if needed) - * These can be kept as static or also generated from DB - */ -async function generateInitialSpaceFiles(communityId: string, config: SystemConfig) { - // For now, we'll keep initial space files as static - // They can be migrated to DB later if needed - console.log(`Skipping initial space files for ${communityId} (keeping static)`); -} - -/** - * Main generation function - */ -async function generateConfigFiles(communityId: string) { - console.log(`Generating config files for ${communityId}...`); - - // Fetch config from database - const config = await fetchConfigFromDB(communityId); - - if (!config) { - console.warn(`No config found in DB for ${communityId}, skipping generation`); - return false; - } - - const configDir = join(process.cwd(), 'src', 'config', communityId); - - // Ensure directory exists - await mkdir(configDir, { recursive: true }); - - // Generate individual section files - const sections = [ - 'brand', - 'assets', - 'theme', - 'community', - 'fidgets', - 'home', - 'explore', - 'navigation', - 'ui' - ]; - - for (const section of sections) { - const sectionData = config[section as keyof SystemConfig]; - if (sectionData !== undefined) { - const content = generateConfigFile(section, sectionData, communityId); - const filePath = join(configDir, `${communityId}.${section}.ts`); - await writeFile(filePath, content, 'utf-8'); - console.log(` ✓ Generated ${filePath}`); - } - } - - // Generate index file - const indexContent = generateIndexFile( - communityId, - sections.filter(s => config[s as keyof SystemConfig] !== undefined) - ); - const indexPath = join(configDir, 'index.ts'); - await writeFile(indexPath, indexContent, 'utf-8'); - console.log(` ✓ Generated ${indexPath}`); - - // Generate initial space files (optional, can keep static) - await generateInitialSpaceFiles(communityId, config); - - console.log(`✓ Successfully generated config files for ${communityId}`); - return true; -} - -/** - * Update main config index to include generated configs - */ -async function updateMainConfigIndex(communityIds: string[]) { - const indexPath = join(process.cwd(), 'src', 'config', 'index.ts'); - - // Read existing file - const fs = await import('fs/promises'); - let content = await fs.readFile(indexPath, 'utf-8'); - - // Add imports for generated configs - const imports = communityIds.map(id => { - const configName = `${id}SystemConfig`; - return `import { ${configName} } from './${id}/index';`; - }).join('\n'); - - // Update switch statement - const switchCases = communityIds.map(id => { - return ` case '${id}':\n return ${id}SystemConfig;`; - }).join('\n'); - - // This is a simplified version - actual implementation would need - // more sophisticated parsing/updating of the existing file - console.log('Note: Main config index may need manual updates'); -} - -/** - * Main execution - */ -async function main() { - const communities = process.env.GENERATE_CONFIGS?.split(',') || - ['nouns', 'clanker', 'example']; - - console.log('Starting config generation from database...'); - console.log(`Communities: ${communities.join(', ')}`); - - const results = await Promise.all( - communities.map(id => generateConfigFiles(id)) - ); - - const successCount = results.filter(Boolean).length; - console.log(`\n✓ Generated ${successCount}/${communities.length} configs`); - - if (successCount < communities.length) { - console.warn('Some configs failed to generate. Build will use static fallbacks.'); - process.exit(0); // Don't fail build, allow fallback - } -} - -main().catch(error => { - console.error('Config generation failed:', error); - process.exit(1); -}); -``` - -### 2. Package.json Scripts - -Add scripts to run the generator before build: - -```json -{ - "scripts": { - "generate-configs": "tsx scripts/generate-configs.ts", - "prebuild": "npm run generate-configs", - "build": "next build", - "dev": "next dev", - "dev:with-configs": "npm run generate-configs && next dev" - } -} -``` - -### 3. Updated Configuration Loader - -The loader remains mostly the same, but now uses the generated files: - -```typescript -// src/config/index.ts - -// Import generated configs (these are created at build time) -import { nounsSystemConfig } from './nouns/index'; -import { exampleSystemConfig } from './example/index'; -import { clankerSystemConfig } from './clanker/index'; -import { SystemConfig } from './systemConfig'; - -// Fallback to static configs if generated ones don't exist -// This provides safety if DB fetch fails during build -const STATIC_FALLBACKS = { - nouns: nounsSystemConfig, - example: exampleSystemConfig, - clanker: clankerSystemConfig as unknown as SystemConfig, -}; - -export const loadSystemConfig = (): SystemConfig => { - const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - - // Use generated config (from build-time DB fetch) - // Falls back to static if generation failed - const config = STATIC_FALLBACKS[communityConfig.toLowerCase() as keyof typeof STATIC_FALLBACKS]; - - if (!config) { - console.warn( - `Invalid community configuration: "${communityConfig}". ` + - `Available options: ${Object.keys(STATIC_FALLBACKS).join(', ')}. ` + - `Falling back to "nouns" configuration.` - ); - return STATIC_FALLBACKS.nouns; - } - - return config; -}; -``` - -### 4. Admin Service (Runtime - For Admin Interface Only) - -Create a service for admin operations (only used in admin interface, not in main app): - -```typescript -// src/common/data/services/adminConfigService.ts - -import { createClient } from '@supabase/supabase-js'; -import { SystemConfig } from '@/config/systemConfig'; - -/** - * Admin-only service for updating configs in database - * This is NOT used by the main application at runtime - */ -export class AdminConfigService { - private supabase = createClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.SUPABASE_SERVICE_ROLE_KEY! // Use service role for admin operations - ); - - /** - * Update configuration (admin only) - */ - async updateConfig( - communityId: string, - config: Partial, - adminIdentity: string, - changeNotes?: string - ): Promise { - // Get current config - const { data: currentData } = await this.supabase - .rpc('get_active_community_config', { p_community_id: communityId }) - .single(); - - if (!currentData) { - throw new Error('Current config not found'); - } - - const currentConfig = currentData as SystemConfig; - - // Merge with new config - const updatedConfig: SystemConfig = { - ...currentConfig, - ...config, - brand: { ...currentConfig.brand, ...config.brand }, - assets: { ...currentConfig.assets, ...config.assets }, - theme: { ...currentConfig.theme, ...config.theme }, - community: { ...currentConfig.community, ...config.community }, - fidgets: { ...currentConfig.fidgets, ...config.fidgets }, - homePage: { ...currentConfig.homePage, ...config.homePage }, - explorePage: { ...currentConfig.explorePage, ...config.explorePage }, - navigation: config.navigation ?? currentConfig.navigation, - ui: config.ui ?? currentConfig.ui, - }; - - // Create new version - const { error } = await this.supabase.rpc('create_config_version', { - p_community_id: communityId, - p_config_data: updatedConfig, - p_change_notes: changeNotes, - p_admin_identity: adminIdentity - }); - - if (error) { - console.error('Failed to update config:', error); - return false; - } - - return true; - } - - /** - * Get config history - */ - async getConfigHistory(communityId: string, limit: number = 10): Promise { - const { data, error } = await this.supabase - .from('community_configs') - .select(` - id, - version, - created_at, - created_by, - notes - `) - .eq('community_id', communityId) - .order('version', { ascending: false }) - .limit(limit); - - if (error) { - console.error('Failed to fetch history:', error); - return []; - } - - return data || []; - } - - /** - * Trigger rebuild (webhook or manual) - */ - async triggerRebuild(communityId: string): Promise { - // This would trigger your CI/CD to rebuild - // Implementation depends on your deployment setup - // Could use GitHub Actions API, Vercel API, etc. - console.log(`Rebuild triggered for ${communityId}`); - return true; - } -} -``` - -### 5. Updated Hook (No Changes Needed) - -The hook remains simple since it's using static files: - -```typescript -// src/common/lib/hooks/useSystemConfig.ts - -import { useMemo } from 'react'; -import { loadSystemConfig, SystemConfig } from '@/config'; - -let cachedConfig: SystemConfig | null = null; - -export const useSystemConfig = (): SystemConfig => { - return useMemo(() => { - if (cachedConfig) { - return cachedConfig; - } - - cachedConfig = loadSystemConfig(); - return cachedConfig; - }, []); -}; - -export { loadSystemConfig }; -``` - -## Migration Strategy - -### Phase 1: Database Setup (Week 1) - -1. **Create migration files** - - Create tables: `community_configs`, `community_config_history`, `community_config_admins` - - Set up RLS policies - - Create database functions (`get_active_community_config`, `create_config_version`) - -2. **Seed initial data** - - Migrate existing static configs to database - - Create admin accounts for each community - - Set up initial published versions - -### Phase 2: Build-Time Generator (Week 1-2) - -1. **Create config generator script** - - `scripts/generate-configs.ts` - Fetches from DB and generates TypeScript files - - Add `generate-configs` script to package.json - - Add `prebuild` hook to run generator before build - -2. **Test generator** - - Test fetching from database - - Test file generation - - Test fallback to static configs if DB unavailable - -### Phase 3: Update Build Process (Week 2) - -1. **Update package.json scripts** - - Add `prebuild` hook - - Update CI/CD to run generator before build - - Ensure environment variables are available during build - -2. **Update config loader** - - Keep existing structure (uses generated files) - - Maintain fallback to static configs - - No runtime changes needed - -### Phase 4: Admin Interface (Week 2-3) - -1. **Create admin pages** - - Config editor UI - - Section-by-section editing - - Preview functionality - - History viewer - -2. **API endpoints** - - `/api/admin/config/[communityId]` - GET/PUT config - - `/api/admin/config/[communityId]/history` - GET history - - `/api/admin/config/[communityId]/rebuild` - Trigger rebuild - -3. **Rebuild trigger** - - Webhook or manual trigger to rebuild after config updates - - Integration with CI/CD (GitHub Actions, Vercel, etc.) - -### Phase 5: Testing & Rollout (Week 3-4) - -1. **Testing** - - Test config generator script - - Test build process with generated configs - - Test admin interface updates - - Test rebuild trigger - -2. **Gradual rollout** - - Start with one community (e.g., 'example') - - Monitor build process and generated files - - Roll out to other communities - -### Phase 6: Cleanup & Optimization (Week 4+) - -1. **Keep static configs as fallback** - - Always maintain static configs as safety net - - Generated configs take precedence - - Static configs used if generation fails - -2. **Optimization** - - Add validation to generator - - Add error handling and logging - - Monitor build times - - Consider caching generated files in CI/CD - -## Admin Interface Design - -### Pages Needed - -1. **Config Dashboard** (`/admin/config`) - - List all communities - - Active/published status - - Quick actions - - Rebuild status/triggers - -2. **Config Editor** (`/admin/config/[communityId]`) - - Section tabs (Brand, Assets, Theme, etc.) - - Form inputs for each section - - Preview pane - - Save/Publish buttons - - "Trigger Rebuild" button after saving - -3. **Config History** (`/admin/config/[communityId]/history`) - - Version list - - Diff viewer - - Rollback functionality - - Rebuild trigger for any version - -4. **Admin Management** (`/admin/config/[communityId]/admins`) - - Add/remove admins - - Permission management - -### Rebuild Trigger Options - -**Option 1: Manual Trigger (Recommended for MVP)** -- Admin clicks "Trigger Rebuild" button -- Calls API endpoint that triggers CI/CD rebuild -- Shows build status/link - -**Option 2: Automatic Trigger** -- Webhook from database (PostgreSQL triggers) -- Calls CI/CD API automatically on config update -- More complex but seamless - -**Option 3: Scheduled Rebuilds** -- Daily/hourly rebuilds to pick up changes -- Simple but less immediate - -**Implementation Example (GitHub Actions):** -```typescript -// API endpoint: /api/admin/config/[communityId]/rebuild -export async function POST(request: Request) { - const { communityId } = await request.json(); - - // Trigger GitHub Actions workflow - const response = await fetch( - `https://api.github.com/repos/${owner}/${repo}/actions/workflows/${workflowId}/dispatches`, - { - method: 'POST', - headers: { - 'Authorization': `token ${GITHUB_TOKEN}`, - 'Accept': 'application/vnd.github.v3+json', - }, - body: JSON.stringify({ - ref: 'main', - inputs: { - community: communityId, - }, - }), - } - ); - - return Response.json({ success: response.ok }); -} -``` - -## Security Considerations - -1. **Authentication** - - Use existing identity system (cryptographic keys) - - Verify admin permissions before allowing edits - -2. **Authorization** - - RLS policies enforce database-level security - - Application-level checks as backup - -3. **Validation** - - Validate config structure before saving - - Type checking against `SystemConfig` interface - - Sanitize JSON inputs - -4. **Audit Trail** - - All changes logged in history table - - Track who made changes and when - -## Performance Considerations - -1. **Build-Time Generation** - - Configs fetched once during build (not at runtime) - - Zero database queries in production - - Generated files are static TypeScript (fast imports) - - No runtime caching needed - -2. **Database Optimization** - - Indexes on frequently queried columns (for admin interface) - - Build-time queries are infrequent (only during builds) - - Admin queries are lightweight (only used in admin interface) - -3. **Build Process** - - Generator runs in parallel for multiple communities - - Failed generations don't break build (fallback to static) - - Generated files can be cached in CI/CD - - Build time impact is minimal (~1-2 seconds per community) - -## Rollback Plan - -If issues arise: - -1. **Immediate**: Remove `prebuild` hook, build uses static configs directly -2. **Short-term**: Fix generator script or database issues, re-enable generation -3. **Long-term**: Keep static configs as permanent fallback (always available) - -**Advantages of this approach:** -- Static configs are always available as fallback -- No runtime dependencies on database -- Build failures don't affect runtime -- Easy to rollback by simply removing prebuild hook - -## Future Enhancements - -1. **Draft/Preview System** - - Save drafts without publishing - - Preview changes before publishing - - Scheduled publishing - -2. **Multi-environment Support** - - Dev/staging/production configs - - Environment-specific overrides - -3. **Config Templates** - - Save configs as templates - - Clone configs for new communities - -4. **API for External Tools** - - REST API for config management - - Webhook notifications on changes - -5. **Advanced Permissions** - - Section-level permissions - - Read-only admins - - Approval workflows - -## Summary - -This migration plan provides: - -- ✅ **Database schema** for storing configs with versioning -- ✅ **Admin permission system** using existing identity infrastructure -- ✅ **Build-time generation** - Zero runtime database queries -- ✅ **Backward compatibility** - Static configs always available as fallback -- ✅ **Migration path** with phased rollout -- ✅ **Security** through RLS and validation -- ✅ **Audit trail** for all changes -- ✅ **Performance** - Static files at runtime, DB only at build time - -**Key Benefits:** -- **Zero runtime overhead** - Configs are static files at runtime -- **Admin updates** - Changes made in database, reflected in next build -- **Safe fallback** - Static configs always available if generation fails -- **Fast builds** - Generator runs in parallel, minimal build time impact -- **Easy rollback** - Simply remove prebuild hook to use static configs - -The system maintains backward compatibility while enabling admin updates through the database, with changes reflected in builds rather than at runtime. - diff --git a/docs/_archived/database-config/E2BIG_SOLUTION.md b/docs/_archived/database-config/E2BIG_SOLUTION.md deleted file mode 100644 index eb931e7b7..000000000 --- a/docs/_archived/database-config/E2BIG_SOLUTION.md +++ /dev/null @@ -1,99 +0,0 @@ -# E2BIG Error Solution: File-Based Config Instead of Env Var - -## Problem - -The `NEXT_PUBLIC_BUILD_TIME_CONFIG` environment variable is too large (E2BIG error). Environment variables have size limits: -- **Linux/macOS**: ~128KB - 2MB (varies by system) -- **Windows**: ~32KB - -Our config includes: -- Multiple theme definitions with HTML/CSS -- Home page configs with fidget instances -- Explore page configs -- All can easily exceed 100KB+ - -## Solution: Generate TypeScript File Instead - -Instead of storing config in an environment variable, generate a TypeScript file at build time. - -### Approach 1: Single Generated Config File (Recommended) - -Generate `src/config/db-config.ts` at build time, import it at runtime. - -**Pros:** -- ✅ No size limits -- ✅ Type-safe -- ✅ Simple to implement -- ✅ Works with Next.js build system - -**Cons:** -- ⚠️ Generates one file - -### Approach 2: Compress Config - -Compress the JSON before storing in env var. - -**Pros:** -- ✅ Keeps env var approach -- ✅ Reduces size significantly - -**Cons:** -- ⚠️ Still has limits -- ⚠️ Requires decompression -- ⚠️ More complex - -### Approach 3: Split Config - -Store different parts in separate env vars. - -**Pros:** -- ✅ Keeps env var approach - -**Cons:** -- ⚠️ Complex to manage -- ⚠️ Still has limits -- ⚠️ Harder to maintain - -## Recommended: File-Based Approach - -Generate a TypeScript file at build time: - -```javascript -// next.config.mjs - -async function generateConfigFile() { - // Fetch config from DB - const config = await fetchConfigFromDB(); - - if (!config) return; // Fall back to static - - // Generate TypeScript file - const content = `// Auto-generated at build time -import { SystemConfig } from './systemConfig'; - -export const dbConfig: SystemConfig | null = ${JSON.stringify(config, null, 2)} as SystemConfig; -`; - - await writeFile('src/config/db-config.ts', content); -} -``` - -```typescript -// src/config/index.ts - -let dbConfig: SystemConfig | null = null; -try { - const dbModule = require('./db-config'); - dbConfig = dbModule.dbConfig; -} catch { - // File doesn't exist, use static -} - -export const loadSystemConfig = (): SystemConfig => { - if (dbConfig) { - return dbConfig; - } - // Fall back to static... -}; -``` - diff --git a/docs/_archived/database-config/NAVIGATION_SPACE_REFERENCE_APPROACH.md b/docs/_archived/database-config/NAVIGATION_SPACE_REFERENCE_APPROACH.md deleted file mode 100644 index 172b1b2b3..000000000 --- a/docs/_archived/database-config/NAVIGATION_SPACE_REFERENCE_APPROACH.md +++ /dev/null @@ -1,313 +0,0 @@ -# Navigation-Space Reference Approach - -## Overview - -Instead of storing `homePage` and `explorePage` configs directly in `community_configs`, navigation items reference Spaces in the database. Pages are fetched at build time based on navigation entries. - -## Architecture - -``` -Navigation Config - ├── items: [ - │ { id: 'home', spaceId: 'uuid-1', ... }, - │ { id: 'explore', spaceId: 'uuid-2', ... }, - │ ... - │ ] - │ - └── Build Time: - ├── Fetch nav config from community_configs - ├── For each nav item with spaceId: - │ └── Fetch Space from database/storage - └── Build page configs from fetched Spaces -``` - -## Benefits - -1. **Dramatically reduces config size** - Removes 71% (20.6 KB) of config -2. **Unified architecture** - Everything is Spaces -3. **Navigation as source of truth** - Nav defines what pages exist -4. **Flexible** - Any nav item can reference a Space -5. **Reuses existing infrastructure** - Uses Space storage/retrieval - -## Implementation - -### 1. Update NavigationItem Interface - -```typescript -// src/config/systemConfig.ts - -export interface NavigationItem { - id: string; - label: string; - href: string; - icon?: 'home' | 'explore' | 'notifications' | 'search' | 'space' | 'robot' | 'custom'; - openInNewTab?: boolean; - requiresAuth?: boolean; - spaceId?: string; // ← NEW: Reference to Space in database -} -``` - -### 2. Add 'navPage' Space Type - -```sql --- Migration: Add navPage to spaceType enum -ALTER TABLE "public"."spaceRegistrations" - DROP CONSTRAINT IF EXISTS valid_space_type; - -ALTER TABLE "public"."spaceRegistrations" - ADD CONSTRAINT valid_space_type CHECK ( - "spaceType" IN ('profile', 'token', 'proposal', 'channel', 'navPage') - ); -``` - -### 3. Update Navigation Config Structure - -```typescript -// src/config/nouns/nouns.navigation.ts - -export const nounsNavigation: NavigationConfig = { - logoTooltip: { - text: "wtf is nouns?", - href: "https://nouns.wtf", - }, - items: [ - { - id: 'home', - label: 'Home', - href: '/home', - icon: 'home', - spaceId: '550e8400-e29b-41d4-a716-446655440000' // ← Reference to Space - }, - { - id: 'explore', - label: 'Explore', - href: '/explore', - icon: 'explore', - spaceId: '550e8400-e29b-41d4-a716-446655440001' // ← Reference to Space - }, - { - id: 'notifications', - label: 'Notifications', - href: '/notifications', - icon: 'notifications', - requiresAuth: true - // No spaceId - not a Space-based page - }, - ], - showMusicPlayer: true, - showSocials: true, -}; -``` - -### 4. Update Database Schema - -```sql --- Remove home_page_config and explore_page_config from community_configs -ALTER TABLE "public"."community_configs" - DROP COLUMN IF EXISTS "home_page_config", - DROP COLUMN IF EXISTS "explore_page_config"; - --- Navigation config now contains spaceId references --- No schema changes needed - navigation_config JSONB column handles it -``` - -### 5. Update Build-Time Config Generation - -```javascript -// next.config.mjs - -async function generateConfigFile() { - // Fetch main config (now much smaller - no homePage/explorePage!) - const { data: config } = await supabase - .rpc('get_active_community_config', { p_community_id: community }) - .single(); - - // Fetch Spaces for nav items that have spaceId - const navItems = config.navigation?.items || []; - const spaceIds = navItems - .filter(item => item.spaceId) - .map(item => item.spaceId); - - // Fetch Spaces from database/storage - const spaces = {}; - for (const spaceId of spaceIds) { - // Fetch Space based on how Spaces are stored - // (Could be from spaceRegistrations + Storage, or new table) - const space = await fetchSpace(spaceId); - if (space) { - spaces[spaceId] = space; - } - } - - // Build page configs from Spaces - const pageConfigs = {}; - navItems.forEach(item => { - if (item.spaceId && spaces[item.spaceId]) { - // Map Space to page config format - pageConfigs[item.id] = convertSpaceToPageConfig(spaces[item.spaceId]); - } - }); - - // Combine configs - const fullConfig = { - ...config, - // Add page configs based on nav items - pages: pageConfigs, - }; - - // Generate file - await writeFile('src/config/db-config.ts', ...); -} -``` - -### 6. Update Config Loader - -```typescript -// src/config/index.ts - -export const loadSystemConfig = (): SystemConfig => { - const config = dbConfig || staticConfig; - - // Get page configs from nav items - const navItems = config.navigation?.items || []; - const pages = {}; - - navItems.forEach(item => { - if (item.spaceId && config.pages?.[item.id]) { - pages[item.id] = config.pages[item.id]; - } - }); - - // Map to legacy structure for backward compatibility - return { - ...config, - homePage: pages['home'] || staticConfig.homePage, - explorePage: pages['explore'] || staticConfig.explorePage, - }; -}; -``` - -## Space Storage Options - -### Option A: Use Existing Space Storage System - -Spaces stored in Supabase Storage: -- Path: `spaces/{spaceId}/tabs/{tabName}` -- Encrypted/signed files -- Requires decryption at build time - -**Pros:** -- Uses existing infrastructure -- No schema changes needed - -**Cons:** -- Requires encryption/decryption logic at build time -- More complex fetching - -### Option B: New Database Table for Nav Pages - -```sql -CREATE TABLE community_nav_pages ( - id UUID PRIMARY KEY, - community_id VARCHAR(50), - nav_item_id VARCHAR(50), -- 'home', 'explore', etc. - space_config JSONB NOT NULL, - version INTEGER DEFAULT 1, - created_at TIMESTAMP DEFAULT NOW() -); -``` - -**Pros:** -- Simple database queries -- No encryption needed (public pages) -- Easy to version - -**Cons:** -- New table needed -- Duplicates Space structure - -### Option C: Use spaceRegistrations + Storage - -Store nav pages as Spaces in `spaceRegistrations` with `spaceType = 'navPage'`: -- Register in `spaceRegistrations` table -- Store config in Supabase Storage -- Fetch at build time - -**Pros:** -- Uses existing Space infrastructure -- Consistent with other Spaces -- Can reuse Space APIs - -**Cons:** -- Requires spaceRegistrations entry -- Need to handle Storage fetching - -## Recommended: Option C (spaceRegistrations + Storage) - -**Why:** -- ✅ Uses existing Space system -- ✅ Consistent architecture -- ✅ Can reuse Space loading logic -- ✅ No new tables needed - -## Migration Steps - -1. **Add 'navPage' to spaceType enum** -2. **Create Spaces for homePage/explorePage** - - Register in `spaceRegistrations` with `spaceType = 'navPage'` - - Store configs in Supabase Storage -3. **Update navigation configs** - - Add `spaceId` to home/explore nav items -4. **Update build-time fetching** - - Fetch Spaces based on nav items - - Build page configs from Spaces -5. **Remove homePage/explorePage from community_configs** - - Update schema - - Update seed script - -## Size Reduction - -**Before:** -- Config: ~29 KB -- homePage: 19.2 KB -- explorePage: 1.4 KB - -**After:** -- Config: ~8.4 KB (71% reduction!) -- Navigation: ~500 bytes (includes spaceId references) -- Spaces: Fetched separately, no size limit - -**Result:** Config easily fits in env vars or generated file! - -## Example: Updated Navigation Config - -```typescript -export const nounsNavigation: NavigationConfig = { - items: [ - { - id: 'home', - label: 'Home', - href: '/home', - icon: 'home', - spaceId: 'nouns-home-space-uuid' // ← References Space - }, - { - id: 'explore', - label: 'Explore', - href: '/explore', - icon: 'explore', - spaceId: 'nouns-explore-space-uuid' // ← References Space - }, - ], -}; -``` - -## Benefits Summary - -✅ **Solves E2BIG** - Config size reduced by 71% -✅ **Unified architecture** - Everything is Spaces -✅ **Navigation as source of truth** - Nav defines pages -✅ **Flexible** - Any nav item can be a Space -✅ **Reuses infrastructure** - Uses existing Space system -✅ **No breaking changes** - Can maintain backward compatibility - diff --git a/docs/_archived/database-config/NAVIGATION_SPACE_REFERENCE_IMPLEMENTATION.md b/docs/_archived/database-config/NAVIGATION_SPACE_REFERENCE_IMPLEMENTATION.md deleted file mode 100644 index cdb95e1c1..000000000 --- a/docs/_archived/database-config/NAVIGATION_SPACE_REFERENCE_IMPLEMENTATION.md +++ /dev/null @@ -1,357 +0,0 @@ -# Navigation-Space Reference Implementation Plan - -## Architecture Overview - -**Key Insight:** Navigation items reference Spaces. Pages are fetched from Spaces at build time. - -``` -Navigation Config (in community_configs) - └── items: [ - { id: 'home', spaceId: 'uuid', ... }, - { id: 'explore', spaceId: 'uuid', ... } - ] - ↓ - Build Time: - ↓ - Fetch Spaces by spaceId - ↓ - Build page configs from Spaces -``` - -## Changes Required - -### 1. Add 'navPage' Space Type - -**File:** `src/common/types/spaceData.ts` - -```typescript -export const SPACE_TYPES = { - PROFILE: 'profile', - TOKEN: 'token', - PROPOSAL: 'proposal', - CHANNEL: 'channel', - NAV_PAGE: 'navPage', // ← NEW -} as const; -``` - -**Migration:** `supabase/migrations/YYYYMMDDHHMMSS_add_navpage_space_type.sql` - -```sql --- Add navPage to spaceType constraint -ALTER TABLE "public"."spaceRegistrations" - DROP CONSTRAINT IF EXISTS valid_space_type; - -ALTER TABLE "public"."spaceRegistrations" - ADD CONSTRAINT valid_space_type CHECK ( - "spaceType" IN ('profile', 'token', 'proposal', 'channel', 'navPage') - ); -``` - -### 2. Update NavigationItem Interface - -**File:** `src/config/systemConfig.ts` - -```typescript -export interface NavigationItem { - id: string; - label: string; - href: string; - icon?: 'home' | 'explore' | 'notifications' | 'search' | 'space' | 'robot' | 'custom'; - openInNewTab?: boolean; - requiresAuth?: boolean; - spaceId?: string; // ← NEW: Optional reference to Space -} -``` - -### 3. Update Navigation Configs - -**File:** `src/config/nouns/nouns.navigation.ts` - -```typescript -export const nounsNavigation: NavigationConfig = { - logoTooltip: { - text: "wtf is nouns?", - href: "https://nouns.wtf", - }, - items: [ - { - id: 'home', - label: 'Home', - href: '/home', - icon: 'home', - spaceId: 'nouns-home-space-id' // ← Reference to Space - }, - { - id: 'explore', - label: 'Explore', - href: '/explore', - icon: 'explore', - spaceId: 'nouns-explore-space-id' // ← Reference to Space - }, - { - id: 'notifications', - label: 'Notifications', - href: '/notifications', - icon: 'notifications', - requiresAuth: true - // No spaceId - not a Space-based page - }, - ], - showMusicPlayer: true, - showSocials: true, -}; -``` - -### 4. Update Database Schema - -**Migration:** Remove homePage/explorePage columns - -```sql --- Remove large page config columns -ALTER TABLE "public"."community_configs" - DROP COLUMN IF EXISTS "home_page_config", - DROP COLUMN IF EXISTS "explore_page_config"; - --- Update function to exclude page configs -CREATE OR REPLACE FUNCTION "public"."get_active_community_config"( - p_community_id VARCHAR(50) -) -RETURNS JSONB -LANGUAGE plpgsql -SECURITY DEFINER -AS $$ -DECLARE - v_config JSONB; -BEGIN - SELECT jsonb_build_object( - 'brand', "brand_config", - 'assets', "assets_config", - 'theme', "theme_config", - 'community', "community_config", - 'fidgets', "fidgets_config", - 'navigation', "navigation_config", -- Contains spaceId references - 'ui', "ui_config" - ) - INTO v_config - FROM "public"."community_configs" - WHERE "community_id" = p_community_id - AND "is_active" = true - AND "is_published" = true - ORDER BY "version" DESC - LIMIT 1; - - RETURN v_config; -END; -$$; -``` - -### 5. Update Build-Time Config Generation - -**File:** `next.config.mjs` - -```javascript -async function generateConfigFile() { - // Fetch main config (now much smaller!) - const { data: config } = await supabase - .rpc('get_active_community_config', { p_community_id: community }) - .single(); - - if (!config) return; - - // Extract spaceIds from navigation items - const navItems = config.navigation?.items || []; - const spaceIds = navItems - .filter(item => item.spaceId) - .map(item => ({ navId: item.id, spaceId: item.spaceId })); - - // Fetch Spaces for nav items - const pageConfigs = {}; - for (const { navId, spaceId } of spaceIds) { - try { - // Fetch Space from spaceRegistrations + Storage - const space = await fetchSpaceBySpaceId(spaceId); - if (space) { - // Convert Space config to page config format - pageConfigs[navId] = convertSpaceToPageConfig(space); - } - } catch (error) { - console.warn(`⚠️ Failed to fetch Space ${spaceId} for nav item ${navId}:`, error.message); - } - } - - // Combine configs - const fullConfig = { - ...config, - pages: pageConfigs, // Add page configs - }; - - // Generate TypeScript file - const configFile = `// Auto-generated at build time -import { SystemConfig } from './systemConfig'; - -export const dbConfig: SystemConfig | null = ${JSON.stringify(fullConfig, null, 2)} as SystemConfig; -`; - - await writeFile('src/config/db-config.ts', configFile, 'utf-8'); -} - -async function fetchSpaceBySpaceId(spaceId: string) { - // Option 1: Fetch from spaceRegistrations + Storage - const { data: registration } = await supabase - .from('spaceRegistrations') - .select('*') - .eq('spaceId', spaceId) - .eq('spaceType', 'navPage') - .single(); - - if (!registration) return null; - - // Fetch Space config from Storage - const { data } = await supabase.storage - .from('spaces') - .download(`${spaceId}/tabs/default`); // Or fetch all tabs - - if (!data) return null; - - // Parse and return Space config - const fileData = JSON.parse(await data.text()); - return fileData; // Return SpaceConfig -} - -function convertSpaceToPageConfig(space: SpaceConfig): HomePageConfig { - // Convert Space config to HomePageConfig/ExplorePageConfig format - // This maps Space tabs to page tabs - return { - defaultTab: space.defaultTab || 'Home', - tabOrder: Object.keys(space.tabs || {}), - tabs: space.tabs || {}, - layout: { - defaultLayoutFidget: space.layout?.defaultLayoutFidget || 'grid', - gridSpacing: space.layout?.gridSpacing || 16, - theme: space.theme || {}, - }, - }; -} -``` - -### 6. Update Config Loader - -**File:** `src/config/index.ts` - -```typescript -export const loadSystemConfig = (): SystemConfig => { - const config = dbConfig || staticConfig; - - // Extract page configs from pages object (built from nav items) - const homePage = config.pages?.['home'] || staticConfig.homePage; - const explorePage = config.pages?.['explore'] || staticConfig.explorePage; - - return { - ...config, - homePage, // Map from pages['home'] - explorePage, // Map from pages['explore'] - }; -}; -``` - -## Space Storage Strategy - -### How Nav Pages Are Stored - -1. **Register in spaceRegistrations:** - ```sql - INSERT INTO spaceRegistrations ( - "spaceId", - "spaceName", - "spaceType", - "identityPublicKey", - "signature", - "timestamp" - ) VALUES ( - 'nouns-home-space-id', - 'nouns-home', - 'navPage', - 'system-identity-key', - 'signature', - NOW() - ); - ``` - -2. **Store config in Supabase Storage:** - - Path: `spaces/{spaceId}/tabs/{tabName}` - - Format: Same as other Spaces (SpaceConfig JSON) - -3. **Fetch at build time:** - - Query `spaceRegistrations` for `spaceType = 'navPage'` - - Download from Storage - - Parse and convert to page config format - -## Size Reduction - -**Before:** -- Config: ~29 KB -- homePage: 19.2 KB (66.5%) -- explorePage: 1.4 KB (4.8%) - -**After:** -- Config: ~8.4 KB (71% reduction!) -- Navigation: ~500 bytes (includes spaceId strings) -- Spaces: Fetched separately, no size limit - -**Result:** Config easily fits in env vars or generated file! - -## Migration Path - -1. **Add navPage spaceType** - Migration + TypeScript constants -2. **Create Spaces for existing pages** - Register homePage/explorePage as Spaces -3. **Update navigation configs** - Add spaceId to nav items -4. **Update build-time fetching** - Fetch Spaces based on nav items -5. **Remove page configs from schema** - Drop home_page_config/explore_page_config columns -6. **Update seed script** - Don't seed page configs, seed Spaces instead - -## Benefits - -✅ **Solves E2BIG** - Config size reduced by 71% -✅ **Unified architecture** - Everything is Spaces -✅ **Navigation as source of truth** - Nav defines what pages exist -✅ **Flexible** - Any nav item can reference a Space -✅ **Reuses infrastructure** - Uses existing Space system -✅ **No breaking changes** - Can maintain backward compatibility during migration - -## Example: Complete Flow - -### 1. Navigation Config (in DB) -```json -{ - "navigation": { - "items": [ - { "id": "home", "label": "Home", "href": "/home", "spaceId": "uuid-1" }, - { "id": "explore", "label": "Explore", "href": "/explore", "spaceId": "uuid-2" } - ] - } -} -``` - -### 2. Build Time -```javascript -// Fetch nav config → see spaceIds -// Fetch Spaces: uuid-1, uuid-2 -// Convert Spaces to page configs -// Generate: -{ - ...config, - pages: { - 'home': { /* Space config converted */ }, - 'explore': { /* Space config converted */ } - } -} -``` - -### 3. Runtime -```typescript -// Load config -const config = loadSystemConfig(); -// config.homePage comes from config.pages['home'] -// config.explorePage comes from config.pages['explore'] -``` - diff --git a/docs/_archived/database-config/NEXTJS_CONFIG_APPROACHES.md b/docs/_archived/database-config/NEXTJS_CONFIG_APPROACHES.md deleted file mode 100644 index 977038674..000000000 --- a/docs/_archived/database-config/NEXTJS_CONFIG_APPROACHES.md +++ /dev/null @@ -1,347 +0,0 @@ -# Next.js-Specific Approaches for Build-Time Config Generation - -Next.js offers several clever ways to handle build-time configuration generation. Here are the most Next.js-native approaches: - -## Approach 1: Generate in `next.config.mjs` (Recommended) - -**Most Next.js-Native**: Since `next.config.mjs` executes at build time, you can generate files directly there. - -### Implementation - -```javascript -// next.config.mjs - -import { generateConfigs } from './scripts/generate-configs.mjs'; - -// Generate configs before Next.js config is created -await generateConfigs(); - -import bundlerAnalyzer from "@next/bundle-analyzer"; -// ... rest of your config -``` - -**Pros:** -- ✅ Runs automatically before every build -- ✅ No need for `prebuild` hook -- ✅ Guaranteed to run before Next.js processes files -- ✅ Can use async/await -- ✅ Native Next.js approach - -**Cons:** -- ⚠️ Makes `next.config.mjs` more complex -- ⚠️ Harder to test in isolation - -### Example Implementation - -```javascript -// next.config.mjs - -import { createClient } from '@supabase/supabase-js'; -import { writeFile, mkdir } from 'fs/promises'; -import { join } from 'path'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -async function generateConfigs() { - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; - - if (!supabaseUrl || !supabaseKey) { - console.warn('⚠️ Missing Supabase env vars, skipping config generation'); - return; - } - - const supabase = createClient(supabaseUrl, supabaseKey); - const communities = process.env.GENERATE_CONFIGS?.split(',') || ['nouns']; - - console.log('📦 Generating configs from database...'); - - for (const communityId of communities) { - try { - const { data, error } = await supabase - .rpc('get_active_community_config', { p_community_id: communityId }) - .single(); - - if (error || !data) { - console.warn(`⚠️ Failed to fetch ${communityId} config:`, error?.message); - continue; - } - - const configDir = join(__dirname, 'src', 'config', communityId); - await mkdir(configDir, { recursive: true }); - - // Generate config files... - // (same as before) - - console.log(`✅ Generated config for ${communityId}`); - } catch (error) { - console.error(`❌ Error generating ${communityId} config:`, error); - } - } -} - -// Generate configs before creating Next.js config -await generateConfigs(); - -// Now create Next.js config -import bundlerAnalyzer from "@next/bundle-analyzer"; -// ... rest of config -``` - ---- - -## Approach 2: Webpack Plugin (Most Flexible) - -**Most Flexible**: Create a custom webpack plugin that generates files during the build process. - -### Implementation - -```javascript -// scripts/config-generator-plugin.js - -class ConfigGeneratorPlugin { - apply(compiler) { - compiler.hooks.beforeCompile.tapAsync( - 'ConfigGeneratorPlugin', - async (params, callback) => { - try { - await generateConfigs(); - callback(); - } catch (error) { - console.error('Config generation failed:', error); - callback(error); - } - } - ); - } -} - -module.exports = ConfigGeneratorPlugin; -``` - -```javascript -// next.config.mjs - -import ConfigGeneratorPlugin from './scripts/config-generator-plugin.js'; - -const nextConfig = { - webpack: (config, { isServer }) => { - if (!isServer) { - config.plugins.push(new ConfigGeneratorPlugin()); - } - return config; - }, - // ... rest of config -}; -``` - -**Pros:** -- ✅ Runs during webpack compilation -- ✅ Can access webpack context -- ✅ More control over when it runs -- ✅ Can be conditional based on build mode - -**Cons:** -- ⚠️ More complex setup -- ⚠️ Requires understanding webpack hooks -- ⚠️ Runs during compilation (slightly later than `next.config.mjs`) - ---- - -## Approach 3: Next.js Environment Variables with Build Script - -**Simplest**: Use environment variables that are loaded at build time, combined with a build script. - -### Implementation - -```javascript -// next.config.mjs - -const nextConfig = { - env: { - // These are loaded at build time - NEXT_PUBLIC_CONFIG_VERSION: process.env.CONFIG_VERSION || 'static', - }, - // ... rest of config -}; -``` - -```json -// package.json -{ - "scripts": { - "generate-configs": "node scripts/generate-configs.mjs", - "build": "npm run generate-configs && next build" - } -} -``` - -**Pros:** -- ✅ Simple and explicit -- ✅ Easy to understand -- ✅ Can be skipped if needed - -**Cons:** -- ⚠️ Requires remembering to run script -- ⚠️ Can be forgotten in CI/CD - ---- - -## Approach 4: Next.js Server Components (For Runtime Config) - -**Note**: This is for runtime config, not build-time, but worth mentioning. - -If you wanted runtime config (not recommended for your use case), you could use Server Components: - -```typescript -// app/config-provider.tsx (Server Component) - -import { createClient } from '@supabase/supabase-js'; - -export async function ConfigProvider({ children }) { - const supabase = createClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! - ); - - const { data } = await supabase - .rpc('get_active_community_config', { - p_community_id: process.env.NEXT_PUBLIC_COMMUNITY || 'nouns' - }) - .single(); - - return ( - - {children} - - ); -} -``` - -**Pros:** -- ✅ No build-time generation needed -- ✅ Always up-to-date - -**Cons:** -- ❌ Runtime database queries (not what you want) -- ❌ Slower page loads -- ❌ Requires database connection - ---- - -## Approach 5: Next.js `generateStaticParams` Pattern (For Routes) - -**For Route-Based Configs**: If configs were route-specific, you could use `generateStaticParams`: - -```typescript -// app/[community]/layout.tsx - -export async function generateStaticParams() { - // Fetch all communities from DB at build time - const communities = await fetchCommunitiesFromDB(); - return communities.map(community => ({ - community: community.id, - })); -} - -export default async function CommunityLayout({ params }) { - // This runs at build time for each community - const config = await fetchConfigForCommunity(params.community); - return ...; -} -``` - -**Pros:** -- ✅ Native Next.js pattern -- ✅ Build-time generation -- ✅ Per-route optimization - -**Cons:** -- ⚠️ Only works for route-based configs -- ⚠️ Not ideal for global configs - ---- - -## Recommended Approach: Hybrid - -**Best of Both Worlds**: Combine `next.config.mjs` generation with a fallback script. - -```javascript -// next.config.mjs - -import { generateConfigs } from './scripts/generate-configs.mjs'; - -// Try to generate configs, but don't fail build if it fails -try { - await generateConfigs(); -} catch (error) { - console.warn('⚠️ Config generation failed, using static configs:', error.message); -} - -// Continue with Next.js config... -``` - -```json -// package.json -{ - "scripts": { - "generate-configs": "node scripts/generate-configs.mjs", - "prebuild": "npm run generate-configs || true", // Don't fail if generation fails - "build": "next build" - } -} -``` - -**Benefits:** -- ✅ Automatic generation in `next.config.mjs` -- ✅ Fallback script for manual runs -- ✅ Build doesn't fail if DB unavailable -- ✅ Works in all environments - ---- - -## Comparison Table - -| Approach | When It Runs | Complexity | Next.js Native | Recommended | -|----------|--------------|------------|----------------|-------------| -| `next.config.mjs` | Before config load | Low | ✅ Yes | ⭐⭐⭐⭐⭐ | -| Webpack Plugin | During compilation | Medium | ✅ Yes | ⭐⭐⭐⭐ | -| Prebuild Script | Before build | Low | ⚠️ No | ⭐⭐⭐ | -| Server Components | Runtime | Low | ✅ Yes | ❌ No (runtime) | -| `generateStaticParams` | Build time | Medium | ✅ Yes | ⭐⭐ (route-specific) | - ---- - -## Recommended Implementation - -For your use case, I recommend **Approach 1** (`next.config.mjs`) because: - -1. ✅ **Most Next.js-native** - Uses Next.js's build-time execution -2. ✅ **Automatic** - Runs before every build without extra scripts -3. ✅ **Simple** - No webpack plugins or complex setup -4. ✅ **Reliable** - Guaranteed to run before Next.js processes files -5. ✅ **Flexible** - Can easily add error handling and fallbacks - -### Final Implementation - -```javascript -// next.config.mjs - -import { generateConfigs } from './scripts/generate-configs.mjs'; - -// Generate configs at build time -await generateConfigs().catch(error => { - console.warn('⚠️ Config generation failed, using static configs'); - console.warn(error.message); -}); - -// Continue with your existing Next.js config -import bundlerAnalyzer from "@next/bundle-analyzer"; -// ... rest of config -``` - -This gives you the best of both worlds: automatic generation with graceful fallback to static configs. - diff --git a/docs/_archived/database-config/README.md b/docs/_archived/database-config/README.md deleted file mode 100644 index 4c079081c..000000000 --- a/docs/_archived/database-config/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# Archived Database Config Documentation - -This directory contains documentation files that were created during the exploration and planning phase of the database-backed configuration system. These files have been consolidated into: - -- `DATABASE_CONFIG_GUIDE.md` - Main architecture and overview guide -- `DATABASE_CONFIG_IMPLEMENTATION.md` - Detailed implementation plan -- `QUICK_START_IMPLEMENTATION.md` - Quick start guide -- `QUICK_START_TESTING.md` - Testing guide - -## Why These Were Archived - -These files contain: -- Exploration of different approaches (some we decided against) -- Outdated information (e.g., TypeScript file generation instead of env vars) -- Redundant information (consolidated into main docs) -- Early planning details (superseded by final implementation) - -## Files Archived - -- `DATABASE_CONFIG_MIGRATION_PLAN.md` - Original migration plan (had themes/pages in DB) -- `BUILD_TIME_CONFIG_SUMMARY.md` - Mentioned TS file generation (outdated) -- `E2BIG_SOLUTION.md` - Solution for E2BIG error (now use env vars) -- `UPDATED_IMPLEMENTATION_SUMMARY.md` - Summary with outdated TS file info -- `NAVIGATION_SPACE_REFERENCE_APPROACH.md` - Consolidated into main guide -- `NAVIGATION_SPACE_REFERENCE_IMPLEMENTATION.md` - Consolidated into main guide -- `SHARED_THEMES_APPROACH.md` - Consolidated into main guide -- `SPACE_REFERENCE_APPROACH.md` - Consolidated into main guide -- `SIMPLE_BUILD_TIME_CONFIG.md` - Exploration doc -- `NEXTJS_CONFIG_APPROACHES.md` - Exploration doc -- `CONFIG_DATABASE_SCHEMA_DETAILED.md` - Consolidated into main guide -- `ASSET_HANDLING_STRATEGY.md` - Consolidated (future phase) -- `ADMIN_ASSET_UPLOAD_STRATEGY.md` - Consolidated (future phase) -- `ASSET_PERFORMANCE_OPTIMIZATION.md` - Consolidated (future phase) -- `ASSETS_CONFIG_STORAGE_RELATIONSHIP.md` - Consolidated (future phase) - -## Current Approach - -See the main documentation files for the current, finalized approach: -- Configs stored in DB (~2.8 KB) -- Loaded at build time into `NEXT_PUBLIC_BUILD_TIME_CONFIG` env var -- Themes in shared file (`src/config/shared/themes.ts`) -- Pages as Spaces (referenced by navigation items) - diff --git a/docs/_archived/database-config/SHARED_THEMES_APPROACH.md b/docs/_archived/database-config/SHARED_THEMES_APPROACH.md deleted file mode 100644 index 5b52cbe84..000000000 --- a/docs/_archived/database-config/SHARED_THEMES_APPROACH.md +++ /dev/null @@ -1,255 +0,0 @@ -# Shared Themes Approach - -## Overview - -Move themes out of individual community configs into a shared file, since themes are reusable across communities with only minor customizations. - -## Current State - -- Each community has its own `{community}.theme.ts` file -- Themes are mostly identical (same structure, different values) -- Themes take up 5.5 KB (66.7% of remaining config) - -## Proposed Location Options - -### Option 1: `src/config/shared/themes.ts` (Recommended) - -**Structure:** -``` -src/config/ -├── shared/ -│ └── themes.ts # Shared theme definitions -├── nouns/ -│ └── nouns.theme.ts # Community-specific overrides (optional) -└── ... -``` - -**Pros:** -- ✅ Clear organization - shared configs in `shared/` folder -- ✅ Easy to find - obvious location -- ✅ Extensible - can add other shared configs later -- ✅ Separates shared from community-specific - -**Cons:** -- ⚠️ New directory structure - -### Option 2: `src/config/themes.ts` - -**Structure:** -``` -src/config/ -├── themes.ts # Shared themes at root -├── nouns/ -└── ... -``` - -**Pros:** -- ✅ Simple - no new directory -- ✅ Easy to import - -**Cons:** -- ⚠️ Mixes shared with community configs -- ⚠️ Less clear organization - -### Option 3: `src/common/config/themes.ts` - -**Structure:** -``` -src/common/ -├── config/ -│ └── themes.ts # Shared themes -└── ... -``` - -**Pros:** -- ✅ In `common/` (shared code) -- ✅ Separated from community configs - -**Cons:** -- ⚠️ New directory structure -- ⚠️ Mixes config with common utilities - -## Recommendation: `src/config/shared/themes.ts` - -**Why:** -1. **Clear organization** - `shared/` folder makes intent obvious -2. **Extensible** - Can add other shared configs (e.g., `shared/defaultFidgets.ts`) -3. **Consistent** - Keeps configs in config directory -4. **Easy imports** - `import { themes } from '@/config/shared/themes'` - -## Implementation Approach - -### Option A: Single Shared File (All Themes) - -Store all theme variants in one shared file: - -```typescript -// src/config/shared/themes.ts - -export const sharedThemes = { - default: { /* ... */ }, - nounish: { /* ... */ }, - gradientAndWave: { /* ... */ }, - colorBlobs: { /* ... */ }, - floatingShapes: { /* ... */ }, - imageParallax: { /* ... */ }, - shootingStar: { /* ... */ }, - squareGrid: { /* ... */ }, - tesseractPattern: { /* ... */ }, - retro: { /* ... */ }, -}; -``` - -**Pros:** -- ✅ Single source of truth -- ✅ Easy to maintain -- ✅ All communities use same themes - -**Cons:** -- ⚠️ No community customization -- ⚠️ Can't override specific themes per community - -### Option B: Shared Base + Community Overrides - -Store base themes in shared file, allow community-specific overrides: - -```typescript -// src/config/shared/themes.ts - -export const baseThemes = { - default: { /* ... */ }, - nounish: { /* ... */ }, - // ... all themes -}; - -// src/config/nouns/nouns.theme.ts - -import { baseThemes } from '../../shared/themes'; - -export const nounsTheme = { - ...baseThemes, - // Override specific themes if needed - default: { - ...baseThemes.default, - properties: { - ...baseThemes.default.properties, - musicURL: "https://...", // Nouns-specific music - }, - }, -}; -``` - -**Pros:** -- ✅ Shared base themes -- ✅ Allows community customization -- ✅ Best of both worlds - -**Cons:** -- ⚠️ More complex -- ⚠️ Need merge logic - -### Option C: Shared Themes + Community Theme Config - -Store themes in shared file, reference from community config: - -```typescript -// src/config/shared/themes.ts - -export const themes = { - default: { /* ... */ }, - nounish: { /* ... */ }, - // ... all themes -}; - -// src/config/nouns/nouns.theme.ts - -import { themes } from '../../shared/themes'; - -// Just export shared themes (or override if needed) -export const nounsTheme = themes; -``` - -**Pros:** -- ✅ Simplest approach -- ✅ Single source of truth -- ✅ Easy to use - -**Cons:** -- ⚠️ No community customization (but maybe that's fine?) - -## Recommended: Option C (Simple Shared Reference) - -**Why:** -- Themes are visual templates, not community-specific -- Communities can customize via theme editor at runtime -- Keeps config simple and maintainable - -## Database Storage - -**Option 1: Store in Database as Shared Resource** - -```sql -CREATE TABLE shared_themes ( - id UUID PRIMARY KEY, - theme_id VARCHAR(50) UNIQUE, -- 'default', 'nounish', etc. - theme_config JSONB NOT NULL, - version INTEGER DEFAULT 1, - created_at TIMESTAMP DEFAULT NOW() -); -``` - -**Option 2: Store in Code (Recommended for Now)** - -Keep themes in code, reference from config: -- Themes are code/templates, not data -- Easier to version control -- Can move to DB later if needed - -## Size Impact - -**Before:** -- theme: 5.5 KB (66.7% of config) - -**After:** -- theme: Removed from config (0 KB) -- Config size: ~2.8 KB (down from 8.3 KB) -- **Total reduction: 90%** (from 29 KB to 2.8 KB) - -## Migration Steps - -1. **Create `src/config/shared/themes.ts`** - - Move theme definitions from nouns.theme.ts - - Export as `themes` object - -2. **Update community configs** - - Change `nouns.theme.ts` to import from shared - - Update other communities similarly - -3. **Update SystemConfig interface** - - Keep `theme: ThemeConfig` in interface - - But it now references shared themes - -4. **Update database schema** - - Remove `theme_config` column (or keep as reference) - - Or store theme reference IDs - -5. **Update build-time config** - - Themes loaded from shared file, not DB - - Or fetch from DB if storing there - -## Final Config Size - -After removing homePage, explorePage, and theme: - -| Section | Size | Percentage | -|---------|------|------------| -| community | 1.4 KB | 50% | -| navigation | 0.5 KB | 18% | -| assets | 0.3 KB | 11% | -| brand | 0.2 KB | 7% | -| fidgets | 0.2 KB | 7% | -| ui | 0.2 KB | 7% | -| **TOTAL** | **~2.8 KB** | **100%** | - -**Result:** Config is now tiny! Easily fits in env vars or generated file. - diff --git a/docs/_archived/database-config/SIMPLE_BUILD_TIME_CONFIG.md b/docs/_archived/database-config/SIMPLE_BUILD_TIME_CONFIG.md deleted file mode 100644 index cd256b262..000000000 --- a/docs/_archived/database-config/SIMPLE_BUILD_TIME_CONFIG.md +++ /dev/null @@ -1,424 +0,0 @@ -# Simple Build-Time Config: Variable Assignment Approach - -## Overview - -Instead of generating multiple TypeScript files, we can simply fetch configs from the database at build time and assign them to variables. Much simpler! - -## Approach 1: Single Generated Config Module (Recommended) - -Create a single module that fetches and exports configs at build time. - -### Implementation - -```typescript -// src/config/generated.ts -// This file is generated at build time by next.config.mjs - -import { SystemConfig } from './systemConfig'; - -// Fetch from DB at build time and assign here -// If DB fetch fails, these will be undefined and we fall back to static configs - -export const generatedConfigs: Record = { - // These are populated at build time by next.config.mjs - nouns: undefined, // Will be replaced with DB config if available - clanker: undefined, - example: undefined, -}; -``` - -```javascript -// next.config.mjs - -import { createClient } from '@supabase/supabase-js'; -import { writeFile } from 'fs/promises'; -import { fileURLToPath } from 'url'; -import { dirname, join } from 'path'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -async function generateConfigModule() { - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; - - if (!supabaseUrl || !supabaseKey) { - console.warn('⚠️ Missing Supabase env vars, skipping config generation'); - return; - } - - const supabase = createClient(supabaseUrl, supabaseKey); - const communities = ['nouns', 'clanker', 'example']; - const configs = {}; - - console.log('📦 Fetching configs from database...'); - - for (const communityId of communities) { - try { - const { data, error } = await supabase - .rpc('get_active_community_config', { p_community_id: communityId }) - .single(); - - if (error || !data) { - console.warn(`⚠️ Failed to fetch ${communityId} config:`, error?.message); - configs[communityId] = null; // Will use static fallback - continue; - } - - configs[communityId] = data; - console.log(`✅ Fetched config for ${communityId}`); - } catch (error) { - console.error(`❌ Error fetching ${communityId} config:`, error); - configs[communityId] = null; - } - } - - // Generate the config module - const configModule = `// Auto-generated at build time - DO NOT EDIT -import { SystemConfig } from './systemConfig'; - -export const generatedConfigs: Record = ${JSON.stringify(configs, null, 2)}; -`; - - const filePath = join(__dirname, 'src', 'config', 'generated.ts'); - await writeFile(filePath, configModule, 'utf-8'); - console.log('✅ Generated config module'); -} - -// Generate config module before Next.js config -await generateConfigModule().catch(error => { - console.warn('⚠️ Config generation failed, will use static configs'); -}); - -// Continue with Next.js config... -import bundlerAnalyzer from "@next/bundle-analyzer"; -// ... rest of config -``` - -```typescript -// src/config/index.ts - -import { SystemConfig } from './systemConfig'; -import { nounsSystemConfig } from './nouns/index'; -import { exampleSystemConfig } from './example/index'; -import { clankerSystemConfig } from './clanker/index'; - -// Import generated configs (may not exist if generation failed) -let generatedConfigs: Record = {}; -try { - const generated = await import('./generated'); - generatedConfigs = generated.generatedConfigs || {}; -} catch { - // Generated file doesn't exist, use static configs -} - -// Static fallbacks -const STATIC_CONFIGS: Record = { - nouns: nounsSystemConfig, - example: exampleSystemConfig, - clanker: clankerSystemConfig as unknown as SystemConfig, -}; - -export const loadSystemConfig = (): SystemConfig => { - const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - const community = communityConfig.toLowerCase(); - - // Use generated config if available, otherwise fall back to static - const config = generatedConfigs[community] || STATIC_CONFIGS[community]; - - if (!config) { - console.warn( - `Invalid community configuration: "${communityConfig}". ` + - `Falling back to "nouns" configuration.` - ); - return STATIC_CONFIGS.nouns; - } - - return config; -}; -``` - ---- - -## Approach 2: Direct Assignment in `next.config.mjs` - -Even simpler - assign configs directly in `next.config.mjs` and export them. - -### Implementation - -```javascript -// next.config.mjs - -import { createClient } from '@supabase/supabase-js'; - -// Fetch configs at build time -let dbConfigs = {}; - -async function fetchConfigs() { - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; - - if (!supabaseUrl || !supabaseKey) { - console.warn('⚠️ Missing Supabase env vars, using static configs'); - return {}; - } - - const supabase = createClient(supabaseUrl, supabaseKey); - const community = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - - try { - const { data, error } = await supabase - .rpc('get_active_community_config', { p_community_id: community }) - .single(); - - if (error || !data) { - console.warn('⚠️ Failed to fetch config, using static'); - return {}; - } - - return { [community]: data }; - } catch (error) { - console.warn('⚠️ Error fetching config:', error.message); - return {}; - } -} - -// Fetch configs before creating Next.js config -dbConfigs = await fetchConfigs().catch(() => ({})); - -// Export configs for use in the app -// We'll make them available via environment variables or a module - -// Continue with Next.js config... -import bundlerAnalyzer from "@next/bundle-analyzer"; -// ... rest of config - -export default withBundleAnalyzer(nextConfig); -``` - -**Problem:** `next.config.mjs` exports Next.js config, not app config. We need a different approach. - ---- - -## Approach 3: Environment Variables (Simplest!) - -Set configs as environment variables at build time. - -### Implementation - -```javascript -// next.config.mjs - -import { createClient } from '@supabase/supabase-js'; - -async function setConfigEnvVars() { - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; - - if (!supabaseUrl || !supabaseKey) { - return; // Use static configs - } - - const supabase = createClient(supabaseUrl, supabaseKey); - const community = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - - try { - const { data, error } = await supabase - .rpc('get_active_community_config', { p_community_id: community }) - .single(); - - if (!error && data) { - // Set as environment variable (available at build time) - process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(data); - console.log('✅ Loaded config from database'); - } - } catch (error) { - console.warn('⚠️ Failed to load config from DB:', error.message); - } -} - -await setConfigEnvVars(); - -// Continue with Next.js config... -``` - -```typescript -// src/config/index.ts - -import { SystemConfig } from './systemConfig'; -import { nounsSystemConfig } from './nouns/index'; -// ... other static configs - -export const loadSystemConfig = (): SystemConfig => { - const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - - // Try to use build-time config from env var - const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; - if (buildTimeConfig) { - try { - const config = JSON.parse(buildTimeConfig) as SystemConfig; - if (config) return config; - } catch (error) { - console.warn('Failed to parse build-time config:', error); - } - } - - // Fall back to static configs - switch (communityConfig.toLowerCase()) { - case 'nouns': - return nounsSystemConfig; - case 'example': - return exampleSystemConfig; - case 'clanker': - return clankerSystemConfig as unknown as SystemConfig; - default: - return nounsSystemConfig; - } -}; -``` - -**Pros:** -- ✅ Simplest approach -- ✅ No file generation -- ✅ No new modules -- ✅ Works with Next.js env var system - -**Cons:** -- ⚠️ Large configs in env vars (but Next.js handles this fine) -- ⚠️ Need to parse JSON - ---- - -## Approach 4: Single Config Module with Direct Import (Best!) - -Create a single module that's generated at build time, but keep it simple. - -### Implementation - -```javascript -// next.config.mjs - -import { createClient } from '@supabase/supabase-js'; -import { writeFile } from 'fs/promises'; -import { fileURLToPath } from 'url'; -import { dirname, join } from 'path'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -async function generateConfig() { - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; - - if (!supabaseUrl || !supabaseKey) { - return; // Will use static configs - } - - const supabase = createClient(supabaseUrl, supabaseKey); - const community = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - - try { - const { data, error } = await supabase - .rpc('get_active_community_config', { p_community_id: community }) - .single(); - - if (error || !data) { - console.warn('⚠️ No config found in DB, using static'); - return; - } - - // Generate simple module with just the config - const moduleContent = `// Auto-generated at build time -import { SystemConfig } from './systemConfig'; - -export const dbConfig: SystemConfig | null = ${JSON.stringify(data, null, 2)} as SystemConfig; -`; - - const filePath = join(__dirname, 'src', 'config', 'db-config.ts'); - await writeFile(filePath, moduleContent, 'utf-8'); - console.log('✅ Generated config from database'); - } catch (error) { - console.warn('⚠️ Error generating config:', error.message); - } -} - -await generateConfig(); - -// Continue with Next.js config... -``` - -```typescript -// src/config/index.ts - -import { SystemConfig } from './systemConfig'; -import { nounsSystemConfig } from './nouns/index'; -import { exampleSystemConfig } from './example/index'; -import { clankerSystemConfig } from './clanker/index'; - -// Try to import DB config (may not exist) -let dbConfig: SystemConfig | null = null; -try { - const dbModule = require('./db-config'); - dbConfig = dbModule.dbConfig; -} catch { - // File doesn't exist, will use static -} - -// Static fallbacks -const STATIC_CONFIGS: Record = { - nouns: nounsSystemConfig, - example: exampleSystemConfig, - clanker: clankerSystemConfig as unknown as SystemConfig, -}; - -export const loadSystemConfig = (): SystemConfig => { - const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - - // Use DB config if available and matches current community - if (dbConfig) { - return dbConfig; - } - - // Fall back to static configs - const config = STATIC_CONFIGS[communityConfig.toLowerCase()]; - return config || STATIC_CONFIGS.nouns; -}; -``` - -**Pros:** -- ✅ Single file generation (simple) -- ✅ Direct variable assignment -- ✅ Type-safe -- ✅ Easy to understand -- ✅ Clean fallback - ---- - -## Comparison - -| Approach | Files Generated | Complexity | Type Safety | Recommended | -|----------|----------------|------------|-------------|-------------| -| **Single Module** | 1 file | Low | ✅ Yes | ⭐⭐⭐⭐⭐ | -| **Env Variables** | 0 files | Very Low | ⚠️ Manual | ⭐⭐⭐⭐ | -| **Multiple Files** | Many files | High | ✅ Yes | ⭐⭐⭐ | - ---- - -## Recommended: Approach 4 (Single Config Module) - -**Why:** -- ✅ Generates only ONE file (`db-config.ts`) -- ✅ Simple variable assignment (`dbConfig`) -- ✅ Type-safe (imports `SystemConfig`) -- ✅ Easy to understand -- ✅ Clean fallback to static configs -- ✅ No complex file structure - -**Implementation:** - -1. **`next.config.mjs`** - Fetches config, generates single `db-config.ts` file -2. **`src/config/index.ts`** - Imports `dbConfig`, falls back to static if not available -3. **That's it!** - Much simpler than generating multiple files - -This gives you all the benefits with minimal complexity! - diff --git a/docs/_archived/database-config/SPACE_REFERENCE_APPROACH.md b/docs/_archived/database-config/SPACE_REFERENCE_APPROACH.md deleted file mode 100644 index 873f1cafc..000000000 --- a/docs/_archived/database-config/SPACE_REFERENCE_APPROACH.md +++ /dev/null @@ -1,202 +0,0 @@ -# Space Reference Approach for Page Configs - -## The Idea - -Instead of storing `homePage` and `explorePage` configs (71% of total size) directly in `community_configs`, store them as Spaces and reference them by ID. - -## Current Problem - -- `homePage`: 19.2 KB (66.5% of config) -- `explorePage`: 1.4 KB (4.8% of config) -- **Combined: 20.6 KB (71.3% of total)** - -## Proposed Solution - -### Option A: Store as Spaces in Supabase Storage - -**How it works:** -1. Create Spaces for `homePage` and `explorePage` in Supabase Storage -2. Store Space IDs in `community_configs`: - ```json - { - "homePageSpaceId": "uuid-here", - "explorePageSpaceId": "uuid-here" - } - ``` -3. Fetch Spaces at build time along with config - -**Pros:** -- ✅ Uses existing Space infrastructure -- ✅ Dramatically reduces config size (~71% reduction) -- ✅ Spaces can be edited independently -- ✅ Reuses Space storage/retrieval logic - -**Cons:** -- ⚠️ Requires fetching Spaces at build time (additional DB calls) -- ⚠️ Spaces stored in Storage, not database (need to check how they're accessed) -- ⚠️ More complex build-time logic - -### Option B: Store in Separate Database Table - -**How it works:** -1. Create `community_page_configs` table: - ```sql - CREATE TABLE community_page_configs ( - id UUID PRIMARY KEY, - community_id VARCHAR(50), - page_type VARCHAR(20), -- 'homePage' or 'explorePage' - config JSONB NOT NULL, - version INTEGER DEFAULT 1, - created_at TIMESTAMP DEFAULT NOW() - ); - ``` -2. Store page configs there -3. Reference by ID in `community_configs`: - ```json - { - "homePageConfigId": "uuid-here", - "explorePageConfigId": "uuid-here" - } - ``` -4. Fetch at build time with a JOIN or separate query - -**Pros:** -- ✅ Database-backed (easier to query/manage) -- ✅ Version history possible -- ✅ Dramatically reduces config size -- ✅ Can use JOINs for efficient fetching - -**Cons:** -- ⚠️ Requires new table -- ⚠️ Additional build-time query - -### Option C: Store in Same Table, Separate Columns (Simplest) - -**How it works:** -1. Keep `home_page_config` and `explore_page_config` columns -2. But fetch them separately at build time -3. Only include IDs in the main config JSONB - -**Pros:** -- ✅ Simplest implementation -- ✅ No schema changes needed -- ✅ Still reduces size if we only store IDs - -**Cons:** -- ⚠️ Still stores full configs in database (just separately) -- ⚠️ Doesn't solve the E2BIG issue if we're putting them in env vars - -## Size Reduction Analysis - -### Current: -```json -{ - "homePage": { /* 19.2 KB */ }, - "explorePage": { /* 1.4 KB */ } -} -``` -**Total: 20.6 KB** - -### With References: -```json -{ - "homePageSpaceId": "550e8400-e29b-41d4-a716-446655440000", // ~36 bytes - "explorePageSpaceId": "550e8400-e29b-41d4-a716-446655440001" // ~36 bytes -} -``` -**Total: ~72 bytes** - -**Reduction: 99.6%** (from 20.6 KB to 72 bytes) - -### New Total Config Size: -- Current: ~29 KB -- With references: ~8.4 KB (29 KB - 20.6 KB + 72 bytes) -- **Reduction: 71%** - -## Implementation Considerations - -### Build-Time Fetching - -```javascript -// next.config.mjs - -async function generateConfigFile() { - // Fetch main config - const { data: config } = await supabase - .rpc('get_active_community_config', { p_community_id: community }) - .single(); - - // Fetch page configs separately - const { data: homePage } = await supabase - .from('community_page_configs') - .select('config') - .eq('community_id', community) - .eq('page_type', 'homePage') - .single(); - - const { data: explorePage } = await supabase - .from('community_page_configs') - .select('config') - .eq('community_id', community) - .eq('page_type', 'explorePage') - .single(); - - // Combine - const fullConfig = { - ...config, - homePage: homePage?.config, - explorePage: explorePage?.config, - }; - - // Generate file (now much smaller!) - await writeFile('src/config/db-config.ts', ...); -} -``` - -### Database Function Update - -```sql --- Updated function that excludes page configs -CREATE OR REPLACE FUNCTION get_active_community_config(...) -RETURNS JSONB AS $$ - SELECT jsonb_build_object( - 'brand', brand_config, - 'assets', assets_config, - 'theme', theme_config, - 'community', community_config, - 'fidgets', fidgets_config, - 'homePageSpaceId', home_page_space_id, -- Just the ID - 'explorePageSpaceId', explore_page_space_id, -- Just the ID - 'navigation', navigation_config, - 'ui', ui_config - ) - FROM community_configs - WHERE ... -$$; -``` - -## Recommendation - -**Option B (Separate Table)** because: -1. ✅ Database-backed (easier to manage) -2. ✅ Can add versioning/history -3. ✅ Efficient queries -4. ✅ Dramatically reduces config size -5. ✅ Clear separation of concerns - -## Migration Path - -1. Create `community_page_configs` table -2. Migrate existing `homePage` and `explorePage` configs to new table -3. Update `community_configs` to store IDs instead -4. Update build-time fetching to include page configs -5. Update `get_active_community_config` function - -## Benefits - -- ✅ **Solves E2BIG** - Config now ~8.4 KB (well under limits) -- ✅ **Better organization** - Page configs separate from community config -- ✅ **Easier editing** - Admins can edit pages independently -- ✅ **Versioning** - Can version page configs separately -- ✅ **Reusability** - Page configs could be shared/referenced - diff --git a/docs/_archived/database-config/UPDATED_IMPLEMENTATION_SUMMARY.md b/docs/_archived/database-config/UPDATED_IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index c62f9d0fd..000000000 --- a/docs/_archived/database-config/UPDATED_IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,147 +0,0 @@ -# Updated Implementation Plan Summary - -## Key Architectural Changes - -### 1. **Navigation-Space Reference** -- `homePage` and `explorePage` removed from `community_configs` -- Navigation items reference Spaces via `spaceId` -- Pages fetched from Spaces at build time -- **Size reduction: 71%** (removes 20.6 KB) - -### 2. **Shared Themes** -- Themes moved to `src/config/shared/themes.ts` -- All communities use same shared themes -- Themes removed from `community_configs` -- **Size reduction: 66%** (removes 5.5 KB) - -### 3. **File-Based Config (Not Env Var)** -- Config generated as TypeScript file (`src/config/db-config.ts`) -- Avoids E2BIG error (env var size limits) -- No size restrictions - -## Final Config Structure - -### In Database (`community_configs`): -```json -{ - "brand_config": { /* 0.2 KB */ }, - "assets_config": { /* 0.3 KB */ }, - "community_config": { /* 1.4 KB */ }, - "fidgets_config": { /* 0.2 KB */ }, - "navigation_config": { /* 0.5 KB - includes spaceId references */ }, - "ui_config": { /* 0.2 KB */ } - // NO theme_config (in shared file) - // NO home_page_config (in Spaces) - // NO explore_page_config (in Spaces) -} -``` - -**Total: ~2.8 KB** (down from ~29 KB - **90% reduction!**) - -### In Code (`src/config/shared/themes.ts`): -```typescript -export const themes = { - default: { /* ... */ }, - nounish: { /* ... */ }, - // ... all 10 themes -}; -``` - -### In Spaces (via navigation): -- `homePage` → Space referenced by nav item `spaceId` -- `explorePage` → Space referenced by nav item `spaceId` -- Stored in `spaceRegistrations` with `spaceType = 'navPage'` -- Config stored in Supabase Storage - -## Updated Database Schema - -### `community_configs` Table: -```sql -CREATE TABLE community_configs ( - id UUID PRIMARY KEY, - community_id VARCHAR(50) UNIQUE, - brand_config JSONB, -- ✅ Kept - assets_config JSONB, -- ✅ Kept - community_config JSONB, -- ✅ Kept - fidgets_config JSONB, -- ✅ Kept - navigation_config JSONB, -- ✅ Kept (now includes spaceId) - ui_config JSONB, -- ✅ Kept - -- ❌ REMOVED: theme_config (in shared file) - -- ❌ REMOVED: home_page_config (in Spaces) - -- ❌ REMOVED: explore_page_config (in Spaces) -); -``` - -### `spaceRegistrations` Table: -```sql --- Add navPage spaceType -ALTER TABLE spaceRegistrations - ADD CONSTRAINT valid_space_type CHECK ( - "spaceType" IN ('profile', 'token', 'proposal', 'channel', 'navPage') - ); -``` - -## Updated Build Process - -```javascript -// next.config.mjs - -1. Fetch main config from DB (small - ~2.8 KB) -2. Import shared themes from code -3. Extract spaceIds from navigation items -4. Fetch Spaces for nav items -5. Convert Spaces to page configs -6. Combine: config + themes + pages -7. Generate TypeScript file -``` - -## Updated Config Loader - -```typescript -// src/config/index.ts - -1. Try to import db-config.ts (generated file) -2. If exists: - - Use DB config - - Add shared themes - - Map pages['home'] → homePage - - Map pages['explore'] → explorePage -3. If not exists: - - Fall back to static configs -``` - -## Size Comparison - -| Stage | Config Size | Reduction | -|-------|-------------|-----------| -| **Original** | ~29 KB | - | -| **After removing pages** | ~8.3 KB | 71% | -| **After removing themes** | **~2.8 KB** | **90%** | - -## Migration Impact - -### Phase 1 (Database Schema): -- ✅ Create `community_configs` (without page/theme columns) -- ✅ Add `navPage` spaceType -- ✅ Seed configs (without themes/pages) - -### Phase 2 (Config Loading): -- ✅ Create `src/config/shared/themes.ts` -- ✅ Update community configs to import shared themes -- ✅ Fetch Spaces for nav items at build time -- ✅ Generate TypeScript file (not env var) - -### Phase 3+ (Admin/UI): -- ✅ Admin can edit config (smaller now) -- ✅ Admin can edit nav page Spaces -- ✅ Themes edited in code (shared file) - -## Benefits Summary - -✅ **Solves E2BIG** - Config now ~2.8 KB (well under limits) -✅ **Unified architecture** - Pages are Spaces -✅ **Shared themes** - Single source of truth -✅ **Navigation as source of truth** - Nav defines pages -✅ **Flexible** - Any nav item can reference a Space -✅ **Maintainable** - Clear separation of concerns - From c9dc6a87b0513161e83bc8a040db0f28e21d0700 Mon Sep 17 00:00:00 2001 From: Jesse Paterson Date: Sun, 30 Nov 2025 15:55:12 -0600 Subject: [PATCH 112/155] minor sourcing fix --- docs/README.md | 1 + docs/SYSTEMS/CONFIGURATION/OVERVIEW.md | 1 + docs/SYSTEMS/CONFIGURATION/TESTING.md | 408 +++++++++++++++++++++++++ src/config/index.ts | 3 + src/config/shared/themes.ts | 12 + src/constants/themes.ts | 44 ++- 6 files changed, 446 insertions(+), 23 deletions(-) create mode 100644 docs/SYSTEMS/CONFIGURATION/TESTING.md create mode 100644 src/config/shared/themes.ts diff --git a/docs/README.md b/docs/README.md index c3c610951..ed21dcf7e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -43,6 +43,7 @@ Nounspace is a highly customizable Farcaster client funded by Nouns DAO. This do ### Configuration - [Configuration System](SYSTEMS/CONFIGURATION/OVERVIEW.md) - Database-backed configuration system +- [Testing Guide](SYSTEMS/CONFIGURATION/TESTING.md) - How to test the configuration system ### Discovery - [Mini App Discovery System](SYSTEMS/DISCOVERY/MINI_APP_DISCOVERY_SYSTEM.md) - Mini-app discovery system diff --git a/docs/SYSTEMS/CONFIGURATION/OVERVIEW.md b/docs/SYSTEMS/CONFIGURATION/OVERVIEW.md index fa3838866..eefde377e 100644 --- a/docs/SYSTEMS/CONFIGURATION/OVERVIEW.md +++ b/docs/SYSTEMS/CONFIGURATION/OVERVIEW.md @@ -309,4 +309,5 @@ function Navigation() { - **Route Handler**: `src/app/[navSlug]/[[...tabName]]/page.tsx` - Dynamic navigation - **Space Seeding**: `scripts/seed-navpage-spaces.ts` - Uploads space configs to Storage - **Shared Themes**: `src/config/shared/themes.ts` - Theme definitions +- **Testing Guide**: [Testing Guide](TESTING.md) - How to test the configuration system diff --git a/docs/SYSTEMS/CONFIGURATION/TESTING.md b/docs/SYSTEMS/CONFIGURATION/TESTING.md new file mode 100644 index 000000000..16c8cf3df --- /dev/null +++ b/docs/SYSTEMS/CONFIGURATION/TESTING.md @@ -0,0 +1,408 @@ +# Testing the Database-Backed Configuration System + +This guide walks you through testing the database-backed configuration system to ensure everything is working correctly. + +## Prerequisites + +1. **Supabase Setup** + - Local Supabase running (`supabase start`) OR + - Remote Supabase project with access + +2. **Environment Variables** + ```bash + NEXT_PUBLIC_SUPABASE_URL=your_supabase_url + SUPABASE_SERVICE_ROLE_KEY=your_service_role_key + NEXT_PUBLIC_COMMUNITY=nouns # or 'example', 'clanker' + ``` + +3. **Database Seeded** + - Run migrations: `supabase db reset` (includes seed.sql) + - Or manually seed: `tsx scripts/seed-community-configs.ts` + - Seed navigation spaces: `tsx scripts/seed-navpage-spaces.ts` + +## Testing Checklist + +### 1. Verify Database Setup + +**Check that configs exist in database:** + +```bash +# Using Supabase CLI +supabase db reset + +# Or query directly +psql -h localhost -p 54322 -U postgres -d postgres -c "SELECT community_id, is_published FROM community_configs;" +``` + +**Expected:** Rows for 'nouns', 'example', 'clanker' with `is_published = true` + +**Check navigation spaces exist:** + +```bash +psql -h localhost -p 54322 -U postgres -d postgres -c "SELECT \"spaceName\", \"spaceType\" FROM \"spaceRegistrations\" WHERE \"spaceType\" = 'navPage';" +``` + +**Expected:** Rows for 'nouns-home', 'nouns-explore', 'clanker-home', etc. + +### 2. Test Build-Time Loading + +**Important:** `next.config.mjs` runs both during `npm run build` AND `npm run dev`. The config is loaded when the Next.js process starts. + +**Build the app and check logs:** + +```bash +npm run build +``` + +**Or start dev server and check logs:** + +```bash +npm run dev +``` + +**Look for these log messages in the terminal where you run the command:** + +✅ **Success:** +``` +✅ Loaded config from database +✅ Using config from database +``` + +⚠️ **Fallback (if DB unavailable):** +``` +ℹ️ Using static configs (no DB credentials) +ℹ️ Using static configs +``` + +**Note:** The config is loaded when the dev server starts. If you update the database config, you need to **restart the dev server** (`Ctrl+C` then `npm run dev` again) to pick up changes. + +### 3. Test Runtime Access + +**Start the dev server (if not already running):** + +```bash +npm run dev +``` + +**Check the terminal where you started dev server:** +- Should see: `✅ Loaded config from database` (if DB is available) +- Or: `ℹ️ Using static configs` (if DB unavailable) + +**Check browser console for config loading:** + +Open browser DevTools → Console, look for: +- No errors related to config loading +- Navigation items render correctly +- Brand name displays correctly + +**Verify config in components:** + +Add a temporary log in a component: + +```typescript +// In any component +import { loadSystemConfig } from '@/config'; + +const config = loadSystemConfig(); +console.log('Config loaded:', { + brand: config.brand.displayName, + hasNavigation: !!config.navigation, + navItems: config.navigation?.items?.length || 0, +}); +``` + +**Expected:** +- `brand.displayName` matches your community (e.g., "Nouns") +- `navigation.items` contains navigation items +- Config structure matches `SystemConfig` interface + +### 4. Test Navigation Pages + +**Test home page:** + +1. Navigate to `/home` (should redirect to default tab) +2. Navigate to `/home/{tabName}` (e.g., `/home/Nouns`) +3. Verify: + - Page loads without errors + - Tabs display correctly + - Tab bar shows correct tabs + - Content renders for each tab + +**Test explore page (if exists):** + +1. Navigate to `/explore` (should redirect to default tab) +2. Navigate to `/explore/{tabName}` +3. Verify same as above + +**Check browser Network tab:** + +- No failed requests to Supabase Storage (for navPage spaces) +- Pages load from Storage correctly + +### 5. Test Fallback Behavior + +**Test without database credentials:** + +```bash +# Temporarily remove/comment out Supabase env vars +unset NEXT_PUBLIC_SUPABASE_URL +unset SUPABASE_SERVICE_ROLE_KEY + +npm run build +``` + +**Expected:** +- Build succeeds +- Logs show: `ℹ️ Using static configs (no DB credentials)` +- App still works (uses static TypeScript configs) + +**Test with invalid community:** + +```bash +NEXT_PUBLIC_COMMUNITY=invalid npm run build +``` + +**Expected:** +- Build succeeds +- Falls back to 'nouns' config +- Warning logged about invalid community + +### 6. Test Different Communities + +**Test Nouns:** + +```bash +NEXT_PUBLIC_COMMUNITY=nouns npm run build +npm run dev +``` + +**Verify:** +- Brand name: "Nouns" +- Navigation items match Nouns config +- Home page tabs match Nouns config + +**Test Clanker:** + +```bash +NEXT_PUBLIC_COMMUNITY=clanker npm run build +npm run dev +``` + +**Verify:** +- Brand name: "Clanker" +- Navigation items match Clanker config +- Home page tabs match Clanker config + +### 7. Test Shared Themes + +**Verify themes are loaded:** + +```typescript +// In any component +import { themes } from '@/config/shared/themes'; + +console.log('Available themes:', Object.keys(themes)); +``` + +**Expected:** +- Themes object exists +- Contains theme definitions (default, nounish, etc.) +- Themes are shared across all communities + +### 8. Test Database Updates + +**Update a config in database:** + +```sql +UPDATE community_configs +SET brand_config = jsonb_set( + brand_config, + '{displayName}', + '"Nouns Updated"' +) +WHERE community_id = 'nouns'; +``` + +**Rebuild and verify:** + +```bash +npm run build +npm run dev +``` + +**Expected:** +- New build picks up updated config +- Brand name shows "Nouns Updated" +- No need to change code + +### 9. Test Navigation Space References + +**Verify navigation items reference spaces:** + +```sql +SELECT + n.id, + n.label, + n."spaceId", + sr."spaceName", + sr."spaceType" +FROM jsonb_array_elements( + (SELECT "navigation_config"->'items' FROM community_configs WHERE community_id = 'nouns') +) AS n +LEFT JOIN "spaceRegistrations" sr ON sr."spaceId"::text = n->>'spaceId'; +``` + +**Expected:** +- Navigation items with `spaceId` reference valid `navPage` spaces +- Space names match (e.g., 'nouns-home', 'nouns-explore') + +**Verify space configs in Storage:** + +Check that space configs exist in Supabase Storage: +- `spaces/{spaceId}/tabOrder` +- `spaces/{spaceId}/tabs/{tabName}` + +### 10. Integration Test + +**Full flow test:** + +1. **Reset database:** + ```bash + supabase db reset + tsx scripts/seed-navpage-spaces.ts + ``` + +2. **Build:** + ```bash + npm run build + ``` + +3. **Start:** + ```bash + npm run dev + ``` + +4. **Navigate through app:** + - Visit `/` (should redirect to `/home/{defaultTab}`) + - Visit `/home` (should redirect to default tab) + - Visit `/home/Nouns` (or other tabs) + - Visit `/explore` (if exists) + - Click navigation items + - Verify brand header shows correct logo/name + +5. **Verify in browser:** + - No console errors + - All pages load correctly + - Navigation works + - Branding is correct + - Themes apply correctly + +## Common Issues + +### Issue: "Using static configs" when DB is available + +**Check:** +- `NEXT_PUBLIC_SUPABASE_URL` is set correctly +- `SUPABASE_SERVICE_ROLE_KEY` is set correctly +- Database is running and accessible +- `community_configs` table has data with `is_published = true` + +### Issue: Navigation pages return 404 + +**Check:** +- `navPage` spaces exist in `spaceRegistrations` +- Space configs uploaded to Storage via `seed-navpage-spaces.ts` +- Navigation items have correct `spaceId` references + +### Issue: Config not updating after DB change + +**Solution:** +- **For dev mode:** Restart the dev server (`Ctrl+C` then `npm run dev` again) +- **For production:** Rebuild the app (`npm run build`) +- Config is loaded when the Next.js process starts (both dev and build) +- Changes require restarting the process + +### Issue: Build fails with "supabaseKey is required" + +**Check:** +- `SUPABASE_SERVICE_ROLE_KEY` is set in environment +- Not `SUPABASE_SERVICE_KEY` (wrong name) +- Key has service role permissions + +## Quick Test Script + +Create a test script to verify everything: + +```typescript +// scripts/test-config.ts +import { createClient } from '@supabase/supabase-js'; +import { loadSystemConfig } from '../src/config'; + +async function testConfig() { + // Test 1: Database connection + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + + if (!supabaseUrl || !supabaseKey) { + console.log('❌ Missing Supabase credentials'); + return; + } + + const supabase = createClient(supabaseUrl, supabaseKey); + + // Test 2: Config exists in DB + const { data: config, error } = await supabase + .rpc('get_active_community_config', { p_community_id: 'nouns' }) + .single(); + + if (error || !config) { + console.log('❌ Config not found in database:', error?.message); + return; + } + + console.log('✅ Config found in database'); + console.log(' Brand:', config.brand?.displayName); + console.log(' Navigation items:', config.navigation?.items?.length || 0); + + // Test 3: Runtime config loading + const runtimeConfig = loadSystemConfig(); + console.log('✅ Runtime config loaded'); + console.log(' Brand:', runtimeConfig.brand.displayName); + console.log(' Has navigation:', !!runtimeConfig.navigation); + + // Test 4: Navigation spaces + const { data: navSpaces } = await supabase + .from('spaceRegistrations') + .select('spaceName, spaceType') + .eq('spaceType', 'navPage'); + + console.log('✅ Navigation spaces:', navSpaces?.length || 0); + + console.log('\n✅ All tests passed!'); +} + +testConfig(); +``` + +Run with: +```bash +tsx scripts/test-config.ts +``` + +## Summary + +The testing process verifies: + +1. ✅ Database has configs +2. ✅ Build loads config from DB +3. ✅ Runtime accesses config correctly +4. ✅ Navigation pages load as Spaces +5. ✅ Fallback works when DB unavailable +6. ✅ Different communities work +7. ✅ Shared themes work +8. ✅ DB updates require rebuild +9. ✅ Navigation spaces are linked correctly +10. ✅ Full integration works + +If all tests pass, the database-backed configuration system is working correctly! + diff --git a/src/config/index.ts b/src/config/index.ts index 2317a3651..7b9133171 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -2,6 +2,7 @@ import { nounsSystemConfig } from './nouns/index'; import { exampleSystemConfig } from './example/index'; import { clankerSystemConfig } from './clanker/index'; import { SystemConfig } from './systemConfig'; +import { themes } from './shared/themes'; // Available community configurations const AVAILABLE_CONFIGURATIONS = ['nouns', 'example', 'clanker'] as const; @@ -21,8 +22,10 @@ export const loadSystemConfig = (): SystemConfig => { if (dbConfig && dbConfig.brand && dbConfig.assets) { console.log('✅ Using config from database'); // Map pages object to homePage/explorePage for backward compatibility + // Add themes from shared file (themes are not in database) const mappedConfig: SystemConfig = { ...dbConfig, + theme: themes, // Themes come from shared file homePage: dbConfig.pages?.['home'] || dbConfig.homePage || null, explorePage: dbConfig.pages?.['explore'] || dbConfig.explorePage || null, }; diff --git a/src/config/shared/themes.ts b/src/config/shared/themes.ts new file mode 100644 index 000000000..4c49f9df1 --- /dev/null +++ b/src/config/shared/themes.ts @@ -0,0 +1,12 @@ +// Shared theme definitions used across all communities +// Themes are stored here instead of in individual community configs or the database + +import { ThemeConfig } from '../systemConfig'; +import { nounsTheme } from '../nouns/nouns.theme'; + +// Export themes - all communities use the same theme definitions +export const themes: ThemeConfig = nounsTheme; + +// Also export as default for convenience +export default themes; + diff --git a/src/constants/themes.ts b/src/constants/themes.ts index 188245309..b721c93f0 100644 --- a/src/constants/themes.ts +++ b/src/constants/themes.ts @@ -9,74 +9,72 @@ import { retro, nounish, } from "./animatedBackgroundsHtml"; -import { loadSystemConfig } from "@/config"; - -// Load system configuration -const config = loadSystemConfig(); +import { themes } from "@/config/shared/themes"; +// Use themes from shared file (not from config, since themes are shared across communities) // Convert configuration themes to the expected format export const THEMES = [ - config.theme.default, + themes.default, { - ...config.theme.gradientAndWave, + ...themes.gradientAndWave, properties: { - ...config.theme.gradientAndWave.properties, + ...themes.gradientAndWave.properties, backgroundHTML: gradientAndWave, }, }, { - ...config.theme.colorBlobs, + ...themes.colorBlobs, properties: { - ...config.theme.colorBlobs.properties, + ...themes.colorBlobs.properties, backgroundHTML: colorBlobs, }, }, { - ...config.theme.floatingShapes, + ...themes.floatingShapes, properties: { - ...config.theme.floatingShapes.properties, + ...themes.floatingShapes.properties, backgroundHTML: floatingShapes, }, }, { - ...config.theme.imageParallax, + ...themes.imageParallax, properties: { - ...config.theme.imageParallax.properties, + ...themes.imageParallax.properties, backgroundHTML: imageParallax, }, }, { - ...config.theme.shootingStar, + ...themes.shootingStar, properties: { - ...config.theme.shootingStar.properties, + ...themes.shootingStar.properties, backgroundHTML: shootingStar, }, }, { - ...config.theme.squareGrid, + ...themes.squareGrid, properties: { - ...config.theme.squareGrid.properties, + ...themes.squareGrid.properties, backgroundHTML: squareGrid, }, }, { - ...config.theme.tesseractPattern, + ...themes.tesseractPattern, properties: { - ...config.theme.tesseractPattern.properties, + ...themes.tesseractPattern.properties, backgroundHTML: tesseractPattern, }, }, { - ...config.theme.retro, + ...themes.retro, properties: { - ...config.theme.retro.properties, + ...themes.retro.properties, backgroundHTML: retro, }, }, { - ...config.theme.nounish, + ...themes.nounish, properties: { - ...config.theme.nounish.properties, + ...themes.nounish.properties, backgroundHTML: nounish, }, }, From 46ae01b6a8b09afa51f8ac736c73346a32cb6375 Mon Sep 17 00:00:00 2001 From: Jesse Paterson Date: Sun, 30 Nov 2025 16:39:58 -0600 Subject: [PATCH 113/155] first stab at remote images compiled at build time --- next.config.mjs | 156 ++++- public/images/nouns/logo.svg | 6 + public/images/nouns/noggles.svg | 9 + public/images/nouns/og.svg | 14 + public/images/nouns/splash.svg | 14 + scripts/analyze-config-size.ts | 83 --- scripts/seed-all.ts | 810 ++++++++++++++++++++++++ scripts/upload-nouns-assets-to-imgbb.ts | 142 +++++ scripts/verify-asset-downloads.ts | 213 +++++++ 9 files changed, 1361 insertions(+), 86 deletions(-) create mode 100644 public/images/nouns/logo.svg create mode 100644 public/images/nouns/noggles.svg create mode 100644 public/images/nouns/og.svg create mode 100644 public/images/nouns/splash.svg delete mode 100644 scripts/analyze-config-size.ts create mode 100644 scripts/seed-all.ts create mode 100644 scripts/upload-nouns-assets-to-imgbb.ts create mode 100644 scripts/verify-asset-downloads.ts diff --git a/next.config.mjs b/next.config.mjs index 25351b687..cd4e55aae 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -2,8 +2,155 @@ import bundlerAnalyzer from "@next/bundle-analyzer"; import packageInfo from "./package.json" with { type: "json" }; import { createRequire } from "node:module"; import { createClient } from '@supabase/supabase-js'; +import { writeFile, mkdir } from 'fs/promises'; +import { existsSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; const require = createRequire(import.meta.url); +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +/** + * Download an asset from external URL and save to public folder + * Returns the local path if successful, original URL if failed + */ +async function downloadAsset(url, localPath) { + try { + // Skip if already a local path (starts with /) + if (url.startsWith('/')) { + return url; + } + + // Skip if not an HTTP(S) URL + if (!url.startsWith('http://') && !url.startsWith('https://')) { + return url; + } + + // Skip if file already exists (cache - avoids re-downloading on every build) + if (existsSync(localPath)) { + const publicPath = localPath.replace(join(__dirname, 'public'), ''); + console.log(`ℹ️ Using cached asset: ${publicPath}`); + return publicPath; + } + + // Download the file + const response = await fetch(url); + if (!response.ok) { + console.warn(`⚠️ Failed to download ${url}: ${response.statusText}`); + return url; // Return original URL as fallback + } + + // Ensure directory exists + const dir = dirname(localPath); + if (!existsSync(dir)) { + await mkdir(dir, { recursive: true }); + } + + // Get file buffer + const buffer = Buffer.from(await response.arrayBuffer()); + + // Write to file + await writeFile(localPath, buffer); + + // Return local path (relative to public folder) + const publicPath = localPath.replace(join(__dirname, 'public'), ''); + console.log(`✅ Downloaded ${url} → ${publicPath}`); + return publicPath; + + } catch (error) { + console.warn(`⚠️ Error downloading ${url}:`, error.message); + return url; // Return original URL as fallback + } +} + +/** + * Download all external assets from config and update paths to local files + */ +async function downloadAndLocalizeAssets(config, community) { + if (!config.assets || !config.assets.logos) { + return config; + } + + const assetsDir = join(__dirname, 'public', 'images', community); + const updatedAssets = { ...config.assets }; + const logos = { ...config.assets.logos }; + + // Download each logo asset + const assetTypes = ['main', 'icon', 'favicon', 'appleTouch', 'og', 'splash']; + + console.log(`\n📥 Downloading assets for community: ${community}`); + console.log(`📂 Target directory: public/images/${community}/\n`); + + let downloadedCount = 0; + let cachedCount = 0; + let skippedCount = 0; + + for (const assetType of assetTypes) { + const url = logos[assetType]; + if (!url) continue; + + // Extract filename from URL or use default + let filename = url.split('/').pop() || `${assetType}.${getExtensionFromUrl(url)}`; + // Remove query params if any + filename = filename.split('?')[0]; + + // If no extension, try to infer from content-type or use common default + if (!filename.includes('.')) { + filename = `${filename}.${getExtensionFromUrl(url) || 'png'}`; + } + + const localPath = join(assetsDir, filename); + const originalUrl = logos[assetType]; + const localUrl = await downloadAsset(url, localPath); + + // Track statistics + if (originalUrl.startsWith('http')) { + if (localUrl !== originalUrl) { + // Successfully downloaded and localized + if (existsSync(localPath)) { + downloadedCount++; + } else { + cachedCount++; + } + } else { + skippedCount++; + } + } + + logos[assetType] = localUrl; + } + + updatedAssets.logos = logos; + + console.log(`\n📊 Asset download summary:`); + console.log(` ✅ Downloaded: ${downloadedCount}`); + console.log(` 📦 Cached: ${cachedCount}`); + if (skippedCount > 0) { + console.log(` ⏭️ Skipped: ${skippedCount} (local paths)`); + } + console.log(''); + + return { ...config, assets: updatedAssets }; +} + +/** + * Infer file extension from URL or common image types + */ +function getExtensionFromUrl(url) { + // Try to get from URL path + const match = url.match(/\.([a-z0-9]+)(?:[?#]|$)/i); + if (match) { + return match[1].toLowerCase(); + } + + // Default based on common patterns + if (url.includes('favicon')) return 'ico'; + if (url.includes('apple')) return 'png'; + if (url.includes('og')) return 'png'; + + return null; +} // Load config from database at build time and set as environment variable // Config is now ~2.8 KB (down from ~29 KB), so env var approach works fine @@ -32,9 +179,12 @@ async function loadConfigFromDB() { return; } + // Download external assets and localize paths + const configWithLocalAssets = await downloadAndLocalizeAssets(data, community); + // Store config in environment variable (now small enough at ~2.8 KB) - process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(data); - console.log('✅ Loaded config from database'); + process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(configWithLocalAssets); + console.log('✅ Loaded config from database and downloaded assets'); } catch (error) { console.warn('⚠️ Error loading config from DB:', error.message); } @@ -102,7 +252,7 @@ const cspHeader = ` /** @type {import('next').NextConfig} */ const nextConfig = { // output: 'export', // Outputs a Single-Page Application (SPA). - // distDir: './dist', // Changes the build output directory to `./dist/`. + // distDir: './dist', // Changes the build output directory to `./dist`. transpilePackages: [ "react-tweet", "react-best-gradient-color-picker", diff --git a/public/images/nouns/logo.svg b/public/images/nouns/logo.svg new file mode 100644 index 000000000..13fa7eaf1 --- /dev/null +++ b/public/images/nouns/logo.svg @@ -0,0 +1,6 @@ + + + Nouns Logo + + + diff --git a/public/images/nouns/noggles.svg b/public/images/nouns/noggles.svg new file mode 100644 index 000000000..25d2b77ea --- /dev/null +++ b/public/images/nouns/noggles.svg @@ -0,0 +1,9 @@ + + + noggles + Nouns noggles logo + + + + + diff --git a/public/images/nouns/og.svg b/public/images/nouns/og.svg new file mode 100644 index 000000000..cfc9ac414 --- /dev/null +++ b/public/images/nouns/og.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + Nounspace + OG Image + + diff --git a/public/images/nouns/splash.svg b/public/images/nouns/splash.svg new file mode 100644 index 000000000..38ee37e1d --- /dev/null +++ b/public/images/nouns/splash.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + Nounspace + Splash + + diff --git a/scripts/analyze-config-size.ts b/scripts/analyze-config-size.ts deleted file mode 100644 index 3b5976014..000000000 --- a/scripts/analyze-config-size.ts +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env tsx -/** - * Analyze config sizes to identify largest sections - */ - -import { nounsSystemConfig } from '../src/config/nouns/index'; - -function getSize(obj: any): number { - return JSON.stringify(obj).length; -} - -function formatSize(bytes: number): string { - if (bytes < 1024) return `${bytes} bytes`; - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`; - return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; -} - -const config = nounsSystemConfig; - -const sizes = { - brand: getSize(config.brand), - assets: getSize(config.assets), - theme: getSize(config.theme), - community: getSize(config.community), - fidgets: getSize(config.fidgets), - homePage: getSize(config.homePage), - explorePage: getSize(config.explorePage), - navigation: getSize(config.navigation || {}), - ui: getSize(config.ui || {}), -}; - -const total = Object.values(sizes).reduce((a, b) => a + b, 0); - -console.log('\n📊 Config Size Analysis\n'); -console.log('Section sizes:'); -console.log('─'.repeat(50)); - -const sorted = Object.entries(sizes) - .sort(([, a], [, b]) => b - a) - .map(([key, size]) => ({ - section: key, - size, - percentage: ((size / total) * 100).toFixed(1), - formatted: formatSize(size), - })); - -sorted.forEach(({ section, formatted, percentage }) => { - console.log(`${section.padEnd(15)} ${formatted.padStart(10)} (${percentage}%)`); -}); - -console.log('─'.repeat(50)); -console.log(`Total: ${formatSize(total).padStart(10)}`); - -// Analyze theme config in detail -console.log('\n🎨 Theme Config Breakdown:\n'); -const themeKeys = Object.keys(config.theme); -themeKeys.forEach((key) => { - const themeSize = getSize(config.theme[key as keyof typeof config.theme]); - console.log(` ${key.padEnd(20)} ${formatSize(themeSize)}`); -}); - -// Analyze homePage tabs -console.log('\n🏠 Home Page Tabs:\n'); -const homeTabs = Object.keys(config.homePage.tabs); -homeTabs.forEach((tab) => { - const tabSize = getSize(config.homePage.tabs[tab]); - console.log(` ${tab.padEnd(20)} ${formatSize(tabSize)}`); -}); - -// Check for large strings (like HTML) -console.log('\n🔍 Large String Analysis:\n'); -function findLargeStrings(obj: any, path = '', threshold = 1000): void { - if (typeof obj === 'string' && obj.length > threshold) { - console.log(` ${path}: ${formatSize(obj.length)}`); - } else if (typeof obj === 'object' && obj !== null) { - Object.keys(obj).forEach((key) => { - findLargeStrings(obj[key], path ? `${path}.${key}` : key, threshold); - }); - } -} - -findLargeStrings(config, '', 1000); - diff --git a/scripts/seed-all.ts b/scripts/seed-all.ts new file mode 100644 index 000000000..139c7a970 --- /dev/null +++ b/scripts/seed-all.ts @@ -0,0 +1,810 @@ +#!/usr/bin/env tsx +/** + * Unified seed script for database and storage + * + * This script: + * 1. Uploads Nouns brand assets to ImgBB + * 2. Seeds the database with community configs (using ImgBB URLs) + * 3. Creates navPage space registrations + * 4. Uploads navPage space configs to Supabase Storage + * + * Usage: + * tsx scripts/seed-all.ts + * + * Requires: + * - NEXT_PUBLIC_SUPABASE_URL + * - SUPABASE_SERVICE_ROLE_KEY + * - NEXT_PUBLIC_IMGBB_API_KEY (optional, only needed for uploading assets) + * + * This script replaces the need to: + * - Run supabase/seed.sql separately + * - Run scripts/upload-nouns-assets-to-imgbb.ts + * - Run scripts/seed-navpage-spaces.ts + */ + +import { createClient } from '@supabase/supabase-js'; +import { readFile } from 'fs/promises'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import stringify from 'fast-json-stable-stringify'; +import moment from 'moment'; +import { SignedFile } from '../src/common/lib/signedFiles'; +import { SpaceConfig } from '../src/app/(spaces)/Space'; + +// Import page configs for navPage spaces +import { nounsHomePage } from '../src/config/nouns/nouns.home'; +import { nounsExplorePage } from '../src/config/nouns/nouns.explore'; +import { clankerHomePage } from '../src/config/clanker/clanker.home'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; +const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; +const imgBBApiKey = process.env.NEXT_PUBLIC_IMGBB_API_KEY; + +if (!supabaseUrl || !supabaseKey) { + console.error('❌ Missing required environment variables:'); + console.error(' NEXT_PUBLIC_SUPABASE_URL'); + console.error(' SUPABASE_SERVICE_ROLE_KEY'); + process.exit(1); +} + +const supabase = createClient(supabaseUrl, supabaseKey); + +/** + * Upload a file to ImgBB using base64 encoding + */ +async function uploadToImgBB(filePath: string, filename: string): Promise { + if (!imgBBApiKey) { + console.warn(`⚠️ NEXT_PUBLIC_IMGBB_API_KEY not set, skipping upload for ${filename}`); + return null; + } + + try { + const fileBuffer = await readFile(filePath); + const base64 = fileBuffer.toString('base64'); + + const params = new URLSearchParams(); + params.append('image', base64); + + const response = await fetch(`https://api.imgbb.com/1/upload?key=${imgBBApiKey}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: params.toString(), + }); + + const data = await response.json(); + + if (!data.success) { + throw new Error(data.error?.message || JSON.stringify(data)); + } + + const imageUrl = data.data.display_url || data.data.url; + console.log(` ✅ Uploaded ${filename} → ${imageUrl}`); + return imageUrl; + } catch (error: any) { + console.error(` ❌ Failed to upload ${filename}:`, error.message); + return null; + } +} + +/** + * Upload Nouns assets to ImgBB and return URLs + */ +async function uploadNounsAssets(): Promise> { + console.log('\n📤 Step 1: Uploading Nouns assets to ImgBB...\n'); + + const assetsDir = join(__dirname, '..', 'src', 'config', 'nouns', 'assets'); + const assetsToUpload = [ + { file: 'logo.svg', key: 'main' }, + { file: 'noggles.svg', key: 'icon' }, + { file: 'og.svg', key: 'og' }, + { file: 'splash.svg', key: 'splash' }, + ]; + + const uploadedUrls: Record = {}; + + for (const asset of assetsToUpload) { + const filePath = join(assetsDir, asset.file); + const url = await uploadToImgBB(filePath, asset.file); + if (url) { + uploadedUrls[asset.key] = url; + } else { + // Fallback to local paths if upload fails or API key missing + uploadedUrls[asset.key] = `/images/nouns/${asset.file}`; + } + } + + // Keep existing paths for favicon and appleTouch + uploadedUrls['favicon'] = '/images/favicon.ico'; + uploadedUrls['appleTouch'] = '/images/apple-touch-icon.png'; + + return uploadedUrls; +} + +/** + * Seed storage buckets + */ +async function seedBuckets() { + console.log('\n📦 Step 2: Creating storage buckets...\n'); + + // Note: We can't directly INSERT into storage.buckets via the client easily + // This is typically done via SQL migrations, but we'll try via RPC or just skip if exists + console.log(' ℹ️ Storage buckets should be created via migrations'); + console.log(' ℹ️ Skipping bucket creation (assumes buckets already exist)'); +} + +/** + * Create navPage space registrations + */ +async function createNavPageSpaces() { + console.log('\n🏗️ Step 3: Creating navPage space registrations...\n'); + + const spaces = [ + { spaceName: 'nouns-home' }, + { spaceName: 'nouns-explore' }, + { spaceName: 'clanker-home' }, + ]; + + for (const space of spaces) { + const { data: existing } = await supabase + .from('spaceRegistrations') + .select('spaceId') + .eq('spaceName', space.spaceName) + .eq('spaceType', 'navPage') + .single(); + + if (existing) { + console.log(` ✅ Space already exists: ${space.spaceName} (${existing.spaceId})`); + continue; + } + + const { data, error } = await supabase + .from('spaceRegistrations') + .insert({ + fid: null, + spaceName: space.spaceName, + spaceType: 'navPage', + identityPublicKey: 'system', + signature: 'system-seed', + timestamp: new Date().toISOString(), + }) + .select('spaceId') + .single(); + + if (error) { + console.error(` ❌ Failed to create ${space.spaceName}:`, error.message); + } else { + console.log(` ✅ Created space: ${space.spaceName} (${data.spaceId})`); + } + } +} + +/** + * Seed community configs with ImgBB URLs + */ +async function seedCommunityConfigs(assetsUrls: Record) { + console.log('\n⚙️ Step 4: Seeding community configs...\n'); + + // Nouns assets config with ImgBB URLs + const nounsAssetsConfig = { + logos: { + main: assetsUrls.main || '/images/nouns/logo.svg', + icon: assetsUrls.icon || '/images/nouns/noggles.svg', + favicon: assetsUrls.favicon || '/images/favicon.ico', + appleTouch: assetsUrls.appleTouch || '/images/apple-touch-icon.png', + og: assetsUrls.og || '/images/nouns/og.svg', + splash: assetsUrls.splash || '/images/nouns/splash.svg', + }, + }; + + // Get space IDs for navigation + const { data: nounsHomeSpace } = await supabase + .from('spaceRegistrations') + .select('spaceId') + .eq('spaceName', 'nouns-home') + .eq('spaceType', 'navPage') + .single(); + + const { data: nounsExploreSpace } = await supabase + .from('spaceRegistrations') + .select('spaceId') + .eq('spaceName', 'nouns-explore') + .eq('spaceType', 'navPage') + .single(); + + // Nouns config + const { error: nounsError } = await supabase + .from('community_configs') + .upsert({ + community_id: 'nouns', + is_published: true, + brand_config: { + name: 'Nouns', + displayName: 'Nouns', + tagline: 'A space for Nouns', + description: 'The social hub for Nouns', + miniAppTags: ['nouns', 'client', 'customizable', 'social', 'link'], + }, + assets_config: nounsAssetsConfig, + community_config: { + type: 'nouns', + urls: { + website: 'https://nouns.com', + discord: 'https://discord.gg/nouns', + twitter: 'https://twitter.com/nounsdao', + github: 'https://github.com/nounsDAO', + forum: 'https://discourse.nouns.wtf', + }, + social: { + farcaster: 'nouns', + discord: 'nouns', + twitter: 'nounsdao', + }, + governance: { + proposals: 'https://nouns.wtf/vote', + delegates: 'https://nouns.wtf/delegates', + treasury: 'https://nouns.wtf/treasury', + }, + tokens: { + erc20Tokens: [ + { + address: '0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab', + symbol: '$SPACE', + decimals: 18, + network: 'base', + }, + ], + nftTokens: [ + { + address: '0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03', + symbol: 'Nouns', + type: 'erc721', + network: 'eth', + }, + ], + }, + contracts: { + nouns: '0x9c8ff314c9bc7f6e59a9d9225fb22946427edc03', + auctionHouse: '0x830bd73e4184cef73443c15111a1df14e495c706', + space: '0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab', + nogs: '0xD094D5D45c06c1581f5f429462eE7cCe72215616', + }, + }, + fidgets_config: { + enabled: [ + 'nounsHome', + 'governance', + 'feed', + 'cast', + 'gallery', + 'text', + 'iframe', + 'links', + 'video', + 'channel', + 'profile', + 'snapshot', + 'swap', + 'rss', + 'market', + 'portfolio', + 'chat', + 'builderScore', + 'framesV2', + ], + disabled: ['example'], + }, + navigation_config: { + logoTooltip: { text: 'wtf is nouns?', href: 'https://nouns.wtf' }, + items: [ + { + id: 'home', + label: 'Home', + href: '/home', + icon: 'home', + spaceId: nounsHomeSpace?.spaceId || null, + }, + { + id: 'explore', + label: 'Explore', + href: '/explore', + icon: 'explore', + spaceId: nounsExploreSpace?.spaceId || null, + }, + { + id: 'notifications', + label: 'Notifications', + href: '/notifications', + icon: 'notifications', + requiresAuth: true, + }, + { + id: 'space-token', + label: '$SPACE', + href: '/t/base/0x48C6740BcF807d6C47C864FaEEA15Ed4dA3910Ab/Profile', + icon: 'space', + }, + ], + showMusicPlayer: true, + showSocials: true, + }, + ui_config: { + primaryColor: 'rgb(37, 99, 235)', + primaryHoverColor: 'rgb(29, 78, 216)', + primaryActiveColor: 'rgb(30, 64, 175)', + castButton: { + backgroundColor: 'rgb(37, 99, 235)', + hoverColor: 'rgb(29, 78, 216)', + activeColor: 'rgb(30, 64, 175)', + }, + }, + }); + + if (nounsError) { + console.error(' ❌ Failed to seed Nouns config:', nounsError.message); + } else { + console.log(' ✅ Seeded Nouns community config'); + } + + // Example config + const { error: exampleError } = await supabase.from('community_configs').upsert({ + community_id: 'example', + is_published: true, + brand_config: { + name: 'Example', + displayName: 'Example Community', + tagline: 'A space for Example Community', + description: 'The social hub for Example Community', + miniAppTags: [], + }, + assets_config: { + logos: { + main: '/images/example_logo.png', + icon: '/images/example_icon.png', + favicon: '/images/example_favicon.ico', + appleTouch: '/images/example_apple_touch.png', + og: '/images/example_og.png', + splash: '/images/example_splash.png', + }, + }, + community_config: { + type: 'example', + urls: { + website: 'https://example.com', + discord: 'https://discord.gg/example', + twitter: 'https://twitter.com/example', + github: 'https://github.com/example', + forum: 'https://forum.example.com', + }, + social: { + farcaster: 'example', + discord: 'example', + twitter: 'example', + }, + governance: { + proposals: 'https://governance.example.com/proposals', + delegates: 'https://governance.example.com/delegates', + treasury: 'https://governance.example.com/treasury', + }, + tokens: { + erc20Tokens: [ + { + address: '0x1234567890123456789012345678901234567890', + symbol: '$EXAMPLE', + decimals: 18, + network: 'mainnet', + }, + ], + nftTokens: [ + { + address: '0x1234567890123456789012345678901234567890', + symbol: 'Example NFT', + type: 'erc721', + network: 'eth', + }, + ], + }, + contracts: { + nouns: '0x1234567890123456789012345678901234567890', + auctionHouse: '0x1234567890123456789012345678901234567890', + space: '0x1234567890123456789012345678901234567890', + nogs: '0x1234567890123456789012345678901234567890', + }, + }, + fidgets_config: { + enabled: [ + 'feed', + 'cast', + 'gallery', + 'text', + 'iframe', + 'links', + 'video', + 'channel', + 'profile', + 'swap', + 'rss', + 'market', + 'portfolio', + 'chat', + 'framesV2', + ], + disabled: ['example', 'nounsHome', 'governance', 'snapshot', 'builderScore'], + }, + navigation_config: null, + ui_config: { + primaryColor: 'rgb(37, 99, 235)', + primaryHoverColor: 'rgb(29, 78, 216)', + primaryActiveColor: 'rgb(30, 64, 175)', + castButton: { + backgroundColor: 'rgb(37, 99, 235)', + hoverColor: 'rgb(29, 78, 216)', + activeColor: 'rgb(30, 64, 175)', + }, + }, + }); + + if (exampleError) { + console.error(' ❌ Failed to seed Example config:', exampleError.message); + } else { + console.log(' ✅ Seeded Example community config'); + } + + // Get Clanker home space ID + const { data: clankerHomeSpace } = await supabase + .from('spaceRegistrations') + .select('spaceId') + .eq('spaceName', 'clanker-home') + .eq('spaceType', 'navPage') + .single(); + + // Clanker config + const { error: clankerError } = await supabase.from('community_configs').upsert({ + community_id: 'clanker', + is_published: true, + brand_config: { + name: 'clanker', + displayName: 'Clanker', + tagline: 'Clank Clank', + description: + 'Explore, launch and trade tokens in the Clanker ecosystem. Create your own tokens and discover trending projects in the community-driven token economy.', + }, + assets_config: { + logos: { + main: '/images/clanker/logo.svg', + icon: '/images/clanker/logo.svg', + favicon: '/images/clanker/favicon.ico', + appleTouch: '/images/clanker/apple.png', + og: '/images/clanker/og.jpg', + splash: '/images/clanker/og.jpg', + }, + }, + community_config: { + type: 'token_platform', + urls: { + website: 'https://clanker.world', + discord: 'https://discord.gg/clanker', + twitter: 'https://twitter.com/clankerworld', + github: 'https://github.com/clanker', + forum: 'https://forum.clanker.world', + }, + social: { + farcaster: 'clanker', + discord: 'clanker', + twitter: 'clankerworld', + }, + governance: { + proposals: 'https://proposals.clanker.world', + delegates: 'https://delegates.clanker.world', + treasury: 'https://treasury.clanker.world', + }, + tokens: { + erc20Tokens: [ + { + address: '0x1bc0c42215582d5a085795f4badbac3ff36d1bcb', + symbol: '$CLANKER', + decimals: 18, + network: 'base', + }, + ], + nftTokens: [], + }, + contracts: { + clanker: '0x1bc0c42215582d5a085795f4badbac3ff36d1bcb', + tokenFactory: '0x0000000000000000000000000000000000000000', + space: '0x0000000000000000000000000000000000000000', + trading: '0x0000000000000000000000000000000000000000', + nouns: '0x0000000000000000000000000000000000000000', + auctionHouse: '0x0000000000000000000000000000000000000000', + nogs: '0x0000000000000000000000000000000000000000', + }, + }, + fidgets_config: { + enabled: [ + 'Market', + 'Portfolio', + 'Swap', + 'feed', + 'cast', + 'gallery', + 'text', + 'iframe', + 'links', + 'Video', + 'Chat', + 'BuilderScore', + 'FramesV2', + 'Rss', + 'SnapShot', + ], + disabled: ['nounsHome', 'governance'], + }, + navigation_config: { + logoTooltip: { text: 'clanker.world', href: 'https://www.clanker.world' }, + items: [ + { + id: 'home', + label: 'Home', + href: '/home', + icon: 'home', + spaceId: clankerHomeSpace?.spaceId || null, + }, + { + id: 'notifications', + label: 'Notifications', + href: '/notifications', + icon: 'notifications', + requiresAuth: true, + }, + { + id: 'clanker-token', + label: '$CLANKER', + href: '/t/base/0x1bc0c42215582d5a085795f4badbac3ff36d1bcb/Profile', + icon: 'robot', + }, + ], + showMusicPlayer: false, + showSocials: false, + }, + ui_config: { + primaryColor: 'rgba(136, 131, 252, 1)', + primaryHoverColor: 'rgba(116, 111, 232, 1)', + primaryActiveColor: 'rgba(96, 91, 212, 1)', + castButton: { + backgroundColor: 'rgba(136, 131, 252, 1)', + hoverColor: 'rgba(116, 111, 232, 1)', + activeColor: 'rgba(96, 91, 212, 1)', + }, + }, + }); + + if (clankerError) { + console.error(' ❌ Failed to seed Clanker config:', clankerError.message); + } else { + console.log(' ✅ Seeded Clanker community config'); + } +} + +/** + * Creates a SignedFile wrapper for system-generated files + */ +function createSystemSignedFile(fileData: string): SignedFile { + return { + fileData, + fileType: 'json', + isEncrypted: false, + timestamp: moment().toISOString(), + publicKey: 'nounspace', + signature: 'not applicable, machine generated file', + }; +} + +/** + * Creates a SignedFile for tab order + */ +function createTabOrderSignedFile(spaceId: string, tabOrder: string[]): SignedFile { + const tabOrderData = { + spaceId, + timestamp: moment().toISOString(), + tabOrder, + publicKey: 'nounspace', + signature: 'not applicable, machine generated file', + }; + return createSystemSignedFile(stringify(tabOrderData)); +} + +/** + * Uploads a single tab config to Supabase Storage + */ +async function uploadTab(spaceId: string, tabName: string, tabConfig: SpaceConfig): Promise { + const signedFile = createSystemSignedFile(stringify(tabConfig)); + const filePath = `${spaceId}/tabs/${tabName}`; + + const { error } = await supabase.storage + .from('spaces') + .upload(filePath, new Blob([stringify(signedFile)], { type: 'application/json' }), { + upsert: true, + }); + + if (error) { + console.error(` ❌ Failed to upload tab ${tabName}:`, error.message); + return false; + } + + console.log(` ✅ Uploaded tab: ${tabName}`); + return true; +} + +/** + * Uploads tab order to Supabase Storage + */ +async function uploadTabOrder(spaceId: string, tabOrder: string[]): Promise { + const signedFile = createTabOrderSignedFile(spaceId, tabOrder); + const filePath = `${spaceId}/tabOrder`; + + const { error } = await supabase.storage + .from('spaces') + .upload(filePath, new Blob([stringify(signedFile)], { type: 'application/json' }), { + upsert: true, + }); + + if (error) { + console.error(` ❌ Failed to upload tab order:`, error.message); + return false; + } + + console.log(` ✅ Uploaded tab order: [${tabOrder.join(', ')}]`); + return true; +} + +/** + * Gets spaceId from database by spaceName + */ +async function getSpaceId(spaceName: string): Promise { + const { data, error } = await supabase + .from('spaceRegistrations') + .select('spaceId') + .eq('spaceName', spaceName) + .eq('spaceType', 'navPage') + .single(); + + if (error || !data) { + console.error(` ❌ Space not found: ${spaceName}`, error?.message); + return null; + } + + return data.spaceId; +} + +/** + * Type for page configs with tabs + */ +type PageConfigWithSpaceTabs = { + defaultTab: string; + tabOrder: string[]; + tabs: Record; +}; + +/** + * Uploads a page config (homePage or explorePage) as a Space + */ +async function uploadPageConfig( + spaceName: string, + pageConfig: PageConfigWithSpaceTabs, +): Promise { + const spaceId = await getSpaceId(spaceName); + if (!spaceId) { + return false; + } + + console.log(` 📦 Uploading ${spaceName} (${spaceId})`); + + // Upload each tab + const tabNames = Object.keys(pageConfig.tabs); + const tabResults = await Promise.all( + tabNames.map((tabName) => { + const tabConfig = pageConfig.tabs[tabName]; + const spaceConfig: SpaceConfig = { + fidgetInstanceDatums: tabConfig.fidgetInstanceDatums, + layoutID: tabConfig.layoutID, + layoutDetails: tabConfig.layoutDetails, + isEditable: tabConfig.isEditable ?? false, + fidgetTrayContents: tabConfig.fidgetTrayContents, + theme: tabConfig.theme, + timestamp: tabConfig.timestamp, + tabNames: tabConfig.tabNames, + fid: tabConfig.fid, + }; + return uploadTab(spaceId, tabName, spaceConfig); + }), + ); + + const allTabsUploaded = tabResults.every((result) => result); + if (!allTabsUploaded) { + console.error(` ❌ Some tabs failed to upload for ${spaceName}`); + return false; + } + + // Upload tab order + const tabOrderUploaded = await uploadTabOrder(spaceId, pageConfig.tabOrder); + if (!tabOrderUploaded) { + console.error(` ❌ Failed to upload tab order for ${spaceName}`); + return false; + } + + return true; +} + +/** + * Upload navPage space configs to Supabase Storage + */ +async function uploadNavPageSpaces() { + console.log('\n📤 Step 5: Uploading navPage space configs to Storage...\n'); + + const spaceConfigs: Array<{ spaceName: string; config: PageConfigWithSpaceTabs }> = [ + { spaceName: 'nouns-home', config: nounsHomePage }, + { spaceName: 'nouns-explore', config: nounsExplorePage }, + { spaceName: 'clanker-home', config: clankerHomePage }, + ]; + + const results = await Promise.allSettled( + spaceConfigs.map(({ spaceName, config }) => uploadPageConfig(spaceName, config)), + ); + + const successCount = results.filter((r) => r.status === 'fulfilled' && r.value).length; + const failCount = results.length - successCount; + + if (failCount > 0) { + console.error(`\n ❌ ${failCount} space(s) failed to upload`); + return false; + } + + console.log(`\n ✅ All ${successCount} spaces uploaded successfully`); + return true; +} + +/** + * Main seeding function + */ +async function main() { + console.log('🚀 Starting unified database seeding...\n'); + + try { + // Step 1: Upload assets to ImgBB + const assetsUrls = await uploadNounsAssets(); + + // Step 2: Seed storage buckets (skip, assume created via migrations) + await seedBuckets(); + + // Step 3: Create navPage space registrations + await createNavPageSpaces(); + + // Step 4: Seed community configs + await seedCommunityConfigs(assetsUrls); + + // Step 5: Upload navPage space configs + const spacesUploaded = await uploadNavPageSpaces(); + + if (!spacesUploaded) { + console.error('\n❌ Some steps failed. Check errors above.'); + process.exit(1); + } + + console.log('\n✅ All seeding completed successfully!'); + console.log('\n📋 Summary:'); + console.log(' ✓ Nouns assets uploaded to ImgBB'); + console.log(' ✓ NavPage spaces created'); + console.log(' ✓ Community configs seeded'); + console.log(' ✓ NavPage space configs uploaded to Storage'); + } catch (error: any) { + console.error('\n❌ Fatal error:', error); + process.exit(1); + } +} + +main(); + diff --git a/scripts/upload-nouns-assets-to-imgbb.ts b/scripts/upload-nouns-assets-to-imgbb.ts new file mode 100644 index 000000000..2d39f0f26 --- /dev/null +++ b/scripts/upload-nouns-assets-to-imgbb.ts @@ -0,0 +1,142 @@ +#!/usr/bin/env tsx +/** + * Upload Nouns brand assets to ImgBB and generate updated seed SQL + * + * This script: + * 1. Reads image files from src/config/nouns/assets/ + * 2. Uploads them to ImgBB + * 3. Outputs the URLs and updated SQL for seed.sql + * + * Usage: + * tsx scripts/upload-nouns-assets-to-imgbb.ts + * + * Requires: + * - NEXT_PUBLIC_IMGBB_API_KEY environment variable + * + * After running, copy the generated SQL into supabase/seed.sql + */ + +import { readFile } from 'fs/promises'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { createRequire } from 'module'; + +const require = createRequire(import.meta.url); +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const imgBBApiKey = process.env.NEXT_PUBLIC_IMGBB_API_KEY; + +if (!imgBBApiKey) { + console.error('❌ Missing required environment variable: NEXT_PUBLIC_IMGBB_API_KEY'); + console.error(' Get your API key from: https://api.imgbb.com/'); + process.exit(1); +} + +/** + * Upload a file to ImgBB using base64 encoding + */ +async function uploadToImgBB(filePath: string, filename: string): Promise { + try { + // Read file from filesystem + const fileBuffer = await readFile(filePath); + + // Convert to base64 (ImgBB accepts base64-encoded images via form-urlencoded) + const base64 = fileBuffer.toString('base64'); + + // ImgBB API accepts base64 as 'image' parameter via application/x-www-form-urlencoded + const params = new URLSearchParams(); + params.append('image', base64); + + const response = await fetch(`https://api.imgbb.com/1/upload?key=${imgBBApiKey}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: params.toString(), + }); + + const data = await response.json(); + + if (!data.success) { + throw new Error(data.error?.message || JSON.stringify(data)); + } + + const imageUrl = data.data.display_url || data.data.url; + console.log(`✅ Uploaded ${filename} → ${imageUrl}`); + return imageUrl; + + } catch (error: any) { + console.error(`❌ Failed to upload ${filename}:`, error.message); + throw error; + } +} + +/** + * Main function + */ +async function main() { + const assetsDir = join(__dirname, '..', 'src', 'config', 'nouns', 'assets'); + + // Define which assets to upload + const assetsToUpload = [ + { file: 'logo.svg', key: 'main' }, + { file: 'noggles.svg', key: 'icon' }, + { file: 'og.svg', key: 'og' }, + { file: 'splash.svg', key: 'splash' }, + ]; + + console.log('📤 Uploading Nouns assets to ImgBB...\n'); + + const uploadedUrls: Record = {}; + + // Upload each asset + for (const asset of assetsToUpload) { + const filePath = join(assetsDir, asset.file); + try { + const url = await uploadToImgBB(filePath, asset.file); + uploadedUrls[asset.key] = url; + } catch (error) { + console.error(`Failed to upload ${asset.file}, skipping...`); + } + } + + // Keep existing paths for favicon and appleTouch (these are already in public folder) + uploadedUrls['favicon'] = '/images/favicon.ico'; + uploadedUrls['appleTouch'] = '/images/apple-touch-icon.png'; + + console.log('\n✅ All assets uploaded!\n'); + console.log('📋 Copy this SQL snippet into supabase/seed.sql (replace line 101, the assets_config value):\n'); + console.log('─'.repeat(80)); + + // Generate SQL with proper escaping for PostgreSQL JSON + // PostgreSQL JSON strings need single quotes, and single quotes need to be doubled + const assetsConfig = { + logos: { + main: uploadedUrls.main || '/images/nouns/logo.svg', + icon: uploadedUrls.icon || '/images/nouns/noggles.svg', + favicon: uploadedUrls.favicon || '/images/favicon.ico', + appleTouch: uploadedUrls.appleTouch || '/images/apple-touch-icon.png', + og: uploadedUrls.og || '/images/nouns/og.svg', + splash: uploadedUrls.splash || '/images/nouns/splash.svg', + }, + }; + + // Convert to JSON string, escape single quotes for SQL + const jsonString = JSON.stringify(assetsConfig); + const sqlEscaped = jsonString.replace(/'/g, "''"); // Double single quotes for SQL + + const sqlSnippet = ` '${sqlEscaped}'::jsonb,`; + + console.log(sqlSnippet); + console.log('─'.repeat(80)); + console.log('\n📝 Full assets_config JSON for reference:\n'); + console.log(JSON.stringify(assetsConfig, null, 2)); + console.log('\n💡 Replace line 101 in supabase/seed.sql with the SQL snippet above.\n'); +} + +main().catch((error) => { + console.error('❌ Script failed:', error); + process.exit(1); +}); + diff --git a/scripts/verify-asset-downloads.ts b/scripts/verify-asset-downloads.ts new file mode 100644 index 000000000..e98515176 --- /dev/null +++ b/scripts/verify-asset-downloads.ts @@ -0,0 +1,213 @@ +#!/usr/bin/env tsx +/** + * Verification script to check if ImgBB assets are being downloaded during build + * + * This script: + * 1. Checks the database for ImgBB URLs in assets_config + * 2. Verifies if files exist in public/images/{community}/ + * 3. Shows what URLs are stored vs what should be downloaded + * + * Usage: + * tsx scripts/verify-asset-downloads.ts + * + * Requires: + * - NEXT_PUBLIC_SUPABASE_URL + * - SUPABASE_SERVICE_ROLE_KEY + * - NEXT_PUBLIC_COMMUNITY (optional, defaults to 'nouns') + */ + +import { createClient } from '@supabase/supabase-js'; +import { existsSync, readdirSync, statSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; +const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; +const community = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; + +if (!supabaseUrl || !supabaseKey) { + console.error('❌ Missing required environment variables:'); + console.error(' NEXT_PUBLIC_SUPABASE_URL'); + console.error(' SUPABASE_SERVICE_ROLE_KEY'); + process.exit(1); +} + +const supabase = createClient(supabaseUrl, supabaseKey); + +/** + * Check if URL is from ImgBB + */ +function isImgBBUrl(url: string): boolean { + return url.includes('i.ibb.co') || url.includes('ibb.co') || url.includes('imgbb.com'); +} + +/** + * Extract filename from URL + */ +function getFilenameFromUrl(url: string): string { + try { + const urlObj = new URL(url); + const pathname = urlObj.pathname; + const filename = pathname.split('/').pop() || ''; + // Remove query params + return filename.split('?')[0] || 'unknown'; + } catch { + // If not a valid URL, treat as path + return url.split('/').pop() || 'unknown'; + } +} + +/** + * Main verification function + */ +async function verifyAssetDownloads() { + console.log('🔍 Verifying asset downloads from ImgBB...\n'); + console.log(`📦 Community: ${community}\n`); + + // Fetch config from database + const { data: config, error } = await supabase + .rpc('get_active_community_config', { p_community_id: community }) + .single(); + + if (error || !config) { + console.error('❌ Failed to fetch config from database:', error?.message); + process.exit(1); + } + + if (!config.assets?.logos) { + console.error('❌ No assets_config.logos found in config'); + process.exit(1); + } + + const logos = config.assets.logos; + const assetTypes = ['main', 'icon', 'favicon', 'appleTouch', 'og', 'splash']; + + console.log('📋 Assets in database:\n'); + + const imgBBUrls: Array<{ type: string; url: string }> = []; + const localPaths: Array<{ type: string; path: string }> = []; + + for (const assetType of assetTypes) { + const url = logos[assetType]; + if (!url) continue; + + if (isImgBBUrl(url)) { + imgBBUrls.push({ type: assetType, url }); + console.log(` ${assetType.padEnd(12)} → 🔗 ${url} (ImgBB)`); + } else if (url.startsWith('/')) { + localPaths.push({ type: assetType, path: url }); + console.log(` ${assetType.padEnd(12)} → 📁 ${url} (local)`); + } else { + console.log(` ${assetType.padEnd(12)} → ❓ ${url} (unknown format)`); + } + } + + if (imgBBUrls.length === 0) { + console.log('\n⚠️ No ImgBB URLs found in database!'); + console.log(' This means either:'); + console.log(' - Assets were not uploaded to ImgBB'); + console.log(' - Database still has local paths'); + console.log(' - You need to run: tsx scripts/seed-all.ts'); + return; + } + + console.log(`\n✅ Found ${imgBBUrls.length} ImgBB URL(s)`); + + // Check if downloaded files exist + const assetsDir = join(__dirname, '..', 'public', 'images', community); + console.log(`\n📂 Checking download directory: ${assetsDir}\n`); + + if (!existsSync(assetsDir)) { + console.log('❌ Download directory does not exist!'); + console.log(' Run a build to trigger downloads: npm run build'); + return; + } + + const filesInDir = readdirSync(assetsDir).filter((file) => { + const filePath = join(assetsDir, file); + return statSync(filePath).isFile(); + }); + + console.log(`📁 Files in directory (${filesInDir.length}):`); + filesInDir.forEach((file) => { + const filePath = join(assetsDir, file); + const stats = statSync(filePath); + const sizeKB = (stats.size / 1024).toFixed(2); + console.log(` - ${file} (${sizeKB} KB)`); + }); + + // Check if each ImgBB URL has a corresponding downloaded file + console.log(`\n🔍 Verifying downloads:\n`); + + let allDownloaded = true; + for (const { type, url } of imgBBUrls) { + const expectedFilename = getFilenameFromUrl(url); + const filePath = join(assetsDir, expectedFilename); + const exists = existsSync(filePath); + + if (exists) { + const stats = statSync(filePath); + const sizeKB = (stats.size / 1024).toFixed(2); + console.log(` ✅ ${type.padEnd(12)} → ${expectedFilename} (${sizeKB} KB)`); + } else { + console.log(` ❌ ${type.padEnd(12)} → ${expectedFilename} (NOT FOUND)`); + console.log(` Expected from: ${url}`); + allDownloaded = false; + } + } + + // Summary + console.log('\n' + '─'.repeat(60)); + if (allDownloaded) { + console.log('✅ All ImgBB assets have been downloaded successfully!'); + console.log('\n💡 Next steps:'); + console.log(' 1. Run: npm run build'); + console.log(' 2. Check build logs for: "✅ Downloaded ..." messages'); + console.log(' 3. Verify images load from local paths in the app'); + } else { + console.log('⚠️ Some assets are missing from the download directory'); + console.log('\n💡 To fix:'); + console.log(' 1. Run: npm run build'); + console.log(' 2. The build process should automatically download missing assets'); + console.log(' 3. Check build logs for any download errors'); + } + + // Check build-time config + console.log('\n🔍 Checking build-time config environment variable...\n'); + + const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; + if (buildTimeConfig) { + try { + const parsedConfig = JSON.parse(buildTimeConfig); + if (parsedConfig.assets?.logos) { + console.log('📋 Assets in NEXT_PUBLIC_BUILD_TIME_CONFIG:\n'); + for (const assetType of assetTypes) { + const url = parsedConfig.assets.logos[assetType]; + if (url) { + if (url.startsWith('/')) { + console.log(` ${assetType.padEnd(12)} → 📁 ${url} (localized)`); + } else if (isImgBBUrl(url)) { + console.log(` ${assetType.padEnd(12)} → 🔗 ${url} (still ImgBB - not localized!)`); + } else { + console.log(` ${assetType.padEnd(12)} → ${url}`); + } + } + } + } + } catch (error: any) { + console.log('⚠️ Failed to parse NEXT_PUBLIC_BUILD_TIME_CONFIG:', error.message); + } + } else { + console.log('ℹ️ NEXT_PUBLIC_BUILD_TIME_CONFIG is not set'); + console.log(' This is normal if you haven\'t run a build yet'); + } +} + +verifyAssetDownloads().catch((error) => { + console.error('❌ Verification failed:', error); + process.exit(1); +}); + From 03ae7dd7e8d536b81ae75a0fd3c7ea486b82ef10 Mon Sep 17 00:00:00 2001 From: Jesse Paterson Date: Sun, 30 Nov 2025 16:51:38 -0600 Subject: [PATCH 114/155] removing the fallback --- scripts/verify-asset-downloads.ts | 24 +++++++- src/config/index.ts | 92 ++++++++++++++++--------------- 2 files changed, 69 insertions(+), 47 deletions(-) diff --git a/scripts/verify-asset-downloads.ts b/scripts/verify-asset-downloads.ts index e98515176..17707e4c5 100644 --- a/scripts/verify-asset-downloads.ts +++ b/scripts/verify-asset-downloads.ts @@ -60,6 +60,24 @@ function getFilenameFromUrl(url: string): string { } } +/** + * Type for config returned from database + */ +type DatabaseConfig = { + brand?: any; + assets?: { + logos?: { + main?: string; + icon?: string; + favicon?: string; + appleTouch?: string; + og?: string; + splash?: string; + }; + }; + [key: string]: any; +}; + /** * Main verification function */ @@ -68,15 +86,17 @@ async function verifyAssetDownloads() { console.log(`📦 Community: ${community}\n`); // Fetch config from database - const { data: config, error } = await supabase + const { data, error } = await supabase .rpc('get_active_community_config', { p_community_id: community }) .single(); - if (error || !config) { + if (error || !data) { console.error('❌ Failed to fetch config from database:', error?.message); process.exit(1); } + const config = data as DatabaseConfig; + if (!config.assets?.logos) { console.error('❌ No assets_config.logos found in config'); process.exit(1); diff --git a/src/config/index.ts b/src/config/index.ts index 7b9133171..fa396c304 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -9,58 +9,60 @@ const AVAILABLE_CONFIGURATIONS = ['nouns', 'example', 'clanker'] as const; type CommunityConfig = typeof AVAILABLE_CONFIGURATIONS[number]; // Configuration loader +// REQUIRES database config - no fallback to static configs export const loadSystemConfig = (): SystemConfig => { // Get the community configuration from environment variable const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; - // Try build-time config from database (stored in env var at build time) + // REQUIRED: Build-time config from database (stored in env var at build time) const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; - if (buildTimeConfig) { - try { - const dbConfig = JSON.parse(buildTimeConfig) as any; - // Validate config structure - if (dbConfig && dbConfig.brand && dbConfig.assets) { - console.log('✅ Using config from database'); - // Map pages object to homePage/explorePage for backward compatibility - // Add themes from shared file (themes are not in database) - const mappedConfig: SystemConfig = { - ...dbConfig, - theme: themes, // Themes come from shared file - homePage: dbConfig.pages?.['home'] || dbConfig.homePage || null, - explorePage: dbConfig.pages?.['explore'] || dbConfig.explorePage || null, - }; - return mappedConfig as SystemConfig; - } else { - console.warn('⚠️ Invalid config structure from DB, falling back to static'); - } - } catch (error) { - console.warn('⚠️ Failed to parse build-time config, falling back to static:', error); - } - } - - // Fall back to static configs - console.log('ℹ️ Using static configs'); - // Validate the configuration - if (!isValidCommunityConfig(communityConfig)) { - console.warn( - `Invalid community configuration: "${communityConfig}". ` + - `Available options: ${AVAILABLE_CONFIGURATIONS.join(', ')}. ` + - `Falling back to "nouns" configuration.` - ); + if (!buildTimeConfig) { + const errorMsg = + `❌ NEXT_PUBLIC_BUILD_TIME_CONFIG is not set. ` + + `Database configuration is required. ` + + `Ensure Supabase credentials are set and run: npm run build`; + console.error(errorMsg); + throw new Error(errorMsg); } - - // Switch between available configurations - switch (communityConfig.toLowerCase()) { - case 'nouns': - return nounsSystemConfig; - case 'example': - return exampleSystemConfig; - case 'clanker': - return clankerSystemConfig as unknown as SystemConfig; - // Add more community configurations here as they are created - default: - return nounsSystemConfig; + + try { + const dbConfig = JSON.parse(buildTimeConfig) as any; + + // Validate config structure + if (!dbConfig || !dbConfig.brand || !dbConfig.assets) { + const errorMsg = + `❌ Invalid config structure from database. ` + + `Missing required fields: brand, assets. ` + + `Ensure database is seeded correctly.`; + console.error(errorMsg); + console.error('Config received:', Object.keys(dbConfig || {})); + throw new Error(errorMsg); + } + + console.log('✅ Using config from database'); + + // Map pages object to homePage/explorePage for backward compatibility + // Add themes from shared file (themes are not in database) + const mappedConfig: SystemConfig = { + ...dbConfig, + theme: themes, // Themes come from shared file + homePage: dbConfig.pages?.['home'] || dbConfig.homePage || null, + explorePage: dbConfig.pages?.['explore'] || dbConfig.explorePage || null, + }; + + return mappedConfig as SystemConfig; + } catch (error) { + if (error instanceof SyntaxError) { + const errorMsg = + `❌ Failed to parse build-time config from database. ` + + `Invalid JSON in NEXT_PUBLIC_BUILD_TIME_CONFIG. ` + + `Error: ${error.message}`; + console.error(errorMsg); + throw new Error(errorMsg); + } + // Re-throw validation errors + throw error; } }; From 0c5cb2d137021ce70f87123a080ca553aeeb6d4c Mon Sep 17 00:00:00 2001 From: Jesse Paterson Date: Tue, 2 Dec 2025 12:18:45 -0600 Subject: [PATCH 115/155] require > import --- docs/COMMUNITY_CONFIG_SYSTEM.md | 12 +- docs/CONFIGURATION.md | 103 +++-- docs/PROJECT_STRUCTURE.md | 23 +- docs/README.md | 2 +- docs/SYSTEMS/CONFIGURATION/OVERVIEW.md | 182 +++++--- docs/SYSTEMS/CONFIGURATION/TESTING.md | 408 ------------------ middleware.ts | 55 +++ next.config.mjs | 23 +- scripts/seed-all.ts | 4 +- scripts/seed-community-configs.ts | 9 +- scripts/seed-data.ts | 12 + scripts/seed-navpage-spaces.ts | 4 +- scripts/verify-asset-downloads.ts | 32 +- src/app/[navSlug]/[[...tabName]]/page.tsx | 64 +-- src/app/layout.tsx | 103 ++--- src/app/page.tsx | 8 +- .../components/organisms/NogsGateButton.tsx | 37 +- src/common/lib/theme/BackgroundGenerator.tsx | 12 +- src/config/clanker/index.ts | 29 +- src/config/example/index.ts | 29 +- src/config/index.ts | 152 +++---- src/config/loaders/factory.ts | 134 ++++++ src/config/loaders/index.ts | 20 + src/config/loaders/registry.ts | 56 +++ src/config/loaders/runtimeLoader.ts | 96 +++++ src/config/loaders/types.ts | 53 +++ src/config/nouns/index.ts | 27 +- src/constants/metadata.ts | 82 ++-- src/constants/nogs.ts | 24 +- src/constants/spaceToken.ts | 35 +- .../farcaster/components/CreateCast.tsx | 18 +- 31 files changed, 935 insertions(+), 913 deletions(-) delete mode 100644 docs/SYSTEMS/CONFIGURATION/TESTING.md create mode 100644 middleware.ts create mode 100644 scripts/seed-data.ts create mode 100644 src/config/loaders/factory.ts create mode 100644 src/config/loaders/index.ts create mode 100644 src/config/loaders/registry.ts create mode 100644 src/config/loaders/runtimeLoader.ts create mode 100644 src/config/loaders/types.ts diff --git a/docs/COMMUNITY_CONFIG_SYSTEM.md b/docs/COMMUNITY_CONFIG_SYSTEM.md index 93dcab8bf..9801c7588 100644 --- a/docs/COMMUNITY_CONFIG_SYSTEM.md +++ b/docs/COMMUNITY_CONFIG_SYSTEM.md @@ -435,9 +435,9 @@ interface UIConfig { ## Configuration Loading -The system uses **database-backed configuration** with build-time loading. Configs are fetched from Supabase during build and stored in an environment variable. If the database is unavailable, the system falls back to static TypeScript configs. +The system uses **database-backed configuration** with runtime loading. Configs are fetched from Supabase at runtime based on the request domain. If the database is unavailable, the system falls back to static TypeScript configs. -### Build-Time Configuration +### Runtime Configuration The community is determined at build time via the `NEXT_PUBLIC_COMMUNITY` environment variable: @@ -526,7 +526,7 @@ const MyComponent = () => { ```typescript import { loadSystemConfig } from '@/config'; -// In server components or build-time code +// In server components const config = loadSystemConfig(); const metadata: Metadata = { title: config.brand.displayName, @@ -676,7 +676,7 @@ NEXT_PUBLIC_COMMUNITY=mycommunity npm run build ## Key Features -### 1. Build-Time Configuration +### 1. Runtime Configuration - Configuration is determined at build time - No runtime switching between communities - Optimized bundle size (only one community's config included) @@ -713,7 +713,7 @@ NEXT_PUBLIC_COMMUNITY=mycommunity npm run build - Provides caching and memoization 2. **Direct Loading for Server-Side** - - Use `loadSystemConfig()` in server components or build-time code + - Use `await loadSystemConfig()` in server components - No React hooks available in these contexts 3. **Type Safety** @@ -796,5 +796,5 @@ NEXT_PUBLIC_COMMUNITY=mycommunity npm run build ## Summary -The Community Configuration System provides a comprehensive whitelabeling solution that allows Nounspace to be customized for different communities. It uses build-time configuration for optimal performance, provides full type safety, and maintains a modular structure for easy maintenance and extension. +The Community Configuration System provides a comprehensive whitelabeling solution that allows Nounspace to be customized for different communities. It uses runtime configuration from the database, provides full type safety, and maintains a modular structure for easy maintenance and extension. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 5219307c1..43d40cde1 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -1,6 +1,6 @@ # Configuration Guide -Nounspace uses a **database-backed configuration system** that allows community configurations to be stored in Supabase and loaded at build time. This provides admin-editable configs with zero runtime database queries. +Nounspace uses a **database-backed configuration system** with **domain-based multi-tenant support**. Community configurations are stored in Supabase and loaded dynamically based on the request domain, enabling a single deployment to serve multiple communities. ## Database-Backed Configuration System @@ -13,16 +13,18 @@ The application uses environment variables to configure community-specific setti ### Community Configuration -The most important environment variable for whitelabeling is: +The system automatically detects the community from the request domain via middleware. You can also set: ```bash NEXT_PUBLIC_COMMUNITY=nouns ``` -This determines which community configuration to load. Available options: -- `nouns` (default) - Uses the Nouns community configuration -- `example` - Uses the example community configuration template -- `clanker` - Uses the Clanker community configuration +**Available communities:** +- `nouns` (default) +- `example` +- `clanker` + +**Note:** In production, the community is automatically detected from the domain (e.g., `example.nounspace.com` → `example`). The `NEXT_PUBLIC_COMMUNITY` env var is used as a fallback. ### Required Environment Variables @@ -50,16 +52,13 @@ NEXT_PUBLIC_WEBSITE_URL=http://localhost:3000 ## Configuration Loading -The application loads configurations in the following order: - -1. **Database Config** (if available): Fetched from Supabase during build, stored in `NEXT_PUBLIC_BUILD_TIME_CONFIG` env var -2. **Static Config Fallback**: Falls back to static TypeScript configs if database is unavailable +The application uses **runtime loading** from Supabase. Config is fetched from the database at runtime based on the request domain, enabling multi-tenant support. -See [Configuration System Overview](SYSTEMS/CONFIGURATION/OVERVIEW.md) for details on the build-time loading process. +See [Configuration System Overview](SYSTEMS/CONFIGURATION/OVERVIEW.md) for details. ## Configuration Structure -Configurations are stored in Supabase and loaded at build time. Static TypeScript configs serve as fallback. The structure is organized into community-specific folders: +Configurations are stored in Supabase and loaded dynamically. Static TypeScript configs serve as fallback. The structure is organized into community-specific folders: ``` src/config/ @@ -79,64 +78,74 @@ src/config/ ├── example/ # Example community configuration (fallback) ├── shared/ # Shared configuration │ └── themes.ts # Shared theme definitions (all communities) +├── loaders/ # Configuration loading +│ ├── types.ts # Loader interfaces +│ ├── registry.ts # Domain resolution +│ ├── runtimeLoader.ts # Runtime config loader +│ ├── factory.ts # Loader factory +│ └── index.ts # Public API ├── systemConfig.ts # System configuration interface ├── initialSpaceConfig.ts # Base space configuration -└── index.ts # Main configuration loader (reads from DB or static) +└── index.ts # Main configuration loader ``` **Note:** Themes are stored in `shared/themes.ts` and pages (homePage/explorePage) are stored as Spaces in Supabase Storage. See [Configuration System Overview](SYSTEMS/CONFIGURATION/OVERVIEW.md) for details. -## Build-Time Configuration +## Domain-Based Multi-Tenant Configuration -The system uses build-time configuration to determine which community configuration to use. This means: +The system uses **middleware-based domain detection** to automatically determine which community configuration to load: -1. **Environment Variable**: Set `NEXT_PUBLIC_COMMUNITY` at build time -2. **Build Process**: The configuration is baked into the build -3. **No Runtime Switching**: The configuration cannot be changed after build +1. **Middleware** detects the domain from request headers (e.g., `example.nounspace.com`) +2. **Resolves community ID** from domain (e.g., `example.nounspace.com` → `example`) +3. **Sets headers** for Server Components to read (`x-community-id`, `x-detected-domain`) +4. **Loads config** from database at runtime -### Building Different Communities +### How It Works -**For Nouns community (default):** -```bash -npm run build -# or explicitly -NEXT_PUBLIC_COMMUNITY=nouns npm run build +**Request Flow:** ``` - -**For Example community:** -```bash -NEXT_PUBLIC_COMMUNITY=example npm run build +Browser Request (example.nounspace.com) + ↓ +Middleware (detects domain, sets x-community-id header) + ↓ +Server Component (reads header, loads config) + ↓ +Renders with correct community config ``` -**For Clanker community:** -```bash -NEXT_PUBLIC_COMMUNITY=clanker npm run build -``` +**For All Communities:** +- Config loaded from Supabase database at runtime +- Supports multi-tenant (different domains → different communities) +- Single deployment serves all communities ### Development -**For Nouns development:** -```bash -npm run dev -# or explicitly -NEXT_PUBLIC_COMMUNITY=nouns npm run dev -``` +**Testing Runtime Communities Locally:** + +1. **Use localhost subdomains:** + ```bash + # Visit: example.localhost:3000 + # System detects 'example' from subdomain + ``` -**For Example development:** +2. **Or use environment variable override:** + ```bash + NEXT_PUBLIC_TEST_COMMUNITY=example npm run dev + ``` + +**Testing Specific Communities:** ```bash -NEXT_PUBLIC_COMMUNITY=example npm run dev +NEXT_PUBLIC_COMMUNITY=nouns npm run dev ``` ### What Gets Configured -When you set `NEXT_PUBLIC_COMMUNITY=example` (or `clanker`), the system will: - -1. **Load that community's system config** from Supabase (brand, assets, community settings, fidgets, navigation, UI) -2. **Load shared themes** from `src/config/shared/themes.ts` -3. **Load navigation pages** as Spaces from Supabase Storage (referenced by navigation items) -4. **Use that community's initial space templates** (profile, channel, token, proposal, homebase) +For each community, the system loads: -If the database is unavailable, the system falls back to static TypeScript configs. +1. **System config** from Supabase (brand, assets, community settings, fidgets, navigation, UI) +2. **Shared themes** from `src/config/shared/themes.ts` +3. **Navigation pages** as Spaces from Supabase Storage (referenced by navigation items) +4. **Initial space templates** (profile, channel, token, proposal, homebase) ## Adding New Community Configurations diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md index 937458a69..947e686dd 100644 --- a/docs/PROJECT_STRUCTURE.md +++ b/docs/PROJECT_STRUCTURE.md @@ -16,6 +16,7 @@ nounspace.ts/ ├── supabase/ # Database configuration ├── tests/ # Test files ├── .husky/ # Git hooks +├── middleware.ts # Next.js middleware (domain detection) ├── package.json # Dependencies ├── next.config.mjs # Next.js configuration ├── tailwind.config.js # Tailwind CSS configuration @@ -131,11 +132,11 @@ src/constants/ ### 5. System Configuration (`src/config/`) -Whitelabeling and system configuration for community customization. Configurations are stored in Supabase and loaded at build time. See [Configuration System](SYSTEMS/CONFIGURATION/OVERVIEW.md) for details. +Whitelabeling and system configuration with domain-based multi-tenant support. Configurations are stored in Supabase and loaded dynamically based on request domain. See [Configuration System](SYSTEMS/CONFIGURATION/OVERVIEW.md) for details. ``` src/config/ # System configuration -├── nouns/ # Nouns community configuration +├── nouns/ # Nouns community configuration (fallback) │ ├── nouns.brand.ts # Brand identity │ ├── nouns.assets.ts # Visual assets │ ├── nouns.community.ts # Community integration @@ -146,17 +147,25 @@ src/config/ # System configuration │ ├── nouns.theme.ts # Theme config (references shared) │ ├── nouns.ui.ts # UI colors │ └── initialSpaces/ # Initial space templates -├── clanker/ # Clanker community configuration -├── example/ # Example community configuration +├── clanker/ # Clanker community configuration (fallback) +├── example/ # Example community configuration (fallback) ├── shared/ # Shared configuration -│ └── themes.ts # Shared theme definitions +│ └── themes.ts # Shared theme definitions (all communities) +├── loaders/ # Configuration loading +│ ├── types.ts # Loader interfaces +│ ├── registry.ts # Domain resolution +│ ├── runtimeLoader.ts # Runtime config loader +│ ├── factory.ts # Loader factory +│ └── index.ts # Public API ├── systemConfig.ts # System configuration interface -├── index.ts # Configuration loader (reads from DB or static) +├── index.ts # Main configuration loader └── initialSpaceConfig.ts # Base space configuration ``` **Key Features:** -- **Database-Backed**: Configs stored in Supabase, loaded at build time +- **Database-Backed**: Configs stored in Supabase, loaded dynamically +- **Multi-Tenant**: Domain-based community detection via middleware +- **Runtime Loading**: All communities load config from database at runtime - **Whitelabeling Support**: Complete brand customization through configuration - **Community Integration**: URLs, social handles, governance links, contract addresses - **Shared Themes**: Themes stored in shared file, not per-community diff --git a/docs/README.md b/docs/README.md index ed21dcf7e..0824676d3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -43,7 +43,7 @@ Nounspace is a highly customizable Farcaster client funded by Nouns DAO. This do ### Configuration - [Configuration System](SYSTEMS/CONFIGURATION/OVERVIEW.md) - Database-backed configuration system -- [Testing Guide](SYSTEMS/CONFIGURATION/TESTING.md) - How to test the configuration system +- [Configuration System Overview](SYSTEMS/CONFIGURATION/OVERVIEW.md) - Complete configuration system documentation ### Discovery - [Mini App Discovery System](SYSTEMS/DISCOVERY/MINI_APP_DISCOVERY_SYSTEM.md) - Mini-app discovery system diff --git a/docs/SYSTEMS/CONFIGURATION/OVERVIEW.md b/docs/SYSTEMS/CONFIGURATION/OVERVIEW.md index eefde377e..33033b425 100644 --- a/docs/SYSTEMS/CONFIGURATION/OVERVIEW.md +++ b/docs/SYSTEMS/CONFIGURATION/OVERVIEW.md @@ -2,29 +2,34 @@ ## Overview -Nounspace uses a database-backed configuration system that allows community configurations to be stored in Supabase and loaded at build time. This provides admin-editable configs with zero runtime database queries. +Nounspace uses a database-backed configuration system with **domain-based multi-tenant support**. Community configurations are stored in Supabase and loaded dynamically at runtime based on the request domain, enabling a single deployment to serve multiple communities. ## Architecture ``` ┌─────────────────┐ -│ Database │ -│ (Stores Config)│ +│ Browser │ +│ (Request) │ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│ Middleware │ +│ (Edge Runtime) │ +│ - Detects domain│ +│ - Sets headers │ └────────┬────────┘ │ ▼ ┌─────────────────┐ ┌──────────────────┐ -│ Build Process │─────▶│ Set Env Var │ -│ (next.config.mjs)│ │ NEXT_PUBLIC_ │ -│ │ │ BUILD_TIME_ │ -│ │ │ CONFIG │ -└─────────────────┘ └─────────┬──────────┘ +│ Server Component│─────▶│ Config Loader │ +│ │ │ (Runtime) │ +└─────────────────┘ └─────────┬────────┘ │ ▼ ┌──────────────────┐ - │ Runtime App │ - │ (Reads Env Var │ - │ Zero DB Queries)│ + │ Database │ + │ (Runtime) │ └──────────────────┘ ``` @@ -63,34 +68,41 @@ CREATE TABLE "public"."community_configs" ( - **Themes**: Stored in `src/config/shared/themes.ts` (shared across communities) - **Pages** (homePage/explorePage): Stored as Spaces in Supabase Storage, referenced by navigation items -## Build-Time Loading +## Configuration Loading -### Process +All communities use runtime loading from Supabase: -1. **Fetch Config from Database** - ```javascript - // next.config.mjs runs during build - const { data } = await supabase - .rpc('get_active_community_config', { p_community_id: community }) - .single(); +**Process:** +1. **Middleware Detects Domain** + ```typescript + // middleware.ts + const domain = request.headers.get('host'); // "example.nounspace.com" + const communityId = resolveCommunityFromDomain(domain); // "example" + response.headers.set('x-community-id', communityId); ``` -2. **Store in Environment Variable** - ```javascript - process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(data); +2. **Server Component Reads Header** + ```typescript + // Server Component + const headersList = await headers(); + const communityId = headersList.get('x-community-id'); ``` -3. **Runtime Access** +3. **Fetch Config from Database** (at request time) ```typescript - // src/config/index.ts - const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; - if (buildTimeConfig) { - const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; - return dbConfig; - } - // Falls back to static configs if unavailable + // src/config/loaders/runtimeLoader.ts + const { data } = await supabase + .rpc('get_active_community_config', { p_community_id: communityId }) + .single(); + return data; ``` +**Benefits:** +- Multi-tenant support (different domains → different communities) +- Single deployment serves all communities +- Config can be updated without rebuild +- Dynamic configuration updates + ### Database Function The `get_active_community_config(community_id)` function returns a combined JSON object: @@ -237,40 +249,68 @@ The database config is ~2.8 KB (down from ~29 KB) by: This size reduction makes the environment variable approach viable. -## Runtime Access +## Request Flow + +### Complete Flow Example + +**User visits:** `https://example.nounspace.com/home` + +1. **Middleware** (Edge Runtime) + - Extracts domain: `example.nounspace.com` + - Resolves community ID: `example` + - Sets headers: `x-community-id: example`, `x-detected-domain: example.nounspace.com` + +2. **Server Component** + - Reads `x-community-id` header + - Calls `await loadSystemConfig()` + - Fetches config from database for the detected community + - Renders page with correct config + +3. **Client Component** + - Uses `window.location.hostname` for domain detection + - Config loading is always async (from database) ### Config Loader ```typescript // src/config/index.ts -export const loadSystemConfig = (): SystemConfig => { - const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; - if (buildTimeConfig) { - const dbConfig = JSON.parse(buildTimeConfig) as SystemConfig; - // Map pages object to homePage/explorePage for backward compatibility - return { - ...dbConfig, - homePage: dbConfig.pages?.['home'] || dbConfig.homePage || null, - explorePage: dbConfig.pages?.['explore'] || dbConfig.explorePage || null, - }; - } - // Fall back to static configs -}; +export async function loadSystemConfig(context?: ConfigLoadContext): Promise { + // Server-side: reads from middleware-set headers + // Client-side: uses window.location.hostname + + const factory = getConfigLoaderFactory(); + const loader = factory.getLoader(context); + + // Always uses runtime loader (fetches from database) + return await loader.load(context); +} ``` ### Component Usage +**Server Components** (must await): +```typescript +// Server Component +import { loadSystemConfig } from '@/config'; + +export default async function Layout() { + const config = await loadSystemConfig(); + return
{config.brand.displayName}
; +} +``` + +**Client Components** (always async): ```typescript -// In components +// Client Component import { loadSystemConfig } from '@/config'; -const config = loadSystemConfig(); -const brandName = config.brand.displayName; -const navItems = config.navigation?.items || []; +function Navigation() { + const config = await loadSystemConfig(); // Always async (from database) +} ``` +**React Hook**: ```typescript -// React hook import { useSystemConfig } from '@/common/lib/hooks/useSystemConfig'; function Navigation() { @@ -281,33 +321,47 @@ function Navigation() { ## Environment Variables -**Required for Build:** +**Required:** - `NEXT_PUBLIC_SUPABASE_URL` - Supabase project URL -- `SUPABASE_SERVICE_ROLE_KEY` - Service role key for build-time access -- `NEXT_PUBLIC_COMMUNITY` - Community ID (defaults to 'nouns') +- `NEXT_PUBLIC_SUPABASE_ANON_KEY` - Supabase anon key (for runtime loading) +- `SUPABASE_SERVICE_ROLE_KEY` - Service role key (for seeding) + +**Optional:** +- `NEXT_PUBLIC_COMMUNITY` - Community ID (defaults to 'nouns', used as fallback) +- `NEXT_PUBLIC_TEST_COMMUNITY` - Override for local testing (development only) + +## Domain Resolution + +The system automatically resolves community ID from domain: + +- `example.nounspace.com` → `example` +- `clanker.nounspace.com` → `clanker` +- `example.localhost:3000` → `example` (for local testing) -**Fallback Behavior:** -- If DB credentials missing → Falls back to static configs -- If DB config not found → Falls back to static configs -- App continues to work in all cases +**Priority order:** +1. Middleware-set header (`x-community-id`) +2. Development override (`NEXT_PUBLIC_TEST_COMMUNITY`) +3. Domain resolution +4. Environment variable (`NEXT_PUBLIC_COMMUNITY`) ## Benefits -- **Zero Runtime Overhead** - No database queries in production +- **Multi-Tenant Support** - Single deployment serves multiple communities +- **Domain-Based Routing** - Automatic community detection from domain - **Admin Updates** - Configs can be updated via database -- **Fast Runtime** - Config loaded from env var (instant) -- **Safe Fallback** - Static configs always available -- **Small Config** - Only ~2.8 KB (fits in env vars) +- **Dynamic Updates** - Config changes without rebuild +- **Small Config** - Only ~2.8 KB - **Unified Architecture** - Pages are Spaces, consistent with existing system - **Shared Themes** - Single source of truth, no duplication ## Related Files - **Database**: `supabase/migrations/20251129172847_create_community_configs.sql` -- **Build Config**: `next.config.mjs` - Loads config at build time -- **Config Loader**: `src/config/index.ts` - Reads from env var +- **Middleware**: `middleware.ts` - Domain detection and header setting +- **Build Config**: `next.config.mjs` - Downloads assets at build time +- **Config Loaders**: `src/config/loaders/` - Strategy pattern implementation +- **Config Loader**: `src/config/index.ts` - Main config loading function - **Route Handler**: `src/app/[navSlug]/[[...tabName]]/page.tsx` - Dynamic navigation -- **Space Seeding**: `scripts/seed-navpage-spaces.ts` - Uploads space configs to Storage +- **Space Seeding**: `scripts/seed-all.ts` - Unified seeding script - **Shared Themes**: `src/config/shared/themes.ts` - Theme definitions -- **Testing Guide**: [Testing Guide](TESTING.md) - How to test the configuration system diff --git a/docs/SYSTEMS/CONFIGURATION/TESTING.md b/docs/SYSTEMS/CONFIGURATION/TESTING.md deleted file mode 100644 index 16c8cf3df..000000000 --- a/docs/SYSTEMS/CONFIGURATION/TESTING.md +++ /dev/null @@ -1,408 +0,0 @@ -# Testing the Database-Backed Configuration System - -This guide walks you through testing the database-backed configuration system to ensure everything is working correctly. - -## Prerequisites - -1. **Supabase Setup** - - Local Supabase running (`supabase start`) OR - - Remote Supabase project with access - -2. **Environment Variables** - ```bash - NEXT_PUBLIC_SUPABASE_URL=your_supabase_url - SUPABASE_SERVICE_ROLE_KEY=your_service_role_key - NEXT_PUBLIC_COMMUNITY=nouns # or 'example', 'clanker' - ``` - -3. **Database Seeded** - - Run migrations: `supabase db reset` (includes seed.sql) - - Or manually seed: `tsx scripts/seed-community-configs.ts` - - Seed navigation spaces: `tsx scripts/seed-navpage-spaces.ts` - -## Testing Checklist - -### 1. Verify Database Setup - -**Check that configs exist in database:** - -```bash -# Using Supabase CLI -supabase db reset - -# Or query directly -psql -h localhost -p 54322 -U postgres -d postgres -c "SELECT community_id, is_published FROM community_configs;" -``` - -**Expected:** Rows for 'nouns', 'example', 'clanker' with `is_published = true` - -**Check navigation spaces exist:** - -```bash -psql -h localhost -p 54322 -U postgres -d postgres -c "SELECT \"spaceName\", \"spaceType\" FROM \"spaceRegistrations\" WHERE \"spaceType\" = 'navPage';" -``` - -**Expected:** Rows for 'nouns-home', 'nouns-explore', 'clanker-home', etc. - -### 2. Test Build-Time Loading - -**Important:** `next.config.mjs` runs both during `npm run build` AND `npm run dev`. The config is loaded when the Next.js process starts. - -**Build the app and check logs:** - -```bash -npm run build -``` - -**Or start dev server and check logs:** - -```bash -npm run dev -``` - -**Look for these log messages in the terminal where you run the command:** - -✅ **Success:** -``` -✅ Loaded config from database -✅ Using config from database -``` - -⚠️ **Fallback (if DB unavailable):** -``` -ℹ️ Using static configs (no DB credentials) -ℹ️ Using static configs -``` - -**Note:** The config is loaded when the dev server starts. If you update the database config, you need to **restart the dev server** (`Ctrl+C` then `npm run dev` again) to pick up changes. - -### 3. Test Runtime Access - -**Start the dev server (if not already running):** - -```bash -npm run dev -``` - -**Check the terminal where you started dev server:** -- Should see: `✅ Loaded config from database` (if DB is available) -- Or: `ℹ️ Using static configs` (if DB unavailable) - -**Check browser console for config loading:** - -Open browser DevTools → Console, look for: -- No errors related to config loading -- Navigation items render correctly -- Brand name displays correctly - -**Verify config in components:** - -Add a temporary log in a component: - -```typescript -// In any component -import { loadSystemConfig } from '@/config'; - -const config = loadSystemConfig(); -console.log('Config loaded:', { - brand: config.brand.displayName, - hasNavigation: !!config.navigation, - navItems: config.navigation?.items?.length || 0, -}); -``` - -**Expected:** -- `brand.displayName` matches your community (e.g., "Nouns") -- `navigation.items` contains navigation items -- Config structure matches `SystemConfig` interface - -### 4. Test Navigation Pages - -**Test home page:** - -1. Navigate to `/home` (should redirect to default tab) -2. Navigate to `/home/{tabName}` (e.g., `/home/Nouns`) -3. Verify: - - Page loads without errors - - Tabs display correctly - - Tab bar shows correct tabs - - Content renders for each tab - -**Test explore page (if exists):** - -1. Navigate to `/explore` (should redirect to default tab) -2. Navigate to `/explore/{tabName}` -3. Verify same as above - -**Check browser Network tab:** - -- No failed requests to Supabase Storage (for navPage spaces) -- Pages load from Storage correctly - -### 5. Test Fallback Behavior - -**Test without database credentials:** - -```bash -# Temporarily remove/comment out Supabase env vars -unset NEXT_PUBLIC_SUPABASE_URL -unset SUPABASE_SERVICE_ROLE_KEY - -npm run build -``` - -**Expected:** -- Build succeeds -- Logs show: `ℹ️ Using static configs (no DB credentials)` -- App still works (uses static TypeScript configs) - -**Test with invalid community:** - -```bash -NEXT_PUBLIC_COMMUNITY=invalid npm run build -``` - -**Expected:** -- Build succeeds -- Falls back to 'nouns' config -- Warning logged about invalid community - -### 6. Test Different Communities - -**Test Nouns:** - -```bash -NEXT_PUBLIC_COMMUNITY=nouns npm run build -npm run dev -``` - -**Verify:** -- Brand name: "Nouns" -- Navigation items match Nouns config -- Home page tabs match Nouns config - -**Test Clanker:** - -```bash -NEXT_PUBLIC_COMMUNITY=clanker npm run build -npm run dev -``` - -**Verify:** -- Brand name: "Clanker" -- Navigation items match Clanker config -- Home page tabs match Clanker config - -### 7. Test Shared Themes - -**Verify themes are loaded:** - -```typescript -// In any component -import { themes } from '@/config/shared/themes'; - -console.log('Available themes:', Object.keys(themes)); -``` - -**Expected:** -- Themes object exists -- Contains theme definitions (default, nounish, etc.) -- Themes are shared across all communities - -### 8. Test Database Updates - -**Update a config in database:** - -```sql -UPDATE community_configs -SET brand_config = jsonb_set( - brand_config, - '{displayName}', - '"Nouns Updated"' -) -WHERE community_id = 'nouns'; -``` - -**Rebuild and verify:** - -```bash -npm run build -npm run dev -``` - -**Expected:** -- New build picks up updated config -- Brand name shows "Nouns Updated" -- No need to change code - -### 9. Test Navigation Space References - -**Verify navigation items reference spaces:** - -```sql -SELECT - n.id, - n.label, - n."spaceId", - sr."spaceName", - sr."spaceType" -FROM jsonb_array_elements( - (SELECT "navigation_config"->'items' FROM community_configs WHERE community_id = 'nouns') -) AS n -LEFT JOIN "spaceRegistrations" sr ON sr."spaceId"::text = n->>'spaceId'; -``` - -**Expected:** -- Navigation items with `spaceId` reference valid `navPage` spaces -- Space names match (e.g., 'nouns-home', 'nouns-explore') - -**Verify space configs in Storage:** - -Check that space configs exist in Supabase Storage: -- `spaces/{spaceId}/tabOrder` -- `spaces/{spaceId}/tabs/{tabName}` - -### 10. Integration Test - -**Full flow test:** - -1. **Reset database:** - ```bash - supabase db reset - tsx scripts/seed-navpage-spaces.ts - ``` - -2. **Build:** - ```bash - npm run build - ``` - -3. **Start:** - ```bash - npm run dev - ``` - -4. **Navigate through app:** - - Visit `/` (should redirect to `/home/{defaultTab}`) - - Visit `/home` (should redirect to default tab) - - Visit `/home/Nouns` (or other tabs) - - Visit `/explore` (if exists) - - Click navigation items - - Verify brand header shows correct logo/name - -5. **Verify in browser:** - - No console errors - - All pages load correctly - - Navigation works - - Branding is correct - - Themes apply correctly - -## Common Issues - -### Issue: "Using static configs" when DB is available - -**Check:** -- `NEXT_PUBLIC_SUPABASE_URL` is set correctly -- `SUPABASE_SERVICE_ROLE_KEY` is set correctly -- Database is running and accessible -- `community_configs` table has data with `is_published = true` - -### Issue: Navigation pages return 404 - -**Check:** -- `navPage` spaces exist in `spaceRegistrations` -- Space configs uploaded to Storage via `seed-navpage-spaces.ts` -- Navigation items have correct `spaceId` references - -### Issue: Config not updating after DB change - -**Solution:** -- **For dev mode:** Restart the dev server (`Ctrl+C` then `npm run dev` again) -- **For production:** Rebuild the app (`npm run build`) -- Config is loaded when the Next.js process starts (both dev and build) -- Changes require restarting the process - -### Issue: Build fails with "supabaseKey is required" - -**Check:** -- `SUPABASE_SERVICE_ROLE_KEY` is set in environment -- Not `SUPABASE_SERVICE_KEY` (wrong name) -- Key has service role permissions - -## Quick Test Script - -Create a test script to verify everything: - -```typescript -// scripts/test-config.ts -import { createClient } from '@supabase/supabase-js'; -import { loadSystemConfig } from '../src/config'; - -async function testConfig() { - // Test 1: Database connection - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; - const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; - - if (!supabaseUrl || !supabaseKey) { - console.log('❌ Missing Supabase credentials'); - return; - } - - const supabase = createClient(supabaseUrl, supabaseKey); - - // Test 2: Config exists in DB - const { data: config, error } = await supabase - .rpc('get_active_community_config', { p_community_id: 'nouns' }) - .single(); - - if (error || !config) { - console.log('❌ Config not found in database:', error?.message); - return; - } - - console.log('✅ Config found in database'); - console.log(' Brand:', config.brand?.displayName); - console.log(' Navigation items:', config.navigation?.items?.length || 0); - - // Test 3: Runtime config loading - const runtimeConfig = loadSystemConfig(); - console.log('✅ Runtime config loaded'); - console.log(' Brand:', runtimeConfig.brand.displayName); - console.log(' Has navigation:', !!runtimeConfig.navigation); - - // Test 4: Navigation spaces - const { data: navSpaces } = await supabase - .from('spaceRegistrations') - .select('spaceName, spaceType') - .eq('spaceType', 'navPage'); - - console.log('✅ Navigation spaces:', navSpaces?.length || 0); - - console.log('\n✅ All tests passed!'); -} - -testConfig(); -``` - -Run with: -```bash -tsx scripts/test-config.ts -``` - -## Summary - -The testing process verifies: - -1. ✅ Database has configs -2. ✅ Build loads config from DB -3. ✅ Runtime accesses config correctly -4. ✅ Navigation pages load as Spaces -5. ✅ Fallback works when DB unavailable -6. ✅ Different communities work -7. ✅ Shared themes work -8. ✅ DB updates require rebuild -9. ✅ Navigation spaces are linked correctly -10. ✅ Full integration works - -If all tests pass, the database-backed configuration system is working correctly! - diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 000000000..5504ea92d --- /dev/null +++ b/middleware.ts @@ -0,0 +1,55 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; +import { resolveCommunityFromDomain } from '@/config/loaders/registry'; + +/** + * Middleware for domain-based community detection + * + * Detects the domain from the request and sets x-community-id header + * for Server Components to read. This centralizes domain detection logic + * and avoids URL processing in Server Components. + */ +export function middleware(request: NextRequest) { + // Get domain from request headers (synchronous in middleware) + const host = request.headers.get('host') || + request.headers.get('x-forwarded-host') || + ''; + + // Remove port number if present + const domain = host.split(':')[0]; + + // Resolve community ID from domain + const communityId = domain + ? resolveCommunityFromDomain(domain) + : null; + + // Create response + const response = NextResponse.next(); + + // Set headers for Server Components to read + if (communityId) { + response.headers.set('x-community-id', communityId); + } + + // Also set domain for reference/debugging + if (domain) { + response.headers.set('x-detected-domain', domain); + } + + return response; +} + +export const config = { + matcher: [ + /* + * Match all request paths except for the ones starting with: + * - api (API routes) + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + * - public files (images, etc.) + */ + '/((?!api|_next/static|_next/image|favicon.ico|images|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico)).*)', + ], +}; + diff --git a/next.config.mjs b/next.config.mjs index cd4e55aae..f2f996228 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -152,14 +152,14 @@ function getExtensionFromUrl(url) { return null; } -// Load config from database at build time and set as environment variable -// Config is now ~2.8 KB (down from ~29 KB), so env var approach works fine -async function loadConfigFromDB() { +// Download assets for the community specified in NEXT_PUBLIC_COMMUNITY +// This runs during build to pre-download and localize external assets +async function downloadAssetsForBuild() { const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; if (!supabaseUrl || !supabaseKey) { - console.log('ℹ️ Using static configs (no DB credentials)'); + console.log('ℹ️ Skipping asset download (no DB credentials)'); return; } @@ -172,7 +172,7 @@ async function loadConfigFromDB() { .single(); if (error || !data) { - console.log('ℹ️ Using static configs (no DB config found)'); + console.log('ℹ️ Skipping asset download (no DB config found)'); if (error) { console.log(` Error: ${error.message}`); } @@ -180,18 +180,15 @@ async function loadConfigFromDB() { } // Download external assets and localize paths - const configWithLocalAssets = await downloadAndLocalizeAssets(data, community); - - // Store config in environment variable (now small enough at ~2.8 KB) - process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG = JSON.stringify(configWithLocalAssets); - console.log('✅ Loaded config from database and downloaded assets'); + await downloadAndLocalizeAssets(data, community); + console.log('✅ Downloaded assets for community:', community); } catch (error) { - console.warn('⚠️ Error loading config from DB:', error.message); + console.warn('⚠️ Error downloading assets:', error.message); } } -// Load config before Next.js config is created -await loadConfigFromDB(); +// Download assets before Next.js config is created +await downloadAssetsForBuild(); const withBundleAnalyzer = bundlerAnalyzer({ enabled: process.env.ANALYZE === "true", diff --git a/scripts/seed-all.ts b/scripts/seed-all.ts index 139c7a970..d5e952007 100644 --- a/scripts/seed-all.ts +++ b/scripts/seed-all.ts @@ -32,9 +32,7 @@ import { SignedFile } from '../src/common/lib/signedFiles'; import { SpaceConfig } from '../src/app/(spaces)/Space'; // Import page configs for navPage spaces -import { nounsHomePage } from '../src/config/nouns/nouns.home'; -import { nounsExplorePage } from '../src/config/nouns/nouns.explore'; -import { clankerHomePage } from '../src/config/clanker/clanker.home'; +import { nounsHomePage, nounsExplorePage, clankerHomePage } from './seed-data'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); diff --git a/scripts/seed-community-configs.ts b/scripts/seed-community-configs.ts index 3a96ab9c8..e84a34aae 100644 --- a/scripts/seed-community-configs.ts +++ b/scripts/seed-community-configs.ts @@ -28,8 +28,12 @@ import { nounsBrand } from '../src/config/nouns/nouns.brand'; import { nounsTheme } from '../src/config/nouns/nouns.theme'; import { nounsCommunity } from '../src/config/nouns/nouns.community'; import { nounsFidgets } from '../src/config/nouns/nouns.fidgets'; -import { nounsHomePage } from '../src/config/nouns/nouns.home'; -import { nounsExplorePage } from '../src/config/nouns/nouns.explore'; +import { nounsHomePage, nounsExplorePage, clankerHomePage } from './seed-data'; +// Import remaining config pieces needed for this optional script +import { nounsBrand } from '../src/config/nouns/nouns.brand'; +import { nounsTheme } from '../src/config/nouns/nouns.theme'; +import { nounsCommunity } from '../src/config/nouns/nouns.community'; +import { nounsFidgets } from '../src/config/nouns/nouns.fidgets'; import { nounsNavigation } from '../src/config/nouns/nouns.navigation'; import { nounsUI } from '../src/config/nouns/nouns.ui'; @@ -47,7 +51,6 @@ import { clankerBrand } from '../src/config/clanker/clanker.brand'; import { clankerTheme } from '../src/config/clanker/clanker.theme'; import { clankerCommunity } from '../src/config/clanker/clanker.community'; import { clankerFidgets } from '../src/config/clanker/clanker.fidgets'; -import { clankerHomePage } from '../src/config/clanker/clanker.home'; import { clankerExplorePage } from '../src/config/clanker/clanker.explore'; import { clankerNavigation } from '../src/config/clanker/clanker.navigation'; import { clankerUI } from '../src/config/clanker/clanker.ui'; diff --git a/scripts/seed-data.ts b/scripts/seed-data.ts new file mode 100644 index 000000000..49ffb59dd --- /dev/null +++ b/scripts/seed-data.ts @@ -0,0 +1,12 @@ +/** + * Seed data exports for database seeding scripts + * + * This file re-exports only the config pieces needed for seeding. + * This keeps seed scripts separate from runtime configs. + */ + +// Re-export page configs needed for navPage space seeding +export { nounsHomePage } from '../src/config/nouns/nouns.home'; +export { nounsExplorePage } from '../src/config/nouns/nouns.explore'; +export { clankerHomePage } from '../src/config/clanker/clanker.home'; + diff --git a/scripts/seed-navpage-spaces.ts b/scripts/seed-navpage-spaces.ts index 6cc860c27..5bc9a7a5e 100755 --- a/scripts/seed-navpage-spaces.ts +++ b/scripts/seed-navpage-spaces.ts @@ -28,9 +28,7 @@ import { SignedFile } from '../src/common/lib/signedFiles'; import { SpaceConfig } from '../src/app/(spaces)/Space'; // Import page configs -import { nounsHomePage } from '../src/config/nouns/nouns.home'; -import { nounsExplorePage } from '../src/config/nouns/nouns.explore'; -import { clankerHomePage } from '../src/config/clanker/clanker.home'; +import { nounsHomePage, nounsExplorePage, clankerHomePage } from './seed-data'; const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; diff --git a/scripts/verify-asset-downloads.ts b/scripts/verify-asset-downloads.ts index 17707e4c5..3e96b4f80 100644 --- a/scripts/verify-asset-downloads.ts +++ b/scripts/verify-asset-downloads.ts @@ -195,35 +195,9 @@ async function verifyAssetDownloads() { console.log(' 3. Check build logs for any download errors'); } - // Check build-time config - console.log('\n🔍 Checking build-time config environment variable...\n'); - - const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; - if (buildTimeConfig) { - try { - const parsedConfig = JSON.parse(buildTimeConfig); - if (parsedConfig.assets?.logos) { - console.log('📋 Assets in NEXT_PUBLIC_BUILD_TIME_CONFIG:\n'); - for (const assetType of assetTypes) { - const url = parsedConfig.assets.logos[assetType]; - if (url) { - if (url.startsWith('/')) { - console.log(` ${assetType.padEnd(12)} → 📁 ${url} (localized)`); - } else if (isImgBBUrl(url)) { - console.log(` ${assetType.padEnd(12)} → 🔗 ${url} (still ImgBB - not localized!)`); - } else { - console.log(` ${assetType.padEnd(12)} → ${url}`); - } - } - } - } - } catch (error: any) { - console.log('⚠️ Failed to parse NEXT_PUBLIC_BUILD_TIME_CONFIG:', error.message); - } - } else { - console.log('ℹ️ NEXT_PUBLIC_BUILD_TIME_CONFIG is not set'); - console.log(' This is normal if you haven\'t run a build yet'); - } + // Note: Config is now loaded at runtime from database, not from env var + console.log('\nℹ️ Config is loaded at runtime from database'); + console.log(' Assets are downloaded during build and stored in public/images/'); } verifyAssetDownloads().catch((error) => { diff --git a/src/app/[navSlug]/[[...tabName]]/page.tsx b/src/app/[navSlug]/[[...tabName]]/page.tsx index f0fd1aeb6..57be9f7f5 100644 --- a/src/app/[navSlug]/[[...tabName]]/page.tsx +++ b/src/app/[navSlug]/[[...tabName]]/page.tsx @@ -18,7 +18,6 @@ async function loadSpaceAsPageConfig(spaceId: string): Promise = []; - - // Check if Supabase credentials are available for space loading - const hasSupabaseCredentials = !!( - process.env.NEXT_PUBLIC_SUPABASE_URL && - (process.env.SUPABASE_SERVICE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY) - ); - - // For each navigation item, generate params for all its tabs - // Note: We don't generate params for base path (no tabName) - it will redirect at runtime - // Note: If Supabase credentials aren't available, we skip spaces and only generate for legacy configs - for (const item of navItems) { - if (!item.href?.startsWith('/')) continue; - - const navSlug = item.href.slice(1); // Remove leading '/' - - // If nav item has a spaceId and we have credentials, try to load from storage - if (item.spaceId && hasSupabaseCredentials) { - try { - const pageConfig = await loadSpaceAsPageConfig(item.spaceId); - if (pageConfig) { - // Generate params for each tab - for (const tabName of pageConfig.tabOrder) { - params.push({ navSlug, tabName: [tabName] }); - } - } - // If loadSpaceAsPageConfig returns null (error loading), skip static generation - // The page will render at runtime instead - } catch (error) { - // Skip this space if there's an error - will render at runtime - console.warn(`Skipping static generation for ${navSlug} (space ${item.spaceId}):`, error); - } - } else if (!item.spaceId) { - // Fallback: check legacy configs for tabs (when no spaceId) - if (navSlug === 'home' && config.homePage) { - for (const tabName of config.homePage.tabOrder) { - params.push({ navSlug, tabName: [tabName] }); - } - } else if (navSlug === 'explore' && config.explorePage) { - for (const tabName of config.explorePage.tabOrder) { - params.push({ navSlug, tabName: [tabName] }); - } - } - } - // If item has spaceId but no credentials, skip static generation - will render at runtime - } - - return params; - } catch (error) { - // If anything fails in generateStaticParams, return empty array - // Pages will render at runtime instead - console.warn('Error in generateStaticParams, skipping static generation:', error); - return []; - } -} +// Force dynamic rendering for all pages +export const dynamic = 'force-dynamic'; export default async function NavPage({ params, @@ -166,7 +108,7 @@ export default async function NavPage({ notFound(); } - const config = loadSystemConfig(); + const config = await loadSystemConfig(); // Find navigation item by href const navItems = config.navigation?.items || []; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 05f3a3365..285e1f719 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -9,62 +9,63 @@ import ClientMobileHeaderWrapper from "@/common/components/organisms/ClientMobil import ClientSidebarWrapper from "@/common/components/organisms/ClientSidebarWrapper"; import type { Metadata } from 'next' // Migrating next/head -// Load system configuration -const config = loadSystemConfig(); - -// Create default frame from configuration -const defaultFrame = { - version: "next", - imageUrl: `${WEBSITE_URL}${config.assets.logos.og}`, - button: { - title: config.brand.name, - action: { - type: "launch_frame", - url: WEBSITE_URL, - name: config.brand.displayName, - splashImageUrl: `${WEBSITE_URL}${config.assets.logos.splash}`, - splashBackgroundColor: "#FFFFFF", +// Generate metadata dynamically (async) +export async function generateMetadata(): Promise { + const config = await loadSystemConfig(); + + const defaultFrame = { + version: "next", + imageUrl: `${WEBSITE_URL}${config.assets.logos.og}`, + button: { + title: config.brand.name, + action: { + type: "launch_frame", + url: WEBSITE_URL, + name: config.brand.displayName, + splashImageUrl: `${WEBSITE_URL}${config.assets.logos.splash}`, + splashBackgroundColor: "#FFFFFF", + } } - } -}; + }; -export const metadata: Metadata = { - title: config.brand.displayName, - description: config.brand.description, - openGraph: { - siteName: config.brand.displayName, + return { title: config.brand.displayName, - type: "website", description: config.brand.description, - images: { - url: `${WEBSITE_URL}${config.assets.logos.og}`, - type: "image/png", - width: 1200, - height: 737, - }, - url: WEBSITE_URL, - }, - icons: { - icon: [ - { - url: config.assets.logos.favicon, - }, - { - url: "/images/favicon-32x32.png", - sizes: "32x32", - }, - { - url: "/images/favicon-16x16.png", - sizes: "16x16", + openGraph: { + siteName: config.brand.displayName, + title: config.brand.displayName, + type: "website", + description: config.brand.description, + images: { + url: `${WEBSITE_URL}${config.assets.logos.og}`, + type: "image/png", + width: 1200, + height: 737, }, - ], - // Apple touch icon should be a PNG; configs provide a valid PNG path now - apple: config.assets.logos.appleTouch, - }, - other: { - "fc:frame": JSON.stringify(defaultFrame), - }, -}; + url: WEBSITE_URL, + }, + icons: { + icon: [ + { + url: config.assets.logos.favicon, + }, + { + url: "/images/favicon-32x32.png", + sizes: "32x32", + }, + { + url: "/images/favicon-16x16.png", + sizes: "16x16", + }, + ], + // Apple touch icon should be a PNG; configs provide a valid PNG path now + apple: config.assets.logos.appleTouch, + }, + other: { + "fc:frame": JSON.stringify(defaultFrame), + }, + }; +} // TO DO: Add global cookie check for a signature of a timestamp (within the last minute) // And a public key. If valid, we can prerender as if it is that user signed in diff --git a/src/app/page.tsx b/src/app/page.tsx index dfc785545..29bc9ca6f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,13 +1,13 @@ import { redirect } from "next/navigation"; import { loadSystemConfig } from "@/config"; -export default function RootRedirect() { - const config = loadSystemConfig(); +export default async function RootRedirect() { + const config = await loadSystemConfig(); // If homePage exists (legacy config), redirect to its default tab if (config.homePage?.defaultTab) { - const tab = encodeURIComponent(config.homePage.defaultTab); - redirect(`/home/${tab}`); + const tab = encodeURIComponent(config.homePage.defaultTab); + redirect(`/home/${tab}`); return null; } diff --git a/src/common/components/organisms/NogsGateButton.tsx b/src/common/components/organisms/NogsGateButton.tsx index 7f55d14a5..0d5350cfb 100644 --- a/src/common/components/organisms/NogsGateButton.tsx +++ b/src/common/components/organisms/NogsGateButton.tsx @@ -1,7 +1,7 @@ import React, { useState, useCallback, useEffect } from "react"; import { useAppStore } from "@/common/data/stores/app"; import { RECHECK_BACKOFF_FACTOR } from "@/common/data/stores/app/setup"; -import { NOGS_CONTRACT_ADDR } from "@/constants/nogs"; +import { getNogsContractAddr } from "@/constants/nogs"; import { ALCHEMY_API } from "@/constants/urls"; import { AlchemyIsHolderOfContract } from "@/pages/api/signerRequests"; import { usePrivy } from "@privy-io/react-auth"; @@ -13,7 +13,7 @@ import { isUndefined } from "lodash"; import { useBalance } from "wagmi"; import { Address, formatUnits, zeroAddress } from "viem"; import { base } from "viem/chains"; -import { SPACE_CONTRACT_ADDR } from "@/constants/spaceToken"; +import { getSpaceContractAddr } from "@/constants/spaceToken"; const MIN_SPACE_TOKENS_FOR_UNLOCK = 1111; @@ -51,15 +51,23 @@ const NogsGateButton = (props: ButtonProps) => { })); const [modalOpen, setModalOpen] = useState(false); + const [spaceContractAddr, setSpaceContractAddr] = useState
(null); + const [nogsContractAddr, setNogsContractAddr] = useState(null); + + // Load contract addresses (async) + useEffect(() => { + getSpaceContractAddr().then(addr => setSpaceContractAddr(addr)); + getNogsContractAddr().then(addr => setNogsContractAddr(addr)); + }, []); // ----- SPACE token gating ----- const walletAddress = user?.wallet?.address as Address | undefined; const { data: spaceBalanceData } = useBalance({ address: walletAddress ?? zeroAddress, - token: SPACE_CONTRACT_ADDR as Address, + token: (spaceContractAddr || zeroAddress) as Address, chainId: base.id, - query: { enabled: Boolean(walletAddress) }, + query: { enabled: Boolean(walletAddress) && !!spaceContractAddr }, }); const userHoldEnoughSpace = spaceBalanceData @@ -71,15 +79,15 @@ const NogsGateButton = (props: ButtonProps) => { // Optional debug logs useEffect(() => { - if (spaceBalanceData) { + if (spaceBalanceData && spaceContractAddr) { console.log( "[DEBUG] SPACE balance:", Number(formatUnits(spaceBalanceData.value, spaceBalanceData.decimals)), ); - console.log("[DEBUG] SPACE contract address:", SPACE_CONTRACT_ADDR); + console.log("[DEBUG] SPACE contract address:", spaceContractAddr); console.log("[DEBUG] Connected wallet:", walletAddress); } - }, [spaceBalanceData, walletAddress]); + }, [spaceBalanceData, spaceContractAddr, walletAddress]); useEffect(() => { console.log("[DEBUG] userHoldEnoughSpace:", userHoldEnoughSpace); @@ -102,14 +110,19 @@ const NogsGateButton = (props: ButtonProps) => { "[DEBUG] Using API:", `${ALCHEMY_API("base")}nft/v3/${process.env.NEXT_PUBLIC_ALCHEMY_API_KEY}/isHolderOfContract`, ); - console.log("[DEBUG] nOGs Contract:", NOGS_CONTRACT_ADDR); + if (!nogsContractAddr) { + console.error("[DEBUG] nOGs contract address not loaded yet"); + return false; + } + + console.log("[DEBUG] nOGs Contract:", nogsContractAddr); const { data } = await axios.get( `${ALCHEMY_API("base")}nft/v3/${process.env.NEXT_PUBLIC_ALCHEMY_API_KEY}/isHolderOfContract`, { params: { wallet: address, - contractAddress: NOGS_CONTRACT_ADDR, + contractAddress: nogsContractAddr, }, }, ); @@ -200,7 +213,7 @@ const NogsGateButton = (props: ButtonProps) => { // Automatically check nOGs on load useEffect(() => { async function checkNogsAuto() { - if (!walletAddress) return; + if (!walletAddress || !nogsContractAddr) return; try { const { data } = await axios.get( @@ -208,7 +221,7 @@ const NogsGateButton = (props: ButtonProps) => { { params: { wallet: walletAddress, - contractAddress: NOGS_CONTRACT_ADDR, + contractAddress: nogsContractAddr, }, }, ); @@ -223,7 +236,7 @@ const NogsGateButton = (props: ButtonProps) => { } void checkNogsAuto(); - }, [walletAddress, setHasNogs]); + }, [walletAddress, nogsContractAddr, setHasNogs]); // ----- Button wrapper (this is what ThemeSettingsEditor uses) ----- diff --git a/src/common/lib/theme/BackgroundGenerator.tsx b/src/common/lib/theme/BackgroundGenerator.tsx index d0a9dc90b..29c465c5c 100644 --- a/src/common/lib/theme/BackgroundGenerator.tsx +++ b/src/common/lib/theme/BackgroundGenerator.tsx @@ -12,7 +12,7 @@ import { usePrivy } from "@privy-io/react-auth"; import { Address, formatUnits, zeroAddress } from "viem"; import { base } from "viem/chains"; import { useBalance } from "wagmi"; -import { SPACE_CONTRACT_ADDR } from "@/constants/spaceToken"; +import { getSpaceContractAddr } from "@/constants/spaceToken"; interface BackgroundGeneratorProps { backgroundHTML: string; @@ -49,10 +49,18 @@ export const BackgroundGenerator = ({ ]; const { user } = usePrivy(); + const [spaceContractAddr, setSpaceContractAddr] = useState
(null); + + // Load space contract address (async) + useEffect(() => { + getSpaceContractAddr().then(addr => setSpaceContractAddr(addr)); + }, []); + const result = useBalance({ address: (user?.wallet?.address as Address) || zeroAddress, - token: SPACE_CONTRACT_ADDR as Address, + token: (spaceContractAddr || zeroAddress) as Address, chainId: base.id, + enabled: !!spaceContractAddr, // Only query when address is loaded }); const spaceHoldAmount = result?.data ? parseInt(formatUnits(result.data.value, result.data.decimals)) diff --git a/src/config/clanker/index.ts b/src/config/clanker/index.ts index e02c44eff..2b163bdd5 100644 --- a/src/config/clanker/index.ts +++ b/src/config/clanker/index.ts @@ -1,25 +1,4 @@ -import { clankerBrand } from './clanker.brand'; -import { clankerAssets } from './clanker.assets'; -import { clankerTheme } from './clanker.theme'; -import { clankerCommunity } from './clanker.community'; -import { clankerFidgets } from './clanker.fidgets'; -import { clankerHomePage } from './clanker.home'; -import { clankerNavigation } from './clanker.navigation'; -import { clankerExplorePage } from './clanker.explore'; -import { clankerUI } from './clanker.ui'; - -export const clankerSystemConfig = { - brand: clankerBrand, - assets: clankerAssets, - theme: clankerTheme, - community: clankerCommunity, - fidgets: clankerFidgets, - homePage: clankerHomePage, - explorePage: clankerExplorePage, - navigation: clankerNavigation, - ui: clankerUI, -}; - +// Export individual config pieces (used by seed scripts and page configs) export { clankerBrand } from './clanker.brand'; export { clankerAssets } from './clanker.assets'; export { clankerTheme } from './clanker.theme'; @@ -27,10 +6,12 @@ export { clankerCommunity } from './clanker.community'; export { clankerFidgets } from './clanker.fidgets'; export { clankerHomePage } from './clanker.home'; export { clankerExplorePage } from './clanker.explore'; +export { clankerNavigation } from './clanker.navigation'; +export { clankerUI } from './clanker.ui'; -// Export the initial space creators from config +// Export the initial space creators (used at runtime) export { default as createInitialProfileSpaceConfigForFid } from './initialSpaces/initialProfileSpace'; export { default as createInitialChannelSpaceConfig } from './initialSpaces/initialChannelSpace'; export { default as createInitialTokenSpaceConfigForAddress } from './initialSpaces/initialTokenSpace'; export { default as createInitialProposalSpaceConfigForProposalId } from './initialSpaces/initialProposalSpace'; -export { default as INITIAL_HOMEBASE_CONFIG } from './initialSpaces/initialHomebase'; +export { default as INITIAL_HOMEBASE_CONFIG, createInitialHomebaseConfig } from './initialSpaces/initialHomebase'; diff --git a/src/config/example/index.ts b/src/config/example/index.ts index 0b7caa1fb..c8f5cb242 100644 --- a/src/config/example/index.ts +++ b/src/config/example/index.ts @@ -1,23 +1,4 @@ -import { exampleBrand } from './example.brand'; -import { exampleAssets } from './example.assets'; -import { exampleTheme } from './example.theme'; -import { exampleCommunity } from './example.community'; -import { exampleFidgets } from './example.fidgets'; -import { exampleHomePage } from './example.home'; -import { exampleExplorePage } from './example.explore'; -import { exampleUI } from './example.ui'; - -export const exampleSystemConfig = { - brand: exampleBrand, - assets: exampleAssets, - theme: exampleTheme, - community: exampleCommunity, - fidgets: exampleFidgets, - homePage: exampleHomePage, - explorePage: exampleExplorePage, - ui: exampleUI, -}; - +// Export individual config pieces (used by seed scripts and page configs) export { exampleBrand } from './example.brand'; export { exampleAssets } from './example.assets'; export { exampleTheme } from './example.theme'; @@ -25,3 +6,11 @@ export { exampleCommunity } from './example.community'; export { exampleFidgets } from './example.fidgets'; export { exampleHomePage } from './example.home'; export { exampleExplorePage } from './example.explore'; +export { exampleUI } from './example.ui'; + +// Export the initial space creators (used at runtime) +export { default as createInitialProfileSpaceConfigForFid } from './initialSpaces/profile'; +export { default as createInitialChannelSpaceConfig } from './initialSpaces/channel'; +export { default as createInitialTokenSpaceConfigForAddress } from './initialSpaces/token'; +export { default as createInitalProposalSpaceConfigForProposalId } from './initialSpaces/proposal'; +export { default as INITIAL_HOMEBASE_CONFIG } from './initialSpaces/homebase'; diff --git a/src/config/index.ts b/src/config/index.ts index fa396c304..1a7962cae 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,70 +1,62 @@ -import { nounsSystemConfig } from './nouns/index'; -import { exampleSystemConfig } from './example/index'; -import { clankerSystemConfig } from './clanker/index'; import { SystemConfig } from './systemConfig'; -import { themes } from './shared/themes'; +import { + getConfigLoaderFactory, + getDomainFromContext, + getCommunityIdFromHeaders, + ConfigLoadContext +} from './loaders'; +import { resolveCommunityFromDomain } from './loaders/registry'; // Available community configurations const AVAILABLE_CONFIGURATIONS = ['nouns', 'example', 'clanker'] as const; type CommunityConfig = typeof AVAILABLE_CONFIGURATIONS[number]; -// Configuration loader -// REQUIRES database config - no fallback to static configs -export const loadSystemConfig = (): SystemConfig => { - // Get the community configuration from environment variable - const communityConfig = process.env.NEXT_PUBLIC_COMMUNITY || 'nouns'; +/** + * Load system configuration from database + * + * All communities use runtime loading from Supabase. + * + * @param context Optional context (communityId, domain) - if not provided, + * will be inferred from environment/domain + * @returns The loaded system configuration (always async) + */ +export async function loadSystemConfig(context?: ConfigLoadContext): Promise { + const factory = getConfigLoaderFactory(); - // REQUIRED: Build-time config from database (stored in env var at build time) - const buildTimeConfig = process.env.NEXT_PUBLIC_BUILD_TIME_CONFIG; - - if (!buildTimeConfig) { - const errorMsg = - `❌ NEXT_PUBLIC_BUILD_TIME_CONFIG is not set. ` + - `Database configuration is required. ` + - `Ensure Supabase credentials are set and run: npm run build`; - console.error(errorMsg); - throw new Error(errorMsg); - } - - try { - const dbConfig = JSON.parse(buildTimeConfig) as any; - - // Validate config structure - if (!dbConfig || !dbConfig.brand || !dbConfig.assets) { - const errorMsg = - `❌ Invalid config structure from database. ` + - `Missing required fields: brand, assets. ` + - `Ensure database is seeded correctly.`; - console.error(errorMsg); - console.error('Config received:', Object.keys(dbConfig || {})); - throw new Error(errorMsg); + // Build context if not provided + // Server-side: uses headers() to detect domain + // Client-side: uses window.location.hostname + const buildContext = async (): Promise => { + if (context) { + return context; } - - console.log('✅ Using config from database'); - // Map pages object to homePage/explorePage for backward compatibility - // Add themes from shared file (themes are not in database) - const mappedConfig: SystemConfig = { - ...dbConfig, - theme: themes, // Themes come from shared file - homePage: dbConfig.pages?.['home'] || dbConfig.homePage || null, - explorePage: dbConfig.pages?.['explore'] || dbConfig.explorePage || null, - }; + // Get domain (async on server, sync on client) + const domain = typeof window === 'undefined' + ? await getDomainFromContext() // Server: uses headers() + : getDomainFromContext(); // Client: uses window.location - return mappedConfig as SystemConfig; - } catch (error) { - if (error instanceof SyntaxError) { - const errorMsg = - `❌ Failed to parse build-time config from database. ` + - `Invalid JSON in NEXT_PUBLIC_BUILD_TIME_CONFIG. ` + - `Error: ${error.message}`; - console.error(errorMsg); - throw new Error(errorMsg); - } - // Re-throw validation errors - throw error; + return { + communityId: process.env.NEXT_PUBLIC_COMMUNITY, + domain, + isServer: typeof window === 'undefined', + }; + }; + + const loadContext = await buildContext(); + const loader = factory.getLoader(loadContext); + + // Log which community is being loaded (in development) + if (process.env.NODE_ENV === 'development') { + const communityId = loadContext.communityId || + (loadContext.domain ? resolveCommunityFromDomain(loadContext.domain) : null) || + 'unknown'; + console.log(`✅ Loading config for community: ${communityId} (domain: ${loadContext.domain || 'none'})`); } -}; + + // Always use runtime loader (async) + return loader.load(loadContext) as Promise; +} // Helper function to validate community configuration function isValidCommunityConfig(config: string): config is CommunityConfig { @@ -74,40 +66,28 @@ function isValidCommunityConfig(config: string): config is CommunityConfig { // Export available configurations for reference export { AVAILABLE_CONFIGURATIONS }; -// Export the configurations -export { nounsSystemConfig } from './nouns/index'; -export { exampleSystemConfig } from './example/index'; -export { clankerSystemConfig } from './clanker/index'; +// Export SystemConfig type (configs are now database-backed, no static exports) export type { SystemConfig }; -// Export individual configuration modules from nouns -export * from './nouns/index'; - -// Export individual configuration modules from example -export * from './example/index'; - -// Export individual configuration modules from clanker -export * from './clanker/index'; - // Space creators - delegate to the active community at runtime // Import creators for all communities under unique aliases -import { default as nounsCreateInitialProfileSpaceConfigForFid } from './nouns/initialSpaces/initialProfileSpace'; -import { default as nounsCreateInitialChannelSpaceConfig } from './nouns/initialSpaces/initialChannelSpace'; -import { default as nounsCreateInitialTokenSpaceConfigForAddress } from './nouns/initialSpaces/initialTokenSpace'; -import { default as nounsCreateInitalProposalSpaceConfigForProposalId } from './nouns/initialSpaces/initialProposalSpace'; -import { default as nounsINITIAL_HOMEBASE_CONFIG } from './nouns/initialSpaces/initialHomebase'; - -import { default as exampleCreateInitialProfileSpaceConfigForFid } from './example/initialSpaces/profile'; -import { default as exampleCreateInitialChannelSpaceConfig } from './example/initialSpaces/channel'; -import { default as exampleCreateInitialTokenSpaceConfigForAddress } from './example/initialSpaces/token'; -import { default as exampleCreateInitalProposalSpaceConfigForProposalId } from './example/initialSpaces/proposal'; -import { default as exampleINITIAL_HOMEBASE_CONFIG } from './example/initialSpaces/homebase'; - -import { default as clankerCreateInitialProfileSpaceConfigForFid } from './clanker/initialSpaces/initialProfileSpace'; -import { default as clankerCreateInitialChannelSpaceConfig } from './clanker/initialSpaces/initialChannelSpace'; -import { default as clankerCreateInitialTokenSpaceConfigForAddress } from './clanker/initialSpaces/initialTokenSpace'; -import { default as clankerCreateInitialProposalSpaceConfigForProposalId } from './clanker/initialSpaces/initialProposalSpace'; -import { default as clankerINITIAL_HOMEBASE_CONFIG, createInitialHomebaseConfig as clankerCreateInitialHomebaseConfig } from './clanker/initialSpaces/initialHomebase'; +import { createInitialProfileSpaceConfigForFid as nounsCreateInitialProfileSpaceConfigForFid } from './nouns/index'; +import { createInitialChannelSpaceConfig as nounsCreateInitialChannelSpaceConfig } from './nouns/index'; +import { createInitialTokenSpaceConfigForAddress as nounsCreateInitialTokenSpaceConfigForAddress } from './nouns/index'; +import { createInitalProposalSpaceConfigForProposalId as nounsCreateInitalProposalSpaceConfigForProposalId } from './nouns/index'; +import { INITIAL_HOMEBASE_CONFIG as nounsINITIAL_HOMEBASE_CONFIG } from './nouns/index'; + +import { createInitialProfileSpaceConfigForFid as exampleCreateInitialProfileSpaceConfigForFid } from './example/index'; +import { createInitialChannelSpaceConfig as exampleCreateInitialChannelSpaceConfig } from './example/index'; +import { createInitialTokenSpaceConfigForAddress as exampleCreateInitialTokenSpaceConfigForAddress } from './example/index'; +import { createInitalProposalSpaceConfigForProposalId as exampleCreateInitalProposalSpaceConfigForProposalId } from './example/index'; +import { INITIAL_HOMEBASE_CONFIG as exampleINITIAL_HOMEBASE_CONFIG } from './example/index'; + +import { createInitialProfileSpaceConfigForFid as clankerCreateInitialProfileSpaceConfigForFid } from './clanker/index'; +import { createInitialChannelSpaceConfig as clankerCreateInitialChannelSpaceConfig } from './clanker/index'; +import { createInitialTokenSpaceConfigForAddress as clankerCreateInitialTokenSpaceConfigForAddress } from './clanker/index'; +import { createInitialProposalSpaceConfigForProposalId as clankerCreateInitialProposalSpaceConfigForProposalId } from './clanker/index'; +import { INITIAL_HOMEBASE_CONFIG as clankerINITIAL_HOMEBASE_CONFIG, createInitialHomebaseConfig as clankerCreateInitialHomebaseConfig } from './clanker/index'; function resolveCommunity(): CommunityConfig { const c = (process.env.NEXT_PUBLIC_COMMUNITY || 'nouns').toLowerCase(); diff --git a/src/config/loaders/factory.ts b/src/config/loaders/factory.ts new file mode 100644 index 000000000..659d561c4 --- /dev/null +++ b/src/config/loaders/factory.ts @@ -0,0 +1,134 @@ +import { ConfigLoader, ConfigLoadContext, ConfigLoadingStrategy } from './types'; +import { RuntimeConfigLoader } from './runtimeLoader'; +import { + resolveCommunityFromDomain, +} from './registry'; + +/** + * Factory for creating the config loader + * All communities use runtime loading + */ +export class ConfigLoaderFactory { + private runtimeLoader: RuntimeConfigLoader; + + constructor() { + this.runtimeLoader = new RuntimeConfigLoader(); + } + + /** + * Get the loader for the given context + * Always returns the runtime loader + */ + getLoader(context: ConfigLoadContext): ConfigLoader { + // Priority order for community ID resolution: + // 1. Explicit context.communityId + // 2. Development override (NEXT_PUBLIC_TEST_COMMUNITY) - for local testing + // 3. Domain resolution (production or localhost subdomains) + // 4. Environment variable (NEXT_PUBLIC_COMMUNITY) - fallback + + let communityId = context.communityId; + + // Development override: allows testing communities locally + // Set NEXT_PUBLIC_TEST_COMMUNITY=example to test 'example' community + if (!communityId && process.env.NODE_ENV === 'development') { + communityId = process.env.NEXT_PUBLIC_TEST_COMMUNITY || undefined; + } + + // Resolve from domain if still no community ID + if (!communityId && context.domain) { + communityId = resolveCommunityFromDomain(context.domain) || undefined; + } + + // Final fallback to env var + if (!communityId) { + communityId = process.env.NEXT_PUBLIC_COMMUNITY || undefined; + } + + // Always use runtime loader + return this.runtimeLoader; + } + + /** + * Get the loading strategy for a given context + */ + getStrategy(_context: ConfigLoadContext): ConfigLoadingStrategy { + return 'runtime'; + } +} + +// Singleton instance +let factoryInstance: ConfigLoaderFactory | null = null; + +/** + * Get the global config loader factory instance + */ +export function getConfigLoaderFactory(): ConfigLoaderFactory { + if (!factoryInstance) { + factoryInstance = new ConfigLoaderFactory(); + } + return factoryInstance; +} + +/** + * Get domain and community ID from middleware-set headers (server-side) or window (client-side) + * + * Server-side: Reads x-community-id and x-detected-domain headers set by middleware + * Client-side: Uses window.location.hostname + * + * Returns undefined if domain cannot be determined (build time, etc.) + */ +export async function getDomainFromContext(): Promise; +export function getDomainFromContext(): string | undefined | Promise { + // Server-side: read from middleware-set headers + if (typeof window === 'undefined') { + try { + // Dynamic import to avoid issues when headers() isn't available (build time) + const { headers } = await import('next/headers'); + const headersList = await headers(); + + // Read domain from middleware-set header (preferred) + const domain = headersList.get('x-detected-domain'); + if (domain) { + return domain; + } + + // Fallback: read directly from headers if middleware didn't set it + const forwardedHost = headersList.get('x-forwarded-host'); + if (forwardedHost) { + return forwardedHost.split(':')[0]; // Remove port + } + + const host = headersList.get('host'); + if (host) { + return host.split(':')[0]; // Remove port + } + } catch (error) { + // Not in request context (static generation, etc.) + // Return undefined to fall back to env vars + return undefined; + } + + return undefined; + } + + // Client-side: use window.location.hostname + return window.location.hostname; +} + +/** + * Get community ID from middleware-set header (server-side only) + * Returns undefined if not available + */ +export async function getCommunityIdFromHeaders(): Promise { + if (typeof window === 'undefined') { + try { + const { headers } = await import('next/headers'); + const headersList = await headers(); + return headersList.get('x-community-id') || undefined; + } catch (error) { + return undefined; + } + } + return undefined; +} + diff --git a/src/config/loaders/index.ts b/src/config/loaders/index.ts new file mode 100644 index 000000000..dcc439dab --- /dev/null +++ b/src/config/loaders/index.ts @@ -0,0 +1,20 @@ +/** + * Configuration loader system + * + * This module provides an interface for loading community configurations + * from the database at runtime. + * + * Usage: + * ```typescript + * import { loadSystemConfig } from '@/config'; + * const config = await loadSystemConfig(); + * ``` + * + * All communities use runtime loading from Supabase. + */ + +export * from './types'; +export * from './registry'; +export * from './runtimeLoader'; +export * from './factory'; + diff --git a/src/config/loaders/registry.ts b/src/config/loaders/registry.ts new file mode 100644 index 000000000..5ff1df710 --- /dev/null +++ b/src/config/loaders/registry.ts @@ -0,0 +1,56 @@ +import { ConfigLoadingStrategy } from './types'; + +/** + * Get the loading strategy for a given community + * All communities use runtime loading + */ +export function getStrategyForCommunity( + _communityId: string, + _registry?: unknown +): ConfigLoadingStrategy { + return 'runtime'; +} + +/** + * Resolve community ID from domain + * + * The domain is used to infer the community ID. + * Supports both production domains and localhost subdomains for local testing. + * + * @param domain The domain/hostname + * @returns The community ID inferred from domain, or null if cannot be determined + */ +export function resolveCommunityFromDomain( + domain: string +): string | null { + if (!domain) return null; + + // Support localhost subdomains for local testing + // e.g., example.localhost:3000 -> example + if (domain.includes('localhost')) { + const parts = domain.split('.'); + if (parts.length > 1 && parts[0] !== 'localhost') { + // Has subdomain before localhost + return parts[0]; + } + // Just localhost - can't determine community + return null; + } + + // Production domain: extract subdomain or use domain as community ID + // Example: subdomain.nounspace.com -> subdomain + if (domain.includes('.')) { + const parts = domain.split('.'); + if (parts.length > 2) { + // Has subdomain (e.g., example.nounspace.com) + return parts[0]; + } + // Use domain name without TLD as community ID (e.g., example.com -> example) + return parts[0]; + } + + // Single word domain - use as community ID + return domain; +} + + diff --git a/src/config/loaders/runtimeLoader.ts b/src/config/loaders/runtimeLoader.ts new file mode 100644 index 000000000..7e2e655ea --- /dev/null +++ b/src/config/loaders/runtimeLoader.ts @@ -0,0 +1,96 @@ +import { ConfigLoader, ConfigLoadContext } from './types'; +import { SystemConfig } from '../systemConfig'; +import { themes } from '../shared/themes'; +import { createClient } from '@supabase/supabase-js'; + +/** + * Runtime config loader + * Fetches configuration from database at runtime based on domain/community + * Used for dynamic communities that aren't built into the app + */ +export class RuntimeConfigLoader implements ConfigLoader { + private supabase: ReturnType | null = null; + + constructor() { + // Initialize Supabase client if credentials are available + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || + process.env.SUPABASE_SERVICE_ROLE_KEY; + + if (supabaseUrl && supabaseKey) { + this.supabase = createClient(supabaseUrl, supabaseKey); + } + } + + getStrategy(): 'runtime' { + return 'runtime'; + } + + canHandle(context: ConfigLoadContext): boolean { + // Can handle if we have Supabase credentials and a community ID + return !!this.supabase && !!context.communityId; + } + + async load(context: ConfigLoadContext): Promise { + if (!this.supabase) { + throw new Error( + `❌ Supabase credentials not configured. ` + + `NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY are required for runtime config loading.` + ); + } + + if (!context.communityId) { + throw new Error( + `❌ Community ID is required for runtime config loading. ` + + `Provide communityId in the load context.` + ); + } + + try { + // Fetch config from database + const { data, error } = await this.supabase + .rpc('get_active_community_config', { + p_community_id: context.communityId + }) + .single(); + + if (error || !data) { + throw new Error( + `❌ Failed to load config from database for community: ${context.communityId}. ` + + `Error: ${error?.message || 'No data returned'}` + ); + } + + // Type assertion for database response + const dbConfig = data as any; + + // Validate config structure + if (!dbConfig.brand || !dbConfig.assets) { + throw new Error( + `❌ Invalid config structure from database. ` + + `Missing required fields: brand, assets. ` + + `Ensure database is seeded correctly.` + ); + } + + // Map pages object to homePage/explorePage for backward compatibility + // Add themes from shared file (themes are not in database) + const mappedConfig: SystemConfig = { + ...dbConfig, + theme: themes, // Themes come from shared file + homePage: dbConfig.pages?.['home'] || dbConfig.homePage || null, + explorePage: dbConfig.pages?.['explore'] || dbConfig.explorePage || null, + }; + + return mappedConfig as SystemConfig; + } catch (error: any) { + if (error.message) { + throw error; + } + throw new Error( + `❌ Unexpected error loading runtime config: ${error.message || 'Unknown error'}` + ); + } + } +} + diff --git a/src/config/loaders/types.ts b/src/config/loaders/types.ts new file mode 100644 index 000000000..78359e86a --- /dev/null +++ b/src/config/loaders/types.ts @@ -0,0 +1,53 @@ +import { SystemConfig } from '../systemConfig'; + +/** + * Configuration loading strategy types + */ +export type ConfigLoadingStrategy = 'runtime'; + +/** + * Context for determining which loading strategy to use + */ +export interface ConfigLoadContext { + /** Community ID (e.g., 'nouns', 'example') */ + communityId?: string; + /** Domain/hostname (e.g., 'nounspace.com', 'example.nounspace.com') */ + domain?: string; + /** Whether we're in a server-side context */ + isServer?: boolean; +} + +/** + * Result of a config load operation + */ +export interface ConfigLoadResult { + config: SystemConfig; + strategy: ConfigLoadingStrategy; + source: string; // e.g., 'runtime-db', etc. +} + +/** + * Interface for config loaders + * All loaders must implement this interface + */ +export interface ConfigLoader { + /** + * Load the system configuration + * @param context Context for loading (community, domain, etc.) + * @returns The loaded system configuration + */ + load(context: ConfigLoadContext): Promise | SystemConfig; + + /** + * Check if this loader can handle the given context + * @param context Context to check + * @returns True if this loader can handle the context + */ + canHandle(context: ConfigLoadContext): boolean; + + /** + * Get the loading strategy this loader implements + */ + getStrategy(): ConfigLoadingStrategy; +} + diff --git a/src/config/nouns/index.ts b/src/config/nouns/index.ts index 1570ed3c0..c4c9d3ac6 100644 --- a/src/config/nouns/index.ts +++ b/src/config/nouns/index.ts @@ -1,25 +1,4 @@ -import { nounsBrand } from './nouns.brand'; -import { nounsAssets } from './nouns.assets'; -import { nounsTheme } from './nouns.theme'; -import { nounsCommunity } from './nouns.community'; -import { nounsFidgets } from './nouns.fidgets'; -import { nounsHomePage } from './nouns.home'; -import { nounsNavigation } from './nouns.navigation'; -import { nounsExplorePage } from './nouns.explore'; -import { nounsUI } from './nouns.ui'; - -export const nounsSystemConfig = { - brand: nounsBrand, - assets: nounsAssets, - theme: nounsTheme, - community: nounsCommunity, - fidgets: nounsFidgets, - homePage: nounsHomePage, - explorePage: nounsExplorePage, - navigation: nounsNavigation, - ui: nounsUI, -}; - +// Export individual config pieces (used by seed scripts and page configs) export { nounsBrand } from './nouns.brand'; export { nounsAssets } from './nouns.assets'; export { nounsTheme } from './nouns.theme'; @@ -27,8 +6,10 @@ export { nounsCommunity } from './nouns.community'; export { nounsFidgets } from './nouns.fidgets'; export { nounsHomePage } from './nouns.home'; export { nounsExplorePage } from './nouns.explore'; +export { nounsNavigation } from './nouns.navigation'; +export { nounsUI } from './nouns.ui'; -// Export the initial space creators from config +// Export the initial space creators (used at runtime) export { default as createInitialProfileSpaceConfigForFid } from './initialSpaces/initialProfileSpace'; export { default as createInitialChannelSpaceConfig } from './initialSpaces/initialChannelSpace'; export { default as createInitialTokenSpaceConfigForAddress } from './initialSpaces/initialTokenSpace'; diff --git a/src/constants/metadata.ts b/src/constants/metadata.ts index 825331593..972e58070 100644 --- a/src/constants/metadata.ts +++ b/src/constants/metadata.ts @@ -1,37 +1,59 @@ import { WEBSITE_URL } from "@/constants/app"; import { loadSystemConfig } from "@/config"; -// Load system configuration -const config = loadSystemConfig(); +// Lazy-load config for module-level constants +// These are used in contexts where async isn't possible, so we cache after first load +let cachedConfig: Awaited> | null = null; -export const defaultFrame = { - version: "next", - imageUrl: `${WEBSITE_URL}${config.assets.logos.og}`, - button: { - title: config.brand.name, - action: { - type: "launch_frame", - url: WEBSITE_URL, - name: config.brand.displayName, - splashImageUrl: `${WEBSITE_URL}${config.assets.logos.splash}`, - splashBackgroundColor: "#FFFFFF", - } +async function getConfig() { + if (cachedConfig) { + return cachedConfig; } + cachedConfig = await loadSystemConfig(); + return cachedConfig; +} + +// Config loading is always async (from database) +// Usage: const frame = await getDefaultFrame(); +export async function getDefaultFrame() { + const config = await getConfig(); + return { + version: "next", + imageUrl: `${WEBSITE_URL}${config.assets.logos.og}`, + button: { + title: config.brand.name, + action: { + type: "launch_frame", + url: WEBSITE_URL, + name: config.brand.displayName, + splashImageUrl: `${WEBSITE_URL}${config.assets.logos.splash}`, + splashBackgroundColor: "#FFFFFF", + } + } + }; +} + +export async function getMetadata() { + const config = await getConfig(); + return { + APP_NAME: config.brand.name, + APP_ICON: `${WEBSITE_URL}${config.assets.logos.icon}`, + APP_SUBTITLE: config.brand.tagline, + APP_BUTTON_TITLE: 'Open Space', + APP_DESCRIPTION: config.brand.description, + APP_TAGS: config.brand.miniAppTags, + APP_SPLASH_IMAGE: `${WEBSITE_URL}${config.assets.logos.splash}`, + SPLASH_BACKGROUND_COLOR: '#FFFFFF', + APP_PRIMARY_CATEGORY: 'social', + APP_HERO_IMAGE: `${WEBSITE_URL}${config.assets.logos.splash}`, + APP_TAGLINE: config.brand.tagline, + APP_OG_TITLE: config.brand.displayName, + APP_OG_DESCRIPTION: config.brand.description, + APP_OG_IMAGE: `${WEBSITE_URL}${config.assets.logos.og}`, + }; } -export const metadata = { - APP_NAME: config.brand.name, - APP_ICON: `${WEBSITE_URL}${config.assets.logos.icon}`, - APP_SUBTITLE: config.brand.tagline, - APP_BUTTON_TITLE: 'Open Space', - APP_DESCRIPTION: config.brand.description, - APP_TAGS: config.brand.miniAppTags, - APP_SPLASH_IMAGE: `${WEBSITE_URL}${config.assets.logos.splash}`, - SPLASH_BACKGROUND_COLOR: '#FFFFFF', - APP_PRIMARY_CATEGORY: 'social', - APP_HERO_IMAGE: `${WEBSITE_URL}${config.assets.logos.splash}`, - APP_TAGLINE: config.brand.tagline, - APP_OG_TITLE: config.brand.displayName, - APP_OG_DESCRIPTION: config.brand.description, - APP_OG_IMAGE: `${WEBSITE_URL}${config.assets.logos.og}`, -} \ No newline at end of file +// Legacy exports for backward compatibility (will be async) +// These should be migrated to use getDefaultFrame() and getMetadata() instead +export const defaultFrame = getDefaultFrame(); +export const metadata = getMetadata(); \ No newline at end of file diff --git a/src/constants/nogs.ts b/src/constants/nogs.ts index 333e9ce68..93b6b7a3a 100644 --- a/src/constants/nogs.ts +++ b/src/constants/nogs.ts @@ -1,6 +1,24 @@ import { loadSystemConfig } from "@/config"; -// Load system configuration -const config = loadSystemConfig(); +// Lazy-load config for module-level constant +// This is used in contexts where async isn't possible, so we cache after first load +let cachedAddress: string | null = null; -export const NOGS_CONTRACT_ADDR = config.community.contracts.nogs; +async function getNogsContractAddress(): Promise { + if (cachedAddress) { + return cachedAddress; + } + + const config = await loadSystemConfig(); + cachedAddress = config.community.contracts.nogs; + return cachedAddress; +} + +// Export as async function - callers must await +export async function getNogsContractAddr(): Promise { + return getNogsContractAddress(); +} + +// Legacy export for backward compatibility (will be a Promise) +// Migrate to getNogsContractAddr() instead +export const NOGS_CONTRACT_ADDR: Promise = getNogsContractAddress(); diff --git a/src/constants/spaceToken.ts b/src/constants/spaceToken.ts index 324504857..7ae35718d 100644 --- a/src/constants/spaceToken.ts +++ b/src/constants/spaceToken.ts @@ -1,17 +1,34 @@ import type { Address } from "viem"; import { isAddress } from "viem"; - import { loadSystemConfig } from "@/config"; -// Load system configuration -const config = loadSystemConfig(); +// Lazy-load config for module-level constant +// This is used in contexts where async isn't possible, so we cache after first load +let cachedAddress: Address | null = null; -const spaceContractAddress = config.community.contracts.space; +async function getSpaceContractAddress(): Promise
{ + if (cachedAddress) { + return cachedAddress; + } + + const config = await loadSystemConfig(); + const address = config.community.contracts.space; + + if (!isAddress(address)) { + throw new Error( + "Invalid space contract address configured. Expected a checksummed 0x-prefixed address.", + ); + } + + cachedAddress = address; + return address; +} -if (!isAddress(spaceContractAddress)) { - throw new Error( - "Invalid space contract address configured. Expected a checksummed 0x-prefixed address.", - ); +// Export as async function - callers must await +export async function getSpaceContractAddr(): Promise
{ + return getSpaceContractAddress(); } -export const SPACE_CONTRACT_ADDR: Address = spaceContractAddress; +// Legacy export for backward compatibility (will be a Promise) +// Migrate to getSpaceContractAddr() instead +export const SPACE_CONTRACT_ADDR: Promise
= getSpaceContractAddress(); diff --git a/src/fidgets/farcaster/components/CreateCast.tsx b/src/fidgets/farcaster/components/CreateCast.tsx index ae98311f2..ef89b0e8c 100644 --- a/src/fidgets/farcaster/components/CreateCast.tsx +++ b/src/fidgets/farcaster/components/CreateCast.tsx @@ -1,3 +1,5 @@ +"use client"; + import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; // import neynar from "@/common/data/api/neynar"; import { @@ -61,10 +63,10 @@ import { ChannelPicker } from "./channelPicker"; import { renderEmbedForUrl } from "./Embeds"; -import { loadSystemConfig } from "@/config"; +import { getSpaceContractAddr } from "@/constants/spaceToken"; -const config = loadSystemConfig(); -const SPACE_CONTRACT_ADDR = config.community.contracts.space; +// SPACE_CONTRACT_ADDR will be loaded when needed (async) +// For now, we'll use it in a way that handles the Promise // Fixed missing imports and incorrect object types const API_URL = process.env.NEXT_PUBLIC_MOD_PROTOCOL_API_URL!; @@ -371,10 +373,18 @@ const CreateCast: React.FC = ({ const sparklesBannerClosed = isBannerClosed(SPARKLES_BANNER_KEY); const { user } = usePrivy(); + const [spaceContractAddr, setSpaceContractAddr] = useState
(null); + + // Load space contract address (async) + useEffect(() => { + getSpaceContractAddr().then(addr => setSpaceContractAddr(addr)); + }, []); + const result = useBalance({ address: (user?.wallet?.address as Address) || zeroAddress, - token: SPACE_CONTRACT_ADDR as Address, + token: (spaceContractAddr || zeroAddress) as Address, chainId: base.id, + enabled: !!spaceContractAddr, // Only query when address is loaded }); const spaceHoldAmount = result?.data ? parseInt(formatUnits(result.data.value, result.data.decimals)) From bc9464a4a637a7dffc7a2f070388dfa64c96fcbb Mon Sep 17 00:00:00 2001 From: Jesse Paterson Date: Tue, 2 Dec 2025 12:26:02 -0600 Subject: [PATCH 116/155] removed unused abstractions --- src/config/index.ts | 33 ++++++---- src/config/loaders/factory.ts | 85 +++++++------------------ src/config/loaders/index.ts | 2 +- src/config/loaders/registry.ts | 13 ---- src/config/loaders/runtimeLoader.ts | 10 --- src/config/loaders/types.ts | 31 +-------- src/config/loaders/utils.ts | 97 +++++++++++++++++++++++++++++ 7 files changed, 146 insertions(+), 125 deletions(-) create mode 100644 src/config/loaders/utils.ts diff --git a/src/config/index.ts b/src/config/index.ts index 1a7962cae..374420ce1 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,16 +1,26 @@ import { SystemConfig } from './systemConfig'; import { - getConfigLoaderFactory, getDomainFromContext, getCommunityIdFromHeaders, + resolveCommunityId, ConfigLoadContext } from './loaders'; -import { resolveCommunityFromDomain } from './loaders/registry'; +import { RuntimeConfigLoader } from './loaders/runtimeLoader'; // Available community configurations const AVAILABLE_CONFIGURATIONS = ['nouns', 'example', 'clanker'] as const; type CommunityConfig = typeof AVAILABLE_CONFIGURATIONS[number]; +// Singleton loader instance +let loaderInstance: RuntimeConfigLoader | null = null; + +function getLoader(): RuntimeConfigLoader { + if (!loaderInstance) { + loaderInstance = new RuntimeConfigLoader(); + } + return loaderInstance; +} + /** * Load system configuration from database * @@ -21,8 +31,6 @@ type CommunityConfig = typeof AVAILABLE_CONFIGURATIONS[number]; * @returns The loaded system configuration (always async) */ export async function loadSystemConfig(context?: ConfigLoadContext): Promise { - const factory = getConfigLoaderFactory(); - // Build context if not provided // Server-side: uses headers() to detect domain // Client-side: uses window.location.hostname @@ -44,18 +52,21 @@ export async function loadSystemConfig(context?: ConfigLoadContext): Promise; + // Load config using runtime loader + return getLoader().load(finalContext); } // Helper function to validate community configuration diff --git a/src/config/loaders/factory.ts b/src/config/loaders/factory.ts index 659d561c4..c71a03a60 100644 --- a/src/config/loaders/factory.ts +++ b/src/config/loaders/factory.ts @@ -1,72 +1,35 @@ -import { ConfigLoader, ConfigLoadContext, ConfigLoadingStrategy } from './types'; -import { RuntimeConfigLoader } from './runtimeLoader'; -import { - resolveCommunityFromDomain, -} from './registry'; +import { ConfigLoadContext } from './types'; +import { resolveCommunityFromDomain } from './registry'; /** - * Factory for creating the config loader - * All communities use runtime loading + * Resolve community ID from context + * + * Priority order: + * 1. Explicit context.communityId + * 2. Development override (NEXT_PUBLIC_TEST_COMMUNITY) - for local testing + * 3. Domain resolution (production or localhost subdomains) + * 4. Environment variable (NEXT_PUBLIC_COMMUNITY) - fallback */ -export class ConfigLoaderFactory { - private runtimeLoader: RuntimeConfigLoader; - - constructor() { - this.runtimeLoader = new RuntimeConfigLoader(); +export function resolveCommunityId(context: ConfigLoadContext): string | undefined { + let communityId = context.communityId; + + // Development override: allows testing communities locally + // Set NEXT_PUBLIC_TEST_COMMUNITY=example to test 'example' community + if (!communityId && process.env.NODE_ENV === 'development') { + communityId = process.env.NEXT_PUBLIC_TEST_COMMUNITY || undefined; } - - /** - * Get the loader for the given context - * Always returns the runtime loader - */ - getLoader(context: ConfigLoadContext): ConfigLoader { - // Priority order for community ID resolution: - // 1. Explicit context.communityId - // 2. Development override (NEXT_PUBLIC_TEST_COMMUNITY) - for local testing - // 3. Domain resolution (production or localhost subdomains) - // 4. Environment variable (NEXT_PUBLIC_COMMUNITY) - fallback - - let communityId = context.communityId; - - // Development override: allows testing communities locally - // Set NEXT_PUBLIC_TEST_COMMUNITY=example to test 'example' community - if (!communityId && process.env.NODE_ENV === 'development') { - communityId = process.env.NEXT_PUBLIC_TEST_COMMUNITY || undefined; - } - - // Resolve from domain if still no community ID - if (!communityId && context.domain) { - communityId = resolveCommunityFromDomain(context.domain) || undefined; - } - - // Final fallback to env var - if (!communityId) { - communityId = process.env.NEXT_PUBLIC_COMMUNITY || undefined; - } - - // Always use runtime loader - return this.runtimeLoader; + + // Resolve from domain if still no community ID + if (!communityId && context.domain) { + communityId = resolveCommunityFromDomain(context.domain) || undefined; } - /** - * Get the loading strategy for a given context - */ - getStrategy(_context: ConfigLoadContext): ConfigLoadingStrategy { - return 'runtime'; + // Final fallback to env var + if (!communityId) { + communityId = process.env.NEXT_PUBLIC_COMMUNITY || undefined; } -} -// Singleton instance -let factoryInstance: ConfigLoaderFactory | null = null; - -/** - * Get the global config loader factory instance - */ -export function getConfigLoaderFactory(): ConfigLoaderFactory { - if (!factoryInstance) { - factoryInstance = new ConfigLoaderFactory(); - } - return factoryInstance; + return communityId; } /** diff --git a/src/config/loaders/index.ts b/src/config/loaders/index.ts index dcc439dab..940a6c345 100644 --- a/src/config/loaders/index.ts +++ b/src/config/loaders/index.ts @@ -16,5 +16,5 @@ export * from './types'; export * from './registry'; export * from './runtimeLoader'; -export * from './factory'; +export { resolveCommunityId, getDomainFromContext, getCommunityIdFromHeaders } from './utils'; diff --git a/src/config/loaders/registry.ts b/src/config/loaders/registry.ts index 5ff1df710..d02bfa0ba 100644 --- a/src/config/loaders/registry.ts +++ b/src/config/loaders/registry.ts @@ -1,16 +1,3 @@ -import { ConfigLoadingStrategy } from './types'; - -/** - * Get the loading strategy for a given community - * All communities use runtime loading - */ -export function getStrategyForCommunity( - _communityId: string, - _registry?: unknown -): ConfigLoadingStrategy { - return 'runtime'; -} - /** * Resolve community ID from domain * diff --git a/src/config/loaders/runtimeLoader.ts b/src/config/loaders/runtimeLoader.ts index 7e2e655ea..afe8ca4c9 100644 --- a/src/config/loaders/runtimeLoader.ts +++ b/src/config/loaders/runtimeLoader.ts @@ -6,7 +6,6 @@ import { createClient } from '@supabase/supabase-js'; /** * Runtime config loader * Fetches configuration from database at runtime based on domain/community - * Used for dynamic communities that aren't built into the app */ export class RuntimeConfigLoader implements ConfigLoader { private supabase: ReturnType | null = null; @@ -22,15 +21,6 @@ export class RuntimeConfigLoader implements ConfigLoader { } } - getStrategy(): 'runtime' { - return 'runtime'; - } - - canHandle(context: ConfigLoadContext): boolean { - // Can handle if we have Supabase credentials and a community ID - return !!this.supabase && !!context.communityId; - } - async load(context: ConfigLoadContext): Promise { if (!this.supabase) { throw new Error( diff --git a/src/config/loaders/types.ts b/src/config/loaders/types.ts index 78359e86a..b38174422 100644 --- a/src/config/loaders/types.ts +++ b/src/config/loaders/types.ts @@ -1,12 +1,7 @@ import { SystemConfig } from '../systemConfig'; /** - * Configuration loading strategy types - */ -export type ConfigLoadingStrategy = 'runtime'; - -/** - * Context for determining which loading strategy to use + * Context for loading configuration */ export interface ConfigLoadContext { /** Community ID (e.g., 'nouns', 'example') */ @@ -17,18 +12,8 @@ export interface ConfigLoadContext { isServer?: boolean; } -/** - * Result of a config load operation - */ -export interface ConfigLoadResult { - config: SystemConfig; - strategy: ConfigLoadingStrategy; - source: string; // e.g., 'runtime-db', etc. -} - /** * Interface for config loaders - * All loaders must implement this interface */ export interface ConfigLoader { /** @@ -36,18 +21,6 @@ export interface ConfigLoader { * @param context Context for loading (community, domain, etc.) * @returns The loaded system configuration */ - load(context: ConfigLoadContext): Promise | SystemConfig; - - /** - * Check if this loader can handle the given context - * @param context Context to check - * @returns True if this loader can handle the context - */ - canHandle(context: ConfigLoadContext): boolean; - - /** - * Get the loading strategy this loader implements - */ - getStrategy(): ConfigLoadingStrategy; + load(context: ConfigLoadContext): Promise; } diff --git a/src/config/loaders/utils.ts b/src/config/loaders/utils.ts new file mode 100644 index 000000000..c71a03a60 --- /dev/null +++ b/src/config/loaders/utils.ts @@ -0,0 +1,97 @@ +import { ConfigLoadContext } from './types'; +import { resolveCommunityFromDomain } from './registry'; + +/** + * Resolve community ID from context + * + * Priority order: + * 1. Explicit context.communityId + * 2. Development override (NEXT_PUBLIC_TEST_COMMUNITY) - for local testing + * 3. Domain resolution (production or localhost subdomains) + * 4. Environment variable (NEXT_PUBLIC_COMMUNITY) - fallback + */ +export function resolveCommunityId(context: ConfigLoadContext): string | undefined { + let communityId = context.communityId; + + // Development override: allows testing communities locally + // Set NEXT_PUBLIC_TEST_COMMUNITY=example to test 'example' community + if (!communityId && process.env.NODE_ENV === 'development') { + communityId = process.env.NEXT_PUBLIC_TEST_COMMUNITY || undefined; + } + + // Resolve from domain if still no community ID + if (!communityId && context.domain) { + communityId = resolveCommunityFromDomain(context.domain) || undefined; + } + + // Final fallback to env var + if (!communityId) { + communityId = process.env.NEXT_PUBLIC_COMMUNITY || undefined; + } + + return communityId; +} + +/** + * Get domain and community ID from middleware-set headers (server-side) or window (client-side) + * + * Server-side: Reads x-community-id and x-detected-domain headers set by middleware + * Client-side: Uses window.location.hostname + * + * Returns undefined if domain cannot be determined (build time, etc.) + */ +export async function getDomainFromContext(): Promise; +export function getDomainFromContext(): string | undefined | Promise { + // Server-side: read from middleware-set headers + if (typeof window === 'undefined') { + try { + // Dynamic import to avoid issues when headers() isn't available (build time) + const { headers } = await import('next/headers'); + const headersList = await headers(); + + // Read domain from middleware-set header (preferred) + const domain = headersList.get('x-detected-domain'); + if (domain) { + return domain; + } + + // Fallback: read directly from headers if middleware didn't set it + const forwardedHost = headersList.get('x-forwarded-host'); + if (forwardedHost) { + return forwardedHost.split(':')[0]; // Remove port + } + + const host = headersList.get('host'); + if (host) { + return host.split(':')[0]; // Remove port + } + } catch (error) { + // Not in request context (static generation, etc.) + // Return undefined to fall back to env vars + return undefined; + } + + return undefined; + } + + // Client-side: use window.location.hostname + return window.location.hostname; +} + +/** + * Get community ID from middleware-set header (server-side only) + * Returns undefined if not available + */ +export async function getCommunityIdFromHeaders(): Promise { + if (typeof window === 'undefined') { + try { + const { headers } = await import('next/headers'); + const headersList = await headers(); + return headersList.get('x-community-id') || undefined; + } catch (error) { + return undefined; + } + } + return undefined; +} + From b7e8eca02b80cbebad79001b43476f82687b349a Mon Sep 17 00:00:00 2001 From: Jesse Paterson Date: Tue, 2 Dec 2025 13:01:53 -0600 Subject: [PATCH 117/155] simplified + cleaned up page types --- docs/TESTING_CONFIG_LOADING.md | 228 + package.json | 3 +- scripts/test-config-loading.ts | 94 + .../[[...tabName]]/NavPageClient.tsx | 8 +- src/app/[navSlug]/[[...tabName]]/page.tsx | 86 +- src/app/explore/ExploreTabPage.tsx | 73 - src/app/page.tsx | 40 +- src/config/clanker/index.ts | 7 - .../initialSpaces/exploreTabs/channel.json | 19088 ---------------- .../initialSpaces/exploreTabs/clanker.json | 18918 --------------- src/config/clanker/initialSpaces/index.ts | 6 - .../initialSpaces/initialChannelSpace.ts | 55 - .../clanker/initialSpaces/initialHomebase.ts | 112 - .../initialSpaces/initialProfileSpace.ts | 118 - .../initialSpaces/initialProposalSpace.ts | 219 - .../initialSpaces/initialTokenSpace.ts | 445 - src/config/createExplorePageConfig.ts | 4 +- src/config/example/index.ts | 7 - src/config/example/initialSpaces/channel.ts | 83 - src/config/example/initialSpaces/homebase.ts | 88 - src/config/example/initialSpaces/index.ts | 6 - src/config/example/initialSpaces/profile.ts | 83 - src/config/example/initialSpaces/proposal.ts | 80 - src/config/example/initialSpaces/token.ts | 115 - src/config/index.ts | 127 +- src/config/loaders/runtimeLoader.ts | 3 - src/config/systemConfig.ts | 9 +- 27 files changed, 405 insertions(+), 39700 deletions(-) create mode 100644 docs/TESTING_CONFIG_LOADING.md create mode 100644 scripts/test-config-loading.ts delete mode 100644 src/app/explore/ExploreTabPage.tsx delete mode 100644 src/config/clanker/initialSpaces/exploreTabs/channel.json delete mode 100644 src/config/clanker/initialSpaces/exploreTabs/clanker.json delete mode 100644 src/config/clanker/initialSpaces/index.ts delete mode 100644 src/config/clanker/initialSpaces/initialChannelSpace.ts delete mode 100644 src/config/clanker/initialSpaces/initialHomebase.ts delete mode 100644 src/config/clanker/initialSpaces/initialProfileSpace.ts delete mode 100644 src/config/clanker/initialSpaces/initialProposalSpace.ts delete mode 100644 src/config/clanker/initialSpaces/initialTokenSpace.ts delete mode 100644 src/config/example/initialSpaces/channel.ts delete mode 100644 src/config/example/initialSpaces/homebase.ts delete mode 100644 src/config/example/initialSpaces/index.ts delete mode 100644 src/config/example/initialSpaces/profile.ts delete mode 100644 src/config/example/initialSpaces/proposal.ts delete mode 100644 src/config/example/initialSpaces/token.ts diff --git a/docs/TESTING_CONFIG_LOADING.md b/docs/TESTING_CONFIG_LOADING.md new file mode 100644 index 000000000..2d8a10788 --- /dev/null +++ b/docs/TESTING_CONFIG_LOADING.md @@ -0,0 +1,228 @@ +# Testing Config Loading Locally + +This guide explains how to test the runtime configuration loading system locally. + +## Quick Start + +The easiest way to test a specific community locally is using the environment variable override: + +```bash +NEXT_PUBLIC_TEST_COMMUNITY=example npm run dev +``` + +Then visit `http://localhost:3000` - it will load the `example` community config. + +## Testing Methods + +### Method 1: Environment Variable Override (Easiest) + +**Best for:** Quick testing of a specific community + +```bash +# Test 'example' community +NEXT_PUBLIC_TEST_COMMUNITY=example npm run dev + +# Test 'clanker' community +NEXT_PUBLIC_TEST_COMMUNITY=clanker npm run dev + +# Test 'nouns' community (or just omit the var) +NEXT_PUBLIC_TEST_COMMUNITY=nouns npm run dev +``` + +**How it works:** +- In development mode, `NEXT_PUBLIC_TEST_COMMUNITY` takes priority over domain resolution +- Visit `http://localhost:3000` - the system will load the specified community config +- Check the console logs to see which community is being loaded + +**Priority order:** +1. `NEXT_PUBLIC_TEST_COMMUNITY` (development only) +2. Domain resolution +3. `NEXT_PUBLIC_COMMUNITY` (fallback) + +### Method 2: Localhost Subdomains (Most Realistic) + +**Best for:** Testing domain-based resolution (closest to production) + +**Setup:** +1. Edit your `/etc/hosts` file (macOS/Linux) or `C:\Windows\System32\drivers\etc\hosts` (Windows): + +```bash +# Add these lines: +127.0.0.1 example.localhost +127.0.0.1 clanker.localhost +127.0.0.1 nouns.localhost +``` + +2. Start the dev server: +```bash +npm run dev +``` + +3. Visit the subdomain URLs: +- `http://example.localhost:3000` → loads `example` community +- `http://clanker.localhost:3000` → loads `clanker` community +- `http://nouns.localhost:3000` → loads `nouns` community +- `http://localhost:3000` → falls back to `NEXT_PUBLIC_COMMUNITY` or 'nouns' + +**How it works:** +- Middleware detects the domain from the `Host` header +- Extracts the subdomain (e.g., `example.localhost` → `example`) +- Sets `x-community-id` header for Server Components +- Config loader reads the header and loads the appropriate config + +**Note:** Make sure `NEXT_PUBLIC_TEST_COMMUNITY` is NOT set when testing subdomains, as it takes priority. + +### Method 3: Explicit Context (Programmatic Testing) + +**Best for:** Unit tests or programmatic access + +```typescript +import { loadSystemConfig } from '@/config'; + +// Test with explicit community ID +const config = await loadSystemConfig({ + communityId: 'example', + domain: 'example.nounspace.com', + isServer: true, +}); + +console.log(config.brand.displayName); // Should show example community name +``` + +## Verifying Config Loading + +### Check Console Logs + +In development mode, the system logs which community is being loaded: + +``` +✅ Loading config for community: example (domain: example.localhost) +``` + +### Check the UI + +1. **Brand name** - Should match the community's `brand.displayName` +2. **Logo** - Should show the community's logo from `assets.logos.main` +3. **Navigation** - Should show the community's navigation items +4. **Theme** - Should use the community's theme settings + +### Check Network Tab + +1. Open browser DevTools → Network tab +2. Look for requests to Supabase +3. Should see a call to `get_active_community_config` RPC function +4. Check the request payload - should include `p_community_id: 'example'` (or your test community) + +### Programmatic Verification + +Create a test script: + +```typescript +// scripts/test-config-loading.ts +import { loadSystemConfig } from '../src/config'; + +async function testConfigLoading() { + console.log('Testing config loading...\n'); + + // Test 1: Explicit community ID + console.log('Test 1: Explicit community ID'); + const config1 = await loadSystemConfig({ communityId: 'example' }); + console.log(`✅ Loaded: ${config1.brand.displayName}`); + console.log(` Community ID: ${config1.communityId || 'not set'}\n`); + + // Test 2: Domain-based resolution + console.log('Test 2: Domain-based resolution'); + const config2 = await loadSystemConfig({ + domain: 'example.localhost', + }); + console.log(`✅ Loaded: ${config2.brand.displayName}\n`); + + // Test 3: Default fallback + console.log('Test 3: Default fallback'); + const config3 = await loadSystemConfig(); + console.log(`✅ Loaded: ${config3.brand.displayName}\n`); +} + +testConfigLoading().catch(console.error); +``` + +Run it: +```bash +npx tsx scripts/test-config-loading.ts +``` + +## Troubleshooting + +### Config Not Loading + +**Problem:** Getting error "Failed to load config from database" + +**Solutions:** +1. Check Supabase credentials: + ```bash + echo $NEXT_PUBLIC_SUPABASE_URL + echo $NEXT_PUBLIC_SUPABASE_ANON_KEY + ``` + +2. Verify the community exists in the database: + ```sql + SELECT community_id, is_active + FROM community_configs + WHERE community_id = 'example'; + ``` + +3. Check the RPC function exists: + ```sql + SELECT routine_name + FROM information_schema.routines + WHERE routine_name = 'get_active_community_config'; + ``` + +### Wrong Community Loading + +**Problem:** Loading 'nouns' when expecting 'example' + +**Solutions:** +1. Check if `NEXT_PUBLIC_TEST_COMMUNITY` is set (it takes priority) +2. Verify domain resolution: + - Visit `http://example.localhost:3000` (not just `localhost:3000`) + - Check middleware logs for detected domain +3. Check `NEXT_PUBLIC_COMMUNITY` fallback value + +### Subdomain Not Working + +**Problem:** `example.localhost:3000` not resolving to 'example' + +**Solutions:** +1. Verify `/etc/hosts` entry: + ```bash + cat /etc/hosts | grep localhost + ``` +2. Clear browser cache/DNS cache +3. Try a different browser or incognito mode +4. Check middleware is running (should see headers in Network tab) + +## Testing Checklist + +- [ ] Test with `NEXT_PUBLIC_TEST_COMMUNITY` environment variable +- [ ] Test with localhost subdomains (`example.localhost:3000`) +- [ ] Verify correct community config loads (brand, logo, navigation) +- [ ] Check console logs show correct community ID +- [ ] Verify Supabase requests in Network tab +- [ ] Test fallback to `NEXT_PUBLIC_COMMUNITY` +- [ ] Test error handling (invalid community ID) +- [ ] Test both server-side and client-side loading + +## Environment Variables Reference + +| Variable | Purpose | Priority | Example | +|----------|---------|----------|---------| +| `NEXT_PUBLIC_TEST_COMMUNITY` | Development override | 1 (dev only) | `example` | +| Domain resolution | From request domain | 2 | `example.localhost` → `example` | +| `NEXT_PUBLIC_COMMUNITY` | Fallback | 3 | `nouns` | + +## Related Documentation + +- [Configuration System Overview](SYSTEMS/CONFIGURATION/OVERVIEW.md) - Complete system documentation +- [Configuration Guide](CONFIGURATION.md) - General configuration guide + diff --git a/package.json b/package.json index 2e1f25229..4d3acfe02 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "test:unit": "vitest tests/neynarMiniApp.unit.test.ts", "test:coverage": "vitest --coverage", "test:watch": "vitest --watch", - "test:ci": "vitest tests/neynarMiniApp.fast.test.ts tests/miniAppDiscovery.api.fast.test.ts --run --reporter=verbose" + "test:ci": "vitest tests/neynarMiniApp.fast.test.ts tests/miniAppDiscovery.api.fast.test.ts --run --reporter=verbose", + "test:config": "tsx scripts/test-config-loading.ts" }, "lint-staged": { "src/**/*": "prettier --write --ignore-unknown" diff --git a/scripts/test-config-loading.ts b/scripts/test-config-loading.ts new file mode 100644 index 000000000..5ba07e47a --- /dev/null +++ b/scripts/test-config-loading.ts @@ -0,0 +1,94 @@ +#!/usr/bin/env tsx +/** + * Test script for verifying config loading works correctly + * + * Usage: + * npx tsx scripts/test-config-loading.ts + * + * Or with specific community: + * NEXT_PUBLIC_TEST_COMMUNITY=example npx tsx scripts/test-config-loading.ts + */ + +import { loadSystemConfig } from '../src/config'; + +async function testConfigLoading() { + console.log('🧪 Testing Config Loading\n'); + console.log('=' .repeat(50)); + + // Test 1: Explicit community ID + console.log('\n📋 Test 1: Explicit community ID'); + console.log('-'.repeat(50)); + try { + const config1 = await loadSystemConfig({ communityId: 'example' }); + console.log(`✅ Successfully loaded config`); + console.log(` Community: ${config1.brand?.displayName || 'Unknown'}`); + console.log(` Logo: ${config1.assets?.logos?.main || 'Not set'}`); + console.log(` Navigation items: ${config1.navigation?.items?.length || 0}`); + } catch (error: any) { + console.error(`❌ Failed: ${error.message}`); + } + + // Test 2: Domain-based resolution + console.log('\n📋 Test 2: Domain-based resolution'); + console.log('-'.repeat(50)); + try { + const config2 = await loadSystemConfig({ + domain: 'example.localhost', + }); + console.log(`✅ Successfully loaded config from domain`); + console.log(` Community: ${config2.brand?.displayName || 'Unknown'}`); + console.log(` Domain: example.localhost`); + } catch (error: any) { + console.error(`❌ Failed: ${error.message}`); + } + + // Test 3: Environment-based (current setup) + console.log('\n📋 Test 3: Environment-based (current setup)'); + console.log('-'.repeat(50)); + try { + const config3 = await loadSystemConfig(); + console.log(`✅ Successfully loaded config from environment`); + console.log(` Community: ${config3.brand?.displayName || 'Unknown'}`); + console.log(` Source: Environment variables / domain detection`); + + // Show which community was resolved + const testCommunity = process.env.NEXT_PUBLIC_TEST_COMMUNITY; + const envCommunity = process.env.NEXT_PUBLIC_COMMUNITY; + if (testCommunity) { + console.log(` Resolved from: NEXT_PUBLIC_TEST_COMMUNITY=${testCommunity}`); + } else if (envCommunity) { + console.log(` Resolved from: NEXT_PUBLIC_COMMUNITY=${envCommunity}`); + } else { + console.log(` Resolved from: Default fallback`); + } + } catch (error: any) { + console.error(`❌ Failed: ${error.message}`); + } + + // Test 4: Invalid community (error handling) + console.log('\n📋 Test 4: Invalid community (error handling)'); + console.log('-'.repeat(50)); + try { + const config4 = await loadSystemConfig({ communityId: 'nonexistent-community-12345' }); + console.log(`⚠️ Unexpectedly succeeded (should have failed)`); + } catch (error: any) { + console.log(`✅ Correctly failed for invalid community`); + console.log(` Error: ${error.message}`); + } + + // Summary + console.log('\n' + '='.repeat(50)); + console.log('✅ Test suite completed'); + console.log('\n💡 Tips:'); + console.log(' - Set NEXT_PUBLIC_TEST_COMMUNITY=example to test specific community'); + console.log(' - Use localhost subdomains (example.localhost:3000) for domain testing'); + console.log(' - Check Supabase credentials if tests fail'); + console.log(''); +} + +// Run tests +testConfigLoading().catch((error) => { + console.error('❌ Test suite failed:', error); + process.exit(1); +}); + diff --git a/src/app/[navSlug]/[[...tabName]]/NavPageClient.tsx b/src/app/[navSlug]/[[...tabName]]/NavPageClient.tsx index 855590188..3c85fd01a 100644 --- a/src/app/[navSlug]/[[...tabName]]/NavPageClient.tsx +++ b/src/app/[navSlug]/[[...tabName]]/NavPageClient.tsx @@ -6,17 +6,15 @@ import SpacePage, { SpacePageArgs } from "@/app/(spaces)/SpacePage"; import { SpaceConfig } from "@/app/(spaces)/Space"; import TabBar from "@/common/components/organisms/TabBar"; import { useMiniKit } from "@coinbase/onchainkit/minikit"; -import type { HomePageConfig, ExplorePageConfig } from "@/config/systemConfig"; - -type PageConfig = HomePageConfig | ExplorePageConfig; +import type { NavPageConfig } from "@/config/systemConfig"; type NavPageClientProps = { - pageConfig: PageConfig; + pageConfig: NavPageConfig; activeTabName: string; navSlug: string; }; -const getTabConfig = (tabName: string, config: PageConfig): SpaceConfig => { +const getTabConfig = (tabName: string, config: NavPageConfig): SpaceConfig => { return (config.tabs[tabName] || config.tabs[config.defaultTab]) as SpaceConfig; }; diff --git a/src/app/[navSlug]/[[...tabName]]/page.tsx b/src/app/[navSlug]/[[...tabName]]/page.tsx index 57be9f7f5..5ff657088 100644 --- a/src/app/[navSlug]/[[...tabName]]/page.tsx +++ b/src/app/[navSlug]/[[...tabName]]/page.tsx @@ -4,15 +4,13 @@ import { loadSystemConfig } from "@/config"; import NavPageClient from "./NavPageClient"; import { createClient } from '@supabase/supabase-js'; import { SignedFile } from "@/common/lib/signedFiles"; -import type { HomePageConfig, ExplorePageConfig } from "@/config/systemConfig"; - -type PageConfig = HomePageConfig | ExplorePageConfig; +import type { NavPageConfig } from "@/config/systemConfig"; /** * Convert a Space stored in storage to a PageConfig * Returns null if Supabase credentials are not available or if the space can't be loaded */ -async function loadSpaceAsPageConfig(spaceId: string): Promise { +async function loadSpaceAsPageConfig(spaceId: string): Promise { // Check if Supabase credentials are available const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseKey = process.env.SUPABASE_SERVICE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY; @@ -69,7 +67,7 @@ async function loadSpaceAsPageConfig(spaceId: string): Promise - ); + // Nav item must have a spaceId to load page config + if (!navItem.spaceId) { + notFound(); } - // Fallback: check if it's a legacy homePage/explorePage - // (for backward compatibility during transition) - if (navSlug === 'home' && config.homePage) { - if (!config.homePage.tabs[activeTabName]) { - notFound(); - } - - return ( - - ); + const pageConfig = await loadSpaceAsPageConfig(navItem.spaceId); + if (!pageConfig) { + notFound(); } - if (navSlug === 'explore' && config.explorePage) { - if (!config.explorePage.tabs[activeTabName]) { - notFound(); - } - - return ( - - ); + // Validate tab exists + if (!pageConfig.tabs[activeTabName]) { + notFound(); } - notFound(); + return ( + + ); } diff --git a/src/app/explore/ExploreTabPage.tsx b/src/app/explore/ExploreTabPage.tsx deleted file mode 100644 index 208a9b414..000000000 --- a/src/app/explore/ExploreTabPage.tsx +++ /dev/null @@ -1,73 +0,0 @@ -"use client"; - -import React, { useEffect } from "react"; -import { useMiniKit } from "@coinbase/onchainkit/minikit"; - -import { useAppStore } from "@/common/data/stores/app"; -import TabBar from "@/common/components/organisms/TabBar"; -import SpacePage, { type SpacePageArgs } from "@/app/(spaces)/SpacePage"; -import { type SpaceConfig } from "@/app/(spaces)/Space"; -import type { ExplorePageConfig } from "@/config/systemConfig"; - -const getTabConfig = ( - tabName: string, - config: ExplorePageConfig, -): SpaceConfig => - (config.tabs[tabName] || config.tabs[config.defaultTab]) as SpaceConfig; - -type ExploreTabPageProps = { - tabName: string; - explorePage: ExplorePageConfig; -}; - -const ExploreTabPage: React.FC = ({ tabName, explorePage }) => { - const { setCurrentTabName, currentTabName } = useAppStore((state) => ({ - setCurrentTabName: state.currentSpace.setCurrentTabName, - currentTabName: state.currentSpace.currentTabName, - })); - - const { setFrameReady, isFrameReady } = useMiniKit(); - - useEffect(() => { - if (!isFrameReady) { - setFrameReady(); - } - }, [isFrameReady, setFrameReady]); - - useEffect(() => { - setCurrentTabName(tabName); - }, [tabName, setCurrentTabName]); - - const activeTabName = currentTabName ?? explorePage.defaultTab; - - const tabBar = ( - `/explore/${encodeURIComponent(tab)}`} - inHomebase={false} - currentTab={activeTabName} - tabList={explorePage.tabOrder} - defaultTab={explorePage.defaultTab} - inEditMode={false} - updateTabOrder={async () => Promise.resolve()} - deleteTab={async () => Promise.resolve()} - createTab={async () => Promise.resolve({ tabName: activeTabName })} - renameTab={async () => Promise.resolve()} - commitTab={async () => Promise.resolve()} - commitTabOrder={async () => Promise.resolve()} - isEditable={false} - /> - ); - - const args: SpacePageArgs = { - config: getTabConfig(activeTabName, explorePage), - saveConfig: async () => {}, - commitConfig: async () => {}, - resetConfig: async () => {}, - tabBar, - showFeedOnMobile: false, - }; - - return ; -}; - -export default ExploreTabPage; diff --git a/src/app/page.tsx b/src/app/page.tsx index 29bc9ca6f..0457b42e6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,14 +4,42 @@ import { loadSystemConfig } from "@/config"; export default async function RootRedirect() { const config = await loadSystemConfig(); - // If homePage exists (legacy config), redirect to its default tab - if (config.homePage?.defaultTab) { - const tab = encodeURIComponent(config.homePage.defaultTab); - redirect(`/home/${tab}`); - return null; + // Find the home navigation item and redirect to its default tab + const navItems = config.navigation?.items || []; + const homeNavItem = navItems.find(item => item.href === '/home'); + + if (homeNavItem?.spaceId) { + // Load default tab from space storage + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = process.env.SUPABASE_SERVICE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY; + + if (supabaseUrl && supabaseKey) { + try { + const { createClient } = await import('@supabase/supabase-js'); + const supabase = createClient(supabaseUrl, supabaseKey); + + const { data: tabOrderData } = await supabase.storage + .from('spaces') + .download(`${homeNavItem.spaceId}/tabOrder`); + + if (tabOrderData) { + const { SignedFile } = await import('@/common/lib/signedFiles'); + const tabOrderFile = JSON.parse(await tabOrderData.text()) as SignedFile; + const tabOrderObj = JSON.parse(tabOrderFile.fileData) as { tabOrder: string[] }; + const defaultTab = tabOrderObj.tabOrder?.[0]; + + if (defaultTab) { + redirect(`/home/${encodeURIComponent(defaultTab)}`); + return null; + } + } + } catch (error) { + console.warn('Failed to load home space default tab:', error); + } + } } - // Otherwise, redirect to /home and let the navigation handler figure out the default tab + // Fallback: redirect to /home and let the navigation handler figure out the default tab redirect('/home'); return null; } diff --git a/src/config/clanker/index.ts b/src/config/clanker/index.ts index 2b163bdd5..d7f59c98f 100644 --- a/src/config/clanker/index.ts +++ b/src/config/clanker/index.ts @@ -8,10 +8,3 @@ export { clankerHomePage } from './clanker.home'; export { clankerExplorePage } from './clanker.explore'; export { clankerNavigation } from './clanker.navigation'; export { clankerUI } from './clanker.ui'; - -// Export the initial space creators (used at runtime) -export { default as createInitialProfileSpaceConfigForFid } from './initialSpaces/initialProfileSpace'; -export { default as createInitialChannelSpaceConfig } from './initialSpaces/initialChannelSpace'; -export { default as createInitialTokenSpaceConfigForAddress } from './initialSpaces/initialTokenSpace'; -export { default as createInitialProposalSpaceConfigForProposalId } from './initialSpaces/initialProposalSpace'; -export { default as INITIAL_HOMEBASE_CONFIG, createInitialHomebaseConfig } from './initialSpaces/initialHomebase'; diff --git a/src/config/clanker/initialSpaces/exploreTabs/channel.json b/src/config/clanker/initialSpaces/exploreTabs/channel.json deleted file mode 100644 index 895e9b73d..000000000 --- a/src/config/clanker/initialSpaces/exploreTabs/channel.json +++ /dev/null @@ -1,19088 +0,0 @@ -{ - "members": [ - { - "address": "fc_fid_1048", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "macbudkowski", - "displayName": "Mac Budkowski", - "fid": 1048, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a43231ca-1d8d-4563-c9b3-6a8e934c2a00/original", - "followers": 101017, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd80c1eed0b442be1fa00f313446d250589d3924a", - "etherscanUrl": "https://etherscan.io/address/0xd80c1eed0b442be1fa00f313446d250589d3924a", - "xHandle": "macbudkowski", - "xUrl": "https://twitter.com/macbudkowski", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_16567", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "serendipity", - "displayName": "Karo K", - "fid": 16567, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7bbdc97b-e9db-424d-3248-6e87c0e40d00/rectcrop3", - "followers": 96493, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfe1572b82b45c561b9011043f9ba4ed6aef7569f", - "etherscanUrl": "https://etherscan.io/address/0xfe1572b82b45c561b9011043f9ba4ed6aef7569f", - "xHandle": "karoverse_x", - "xUrl": "https://twitter.com/karoverse_x", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_4167", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nounishprof", - "displayName": "Nounish Prof ⌐◧-◧🎩", - "fid": 4167, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0e0686be-ae1a-47d2-c737-5beecfe21700/rectcrop3", - "followers": 91821, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9353cc2583356ce6ea8f92e239d7c9eb8412acaf", - "etherscanUrl": "https://etherscan.io/address/0x9353cc2583356ce6ea8f92e239d7c9eb8412acaf", - "xHandle": "profwerder", - "xUrl": "https://twitter.com/profwerder", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_880", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "accountless.eth", - "displayName": "accountless.eth", - "fid": 880, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/96427ea9-02b1-49a2-c538-cd8ba67f1800/original", - "followers": 76231, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdb221863401c5174e51af0489f76caf7b32b5ea9", - "etherscanUrl": "https://etherscan.io/address/0xdb221863401c5174e51af0489f76caf7b32b5ea9", - "xHandle": "alexanderchopan", - "xUrl": "https://twitter.com/alexanderchopan", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_270504", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "proxystudio", - "displayName": "gordie slater", - "fid": 270504, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5a3470a3-f1bc-4372-31b8-8baea35fd900/original", - "followers": 68021, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x689ecdd6221d989863178fe2de6db94195a856c2", - "etherscanUrl": "https://etherscan.io/address/0x689ecdd6221d989863178fe2de6db94195a856c2", - "xHandle": "_proxystudio", - "xUrl": "https://twitter.com/_proxystudio", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_5431", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "esdotge", - "displayName": "S·G", - "fid": 5431, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2c48cc46-362f-4bfa-6e55-0d08fbf54800/original", - "followers": 62580, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7b8a343381a5259e3ea54bc35d694449bbc3418d", - "etherscanUrl": "https://etherscan.io/address/0x7b8a343381a5259e3ea54bc35d694449bbc3418d", - "xHandle": "esdotge", - "xUrl": "https://twitter.com/esdotge", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1996", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "thatalexpalmer.eth", - "displayName": "Alex Palmer", - "fid": 1996, - "pfpUrl": "https://i.seadn.io/gcs/files/368ad79a53ff4d046a28e18939dddc18.jpg?w=500&auto=format", - "followers": 49635, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3601a913fd3466f30f5abb978e484d1b37ce995d", - "etherscanUrl": "https://etherscan.io/address/0x3601a913fd3466f30f5abb978e484d1b37ce995d", - "xHandle": "thatalexpalmer", - "xUrl": "https://twitter.com/thatalexpalmer", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_4434", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "chandresh.eth", - "displayName": "chandresh", - "fid": 4434, - "pfpUrl": "https://i.imgur.com/ZA99OUh.jpg", - "followers": 46692, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd9191100e4913f4933ff3b6bf6dc84569f8e44e9", - "etherscanUrl": "https://etherscan.io/address/0xd9191100e4913f4933ff3b6bf6dc84569f8e44e9", - "xHandle": "thisischandresh", - "xUrl": "https://twitter.com/thisischandresh", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_5543", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sirsu.eth", - "displayName": "Sol Su 🌞", - "fid": 5543, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/927b65ff-f302-444e-9d56-b34a81ca0e00/original", - "followers": 46473, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc2219d6f28899ff4da53fa93914ace3069ab15ee", - "etherscanUrl": "https://etherscan.io/address/0xc2219d6f28899ff4da53fa93914ace3069ab15ee", - "xHandle": "sirsuhayb", - "xUrl": "https://twitter.com/sirsuhayb", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_24235", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "turboflow.eth", - "displayName": "Paul Carrera", - "fid": 24235, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9b171bb0-9f56-4b69-c89e-e94264ce7700/rectcrop3", - "followers": 45320, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x55432de4a9b98d2046b4173b8e59a7b540651f20", - "etherscanUrl": "https://etherscan.io/address/0x55432de4a9b98d2046b4173b8e59a7b540651f20", - "xHandle": "turboroom", - "xUrl": "https://twitter.com/turboroom", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_9933", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "m00npapi.eth", - "displayName": "m00npapi.eth", - "fid": 9933, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1e8e6cb7-8fea-40f9-1dc7-47810f634700/original", - "followers": 22628, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbf4977f1295454cb46d95fe7d8e1d99e32d8aed1", - "etherscanUrl": "https://etherscan.io/address/0xbf4977f1295454cb46d95fe7d8e1d99e32d8aed1", - "xHandle": "m00npapi", - "xUrl": "https://twitter.com/m00npapi", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_403020", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "push-", - "displayName": "Push", - "fid": 403020, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1d6bb063-b545-4416-66e0-c6ba29f64900/original", - "followers": 22193, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2f05371f6b614ca142133c5ab4a8d6631a279fa7", - "etherscanUrl": "https://etherscan.io/address/0x2f05371f6b614ca142133c5ab4a8d6631a279fa7", - "xHandle": "push_gfx", - "xUrl": "https://twitter.com/push_gfx", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_245388", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "199.eth", - "displayName": "199", - "fid": 245388, - "pfpUrl": "https://i.imgur.com/lbMpUng.jpg", - "followers": 19548, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x277b47000e1b88c929c1437713ebfefe1e966a71", - "etherscanUrl": "https://etherscan.io/address/0x277b47000e1b88c929c1437713ebfefe1e966a71", - "xHandle": "199eth", - "xUrl": "https://twitter.com/199eth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_15549", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dish", - "displayName": "Jack Dishman", - "fid": 15549, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/87fbe83b-1455-4f62-f389-ae82d7452900/original", - "followers": 18476, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x308112d06027cd838627b94ddfc16ea6b4d90004", - "etherscanUrl": "https://etherscan.io/address/0x308112d06027cd838627b94ddfc16ea6b4d90004", - "xHandle": "jackdishman", - "xUrl": "https://twitter.com/jackdishman", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_12938", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ashmoney.eth", - "displayName": "ash", - "fid": 12938, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/579c1a06-58e1-441d-8951-b9b49127f300/original", - "followers": 17306, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x30e5a4e6a52b2d6b891454a0fd04938732c55193", - "etherscanUrl": "https://etherscan.io/address/0x30e5a4e6a52b2d6b891454a0fd04938732c55193", - "xHandle": "ashrafstakala", - "xUrl": "https://twitter.com/ashrafstakala", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_327165", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nicolasdavis.eth", - "displayName": "Nicolas Davis ↑🔆🔵", - "fid": 327165, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5a760f3e-ca84-4751-b7c9-155b711be500/original", - "followers": 16356, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xecda4312a6ec1f7860f56dccf0cee5b6962d5b5d", - "etherscanUrl": "https://etherscan.io/address/0xecda4312a6ec1f7860f56dccf0cee5b6962d5b5d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_423255", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "minchow", - "displayName": "min.base.eth", - "fid": 423255, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/122aebf2-5e05-48ed-1d38-791ecef7c500/original", - "followers": 12600, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x756c338c3e3890288846e13972f2a49b7f90e83e", - "etherscanUrl": "https://etherscan.io/address/0x756c338c3e3890288846e13972f2a49b7f90e83e", - "xHandle": "mineth92", - "xUrl": "https://twitter.com/mineth92", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_350139", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "joshisdead.eth", - "displayName": "joshisdead.eth", - "fid": 350139, - "pfpUrl": "https://i.imgur.com/g63pK5y.jpg", - "followers": 12037, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2e5e9c6358c201c4546b971c2b41882585e270df", - "etherscanUrl": "https://etherscan.io/address/0x2e5e9c6358c201c4546b971c2b41882585e270df", - "xHandle": "jsholedamn", - "xUrl": "https://twitter.com/jsholedamn", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_277952", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dummie.eth", - "displayName": "Dvyne", - "fid": 277952, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b60bbabf-b446-4b90-ad66-9b1e69a6b800/rectcrop3", - "followers": 11855, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x41a53bacdbf369377870d1377864e36176751e8d", - "etherscanUrl": "https://etherscan.io/address/0x41a53bacdbf369377870d1377864e36176751e8d", - "xHandle": "dummie_eth", - "xUrl": "https://twitter.com/dummie_eth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_473065", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "wetsocks.eth", - "displayName": "wetsocks", - "fid": 473065, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/010ba5fe-f1ce-4a3c-13b6-1f53ea838e00/original", - "followers": 11706, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x845c1a276ca266181dc20bbb7f113b546a4c1f10", - "etherscanUrl": "https://etherscan.io/address/0x845c1a276ca266181dc20bbb7f113b546a4c1f10", - "xHandle": "wetsocks", - "xUrl": "https://twitter.com/wetsocks", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_466111", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "svvvg3.eth", - "displayName": "SVVVG3", - "fid": 466111, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0a053450-3b84-4d75-b6e9-76811fbb2800/original", - "followers": 11586, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbb14db9f60e806846335f79481d998abc66efc20", - "etherscanUrl": "https://etherscan.io/address/0xbb14db9f60e806846335f79481d998abc66efc20", - "xHandle": "_svvvg3", - "xUrl": "https://twitter.com/_svvvg3", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_284679", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "degenveteran.eth", - "displayName": "DV", - "fid": 284679, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/cda86713-8c95-472d-ec78-aa7c1ee5e000/rectcrop3", - "followers": 10960, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa33cc67e1dd105512de3b94e86724c8876c72de6", - "etherscanUrl": "https://etherscan.io/address/0xa33cc67e1dd105512de3b94e86724c8876c72de6", - "xHandle": "randy_mcelhaney", - "xUrl": "https://twitter.com/randy_mcelhaney", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_239533", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "obey", - "displayName": "OBEY", - "fid": 239533, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/8dd8d388-a7c2-4010-6683-4cc88a9c0700/original", - "followers": 10560, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xded214192212c73c6e56f7a52a50e3dba6b2747a", - "etherscanUrl": "https://etherscan.io/address/0xded214192212c73c6e56f7a52a50e3dba6b2747a", - "xHandle": "obeyz_z", - "xUrl": "https://twitter.com/obeyz_z", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_517425", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "shahbazbaaz", - "displayName": "Muhammad Shahbaz 💎Ⓜ️👽", - "fid": 517425, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/11e24170-ea97-491f-dc92-f34f911ccb00/original", - "followers": 10513, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xeb0ae1f94b3a28510f593d4e121308f7c0b6e6ea", - "etherscanUrl": "https://etherscan.io/address/0xeb0ae1f94b3a28510f593d4e121308f7c0b6e6ea", - "xHandle": "muhammadsh53192", - "xUrl": "https://twitter.com/muhammadsh53192", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_273708", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "siadude", - "displayName": "siadude - $HMBT", - "fid": 273708, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f38b5f27-b531-42b9-7201-d1a1efea9800/original", - "followers": 9164, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3c515f7776f41ffc9df45a4bbd515c85e21aba62", - "etherscanUrl": "https://etherscan.io/address/0x3c515f7776f41ffc9df45a4bbd515c85e21aba62", - "xHandle": "9to5suck", - "xUrl": "https://twitter.com/9to5suck", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_270138", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "thompson", - "displayName": "tknox.eth🟪🎩", - "fid": 270138, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9042ca50-a2a7-478e-0dc2-d32fb4b2f900/original", - "followers": 8900, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe330262ce3b9ae1e58c60d6c60f68d9f3c3776ba", - "etherscanUrl": "https://etherscan.io/address/0xe330262ce3b9ae1e58c60d6c60f68d9f3c3776ba", - "xHandle": "thompsonnft", - "xUrl": "https://twitter.com/thompsonnft", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_486435", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "liai", - "displayName": "Muhammad Yaseen 🎩", - "fid": 486435, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/33e3638a-94ff-4571-2e64-8d7279a6b900/original", - "followers": 7790, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa3eecc89da8e0acbb402c736bd4f901b5bf70b6a", - "etherscanUrl": "https://etherscan.io/address/0xa3eecc89da8e0acbb402c736bd4f901b5bf70b6a", - "xHandle": "trader895210", - "xUrl": "https://twitter.com/trader895210", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_474179", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "crezzang", - "displayName": "Mica.eth", - "fid": 474179, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/83e2a90b-797f-4a77-1454-16e6b1a1a700/original", - "followers": 7759, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x09040ad5e75687dd6ce7b47a0fa31d7bba77eab7", - "etherscanUrl": "https://etherscan.io/address/0x09040ad5e75687dd6ce7b47a0fa31d7bba77eab7", - "xHandle": "crezzang77", - "xUrl": "https://twitter.com/crezzang77", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_514036", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "subhanahmad", - "displayName": "Subhan Ahmad", - "fid": 514036, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9aced606-166a-4ea3-86e9-15b5314b4d00/rectcrop3", - "followers": 7535, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc271b873349d1676adcbcb625f4d263549be6adc", - "etherscanUrl": "https://etherscan.io/address/0xc271b873349d1676adcbcb625f4d263549be6adc", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_346798", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nachovarga", - "displayName": "Nacho", - "fid": 346798, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/dbaba054-d305-4ad0-893b-3628f26d5100/rectcrop3", - "followers": 7144, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbfe5de8bf1e390b9410fb05589f53910354cb67a", - "etherscanUrl": "https://etherscan.io/address/0xbfe5de8bf1e390b9410fb05589f53910354cb67a", - "xHandle": "nachovarga", - "xUrl": "https://twitter.com/nachovarga", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_323144", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "woo-x", - "displayName": "WOO🎩", - "fid": 323144, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2a47a2c1-9608-417b-a3d1-88f0a753dd00/original", - "followers": 6602, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x629d1cfeaaadfcd8c67e40602508968f9c3e2a39", - "etherscanUrl": "https://etherscan.io/address/0x629d1cfeaaadfcd8c67e40602508968f9c3e2a39", - "xHandle": "st_xoo", - "xUrl": "https://twitter.com/st_xoo", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_247653", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "pandateemo", - "displayName": "pandateemo.base.eth", - "fid": 247653, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/900fd5f3-4967-40cc-f409-ca039e386c00/rectcrop3", - "followers": 6545, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x49a93a34ac44e195d6cd5697c648bdff0c133d88", - "etherscanUrl": "https://etherscan.io/address/0x49a93a34ac44e195d6cd5697c648bdff0c133d88", - "xHandle": "pandateemo88", - "xUrl": "https://twitter.com/pandateemo88", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_196041", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "suffuze.eth", - "displayName": "Suffuze", - "fid": 196041, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/875c199b-b43d-4d23-7d1c-9a54d4131c00/original", - "followers": 6364, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa0b223a5f9b732d89d429e747314e9acfea18aa3", - "etherscanUrl": "https://etherscan.io/address/0xa0b223a5f9b732d89d429e747314e9acfea18aa3", - "xHandle": "suffuze", - "xUrl": "https://twitter.com/suffuze", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_189896", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "vibecaster.eth", - "displayName": "ʞɔɐſ", - "fid": 189896, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1925aa99-47ea-41c9-bc94-3e62bd85ae00/original", - "followers": 6296, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x048a74506f9cdd3b241fc2ef3d2208d417aff81c", - "etherscanUrl": "https://etherscan.io/address/0x048a74506f9cdd3b241fc2ef3d2208d417aff81c", - "xHandle": "vibe_caster", - "xUrl": "https://twitter.com/vibe_caster", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1021214", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "marbleheart", - "displayName": "marble", - "fid": 1021214, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f761fd5a-bf81-497a-3bb8-2500da241500/original", - "followers": 6285, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3157497b82ec91a234f11ee44553d2a303e8d59e", - "etherscanUrl": "https://etherscan.io/address/0x3157497b82ec91a234f11ee44553d2a303e8d59e", - "xHandle": "marbleheartx", - "xUrl": "https://twitter.com/marbleheartx", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_197007", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "btayengco", - "displayName": "Bryce", - "fid": 197007, - "pfpUrl": "https://i.imgur.com/EITOCJn.jpg", - "followers": 6093, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6be829428a7c77deef501350e6948afdc03c311e", - "etherscanUrl": "https://etherscan.io/address/0x6be829428a7c77deef501350e6948afdc03c311e", - "xHandle": "btayengco", - "xUrl": "https://twitter.com/btayengco", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_4368", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "amado", - "displayName": "Alex", - "fid": 4368, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f7e7aed1-9808-4def-43cd-a39313d7b000/original", - "followers": 6040, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3914142bef098357494c849098594f9cd5602b72", - "etherscanUrl": "https://etherscan.io/address/0x3914142bef098357494c849098594f9cd5602b72", - "xHandle": "subversieve", - "xUrl": "https://twitter.com/subversieve", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_394023", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dexxcuyy.eth", - "displayName": "dexx - Photography", - "fid": 394023, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0dbd92bd-5bff-49a3-abf2-872335dd1500/original", - "followers": 6034, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbcaa503d49e429e22b3c5ebd53dbf31259db6e15", - "etherscanUrl": "https://etherscan.io/address/0xbcaa503d49e429e22b3c5ebd53dbf31259db6e15", - "xHandle": "humanxdx", - "xUrl": "https://twitter.com/humanxdx", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_13796", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ashwinikumar.eth", - "displayName": "Ashwinikumar", - "fid": 13796, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/352c8458-36f5-4a75-2145-418695523200/original", - "followers": 5978, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xef32e52deb6ebecdacfc8a878a850f21dfb54b42", - "etherscanUrl": "https://etherscan.io/address/0xef32e52deb6ebecdacfc8a878a850f21dfb54b42", - "xHandle": "ashwinidodani", - "xUrl": "https://twitter.com/ashwinidodani", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_888817", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "basedprofilebase", - "displayName": "based", - "fid": 888817, - "pfpUrl": "https://imagedelivery.net/g4iQ0bIzMZrjFMgjAnSGfw/6c529af4-5d73-4a2e-105d-879a9df11800/public", - "followers": 5961, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xce3aff2e781acb95eacf46df75323f9e171595fe", - "etherscanUrl": "https://etherscan.io/address/0xce3aff2e781acb95eacf46df75323f9e171595fe", - "xHandle": "lennixfarcast", - "xUrl": "https://twitter.com/lennixfarcast", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_9318", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "catra", - "displayName": "catra ↑", - "fid": 9318, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f2a9e9cc-5e2b-47ac-679c-9d2f28bd5e00/original", - "followers": 5662, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x036cd064d068eb1d99e07d79848c7a4aab94bd49", - "etherscanUrl": "https://etherscan.io/address/0x036cd064d068eb1d99e07d79848c7a4aab94bd49", - "xHandle": "catradarusman", - "xUrl": "https://twitter.com/catradarusman", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_460229", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "abss", - "displayName": "Abs 🎩", - "fid": 460229, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4a55d833-046e-4209-4899-b6fa8a563800/original", - "followers": 5621, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe30b9dcb54fd380629505f0b24ce45eb8719ceba", - "etherscanUrl": "https://etherscan.io/address/0xe30b9dcb54fd380629505f0b24ce45eb8719ceba", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_335897", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "showan", - "displayName": "showan", - "fid": 335897, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1a01a841-dea5-4177-4d21-485ff998ae00/original", - "followers": 5575, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x99b95b1c8e19f9fbe737bc7e9a48401cad05a2e1", - "etherscanUrl": "https://etherscan.io/address/0x99b95b1c8e19f9fbe737bc7e9a48401cad05a2e1", - "xHandle": "showanurmia", - "xUrl": "https://twitter.com/showanurmia", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_306438", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "komol0", - "displayName": "KoⓂ️ol🎩🍊🧬", - "fid": 306438, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6653eb75-effc-4c9b-ecc3-8509c5aaa100/original", - "followers": 5527, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x944fa0f3f2168d4b27110f7f97972ad9425c4f52", - "etherscanUrl": "https://etherscan.io/address/0x944fa0f3f2168d4b27110f7f97972ad9425c4f52", - "xHandle": "komol02", - "xUrl": "https://twitter.com/komol02", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1158447", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "atupami", - "displayName": "Atupa.base.eth", - "fid": 1158447, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7dfeffad-761d-4c3c-7460-d6630aa69200/original", - "followers": 5502, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcddff021748df7d2e36034ff6683caf575c54b80", - "etherscanUrl": "https://etherscan.io/address/0xcddff021748df7d2e36034ff6683caf575c54b80", - "xHandle": "atupa_mi", - "xUrl": "https://twitter.com/atupa_mi", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_234327", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "treeskulltown.eth", - "displayName": "treeskulltown", - "fid": 234327, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/13943798-2990-404a-052e-675bc76eca00/original", - "followers": 5460, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1ed07d2ed9011ff46eca31f4288768092008c65b", - "etherscanUrl": "https://etherscan.io/address/0x1ed07d2ed9011ff46eca31f4288768092008c65b", - "xHandle": "treeskulltown2", - "xUrl": "https://twitter.com/treeskulltown2", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_516505", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mfa", - "displayName": "mfa.base (1/100🎥)", - "fid": 516505, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6eacf21a-f113-4b48-2708-e9de5425ef00/original", - "followers": 5257, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xff6a3ddf7b726f11f0b4e43d0b700fea8c81888c", - "etherscanUrl": "https://etherscan.io/address/0xff6a3ddf7b726f11f0b4e43d0b700fea8c81888c", - "xHandle": "furqana23714945", - "xUrl": "https://twitter.com/furqana23714945", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_203666", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "leovido.eth", - "displayName": "C. Leovido 🍃🟡", - "fid": 203666, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f64d9eb4-e0b4-4b60-7fca-37a5e3a43600/original", - "followers": 5250, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa159ddbd37a081c6af3e0e0f9ca4133478625dd4", - "etherscanUrl": "https://etherscan.io/address/0xa159ddbd37a081c6af3e0e0f9ca4133478625dd4", - "xHandle": "c_leovido", - "xUrl": "https://twitter.com/c_leovido", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_644823", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0xkesha", - "displayName": "Kesha.base.eth🎩🔵", - "fid": 644823, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/adb26344-0af6-4f56-e9fb-0a6bd9afc500/original", - "followers": 5071, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc42166e9e1ed7b053781be452eebbe86a13c8cdb", - "etherscanUrl": "https://etherscan.io/address/0xc42166e9e1ed7b053781be452eebbe86a13c8cdb", - "xHandle": "itskesha02", - "xUrl": "https://twitter.com/itskesha02", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_474644", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "abeg007.eth", - "displayName": "abeg007 🧬", - "fid": 474644, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5c18acfd-afff-4ed6-d3ea-23121b319300/original", - "followers": 4966, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x74fed975e5f1cb637d9471d1c5e291c8a428ee57", - "etherscanUrl": "https://etherscan.io/address/0x74fed975e5f1cb637d9471d1c5e291c8a428ee57", - "xHandle": "abegashikul", - "xUrl": "https://twitter.com/abegashikul", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_368757", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0xdejun.eth", - "displayName": "Dejun", - "fid": 368757, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/16f79823-159e-4526-a64a-47023a681e00/original", - "followers": 4950, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6306e7b383f4c6a6d2744dea755649a3994fa97e", - "etherscanUrl": "https://etherscan.io/address/0x6306e7b383f4c6a6d2744dea755649a3994fa97e", - "xHandle": "0xdejun", - "xUrl": "https://twitter.com/0xdejun", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_3300", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ywc", - "displayName": "ywc", - "fid": 3300, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/24f06959-c8f1-497a-7903-5e08f2383500/original", - "followers": 4869, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe14fbcaf7b7e87cb1bb7ff2db7ee0adbdf8585b5", - "etherscanUrl": "https://etherscan.io/address/0xe14fbcaf7b7e87cb1bb7ff2db7ee0adbdf8585b5", - "xHandle": "yashwant_eth", - "xUrl": "https://twitter.com/yashwant_eth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_405729", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "hamiiid.eth", - "displayName": "HaMiD 🧬", - "fid": 405729, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/742e6e69-dd5b-4a67-26bf-eb97a4053f00/original", - "followers": 4868, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe2d7fdf3628618bb0a880ae541fc4a19dc3df278", - "etherscanUrl": "https://etherscan.io/address/0xe2d7fdf3628618bb0a880ae541fc4a19dc3df278", - "xHandle": "sayhehich", - "xUrl": "https://twitter.com/sayhehich", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_212581", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bfresh", - "displayName": "hunter", - "fid": 212581, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/49dd11c6-66d7-4c56-6470-a547cb04c900/original", - "followers": 4665, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc0401272c353200da1c6eef2d100a108fda06025", - "etherscanUrl": "https://etherscan.io/address/0xc0401272c353200da1c6eef2d100a108fda06025", - "xHandle": "bfreshhb", - "xUrl": "https://twitter.com/bfreshhb", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_434449", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "warpcast-com", - "displayName": "Zephyros ⛩️", - "fid": 434449, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/06117ec6-b98d-438c-6c58-c83f75b13b00/original", - "followers": 4632, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x240a3df57aaa7089e7d2e261f0d948a99f43c96a", - "etherscanUrl": "https://etherscan.io/address/0x240a3df57aaa7089e7d2e261f0d948a99f43c96a", - "xHandle": "warpcastcom", - "xUrl": "https://twitter.com/warpcastcom", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_395022", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "shadatofficial", - "displayName": "🟦Shadatofficial", - "fid": 395022, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/66a2ef38-766b-4235-4b98-9346d4716700/original", - "followers": 4558, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdc09e11a12f63887065987dac7e8ef8ac6bf3ea3", - "etherscanUrl": "https://etherscan.io/address/0xdc09e11a12f63887065987dac7e8ef8ac6bf3ea3", - "xHandle": "shadatofficial1", - "xUrl": "https://twitter.com/shadatofficial1", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_17714", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "igoryuzo.eth", - "displayName": "Igor Yuzovitskiy", - "fid": 17714, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b70994a1-970c-4274-fcd2-b9b8a9728200/original", - "followers": 4512, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc934ce64152e2a846a977d51060356a9e5ddd351", - "etherscanUrl": "https://etherscan.io/address/0xc934ce64152e2a846a977d51060356a9e5ddd351", - "xHandle": "igoryuzo", - "xUrl": "https://twitter.com/igoryuzo", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_200375", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "adnum.base.eth", - "displayName": "adnum", - "fid": 200375, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e257ca30-d193-4635-538c-5e8389ab7700/original", - "followers": 4461, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb902352464ac345c0889374bf873b074ad73a0dd", - "etherscanUrl": "https://etherscan.io/address/0xb902352464ac345c0889374bf873b074ad73a0dd", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_2242", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "carter", - "displayName": "carter", - "fid": 2242, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ff564550-e03e-4a20-ae1e-87d22d3bf300/original", - "followers": 4406, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5b32c7635afe825703dbd446e0b402b8a67a7051", - "etherscanUrl": "https://etherscan.io/address/0x5b32c7635afe825703dbd446e0b402b8a67a7051", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_17673", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "road.base.eth", - "displayName": "road.base.eth", - "fid": 17673, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6ff23526-e905-475b-deb6-82ffe4804800/original", - "followers": 4405, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe83303b979dad1d966b370262c0e8f16b58bb299", - "etherscanUrl": "https://etherscan.io/address/0xe83303b979dad1d966b370262c0e8f16b58bb299", - "xHandle": "cryptouii", - "xUrl": "https://twitter.com/cryptouii", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_474817", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "attilagaliba.eth", - "displayName": "ㅤㅤㅤㅤㅤㅤ", - "fid": 474817, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/bc29449a-6ef4-4d2c-fd2d-4b2b61113900/original", - "followers": 4400, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbe4b374900f70dcab73a437ace8ef41a13d032e8", - "etherscanUrl": "https://etherscan.io/address/0xbe4b374900f70dcab73a437ace8ef41a13d032e8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_13267", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "daddysgotit.eth", - "displayName": "cosmic-thinker.eth 🎩", - "fid": 13267, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0f024a9a-4563-4779-afc4-45ac4835bb00/original", - "followers": 4332, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcd1ec7cce2932f5a3d7cf0df558058d343d36e46", - "etherscanUrl": "https://etherscan.io/address/0xcd1ec7cce2932f5a3d7cf0df558058d343d36e46", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_212259", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "julieinweb3", - "displayName": "julie.base.eth", - "fid": 212259, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c5826fc9-2b16-42c7-a933-219a5fa8f300/rectcrop3", - "followers": 4224, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6dc544ed82a866b41f3d8042704ea5deb5a60ee8", - "etherscanUrl": "https://etherscan.io/address/0x6dc544ed82a866b41f3d8042704ea5deb5a60ee8", - "xHandle": "juliezize", - "xUrl": "https://twitter.com/juliezize", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_230941", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "willywonka.eth", - "displayName": "willywonka ⌐◨-◨", - "fid": 230941, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c2324ecf-c09f-40f5-936c-80fbbd0cb500/original", - "followers": 4221, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3b525f808df863413afd4c5ae1eed276af266c97", - "etherscanUrl": "https://etherscan.io/address/0x3b525f808df863413afd4c5ae1eed276af266c97", - "xHandle": "willyogo", - "xUrl": "https://twitter.com/willyogo", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_307013", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nasrinmousavi", - "displayName": "Nasrin Mousavi", - "fid": 307013, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d5f38722-2fdd-40db-0fc0-f5085190d000/original", - "followers": 4220, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfe5978ea7f4790ee8a7a82fc5249cf142551e79e", - "etherscanUrl": "https://etherscan.io/address/0xfe5978ea7f4790ee8a7a82fc5249cf142551e79e", - "xHandle": "mooneth05", - "xUrl": "https://twitter.com/mooneth05", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_192702", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "brownalytics.eth", - "displayName": "Máximo T 🎩", - "fid": 192702, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/043c719f-ebae-45f3-e356-7514b0645800/original", - "followers": 4097, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfec9d8c0d6765385f80507538e65875e3a7bcda2", - "etherscanUrl": "https://etherscan.io/address/0xfec9d8c0d6765385f80507538e65875e3a7bcda2", - "xHandle": "degencondition", - "xUrl": "https://twitter.com/degencondition", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_8332", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "awedjob", - "displayName": "AJ 🧬", - "fid": 8332, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4358a797-a428-4d25-40ed-21fecf8ea100/original", - "followers": 3998, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3ebcecea588e5a6dfad865fc43732d14e1ac850c", - "etherscanUrl": "https://etherscan.io/address/0x3ebcecea588e5a6dfad865fc43732d14e1ac850c", - "xHandle": "awedjob", - "xUrl": "https://twitter.com/awedjob", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_445000", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "tonyrich.eth", - "displayName": "TONYFi", - "fid": 445000, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0e238c53-2e13-4b41-7865-8bc45fdab500/original", - "followers": 3991, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbd97296d6ba14382d861208ee8ff928f74da4ec3", - "etherscanUrl": "https://etherscan.io/address/0xbd97296d6ba14382d861208ee8ff928f74da4ec3", - "xHandle": "0xtony0", - "xUrl": "https://twitter.com/0xtony0", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_646397", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "trizsamae", - "displayName": "trizsamae.base.eth 🟦", - "fid": 646397, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a95f9e88-cbb1-4e2f-08b1-7e7931c18c00/original", - "followers": 3975, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5de65272846059efe0120f93e384bd5bcb738dcd", - "etherscanUrl": "https://etherscan.io/address/0x5de65272846059efe0120f93e384bd5bcb738dcd", - "xHandle": "cookiesntrizzzz", - "xUrl": "https://twitter.com/cookiesntrizzzz", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_509017", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "rashedur", - "displayName": "Rashed", - "fid": 509017, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ec5d4b38-dbff-4e9a-6dc4-e6bc87e45c00/original", - "followers": 3933, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x507efcd4ab8741f3609e626778929cf2ca3f4628", - "etherscanUrl": "https://etherscan.io/address/0x507efcd4ab8741f3609e626778929cf2ca3f4628", - "xHandle": "rashedur_", - "xUrl": "https://twitter.com/rashedur_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_486015", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "pornpen", - "displayName": "Pennalopy", - "fid": 486015, - "pfpUrl": "https://i.imgur.com/KQ7oIjL.jpg", - "followers": 3927, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xda632669b52ee9b6f5e250487e9cedfa1fb41a01", - "etherscanUrl": "https://etherscan.io/address/0xda632669b52ee9b6f5e250487e9cedfa1fb41a01", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_969231", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "xexcy", - "displayName": "xexcy", - "fid": 969231, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a8fffabb-b3ef-48f6-25c6-8be93aac8700/original", - "followers": 3910, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x18e4a4a077643e8742505fd83c6f1e485099d3d6", - "etherscanUrl": "https://etherscan.io/address/0x18e4a4a077643e8742505fd83c6f1e485099d3d6", - "xHandle": "xexcy1", - "xUrl": "https://twitter.com/xexcy1", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_296241", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "skrim", - "displayName": "Alexey", - "fid": 296241, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/55a5d10c-cedd-4747-dd52-8d5b238a7800/original", - "followers": 3814, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0b87a1216934d32252e7de4b5acfe036c4c8ee4a", - "etherscanUrl": "https://etherscan.io/address/0x0b87a1216934d32252e7de4b5acfe036c4c8ee4a", - "xHandle": "jeetmacdonald", - "xUrl": "https://twitter.com/jeetmacdonald", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_285604", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "shuk007.eth", - "displayName": "Shuk007.base.eth 🟦 🧬 🟧", - "fid": 285604, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9462bcfb-b99c-44dc-b2da-6f2f3b673200/original", - "followers": 3790, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3f356a894ff5f599e023864a6393d1e1c7d56c31", - "etherscanUrl": "https://etherscan.io/address/0x3f356a894ff5f599e023864a6393d1e1c7d56c31", - "xHandle": "shukrier0077", - "xUrl": "https://twitter.com/shukrier0077", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_419274", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "lyttrushin", - "displayName": "Dmitrii 🎩 ✚ ↻ ♡ ✎ ", - "fid": 419274, - "pfpUrl": "https://i.imgur.com/NWtnOkk.jpg", - "followers": 3750, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x00fa2c2f988983710076196385b035467dc6d914", - "etherscanUrl": "https://etherscan.io/address/0x00fa2c2f988983710076196385b035467dc6d914", - "xHandle": "mongtan17", - "xUrl": "https://twitter.com/mongtan17", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_449096", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "safail", - "displayName": "safail", - "fid": 449096, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ad5d76b7-27a3-459c-8447-8d2e36687b00/original", - "followers": 3744, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc9d53f2a9a2bb83b933b630480e4dbe1ce7e0c92", - "etherscanUrl": "https://etherscan.io/address/0xc9d53f2a9a2bb83b933b630480e4dbe1ce7e0c92", - "xHandle": "safailrashtbari", - "xUrl": "https://twitter.com/safailrashtbari", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_449044", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "pedramiix.eth", - "displayName": "Pedram🎩🍖🐹😇🧾", - "fid": 449044, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9330d80c-0712-4fb6-f0b1-901a158c5900/rectcrop3", - "followers": 3744, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6234ceccfeca398e0a68ca7d18638b9cdad1ea32", - "etherscanUrl": "https://etherscan.io/address/0x6234ceccfeca398e0a68ca7d18638b9cdad1ea32", - "xHandle": "pedramiixeth", - "xUrl": "https://twitter.com/pedramiixeth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_19711", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mary8992", - "displayName": "Mary", - "fid": 19711, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/06195910-860a-494f-4e05-962ef98b1800/original", - "followers": 3680, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfcca4bd5054287eb70602fad51051d93111a99a8", - "etherscanUrl": "https://etherscan.io/address/0xfcca4bd5054287eb70602fad51051d93111a99a8", - "xHandle": "ch_8992", - "xUrl": "https://twitter.com/ch_8992", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_309310", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "metamu", - "displayName": "MetaMu.base.eth", - "fid": 309310, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/278d61ce-b186-4821-d722-9b8a2cb6d200/original", - "followers": 3616, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x15e87052d69ad27532434e9fa6aac7f424fe0f55", - "etherscanUrl": "https://etherscan.io/address/0x15e87052d69ad27532434e9fa6aac7f424fe0f55", - "xHandle": "metam00", - "xUrl": "https://twitter.com/metam00", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_423487", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "onchaineth.base.eth", - "displayName": "ZUKA", - "fid": 423487, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3907f335-ac37-497a-f372-9a353e7bf400/original", - "followers": 3462, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4eb1be3ddb879243e1f0d50ea78ecba466af388f", - "etherscanUrl": "https://etherscan.io/address/0x4eb1be3ddb879243e1f0d50ea78ecba466af388f", - "xHandle": "ecosystem_3", - "xUrl": "https://twitter.com/ecosystem_3", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_380885", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cryp2romz.eth", - "displayName": "romz🎩🧡🟦 🧬", - "fid": 380885, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c76901e8-33c0-4e5b-155e-0d1188db7100/original", - "followers": 3455, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8e2913a92deb13d796e6d99d1471af3d2d4353ab", - "etherscanUrl": "https://etherscan.io/address/0x8e2913a92deb13d796e6d99d1471af3d2d4353ab", - "xHandle": "cryptobasurero7", - "xUrl": "https://twitter.com/cryptobasurero7", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_273791", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "315", - "displayName": "315", - "fid": 273791, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/90a1ef07-8883-4b7e-b46d-d6705d236700/original", - "followers": 3437, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9509ad9a025155857f9d788f34d30f52aa2db8ed", - "etherscanUrl": "https://etherscan.io/address/0x9509ad9a025155857f9d788f34d30f52aa2db8ed", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_510796", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "drrrner.eth", - "displayName": "D-wayñe", - "fid": 510796, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/44b1b236-fbd5-491c-f2c5-fd6b900c6400/original", - "followers": 3404, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6e9cbbc2de389e41db1fa7fed477bc1842fcf766", - "etherscanUrl": "https://etherscan.io/address/0x6e9cbbc2de389e41db1fa7fed477bc1842fcf766", - "xHandle": "mrjushenry", - "xUrl": "https://twitter.com/mrjushenry", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_5429", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "frankdegods", - "displayName": "Frank", - "fid": 5429, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/70cadf43-3f2d-4c1a-518c-0e9c2224fb00/original", - "followers": 3391, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6dc38c512753fe0181864bf98a94ee48506c219e", - "etherscanUrl": "https://etherscan.io/address/0x6dc38c512753fe0181864bf98a94ee48506c219e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_645888", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "akash2539", - "displayName": "Akash", - "fid": 645888, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/900eba00-89eb-4f36-d8f5-a581c112e700/rectcrop3", - "followers": 3383, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x46cfd33b9d326b7b1a56cfdc9445d47b8bdaac76", - "etherscanUrl": "https://etherscan.io/address/0x46cfd33b9d326b7b1a56cfdc9445d47b8bdaac76", - "xHandle": "bikash2539", - "xUrl": "https://twitter.com/bikash2539", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1050713", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "littleanjel", - "displayName": "Little Anjel 🕊️", - "fid": 1050713, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e210c095-0616-43e0-4f4c-bd2711ee5600/original", - "followers": 3330, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xffd439dd5c1594943f6946fac5e0c12d29b97138", - "etherscanUrl": "https://etherscan.io/address/0xffd439dd5c1594943f6946fac5e0c12d29b97138", - "xHandle": "mrxsubhan", - "xUrl": "https://twitter.com/mrxsubhan", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_502916", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "juvelir", - "displayName": "Juvelir. base.eth", - "fid": 502916, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/96575fd1-6ef4-48ac-1a0e-80d37372f600/rectcrop3", - "followers": 3220, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe7a6f2a51de5ae0c6ea17b5ec3c44cecbdf2dadf", - "etherscanUrl": "https://etherscan.io/address/0xe7a6f2a51de5ae0c6ea17b5ec3c44cecbdf2dadf", - "xHandle": "aleksandrblazev", - "xUrl": "https://twitter.com/aleksandrblazev", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_419545", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "eli-web3", - "displayName": "Eliyah", - "fid": 419545, - "pfpUrl": "https://i.imgur.com/2bmpAJJ.jpg", - "followers": 3154, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe97fe00a59a4f5320e79e60f99542e8637a39144", - "etherscanUrl": "https://etherscan.io/address/0xe97fe00a59a4f5320e79e60f99542e8637a39144", - "xHandle": "eliyah_web3", - "xUrl": "https://twitter.com/eliyah_web3", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_406432", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mehrans", - "displayName": "mehran", - "fid": 406432, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1f2eb481-d3c9-4a22-4a27-a043963e0500/rectcrop3", - "followers": 3129, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2dd104ad43fc84494eb40c27cc70a0f3861c19ca", - "etherscanUrl": "https://etherscan.io/address/0x2dd104ad43fc84494eb40c27cc70a0f3861c19ca", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_489158", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "asifbodla", - "displayName": "ASO AI 🤖", - "fid": 489158, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/197deef1-6c12-422f-3409-47b34bcbda00/rectcrop3", - "followers": 3101, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xeeb5ff0a1b2de2d2a19c9d2870ac5e7a33f96613", - "etherscanUrl": "https://etherscan.io/address/0xeeb5ff0a1b2de2d2a19c9d2870ac5e7a33f96613", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_21473", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "zanglee", - "displayName": "Zang 🎩", - "fid": 21473, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0644dd48-3d54-42b8-ad5e-88b0e7108000/original", - "followers": 3099, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1c4beb276654c4fa614fb87f9367ba6ef714140a", - "etherscanUrl": "https://etherscan.io/address/0x1c4beb276654c4fa614fb87f9367ba6ef714140a", - "xHandle": "zanglee1306", - "xUrl": "https://twitter.com/zanglee1306", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_20264", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "lobstermindset.eth", - "displayName": "lily", - "fid": 20264, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/8e478c98-40a3-4fa7-6431-eb135a9aac00/original", - "followers": 3055, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdf98cff357b473fa267c8e09ec967671ac65f0c1", - "etherscanUrl": "https://etherscan.io/address/0xdf98cff357b473fa267c8e09ec967671ac65f0c1", - "xHandle": "lobstermindset", - "xUrl": "https://twitter.com/lobstermindset", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_198116", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jimbocity", - "displayName": "jimbo.base.eth 🎩", - "fid": 198116, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1f7f50d4-e883-46f5-dd5a-692f44d9b100/original", - "followers": 3023, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9c2e009886fec1b9bd98741eefd669817d6fee79", - "etherscanUrl": "https://etherscan.io/address/0x9c2e009886fec1b9bd98741eefd669817d6fee79", - "xHandle": "jimbo_city", - "xUrl": "https://twitter.com/jimbo_city", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_196388", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "tom.base.eth", - "displayName": "tom", - "fid": 196388, - "pfpUrl": "https://tba-mobile.mypinata.cloud/ipfs/QmPfhHCNvL9Y89Zd57SovAion5rXi9bKxoiYVWdHZxZxNA?pinataGatewayToken=PMz6RFTDuk-300OttNnb_U0PSKbbXQdzLmUqdiEq7lesXcsVK8TK7S5GoOtxRGl2", - "followers": 3022, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2aaff4fa12c4606689ffbebeba8ce8ce28733766", - "etherscanUrl": "https://etherscan.io/address/0x2aaff4fa12c4606689ffbebeba8ce8ce28733766", - "xHandle": "neodaoist", - "xUrl": "https://twitter.com/neodaoist", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_999046", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mrbrick", - "displayName": "MrBrick.Base.Eth", - "fid": 999046, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/76776de9-e02a-4667-2d1b-e83dd5b5b400/original", - "followers": 3014, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x34acb0c4a25c49c1b1beddf5a78c9c90b5d85650", - "etherscanUrl": "https://etherscan.io/address/0x34acb0c4a25c49c1b1beddf5a78c9c90b5d85650", - "xHandle": "2kbricktv", - "xUrl": "https://twitter.com/2kbricktv", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_17538", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "tugzpaker.eth", - "displayName": "Ej", - "fid": 17538, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b41ab1f5-2796-4aec-32ab-da4558d7d400/original", - "followers": 2913, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe10474e432baa1931b1c088ce4446f18e1afe60e", - "etherscanUrl": "https://etherscan.io/address/0xe10474e432baa1931b1c088ce4446f18e1afe60e", - "xHandle": "aischub777", - "xUrl": "https://twitter.com/aischub777", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_310531", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "koolkheart.eth", - "displayName": "Koolkheart", - "fid": 310531, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ae184e83-4f97-4dc5-1a51-ea73d44a4700/original", - "followers": 2814, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5df895914305eea293e8eea65313b43e85aa5cad", - "etherscanUrl": "https://etherscan.io/address/0x5df895914305eea293e8eea65313b43e85aa5cad", - "xHandle": "shamaya_kent", - "xUrl": "https://twitter.com/shamaya_kent", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_973613", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kidyflames", - "displayName": "KîdyØffîcîal.base.eth 💙", - "fid": 973613, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/39666b97-3c17-435f-a05c-f242520e4100/original", - "followers": 2788, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x803dceba8321874ccfdd2af94c99676cf8d540cd", - "etherscanUrl": "https://etherscan.io/address/0x803dceba8321874ccfdd2af94c99676cf8d540cd", - "xHandle": "kidy_flames", - "xUrl": "https://twitter.com/kidy_flames", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_312016", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mikos32.eth", - "displayName": "Mikolaj", - "fid": 312016, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1e5da495-7766-414d-69b9-a9b95cdee700/original", - "followers": 2787, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0a94dad510eddc67df0553fcf99a32d55b9e3fbf", - "etherscanUrl": "https://etherscan.io/address/0x0a94dad510eddc67df0553fcf99a32d55b9e3fbf", - "xHandle": "mikolaj664108", - "xUrl": "https://twitter.com/mikolaj664108", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_277433", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0xmoti.eth", - "displayName": "moticaster", - "fid": 277433, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/67beada0-2184-4606-0b77-a6b71c94f100/original", - "followers": 2783, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x463dfcbf3b88f3330646648e185475f98235c74f", - "etherscanUrl": "https://etherscan.io/address/0x463dfcbf3b88f3330646648e185475f98235c74f", - "xHandle": "0xmoti", - "xUrl": "https://twitter.com/0xmoti", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_526510", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mariabazooka", - "displayName": "Maria Bazooka 💜🎩🟪", - "fid": 526510, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1fc505aa-3eca-4ab1-dee2-9c4e3afc8800/rectcrop3", - "followers": 2777, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x47e37b2a03e2e09df1c12d0bf5bc0774b258c148", - "etherscanUrl": "https://etherscan.io/address/0x47e37b2a03e2e09df1c12d0bf5bc0774b258c148", - "xHandle": "mariabazooka", - "xUrl": "https://twitter.com/mariabazooka", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_511842", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "aroosh", - "displayName": "Aroosh 🧡🎩", - "fid": 511842, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/bec64b45-f292-469c-8287-7f86ba2b6d00/original", - "followers": 2773, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x835d482629c8cdb6e243e17b07cf02b1dcd34fde", - "etherscanUrl": "https://etherscan.io/address/0x835d482629c8cdb6e243e17b07cf02b1dcd34fde", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_299583", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nesar", - "displayName": "Md Neser \"base.eth\"", - "fid": 299583, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/8c49fbe8-c4e6-4722-458a-219bffe5b100/rectcrop3", - "followers": 2701, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x80240627034907835878f43feb1bb686d1d24f3e", - "etherscanUrl": "https://etherscan.io/address/0x80240627034907835878f43feb1bb686d1d24f3e", - "xHandle": "kznesar", - "xUrl": "https://twitter.com/kznesar", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_11205", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "branksypop", - "displayName": "Branksy Pop", - "fid": 11205, - "pfpUrl": "https://i.seadn.io/gcs/files/0109ca4c5232db856eb995cb4b38c462.gif?w=500&auto=format", - "followers": 2700, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa6c50dd67ad4df406974a597be7054c35531e4fb", - "etherscanUrl": "https://etherscan.io/address/0xa6c50dd67ad4df406974a597be7054c35531e4fb", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_13939", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nftgo", - "displayName": "Rad.base.eth🎩🎭🔮🐹🟪", - "fid": 13939, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/faff9ef9-0ea1-453a-3b66-2ed08d300d00/original", - "followers": 2691, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf14672ddd7279c1357f54b0b20799eeff14ce0d2", - "etherscanUrl": "https://etherscan.io/address/0xf14672ddd7279c1357f54b0b20799eeff14ce0d2", - "xHandle": "hradman4", - "xUrl": "https://twitter.com/hradman4", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_271946", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kindkknd", - "displayName": "Kknd🎩🍖", - "fid": 271946, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/af847e66-2538-4005-950f-0ca90d1b9900/original", - "followers": 2684, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xac8a6f469adaf40a8c557d10cbc7fb857e86417c", - "etherscanUrl": "https://etherscan.io/address/0xac8a6f469adaf40a8c557d10cbc7fb857e86417c", - "xHandle": "kindkknd", - "xUrl": "https://twitter.com/kindkknd", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_378416", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "aminphantom.eth", - "displayName": "aminphantom.base.eth", - "fid": 378416, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e411744e-4610-4de9-1ef6-e5c78d675a00/rectcrop3", - "followers": 2630, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc8be97914211c3d3bc618bcfd3b6dccaa161b5dc", - "etherscanUrl": "https://etherscan.io/address/0xc8be97914211c3d3bc618bcfd3b6dccaa161b5dc", - "xHandle": "amintabata", - "xUrl": "https://twitter.com/amintabata", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_243907", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "banterlytics.base.eth", - "displayName": "Banterlytics", - "fid": 243907, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0406c7b8-f004-49ed-8719-eb33eaa56100/original", - "followers": 2624, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb7095a95f42c3d3f598cc956049cc5811986435e", - "etherscanUrl": "https://etherscan.io/address/0xb7095a95f42c3d3f598cc956049cc5811986435e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1078524", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "lollipop001.base.eth", - "displayName": "Biglolli", - "fid": 1078524, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/97ab8bc0-4e05-40bd-48a1-cebe0cd15200/original", - "followers": 2606, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x201c8dfdf70d579106b5e9e387774dd2133c103f", - "etherscanUrl": "https://etherscan.io/address/0x201c8dfdf70d579106b5e9e387774dd2133c103f", - "xHandle": "iam_lollipop023", - "xUrl": "https://twitter.com/iam_lollipop023", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_291302", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ajrony", - "displayName": "ajrony.base.eth 🔵🧬", - "fid": 291302, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2cf23c12-6831-4071-6b9d-53e215b30b00/original", - "followers": 2564, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x43f8c3e0c0ab23f0a8660f3ea3ce1a7016f96a33", - "etherscanUrl": "https://etherscan.io/address/0x43f8c3e0c0ab23f0a8660f3ea3ce1a7016f96a33", - "xHandle": "ajrony2", - "xUrl": "https://twitter.com/ajrony2", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_277636", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "hustletrees.base.eth", - "displayName": "hustletrees 🎩🐹🔵", - "fid": 277636, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ca13487c-fc33-4346-9b07-88bee4fce100/original", - "followers": 2534, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa09c2b4c50d70664ff1386cac6bc3696674f3605", - "etherscanUrl": "https://etherscan.io/address/0xa09c2b4c50d70664ff1386cac6bc3696674f3605", - "xHandle": "lfgweb3", - "xUrl": "https://twitter.com/lfgweb3", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1075468", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "warptern", - "displayName": "Intern", - "fid": 1075468, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9e56a16d-dcad-4535-5334-b17c0167c700/original", - "followers": 2529, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe4836f9775feb2b46d6cccf831255ab8e6937d26", - "etherscanUrl": "https://etherscan.io/address/0xe4836f9775feb2b46d6cccf831255ab8e6937d26", - "xHandle": "farcasterintern", - "xUrl": "https://twitter.com/farcasterintern", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_7472", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "goldie", - "displayName": "Goldie 🧟‍♂️", - "fid": 7472, - "pfpUrl": "https://tba-mobile.mypinata.cloud/ipfs/QmeAzxCBV2JhqKoNzt5xBKyi5JkJJdoyHKZuTpNxzFNwpG?pinataGatewayToken=PMz6RFTDuk-300OttNnb_U0PSKbbXQdzLmUqdiEq7lesXcsVK8TK7S5GoOtxRGl2", - "followers": 2523, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcbbcf40247425932d728585da2678567e7f8e42f", - "etherscanUrl": "https://etherscan.io/address/0xcbbcf40247425932d728585da2678567e7f8e42f", - "xHandle": "goldiesnftart", - "xUrl": "https://twitter.com/goldiesnftart", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_347833", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "liwlimuz", - "displayName": "Jonathan 🎩", - "fid": 347833, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7a67a620-3a48-4f83-622f-837f7e92c600/original", - "followers": 2512, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x287ad2a3a8b40d443423faf7734c838ece844b1c", - "etherscanUrl": "https://etherscan.io/address/0x287ad2a3a8b40d443423faf7734c838ece844b1c", - "xHandle": "liwlimuz", - "xUrl": "https://twitter.com/liwlimuz", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_300839", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0xlauraopami", - "displayName": "Opami Laura", - "fid": 300839, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/71b9fbf9-b87a-417d-932d-e414d68ca100/original", - "followers": 2480, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x087c2f2a62c10162885c4c799ad28b5b3e049cb6", - "etherscanUrl": "https://etherscan.io/address/0x087c2f2a62c10162885c4c799ad28b5b3e049cb6", - "xHandle": "120pure", - "xUrl": "https://twitter.com/120pure", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_364615", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "yanga", - "displayName": "Yangaobong.base.eth 🟦 🎩", - "fid": 364615, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c6a27e54-8786-467d-a65b-d5c66bcf8f00/original", - "followers": 2473, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x74441b0bad007b2fd8fb22ad72a1a1db164d8af5", - "etherscanUrl": "https://etherscan.io/address/0x74441b0bad007b2fd8fb22ad72a1a1db164d8af5", - "xHandle": "yangaarts", - "xUrl": "https://twitter.com/yangaarts", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_625770", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bq110", - "displayName": "Shining Star", - "fid": 625770, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3707ee8e-d54f-49ca-748c-9a021c0c1c00/rectcrop3", - "followers": 2439, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3e0a37916e5180ba0cde4af797aa255a4156f0d7", - "etherscanUrl": "https://etherscan.io/address/0x3e0a37916e5180ba0cde4af797aa255a4156f0d7", - "xHandle": "afzaalsyed31652", - "xUrl": "https://twitter.com/afzaalsyed31652", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_417851", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "tracyit", - "displayName": "Tracyit 🫧", - "fid": 417851, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3a4632b4-626b-4725-e66c-c5e2193c9000/original", - "followers": 2378, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x98f047fce42c8689f6ea231e24fe2d394cce7958", - "etherscanUrl": "https://etherscan.io/address/0x98f047fce42c8689f6ea231e24fe2d394cce7958", - "xHandle": "tracyit99", - "xUrl": "https://twitter.com/tracyit99", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_238814", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sardius", - "displayName": "sardius.eth", - "fid": 238814, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/457cb277-cd9b-4fc9-7837-4efc3fa22300/original", - "followers": 2348, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x626522b58b92daf53596f1378bd25b7653c1fc49", - "etherscanUrl": "https://etherscan.io/address/0x626522b58b92daf53596f1378bd25b7653c1fc49", - "xHandle": "0xsardius", - "xUrl": "https://twitter.com/0xsardius", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_262469", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ziyafat9183", - "displayName": "Ziyafat🎩🔥⚡", - "fid": 262469, - "pfpUrl": "https://i.imgur.com/NafNfyg.jpg", - "followers": 2308, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4712fd7ced13e9a2fb38db08f8daeace973c49aa", - "etherscanUrl": "https://etherscan.io/address/0x4712fd7ced13e9a2fb38db08f8daeace973c49aa", - "xHandle": "ziyafat4", - "xUrl": "https://twitter.com/ziyafat4", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_481769", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "iwo-yalda-880", - "displayName": "🌐 Sohrab 🌐", - "fid": 481769, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/98c6729c-d9f5-4dc6-9161-3bc54f022300/rectcrop3", - "followers": 2260, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbbf0cba693003c175e87520556787d5454991d6a", - "etherscanUrl": "https://etherscan.io/address/0xbbf0cba693003c175e87520556787d5454991d6a", - "xHandle": "sohrab_k_60", - "xUrl": "https://twitter.com/sohrab_k_60", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_400462", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "raselhossain", - "displayName": "Rasel Hossain", - "fid": 400462, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e575b705-20a8-4ad6-404c-6f64d4a65f00/original", - "followers": 2253, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa62d1b97afd0e9dc7dfbc59754384d744a5c2bc3", - "etherscanUrl": "https://etherscan.io/address/0xa62d1b97afd0e9dc7dfbc59754384d744a5c2bc3", - "xHandle": "raselasiku", - "xUrl": "https://twitter.com/raselasiku", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_298406", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sajiiiiiiii8", - "displayName": "SΛJΛD", - "fid": 298406, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c74a954a-951b-4fcd-5c9e-191fd5edb100/original", - "followers": 2251, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x31424317a37e8b2f57ba98555b7613a36ff4c9ba", - "etherscanUrl": "https://etherscan.io/address/0x31424317a37e8b2f57ba98555b7613a36ff4c9ba", - "xHandle": "sajiiiiiiii8", - "xUrl": "https://twitter.com/sajiiiiiiii8", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1362424", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "grkx", - "displayName": "GRK", - "fid": 1362424, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/bace7a53-c228-4015-be32-d467d9a98800/original", - "followers": 2234, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x818bbff1815ed8010dcb6b4c720dddc350862506", - "etherscanUrl": "https://etherscan.io/address/0x818bbff1815ed8010dcb6b4c720dddc350862506", - "xHandle": "grkzgrk", - "xUrl": "https://twitter.com/grkzgrk", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_15129", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "violet", - "displayName": "Nazii 🧬", - "fid": 15129, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6792e0ba-473c-4951-cac6-6440973af900/original", - "followers": 2232, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1611b8532f5976004dab91aa1d0a814542cd9452", - "etherscanUrl": "https://etherscan.io/address/0x1611b8532f5976004dab91aa1d0a814542cd9452", - "xHandle": "violet_dev_", - "xUrl": "https://twitter.com/violet_dev_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_859081", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "joybrishti", - "displayName": "Myza", - "fid": 859081, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/33226680-eccc-4607-cecc-9d1fcc867200/rectcrop3", - "followers": 2227, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x75d51c48dfb380960f4fb7bb2465f134c2890efc", - "etherscanUrl": "https://etherscan.io/address/0x75d51c48dfb380960f4fb7bb2465f134c2890efc", - "xHandle": "joybrishti1446", - "xUrl": "https://twitter.com/joybrishti1446", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_240932", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "monteluna", - "displayName": "Monteluna", - "fid": 240932, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7c38549f-874e-4027-5f24-09c454ad6500/original", - "followers": 2219, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xde54fccaa9bec57ddb3cae72a6bcd37c7ee535e1", - "etherscanUrl": "https://etherscan.io/address/0xde54fccaa9bec57ddb3cae72a6bcd37c7ee535e1", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_897406", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "outflow.eth", - "displayName": "Flow", - "fid": 897406, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2add1c9c-8eb7-401c-4106-3ffad084a700/original", - "followers": 2214, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe849355272d614a00cfe10be992cae945d58a5a5", - "etherscanUrl": "https://etherscan.io/address/0xe849355272d614a00cfe10be992cae945d58a5a5", - "xHandle": "goblen_eth", - "xUrl": "https://twitter.com/goblen_eth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_235900", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sepuhh", - "displayName": "zaz 🧲", - "fid": 235900, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/66f62a8a-6f0e-4ebe-a160-85ebf6a13800/original", - "followers": 2180, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc1bbc69f2867ceb2f8813ffc056dbb0bd63c2320", - "etherscanUrl": "https://etherscan.io/address/0xc1bbc69f2867ceb2f8813ffc056dbb0bd63c2320", - "xHandle": "zazalpha", - "xUrl": "https://twitter.com/zazalpha", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_511674", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "yulius7602", - "displayName": "Iyus 🧬", - "fid": 511674, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/bce3d7c8-ece7-49c2-6a6c-9258ee187700/original", - "followers": 2171, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xecfd31add12f4576065b7fd4ecb725250bac2027", - "etherscanUrl": "https://etherscan.io/address/0xecfd31add12f4576065b7fd4ecb725250bac2027", - "xHandle": "arilyone", - "xUrl": "https://twitter.com/arilyone", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1020660", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "milanyia", - "displayName": "Milanyia", - "fid": 1020660, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2a48338b-acda-407b-7a19-b8d51a807700/original", - "followers": 2170, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfd9136fcaddc3b397913b154ec7d753463594576", - "etherscanUrl": "https://etherscan.io/address/0xfd9136fcaddc3b397913b154ec7d753463594576", - "xHandle": "milka1994151333", - "xUrl": "https://twitter.com/milka1994151333", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_18908", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mpryor.eth", - "displayName": "‎", - "fid": 18908, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/844ec0f5-a457-47ca-cd26-e48abb427100/original", - "followers": 2149, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x67493cf7e4e91649fa4e845b78cbc79a50e7b1f7", - "etherscanUrl": "https://etherscan.io/address/0x67493cf7e4e91649fa4e845b78cbc79a50e7b1f7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1042494", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "babartos.base.eth", - "displayName": "BARBATOS", - "fid": 1042494, - "pfpUrl": "https://res.cloudinary.com/base-app/image/upload/f_auto/v1761714024/Screenshot_20251029-120001_Base.jpg.jpg", - "followers": 2118, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x09c69e86ff85fb68c6ddbdcc4ab7cd2a3612e72d", - "etherscanUrl": "https://etherscan.io/address/0x09c69e86ff85fb68c6ddbdcc4ab7cd2a3612e72d", - "xHandle": "honzaido", - "xUrl": "https://twitter.com/honzaido", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_853066", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "suave1010", - "displayName": "Suave🎩 🍖", - "fid": 853066, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/27caadd8-86d8-405b-d16f-d06c83260800/original", - "followers": 2014, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6657c43021c9d3d488d89ceaf37bd53436250fe2", - "etherscanUrl": "https://etherscan.io/address/0x6657c43021c9d3d488d89ceaf37bd53436250fe2", - "xHandle": "suavecito1010", - "xUrl": "https://twitter.com/suavecito1010", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_267076", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jimboy", - "displayName": "Javad 🔵 base.eth", - "fid": 267076, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/963a8e0c-d15d-4c28-817d-3b044d644100/rectcrop3", - "followers": 2013, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0d6cb63a4cedf4fd852b69b2d694b3d6810f70eb", - "etherscanUrl": "https://etherscan.io/address/0x0d6cb63a4cedf4fd852b69b2d694b3d6810f70eb", - "xHandle": "salazar02222", - "xUrl": "https://twitter.com/salazar02222", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1042077", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "substack", - "displayName": "substack", - "fid": 1042077, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e97624c9-4ee4-46cf-bdd0-7c8209c9c800/original", - "followers": 2004, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xee3526124b1422db388df8612581a95d2496049d", - "etherscanUrl": "https://etherscan.io/address/0xee3526124b1422db388df8612581a95d2496049d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_17223", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kindness", - "displayName": "Kindnesss.eth 🌐", - "fid": 17223, - "pfpUrl": "https://i.imgur.com/RR1m9G2.gif", - "followers": 1997, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8dadfa79f0ea5b691def58d99c4527839ca0f75b", - "etherscanUrl": "https://etherscan.io/address/0x8dadfa79f0ea5b691def58d99c4527839ca0f75b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1049184", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "biggydaddy", - "displayName": "Biggydaddy.eth", - "fid": 1049184, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a6119ca0-a9c8-42e7-0a18-d0871fc6bc00/original", - "followers": 1995, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb53338891cb3b6d8c45a5683ee51eac93fb5e987", - "etherscanUrl": "https://etherscan.io/address/0xb53338891cb3b6d8c45a5683ee51eac93fb5e987", - "xHandle": "biggydaddy01", - "xUrl": "https://twitter.com/biggydaddy01", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_849116", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "uniquebeing404", - "displayName": "Uniquebeing.base.eth 👨‍💻", - "fid": 849116, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e1d313d0-f16a-4ee6-08af-3a096d30e100/original", - "followers": 1995, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd6b69e58d44e523eb58645f1b78425c96dfa648c", - "etherscanUrl": "https://etherscan.io/address/0xd6b69e58d44e523eb58645f1b78425c96dfa648c", - "xHandle": "uniquebeing404", - "xUrl": "https://twitter.com/uniquebeing404", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1050410", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "madnet90", - "displayName": "Karol", - "fid": 1050410, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/694bfedc-c75d-4e43-a96e-3f15e3712000/rectcrop3", - "followers": 1968, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xae51b3d7d2a3ce9ce2f57ddabec5a28a97d626ec", - "etherscanUrl": "https://etherscan.io/address/0xae51b3d7d2a3ce9ce2f57ddabec5a28a97d626ec", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_461523", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "hyouka", - "displayName": "Hyouka", - "fid": 461523, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e17526df-14b3-419c-4ed7-fc9f9c901b00/rectcrop3", - "followers": 1958, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x238de5066e5ee72f633811eefa338354b5940087", - "etherscanUrl": "https://etherscan.io/address/0x238de5066e5ee72f633811eefa338354b5940087", - "xHandle": "cryptobtcsats", - "xUrl": "https://twitter.com/cryptobtcsats", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_7681", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "smr18.eth", - "displayName": "smr18", - "fid": 7681, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ca43c1fd-f1fd-450a-1d6c-19f28faa2a00/rectcrop3", - "followers": 1949, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x285e093e334a4ad3d1f37c5e8f8b5761ed0cf1f7", - "etherscanUrl": "https://etherscan.io/address/0x285e093e334a4ad3d1f37c5e8f8b5761ed0cf1f7", - "xHandle": "audfosmr18", - "xUrl": "https://twitter.com/audfosmr18", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_260388", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cryptoprofitjp", - "displayName": "🦑", - "fid": 260388, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4057d5d4-a4b2-4713-6787-d558b478de00/original", - "followers": 1895, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x29f42a382607527d74a4d78b5866764da1fdb0bf", - "etherscanUrl": "https://etherscan.io/address/0x29f42a382607527d74a4d78b5866764da1fdb0bf", - "xHandle": "ndesipp", - "xUrl": "https://twitter.com/ndesipp", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_283718", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "wpwhite.eth", - "displayName": "wpwhite.base.eth", - "fid": 283718, - "pfpUrl": "https://i.imgur.com/s095brZ.jpg", - "followers": 1886, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x74f25777ed6061f30157742a651f28df979734d7", - "etherscanUrl": "https://etherscan.io/address/0x74f25777ed6061f30157742a651f28df979734d7", - "xHandle": "orqlx", - "xUrl": "https://twitter.com/orqlx", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_424529", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kmmaruf65", - "displayName": "Kisuke Urahara 🎭", - "fid": 424529, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c999b515-48f4-4890-d69b-49afeab7e700/original", - "followers": 1877, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdf6e24153488e8db9d5abfb20a367489014ac221", - "etherscanUrl": "https://etherscan.io/address/0xdf6e24153488e8db9d5abfb20a367489014ac221", - "xHandle": "kmmaruf65", - "xUrl": "https://twitter.com/kmmaruf65", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_188259", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "a1z2", - "displayName": "$a1z2", - "fid": 188259, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2f20929e-249f-4863-7b47-6569ea4cfc00/original", - "followers": 1845, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc8fd1c1a9648a23140b95fe54ce403e28b4ea050", - "etherscanUrl": "https://etherscan.io/address/0xc8fd1c1a9648a23140b95fe54ce403e28b4ea050", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_528404", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "farabish", - "displayName": "Farabi", - "fid": 528404, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c5cf9434-68cb-47ca-0ef9-960a7afe5d00/original", - "followers": 1838, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x733b5235d16611efb64a04c0a5c9258e3c6a4e23", - "etherscanUrl": "https://etherscan.io/address/0x733b5235d16611efb64a04c0a5c9258e3c6a4e23", - "xHandle": "shakil11farabi", - "xUrl": "https://twitter.com/shakil11farabi", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_935829", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cryptokot", - "displayName": "Cryptokot", - "fid": 935829, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3f9bb65b-5501-4ed0-b58c-e01f74417800/original", - "followers": 1835, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf981ec0d90de8eaf224a2e7da4067ce42a550f1a", - "etherscanUrl": "https://etherscan.io/address/0xf981ec0d90de8eaf224a2e7da4067ce42a550f1a", - "xHandle": "crypt0cat", - "xUrl": "https://twitter.com/crypt0cat", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_251269", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0xjoppy", - "displayName": "malestbanget.base.eth 🧬", - "fid": 251269, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4181218a-88b9-4df9-36b9-ffe184a4ce00/original", - "followers": 1820, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5966304752f58a95d1d810b56d33ba3bea2147cf", - "etherscanUrl": "https://etherscan.io/address/0x5966304752f58a95d1d810b56d33ba3bea2147cf", - "xHandle": "0xjoppy", - "xUrl": "https://twitter.com/0xjoppy", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_5142", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mrbriandesign", - "displayName": "llbxdll", - "fid": 5142, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/8fc36c7c-2a9b-49df-d91b-a7093c57de00/original", - "followers": 1815, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x66ab779a9802021fec2d5635c0503c9d63d5ed58", - "etherscanUrl": "https://etherscan.io/address/0x66ab779a9802021fec2d5635c0503c9d63d5ed58", - "xHandle": "mrbriandesign", - "xUrl": "https://twitter.com/mrbriandesign", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_14905", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "pasha", - "displayName": "Pasha base.eth", - "fid": 14905, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e1e67c50-9ae3-4d38-1f4f-7a91ce14b800/rectcrop3", - "followers": 1772, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc4d9857937e5a3160614eb432f3f5481001ac3c5", - "etherscanUrl": "https://etherscan.io/address/0xc4d9857937e5a3160614eb432f3f5481001ac3c5", - "xHandle": "tu_khobi", - "xUrl": "https://twitter.com/tu_khobi", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_15380", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "s0tric.eth", - "displayName": "s0tric 🦠", - "fid": 15380, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/71e4c48e-2bec-4f82-509c-fa41b387d700/rectcrop3", - "followers": 1768, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9beadc1297f97fff916a487e49dea1004510c59e", - "etherscanUrl": "https://etherscan.io/address/0x9beadc1297f97fff916a487e49dea1004510c59e", - "xHandle": "s0tric", - "xUrl": "https://twitter.com/s0tric", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_16340", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "soyboy", - "displayName": "soyboy", - "fid": 16340, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6d20e73b-6cae-4052-8474-86a09928e600/original", - "followers": 1764, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd8c733886f57e98590800d75795817887dfe8b1d", - "etherscanUrl": "https://etherscan.io/address/0xd8c733886f57e98590800d75795817887dfe8b1d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_280191", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "vanakkam", - "displayName": "vanakkam 🛡️🔥", - "fid": 280191, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/efde9223-e9e1-4a23-d0cb-879dd7f04c00/original", - "followers": 1756, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9bc917c1c633e82586f27eb2372384a69ced92d7", - "etherscanUrl": "https://etherscan.io/address/0x9bc917c1c633e82586f27eb2372384a69ced92d7", - "xHandle": "thiyagarajansi", - "xUrl": "https://twitter.com/thiyagarajansi", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_372091", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "manocheer", - "displayName": "manocheer", - "fid": 372091, - "pfpUrl": "https://i.imgur.com/XZtsOWh.png", - "followers": 1753, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x012efd9feeaa8b7e1edf4af9f5fff4b1d9431a7d", - "etherscanUrl": "https://etherscan.io/address/0x012efd9feeaa8b7e1edf4af9f5fff4b1d9431a7d", - "xHandle": "manocheer", - "xUrl": "https://twitter.com/manocheer", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_2928", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jbird", - "displayName": "jesse", - "fid": 2928, - "pfpUrl": "https://i.imgur.com/w3mf27t.png", - "followers": 1746, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2db90e08dde38b7873e63eb7aed46ea3eded81c9", - "etherscanUrl": "https://etherscan.io/address/0x2db90e08dde38b7873e63eb7aed46ea3eded81c9", - "xHandle": "sudojbird", - "xUrl": "https://twitter.com/sudojbird", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_282926", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "snrcaptain", - "displayName": "snrcaptain.base.eth 🎩🟧", - "fid": 282926, - "pfpUrl": "https://i.imgur.com/G2DbhLZ.jpg", - "followers": 1738, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xee8929eef238b60458f5d4ff4be8374fc1671719", - "etherscanUrl": "https://etherscan.io/address/0xee8929eef238b60458f5d4ff4be8374fc1671719", - "xHandle": "snrcaptain2", - "xUrl": "https://twitter.com/snrcaptain2", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_438822", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kablat.eth", - "displayName": "HaMid🍖🎩", - "fid": 438822, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9ae27fad-93a3-4e77-baba-460d17ab7100/original", - "followers": 1733, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x197e2a017969dd7972a0a28ffce67234beafef32", - "etherscanUrl": "https://etherscan.io/address/0x197e2a017969dd7972a0a28ffce67234beafef32", - "xHandle": "hamidkabir0", - "xUrl": "https://twitter.com/hamidkabir0", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_223277", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "deefs", - "displayName": "Beaver", - "fid": 223277, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0971e2bd-322f-49ee-6116-61f6e70cf400/original", - "followers": 1724, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x96a4a274357a6038b3162da2cd29e2338f048f42", - "etherscanUrl": "https://etherscan.io/address/0x96a4a274357a6038b3162da2cd29e2338f048f42", - "xHandle": "trilliumgaming", - "xUrl": "https://twitter.com/trilliumgaming", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_334971", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "hoangf", - "displayName": "hoangf.base.eth", - "fid": 334971, - "pfpUrl": "https://i.imgur.com/IRP3GvU.jpg", - "followers": 1720, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1f6f8204aac8d697aa6ec20bedc2c293ab3929a1", - "etherscanUrl": "https://etherscan.io/address/0x1f6f8204aac8d697aa6ec20bedc2c293ab3929a1", - "xHandle": "tranhoangh49", - "xUrl": "https://twitter.com/tranhoangh49", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1118592", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "keturahm", - "displayName": "Keturah Mamman", - "fid": 1118592, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/67ed73b6-b94f-40b8-70a5-b2548ed3ed00/original", - "followers": 1715, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x26f2de6be4b13df8b96e7a1a013a1e53b6e5373a", - "etherscanUrl": "https://etherscan.io/address/0x26f2de6be4b13df8b96e7a1a013a1e53b6e5373a", - "xHandle": "mamman57631", - "xUrl": "https://twitter.com/mamman57631", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_982331", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "gkash", - "displayName": "Only1Gkash", - "fid": 982331, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2a11c800-a24c-49c5-6b3f-136b700f5600/rectcrop3", - "followers": 1707, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe12095e92d9d4d2bee93c81ba355e341e591109d", - "etherscanUrl": "https://etherscan.io/address/0xe12095e92d9d4d2bee93c81ba355e341e591109d", - "xHandle": "only1gkash", - "xUrl": "https://twitter.com/only1gkash", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_493333", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "trongnghiasky", - "displayName": "Johny", - "fid": 493333, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b3f8eaeb-db20-4105-31ad-166790103300/original", - "followers": 1665, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf6a3395f8949568d790f442658c22f8b8e82691a", - "etherscanUrl": "https://etherscan.io/address/0xf6a3395f8949568d790f442658c22f8b8e82691a", - "xHandle": "riley_wood61497", - "xUrl": "https://twitter.com/riley_wood61497", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_230185", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "rostyslavbortman.eth", - "displayName": "rostyk.eth", - "fid": 230185, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f24dd593-6d8e-499b-b3a6-6b202488a700/original", - "followers": 1631, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x17f3c6ef15395a641ac0695b08f0a1eec257f48b", - "etherscanUrl": "https://etherscan.io/address/0x17f3c6ef15395a641ac0695b08f0a1eec257f48b", - "xHandle": "rostyketh", - "xUrl": "https://twitter.com/rostyketh", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_388423", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "skillabsc", - "displayName": "Skillabsc 🧬", - "fid": 388423, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/236006aa-f694-4fb5-ae41-e99e752e4200/original", - "followers": 1627, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7dcff286f8e45b9a5c31bb990c0007d4ee5f7ec0", - "etherscanUrl": "https://etherscan.io/address/0x7dcff286f8e45b9a5c31bb990c0007d4ee5f7ec0", - "xHandle": "cryptoofclown", - "xUrl": "https://twitter.com/cryptoofclown", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_221326", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "vlad-imir", - "displayName": "Vladimir", - "fid": 221326, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/cea9cc05-78e0-4a73-79b5-2d7fc3707200/rectcrop3", - "followers": 1614, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x519416e3ad0920f49655e4ad2e1c91d568b67399", - "etherscanUrl": "https://etherscan.io/address/0x519416e3ad0920f49655e4ad2e1c91d568b67399", - "xHandle": "vladimir_fren3", - "xUrl": "https://twitter.com/vladimir_fren3", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1278092", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "threeb", - "displayName": "bahriyeli.base.eth", - "fid": 1278092, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b35a7330-dbce-4dae-697b-8000e1e70400/original", - "followers": 1609, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcd918d8599bb129b38bd113da1ae6f2024c1526e", - "etherscanUrl": "https://etherscan.io/address/0xcd918d8599bb129b38bd113da1ae6f2024c1526e", - "xHandle": "threeb__", - "xUrl": "https://twitter.com/threeb__", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_866318", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "raisulislam.eth", - "displayName": "raisulislam.eth", - "fid": 866318, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/fd7084ab-ea1d-4fc0-3767-cb21a9bbb900/original", - "followers": 1602, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x73f88cf1dab60c7277389e0a7a8fbe8bcf809003", - "etherscanUrl": "https://etherscan.io/address/0x73f88cf1dab60c7277389e0a7a8fbe8bcf809003", - "xHandle": "gaming_rai79094", - "xUrl": "https://twitter.com/gaming_rai79094", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_415400", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ramina13", - "displayName": "Marina Ya", - "fid": 415400, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5104cc7e-2c3f-4826-5498-e86684f5ac00/original", - "followers": 1592, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xacd9406ea14545354a07d9211e2abe9e610b999c", - "etherscanUrl": "https://etherscan.io/address/0xacd9406ea14545354a07d9211e2abe9e610b999c", - "xHandle": "_ramina13_", - "xUrl": "https://twitter.com/_ramina13_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_265439", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "universaldada", - "displayName": "universaldada.base.eth", - "fid": 265439, - "pfpUrl": "https://i.imgur.com/am7pNWp.jpg", - "followers": 1588, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x52db85d7bf7ea89d441286acab36b249e199f19c", - "etherscanUrl": "https://etherscan.io/address/0x52db85d7bf7ea89d441286acab36b249e199f19c", - "xHandle": "uvdada2020", - "xUrl": "https://twitter.com/uvdada2020", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_300589", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nearxiii", - "displayName": "Nearxiii.sat⚡️", - "fid": 300589, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/edd45e2b-27b3-429e-3717-a991509afa00/rectcrop3", - "followers": 1575, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x91a70261884daae78e56a406d45d5c9a913c627d", - "etherscanUrl": "https://etherscan.io/address/0x91a70261884daae78e56a406d45d5c9a913c627d", - "xHandle": "nearxiii", - "xUrl": "https://twitter.com/nearxiii", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_286431", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "20220408.eth", - "displayName": "Natcat🎩❤️", - "fid": 286431, - "pfpUrl": "https://i.imgur.com/FS0RwEB.jpg", - "followers": 1554, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0fc7332a78b63a15256a2a881da78efbb1438b45", - "etherscanUrl": "https://etherscan.io/address/0x0fc7332a78b63a15256a2a881da78efbb1438b45", - "xHandle": "ghmtoxcl1kli7rh", - "xUrl": "https://twitter.com/ghmtoxcl1kli7rh", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_421645", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "btcwin", - "displayName": "Btcwin ", - "fid": 421645, - "pfpUrl": "https://i.imgur.com/EUrwYtH.jpg", - "followers": 1528, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4bbc56a80a0cd165f7f622f7b1f2451c8160515d", - "etherscanUrl": "https://etherscan.io/address/0x4bbc56a80a0cd165f7f622f7b1f2451c8160515d", - "xHandle": "bfueiwwk13", - "xUrl": "https://twitter.com/bfueiwwk13", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1390332", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "tradingzone.base.eth", - "displayName": "BRO ON FARCASTER 🧢", - "fid": 1390332, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a0b8e912-f92c-46d3-17c0-716beaf89500/original", - "followers": 1521, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x21673ff674aa6cb7dbd30cd0e657ac8426e82965", - "etherscanUrl": "https://etherscan.io/address/0x21673ff674aa6cb7dbd30cd0e657ac8426e82965", - "xHandle": "mdabdullalnayem", - "xUrl": "https://twitter.com/mdabdullalnayem", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_8425", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "niknameste.eth", - "displayName": "niknameste", - "fid": 8425, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f2b77f38-e629-4b9d-39b0-372d26a8c900/original", - "followers": 1510, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x62e2723f64298b493192e6a2cf01c70f9b8f037d", - "etherscanUrl": "https://etherscan.io/address/0x62e2723f64298b493192e6a2cf01c70f9b8f037d", - "xHandle": "niknameste", - "xUrl": "https://twitter.com/niknameste", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_188299", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "katseye.eth", - "displayName": "Lucky🎩", - "fid": 188299, - "pfpUrl": "https://i.imgur.com/EIqkxnG.png", - "followers": 1504, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x76786551c9eb05d1e6cbd156004439184270d8a4", - "etherscanUrl": "https://etherscan.io/address/0x76786551c9eb05d1e6cbd156004439184270d8a4", - "xHandle": "totorifuo", - "xUrl": "https://twitter.com/totorifuo", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1110524", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "meedarh", - "displayName": "Meedarh", - "fid": 1110524, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4e981898-e4c4-4c44-17a6-000a80b01500/original", - "followers": 1503, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x07ed950ab4dee9a79d824202e62fd1e59fed62c4", - "etherscanUrl": "https://etherscan.io/address/0x07ed950ab4dee9a79d824202e62fd1e59fed62c4", - "xHandle": "callmeby_1509_", - "xUrl": "https://twitter.com/callmeby_1509_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_736746", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "haba", - "displayName": "VitaliyHab", - "fid": 736746, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/fe055d3f-47b7-4c68-42f7-2e687f225500/original", - "followers": 1499, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3e96e7d8e7e913f37a417dabd172ca928c163519", - "etherscanUrl": "https://etherscan.io/address/0x3e96e7d8e7e913f37a417dabd172ca928c163519", - "xHandle": "patelalexi55072", - "xUrl": "https://twitter.com/patelalexi55072", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_663832", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "fwad", - "displayName": "Fwad", - "fid": 663832, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/36b59709-a9d1-4806-7e6c-9fefa7445b00/rectcrop3", - "followers": 1485, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x57a7e2b08063d28996a4cc888957edcf2592a499", - "etherscanUrl": "https://etherscan.io/address/0x57a7e2b08063d28996a4cc888957edcf2592a499", - "xHandle": "fwadqamari19", - "xUrl": "https://twitter.com/fwadqamari19", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_12569", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "hanjiang", - "displayName": "hanjiang🎩", - "fid": 12569, - "pfpUrl": "https://ipfs.decentralized-content.com/ipfs/bafybeicto6doldfjy7nrqxn7jiduw47xs7cmzppjd6mm3mrao4z46asiwq", - "followers": 1471, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbf559d14eb05beba73dea1e71a27752a445c2d53", - "etherscanUrl": "https://etherscan.io/address/0xbf559d14eb05beba73dea1e71a27752a445c2d53", - "xHandle": "ghlhchen", - "xUrl": "https://twitter.com/ghlhchen", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_14871", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "themrsazon", - "displayName": "Mr.Sazón 🌶", - "fid": 14871, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a8b790db-defb-411f-cce4-22a21057f600/original", - "followers": 1470, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x320d7265ed32b5b5d035d74be69ace681d37c918", - "etherscanUrl": "https://etherscan.io/address/0x320d7265ed32b5b5d035d74be69ace681d37c918", - "xHandle": "themrsazon", - "xUrl": "https://twitter.com/themrsazon", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_780914", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kingmodaser", - "displayName": "Atreides", - "fid": 780914, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f9fb4fe7-ee37-42de-f5d6-13b10fb1c000/original", - "followers": 1462, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa011b705efac1fa71304474cd5066fa8db5e27b5", - "etherscanUrl": "https://etherscan.io/address/0xa011b705efac1fa71304474cd5066fa8db5e27b5", - "xHandle": "yousuf_has37548", - "xUrl": "https://twitter.com/yousuf_has37548", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_351194", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mrdavid28668", - "displayName": "MR DAVID 🎭🍄🦕", - "fid": 351194, - "pfpUrl": "https://ipfs.decentralized-content.com/ipfs/bafkreigpyrrzgtm7qqioypfg43d23l734gccedkr5ikb4vfoozyd6ubedi", - "followers": 1459, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2e7bff4ec9095bfb296a8c2b8f1fe81a0219f75b", - "etherscanUrl": "https://etherscan.io/address/0x2e7bff4ec9095bfb296a8c2b8f1fe81a0219f75b", - "xHandle": "mrdavid286", - "xUrl": "https://twitter.com/mrdavid286", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_15086", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "alby", - "displayName": "Alby 🎩🧬", - "fid": 15086, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/931494a1-e66f-4239-437b-d12b396fca00/original", - "followers": 1442, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xacbf90a3f03a34faa8235854ca6c3ee0cc8c7546", - "etherscanUrl": "https://etherscan.io/address/0xacbf90a3f03a34faa8235854ca6c3ee0cc8c7546", - "xHandle": "m2ahki", - "xUrl": "https://twitter.com/m2ahki", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_316090", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bithopper", - "displayName": "⚙️Bithopper⚙️", - "fid": 316090, - "pfpUrl": "https://i.imgur.com/2i6SlhB.png", - "followers": 1441, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9d77878512de582b341d50b755c4fb5bac4bca4c", - "etherscanUrl": "https://etherscan.io/address/0x9d77878512de582b341d50b755c4fb5bac4bca4c", - "xHandle": "aidanzx55", - "xUrl": "https://twitter.com/aidanzx55", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_505214", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jarif", - "displayName": "Aila Ahlima", - "fid": 505214, - "pfpUrl": "https://res.cloudinary.com/base-app/image/upload/f_auto/v1762444615/d82aca22-9676-4a9a-b0c7-3b0d63b12aaf.jpg.jpg", - "followers": 1433, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1aefd245be777943c31ea94dfd694cf081120483", - "etherscanUrl": "https://etherscan.io/address/0x1aefd245be777943c31ea94dfd694cf081120483", - "xHandle": "neamulrabby", - "xUrl": "https://twitter.com/neamulrabby", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_207400", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dev0ps", - "displayName": "dev0ps ~ coastalconsulting.eth ~", - "fid": 207400, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/dc663464-87e4-42c7-e000-9e9f854bb000/rectcrop3", - "followers": 1431, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x141851ca7ffbead76491b365c3515bf5bfd9efed", - "etherscanUrl": "https://etherscan.io/address/0x141851ca7ffbead76491b365c3515bf5bfd9efed", - "xHandle": "global_robo", - "xUrl": "https://twitter.com/global_robo", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_427669", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "hamedmbo", - "displayName": "Hamedmb", - "fid": 427669, - "pfpUrl": "https://i.imgur.com/TopuMHD.jpg", - "followers": 1431, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3b6de0800aa87829e6dd15129968d0ddbc6a2712", - "etherscanUrl": "https://etherscan.io/address/0x3b6de0800aa87829e6dd15129968d0ddbc6a2712", - "xHandle": "hamedmbo", - "xUrl": "https://twitter.com/hamedmbo", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_3100", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "xmtp", - "displayName": "XMTP", - "fid": 3100, - "pfpUrl": "https://i.imgur.com/Xxu3vA3.jpg", - "followers": 1417, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf58447b87598158c8aa9ce2e1bb4d1986ac78db1", - "etherscanUrl": "https://etherscan.io/address/0xf58447b87598158c8aa9ce2e1bb4d1986ac78db1", - "xHandle": "xmtp_", - "xUrl": "https://twitter.com/xmtp_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_516933", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "annye", - "displayName": "NickyStixx.eth", - "fid": 516933, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/53f5bca0-f9ff-4b1d-ab30-cce2a4400b00/original", - "followers": 1399, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x88be8ae29699d62cc8818c48ef4dace4ed140273", - "etherscanUrl": "https://etherscan.io/address/0x88be8ae29699d62cc8818c48ef4dace4ed140273", - "xHandle": "angelqueen31155", - "xUrl": "https://twitter.com/angelqueen31155", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_292934", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bluechipstocks", - "displayName": "Lars J.", - "fid": 292934, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b87b16c1-b443-477d-b25f-38e80c244400/rectcrop3", - "followers": 1394, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xef86a797e137ebe39d905fff1a27fbf2afc7d936", - "etherscanUrl": "https://etherscan.io/address/0xef86a797e137ebe39d905fff1a27fbf2afc7d936", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_257704", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bostne", - "displayName": "botmaster.base.eth", - "fid": 257704, - "pfpUrl": "https://i.imgur.com/dwuFkBR.gif", - "followers": 1378, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe3e915c5370ae3e70b44f831858611d464607ede", - "etherscanUrl": "https://etherscan.io/address/0xe3e915c5370ae3e70b44f831858611d464607ede", - "xHandle": "bosshehe0", - "xUrl": "https://twitter.com/bosshehe0", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_16236", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sttsm.eth", - "displayName": "STTSM ⌐◨-◨", - "fid": 16236, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5e2769ab-a640-4298-ea4d-9a9ece090f00/original", - "followers": 1373, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xaa7d0518841640dd9a1dd7f31646260d1ebe8f02", - "etherscanUrl": "https://etherscan.io/address/0xaa7d0518841640dd9a1dd7f31646260d1ebe8f02", - "xHandle": "sttsm_eth", - "xUrl": "https://twitter.com/sttsm_eth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_419242", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "arzakikio.eth", - "displayName": "arzakikio.eth", - "fid": 419242, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/bf070bb1-0b1f-4901-2c24-1356c47a4400/original", - "followers": 1372, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcf65277dea11e8edcf82519ab84fb56ef04be586", - "etherscanUrl": "https://etherscan.io/address/0xcf65277dea11e8edcf82519ab84fb56ef04be586", - "xHandle": "arzakikio", - "xUrl": "https://twitter.com/arzakikio", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_7263", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nuconomy.eth", - "displayName": "nuconomy.⌐◨-◨", - "fid": 7263, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2cc18bb7-ab52-407a-a00f-7c63c537d700/original", - "followers": 1366, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8da555dcb53e65b34a5c86360259601312e59ebb", - "etherscanUrl": "https://etherscan.io/address/0x8da555dcb53e65b34a5c86360259601312e59ebb", - "xHandle": "nuconomist", - "xUrl": "https://twitter.com/nuconomist", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_18229", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nicom", - "displayName": "Nico", - "fid": 18229, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/689a24ab-af57-491c-c4ab-473d25671b00/rectcrop3", - "followers": 1362, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2aa6f3ddc300df0b36db8024d7153770a1d40784", - "etherscanUrl": "https://etherscan.io/address/0x2aa6f3ddc300df0b36db8024d7153770a1d40784", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_18756", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "optichad", - "displayName": "LekeboyxCrypt", - "fid": 18756, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/fff540af-5227-4b51-e42d-72b058dfef00/original", - "followers": 1347, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x439356b793aa7f838f10423fa37b97d7114a792e", - "etherscanUrl": "https://etherscan.io/address/0x439356b793aa7f838f10423fa37b97d7114a792e", - "xHandle": "lekeboyxcrypt", - "xUrl": "https://twitter.com/lekeboyxcrypt", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_332816", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "flipping", - "displayName": "FlippingMfer", - "fid": 332816, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b69e66dc-dfae-4ffd-bdd4-46ce9c3a3200/rectcrop3", - "followers": 1345, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4bc02b53375019e6f212de235d053cac0d36213f", - "etherscanUrl": "https://etherscan.io/address/0x4bc02b53375019e6f212de235d053cac0d36213f", - "xHandle": "flippingmfer", - "xUrl": "https://twitter.com/flippingmfer", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_843635", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bedebah", - "displayName": "𝕰𝖒𝖔𝖈𝖟𝖔", - "fid": 843635, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/02188b9b-17b4-4c49-9823-772db736e500/original", - "followers": 1344, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe6ff471f3a96a718e44a3ccd4c956d518a8b11e5", - "etherscanUrl": "https://etherscan.io/address/0xe6ff471f3a96a718e44a3ccd4c956d518a8b11e5", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_399675", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "trentonfunt", - "displayName": "Trent🎩🍄🐹", - "fid": 399675, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a30c85cf-3d13-4e8e-8ee9-fb7a963f9900/rectcrop3", - "followers": 1339, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa5735c78896966ad6fc899f1f359a61a074387cb", - "etherscanUrl": "https://etherscan.io/address/0xa5735c78896966ad6fc899f1f359a61a074387cb", - "xHandle": "tearhuntt", - "xUrl": "https://twitter.com/tearhuntt", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_365010", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "imcool.base.eth", - "displayName": "Cool", - "fid": 365010, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6be57551-41f1-450b-621f-ab8db31b7600/original", - "followers": 1335, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xad80780f3c02b141377c089083cd6f241433c5f3", - "etherscanUrl": "https://etherscan.io/address/0xad80780f3c02b141377c089083cd6f241433c5f3", - "xHandle": "coolonbase", - "xUrl": "https://twitter.com/coolonbase", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_251545", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "usmanny5", - "displayName": "Ibrahim Usman base.eth", - "fid": 251545, - "pfpUrl": "https://i.imgur.com/qIB3jhK.jpg", - "followers": 1332, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x16b16c1c701f297ecf3a290781b37058e9a85e82", - "etherscanUrl": "https://etherscan.io/address/0x16b16c1c701f297ecf3a290781b37058e9a85e82", - "xHandle": "usmanny001", - "xUrl": "https://twitter.com/usmanny001", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_320618", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "blassa", - "displayName": "blassa.base.eth", - "fid": 320618, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/17c95936-fb53-4f46-9558-54e944e60800/original", - "followers": 1329, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5bfdc6d880cc0d1ed09e1408fd93091edc167460", - "etherscanUrl": "https://etherscan.io/address/0x5bfdc6d880cc0d1ed09e1408fd93091edc167460", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_21191", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dnznjuan", - "displayName": "Denizen Juan", - "fid": 21191, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/49ca8d2b-e9dc-490c-9fb9-91946dabad00/original", - "followers": 1329, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x49375ce55a19c2ad51e42ca593a1c2127bebd578", - "etherscanUrl": "https://etherscan.io/address/0x49375ce55a19c2ad51e42ca593a1c2127bebd578", - "xHandle": "dnznjuan", - "xUrl": "https://twitter.com/dnznjuan", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_239263", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "looktarn", - "displayName": "Looktarn​.base.eth 🎩🔮", - "fid": 239263, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a303db4c-97e8-4ecd-7b49-b54518f95900/original", - "followers": 1326, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7ffa8893b43f697325e533faedca1ea39d44c5c3", - "etherscanUrl": "https://etherscan.io/address/0x7ffa8893b43f697325e533faedca1ea39d44c5c3", - "xHandle": "looktanl_p", - "xUrl": "https://twitter.com/looktanl_p", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_226372", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "billbandito", - "displayName": "billbandito 🎩", - "fid": 226372, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ee08238e-8853-4681-bc71-30e3fe211500/original", - "followers": 1310, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x19c605e89a64b231f1551f72de310e38e9bba5c4", - "etherscanUrl": "https://etherscan.io/address/0x19c605e89a64b231f1551f72de310e38e9bba5c4", - "xHandle": "billbandito", - "xUrl": "https://twitter.com/billbandito", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_257992", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "merahputih.eth", - "displayName": "merahputih.base.eth", - "fid": 257992, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/87f44e20-809f-405e-c077-6b30d6f68c00/original", - "followers": 1308, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe4c45f8c8d7c711ac4c8fd36024a55dfa2793f2b", - "etherscanUrl": "https://etherscan.io/address/0xe4c45f8c8d7c711ac4c8fd36024a55dfa2793f2b", - "xHandle": "fathansaif", - "xUrl": "https://twitter.com/fathansaif", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_315225", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nmnima", - "displayName": "Nima 🎩 🔵 🍖 🔮🎭", - "fid": 315225, - "pfpUrl": "https://i.imgur.com/W0YF5Pp.jpg", - "followers": 1305, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2667b25119e38e2d308ea3685d67a38a4f8021a5", - "etherscanUrl": "https://etherscan.io/address/0x2667b25119e38e2d308ea3685d67a38a4f8021a5", - "xHandle": "voltaj03", - "xUrl": "https://twitter.com/voltaj03", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_307600", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "shazed", - "displayName": "JANNIE", - "fid": 307600, - "pfpUrl": "https://tba-mobile.mypinata.cloud/ipfs/QmfH3dHRcN2qK8VRKNLxcRimafXkxEt7N7kKNFGp1STpTt?pinataGatewayToken=3nq0UVhtd3rYmgYDdb1I9qv7rHsw-_DzwdWkZPRQ-QW1avFI9dCS8knaSfq_R5_q", - "followers": 1303, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfc9e092fb401db10191b7486cfa12d48cc2ca996", - "etherscanUrl": "https://etherscan.io/address/0xfc9e092fb401db10191b7486cfa12d48cc2ca996", - "xHandle": "honestriku15011", - "xUrl": "https://twitter.com/honestriku15011", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_307182", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dixon", - "displayName": "XØXØ", - "fid": 307182, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6d727842-9df6-4795-7385-19b95cefc100/rectcrop3", - "followers": 1302, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9e46c718130e06741bb14506dbd804100b440586", - "etherscanUrl": "https://etherscan.io/address/0x9e46c718130e06741bb14506dbd804100b440586", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_371249", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "yashtech.eth", - "displayName": "Noname", - "fid": 371249, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e2e1c4d7-b888-4810-fb83-1d6672159a00/original", - "followers": 1299, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe53bf0821cda9e79d49ec2191d80a989bcda2b99", - "etherscanUrl": "https://etherscan.io/address/0xe53bf0821cda9e79d49ec2191d80a989bcda2b99", - "xHandle": "serdarysbyr", - "xUrl": "https://twitter.com/serdarysbyr", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_209707", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "greentime1", - "displayName": "greentime1", - "fid": 209707, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/943265a7-295e-4d73-16ac-419a9b562900/original", - "followers": 1286, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x86a3d710c42f568fa165d39c3184f41c9d179470", - "etherscanUrl": "https://etherscan.io/address/0x86a3d710c42f568fa165d39c3184f41c9d179470", - "xHandle": "_greentime", - "xUrl": "https://twitter.com/_greentime", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_878433", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "untitled1", - "displayName": "untitled1.base.eth", - "fid": 878433, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/73282f84-afad-4ac8-cf9d-0c31c8440600/original", - "followers": 1281, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x43c9ca9e89b7a789c63f0cae41377c8e58036bed", - "etherscanUrl": "https://etherscan.io/address/0x43c9ca9e89b7a789c63f0cae41377c8e58036bed", - "xHandle": "saptra_23", - "xUrl": "https://twitter.com/saptra_23", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_328716", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bamland", - "displayName": "Azita 🌹👌🎩 🔵", - "fid": 328716, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3e6f1cd6-ba49-42cf-9733-dd29e709ed00/original", - "followers": 1277, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcb0c6f24c9bb5f12783e143855fe74779389a845", - "etherscanUrl": "https://etherscan.io/address/0xcb0c6f24c9bb5f12783e143855fe74779389a845", - "xHandle": "sibgol3", - "xUrl": "https://twitter.com/sibgol3", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_207750", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cashlax", - "displayName": "Cash", - "fid": 207750, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/810d162c-b938-4725-1612-febbe8c1aa00/original", - "followers": 1277, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x974df0be1c41f1da0df3f8a27521c35f41066b5d", - "etherscanUrl": "https://etherscan.io/address/0x974df0be1c41f1da0df3f8a27521c35f41066b5d", - "xHandle": "cashlaxx", - "xUrl": "https://twitter.com/cashlaxx", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_24204", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ozengk.eth", - "displayName": "ZAN 🎩 🥚 🧬", - "fid": 24204, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2ae51bff-70b0-4114-4497-da254faa2f00/original", - "followers": 1272, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x943962699b2f223848fbd996d8be3fdd98117699", - "etherscanUrl": "https://etherscan.io/address/0x943962699b2f223848fbd996d8be3fdd98117699", - "xHandle": "ozengk_", - "xUrl": "https://twitter.com/ozengk_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1086581", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "pablodey4you", - "displayName": "Pabs", - "fid": 1086581, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3a4e51b9-55c9-478c-1b7e-5e174aa72a00/rectcrop3", - "followers": 1268, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4566d57011682282e4251014f3422abb91727865", - "etherscanUrl": "https://etherscan.io/address/0x4566d57011682282e4251014f3422abb91727865", - "xHandle": "pabsdey4you", - "xUrl": "https://twitter.com/pabsdey4you", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_789298", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "lqthiennhat168", - "displayName": "happy world in you", - "fid": 789298, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/58bc836f-9f84-4fe9-a602-aab727106400/original", - "followers": 1263, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x578b13c82e2ece3c025246360f3b09fc02a752fb", - "etherscanUrl": "https://etherscan.io/address/0x578b13c82e2ece3c025246360f3b09fc02a752fb", - "xHandle": "thienhat168", - "xUrl": "https://twitter.com/thienhat168", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_192426", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "spectrebtc2008", - "displayName": "SpectreBTC2008🦋", - "fid": 192426, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b8cb7c59-5d42-455b-c2ed-5b8c842aa900/original", - "followers": 1261, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9cb28caf662c5ce74725d349b81e74bbc4ae7c8c", - "etherscanUrl": "https://etherscan.io/address/0x9cb28caf662c5ce74725d349b81e74bbc4ae7c8c", - "xHandle": "skynyc0", - "xUrl": "https://twitter.com/skynyc0", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_289331", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "u7", - "displayName": "X0U7", - "fid": 289331, - "pfpUrl": "https://ipfs.decentralized-content.com/ipfs/QmWvnjr1M6CejnhcwZUN6VBqhCor6HzL7UCzDNBEDBKu2H", - "followers": 1257, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x19b118c2fa8b71b0d5ab29dfd7341d085a25f9d6", - "etherscanUrl": "https://etherscan.io/address/0x19b118c2fa8b71b0d5ab29dfd7341d085a25f9d6", - "xHandle": "uzy_assoebangy", - "xUrl": "https://twitter.com/uzy_assoebangy", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_319465", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "stonednouns", - "displayName": "Stoned Nouns ⌐◨-◨", - "fid": 319465, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/80bf4298-29cd-4ee5-10cc-f1acbd5c6f00/rectcrop3", - "followers": 1252, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcfc453b25f6cd83e124c66c0e711e22ace0314c1", - "etherscanUrl": "https://etherscan.io/address/0xcfc453b25f6cd83e124c66c0e711e22ace0314c1", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_259514", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mohsinbnb", - "displayName": "base.eth", - "fid": 259514, - "pfpUrl": "https://beb-public.s3.us-west-1.amazonaws.com/purple.jpg", - "followers": 1237, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xebec03998757e7675270f2dbc8f913149fe65d66", - "etherscanUrl": "https://etherscan.io/address/0xebec03998757e7675270f2dbc8f913149fe65d66", - "xHandle": "mohsin78003", - "xUrl": "https://twitter.com/mohsin78003", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_210841", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bandarbtc", - "displayName": "bandarbtc.base.eth", - "fid": 210841, - "pfpUrl": "https://ipfs.decentralized-content.com/ipfs/bafybeihpwcunaltj6flihfbbftgcj332bxtiz73ddicoculz2pgfyoapbi", - "followers": 1234, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x38e67bcff43a797a5290f976f78ec0df62aba0a6", - "etherscanUrl": "https://etherscan.io/address/0x38e67bcff43a797a5290f976f78ec0df62aba0a6", - "xHandle": "bandar_btc", - "xUrl": "https://twitter.com/bandar_btc", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1103506", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "callguy", - "displayName": "Capt.", - "fid": 1103506, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1b916777-ea35-4549-c103-88610ca5a000/original", - "followers": 1232, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x92650ea5154be475db55090c7ce85b999c53ae84", - "etherscanUrl": "https://etherscan.io/address/0x92650ea5154be475db55090c7ce85b999c53ae84", - "xHandle": "thatcallguy", - "xUrl": "https://twitter.com/thatcallguy", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_645468", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "boyly", - "displayName": "TheBoyly", - "fid": 645468, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d29490e3-0379-43df-5503-96ce82bc2700/original", - "followers": 1232, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd06e6cd41a8524c2f13515fcbcc384137d7da2e0", - "etherscanUrl": "https://etherscan.io/address/0xd06e6cd41a8524c2f13515fcbcc384137d7da2e0", - "xHandle": "theboyly", - "xUrl": "https://twitter.com/theboyly", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1047423", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "adityaranvijay", - "displayName": "adityaranvijay.base.eth", - "fid": 1047423, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d5ba3018-35d0-4b72-643c-d623e008b400/original", - "followers": 1231, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3a0ae1369aa75d37c98af2efb8c3e7c0b658d6ee", - "etherscanUrl": "https://etherscan.io/address/0x3a0ae1369aa75d37c98af2efb8c3e7c0b658d6ee", - "xHandle": "adityaranv27789", - "xUrl": "https://twitter.com/adityaranv27789", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_399399", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "whalesofi", - "displayName": "Whale.base.eth", - "fid": 399399, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/179feaca-00f4-49e6-e581-1294a5c5c800/original", - "followers": 1229, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf103461c97f5aa917dc37d242655b285fb0acd1e", - "etherscanUrl": "https://etherscan.io/address/0xf103461c97f5aa917dc37d242655b285fb0acd1e", - "xHandle": "whale_sofi", - "xUrl": "https://twitter.com/whale_sofi", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1089844", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "josephineoriaku", - "displayName": "Josephine Oriaku®", - "fid": 1089844, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ebe913d9-5bfc-4715-e6eb-08e8575f0f00/rectcrop3", - "followers": 1212, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc54604b9e0a1449644ea0d978fa099291f09935f", - "etherscanUrl": "https://etherscan.io/address/0xc54604b9e0a1449644ea0d978fa099291f09935f", - "xHandle": "josephineoriak_", - "xUrl": "https://twitter.com/josephineoriak_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_327687", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mutianz28", - "displayName": "itsmutia.eth", - "fid": 327687, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/608593dc-7a23-4c42-3868-be70798c1d00/original", - "followers": 1210, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6c0917a0373daa1771467fb32e1fde28a3f7eef7", - "etherscanUrl": "https://etherscan.io/address/0x6c0917a0373daa1771467fb32e1fde28a3f7eef7", - "xHandle": "itsmutiaaaeth", - "xUrl": "https://twitter.com/itsmutiaaaeth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1194", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "elistonberg", - "displayName": "Eli Stonberg", - "fid": 1194, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2b28c3dd-ef00-4506-d353-083fc6882c00/original", - "followers": 1204, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcabf1056fc70943a62334214006f17523df10145", - "etherscanUrl": "https://etherscan.io/address/0xcabf1056fc70943a62334214006f17523df10145", - "xHandle": "elistonberg", - "xUrl": "https://twitter.com/elistonberg", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_262016", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nikug", - "displayName": "xNik", - "fid": 262016, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/81298098-6817-49e8-690f-f70f06605a00/original", - "followers": 1200, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb97714f6bf061d68ec6872d59fdad39e4f447e04", - "etherscanUrl": "https://etherscan.io/address/0xb97714f6bf061d68ec6872d59fdad39e4f447e04", - "xHandle": "jackn1x", - "xUrl": "https://twitter.com/jackn1x", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_259173", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "anfken", - "displayName": "anfken.base.eth", - "fid": 259173, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6693cc89-8afc-483a-6d62-8a5452da0e00/original", - "followers": 1198, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd8ca77284034e40ab4be2051f771b79d8cc38cf5", - "etherscanUrl": "https://etherscan.io/address/0xd8ca77284034e40ab4be2051f771b79d8cc38cf5", - "xHandle": "0xbken", - "xUrl": "https://twitter.com/0xbken", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1082035", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "crypto1claim", - "displayName": "Crypto Claim🟦🟣", - "fid": 1082035, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d2320b1f-91cf-483f-a57c-cbf344c05900/original", - "followers": 1198, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7cc6c8b78844732953fbe92771dfccfce3ec301a", - "etherscanUrl": "https://etherscan.io/address/0x7cc6c8b78844732953fbe92771dfccfce3ec301a", - "xHandle": "crypto_cla_im", - "xUrl": "https://twitter.com/crypto_cla_im", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_494766", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kkknight-artsss", - "displayName": "Knight_Arts", - "fid": 494766, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0f116c45-05cf-4c9b-6988-e59db0b5b800/rectcrop3", - "followers": 1197, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa670f9b94376371c5c3b04304835ba50f7bf9261", - "etherscanUrl": "https://etherscan.io/address/0xa670f9b94376371c5c3b04304835ba50f7bf9261", - "xHandle": "kkknight__artss", - "xUrl": "https://twitter.com/kkknight__artss", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_677707", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "oyale", - "displayName": "Daniel Oyale Emmnanuelaudu", - "fid": 677707, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/95906735-e6c0-4588-cfe4-2a683ba64c00/original", - "followers": 1197, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbbcebe0382ae0a85afc1dc9450f323fa48c2fd96", - "etherscanUrl": "https://etherscan.io/address/0xbbcebe0382ae0a85afc1dc9450f323fa48c2fd96", - "xHandle": "danoyale", - "xUrl": "https://twitter.com/danoyale", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_16355", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nirry.eth", - "displayName": "Nirry Nakiri", - "fid": 16355, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3f5c10c0-8811-4e2d-6c2a-99d959b4fb00/original", - "followers": 1194, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1c161687cf46521def2aff2ffa9130d2d6d2c7b5", - "etherscanUrl": "https://etherscan.io/address/0x1c161687cf46521def2aff2ffa9130d2d6d2c7b5", - "xHandle": "nirryxbt", - "xUrl": "https://twitter.com/nirryxbt", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1025147", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "darkragelight.eth", - "displayName": "darkragelight.eth", - "fid": 1025147, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e24e3b1a-8436-49c9-ec3e-06c5f0a51900/original", - "followers": 1191, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x30a1c31e0685703626c290bed941eb7627d697db", - "etherscanUrl": "https://etherscan.io/address/0x30a1c31e0685703626c290bed941eb7627d697db", - "xHandle": "darkragelight", - "xUrl": "https://twitter.com/darkragelight", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1023927", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "saqib.base.eth", - "displayName": "Saqib", - "fid": 1023927, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ce7b10f4-e22f-4e54-2e7c-21e644c3ba00/original", - "followers": 1180, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x57daec3c41199fc5c9b7fa46bd7ace71f53509eb", - "etherscanUrl": "https://etherscan.io/address/0x57daec3c41199fc5c9b7fa46bd7ace71f53509eb", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_23887", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "metauce.eth", - "displayName": "Degen教父🎩", - "fid": 23887, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a4a4f586-d49d-4b5c-eb56-1b14a8cc9900/original", - "followers": 1175, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe967f46ead13b2b697338e45d8b2a10151ae5671", - "etherscanUrl": "https://etherscan.io/address/0xe967f46ead13b2b697338e45d8b2a10151ae5671", - "xHandle": "yufang_eth", - "xUrl": "https://twitter.com/yufang_eth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_759540", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "homohugh", - "displayName": "$.C.U.M. ILLIONAIRE", - "fid": 759540, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5a3dd175-78fb-459e-532b-482228664200/original", - "followers": 1164, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x283de6b0680367210a41e4847f48c6bd746517b1", - "etherscanUrl": "https://etherscan.io/address/0x283de6b0680367210a41e4847f48c6bd746517b1", - "xHandle": "hughspenxer", - "xUrl": "https://twitter.com/hughspenxer", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_16075", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "myusuf", - "displayName": "Mohammed Yusuf 🧬", - "fid": 16075, - "pfpUrl": "https://i.imgur.com/QmrO9My.jpg", - "followers": 1161, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x878facc8dcf1a37aa7d4f145fe6e0cc5c0ab3343", - "etherscanUrl": "https://etherscan.io/address/0x878facc8dcf1a37aa7d4f145fe6e0cc5c0ab3343", - "xHandle": "pf_yusuf", - "xUrl": "https://twitter.com/pf_yusuf", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_496578", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "the-collective.eth", - "displayName": "ACE Family Management", - "fid": 496578, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b46bef26-9809-41dc-46c1-0e5a91105d00/original", - "followers": 1158, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf3cd7261f2515c815d2bc0bc509ff38e45e05d6d", - "etherscanUrl": "https://etherscan.io/address/0xf3cd7261f2515c815d2bc0bc509ff38e45e05d6d", - "xHandle": "4_tran_inc", - "xUrl": "https://twitter.com/4_tran_inc", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_292291", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "olorunsniper", - "displayName": "1USD BASE", - "fid": 292291, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/052ffc17-e0ca-4c88-1000-e687eff72d00/original", - "followers": 1148, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7b54d4a2f00fb05e86c4eee772529f484353e885", - "etherscanUrl": "https://etherscan.io/address/0x7b54d4a2f00fb05e86c4eee772529f484353e885", - "xHandle": "0x1usdbase", - "xUrl": "https://twitter.com/0x1usdbase", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_506958", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ikaaa", - "displayName": "G€π&J°🎭🍖⚡🎩", - "fid": 506958, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9265adbe-cf17-4223-252a-736a6cf7a900/rectcrop3", - "followers": 1140, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8a2283ba2de94e6adb05833711312f7454cd4dd7", - "etherscanUrl": "https://etherscan.io/address/0x8a2283ba2de94e6adb05833711312f7454cd4dd7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_193960", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kaliki", - "displayName": "Kaliki", - "fid": 193960, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e156a12c-5a49-4483-3c46-fd4239d21100/rectcrop3", - "followers": 1137, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe352f7c2e46b72a5a14339868425dc5037fb209c", - "etherscanUrl": "https://etherscan.io/address/0xe352f7c2e46b72a5a14339868425dc5037fb209c", - "xHandle": "chisnusorn", - "xUrl": "https://twitter.com/chisnusorn", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_264668", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bonux", - "displayName": "Bonux", - "fid": 264668, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/66b8324b-795d-426c-a975-121dde75be00/original", - "followers": 1134, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5e4e5ea537ce7e79b8a0fc4f0cb520e9b8b5dae5", - "etherscanUrl": "https://etherscan.io/address/0x5e4e5ea537ce7e79b8a0fc4f0cb520e9b8b5dae5", - "xHandle": "samuelcheq1", - "xUrl": "https://twitter.com/samuelcheq1", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1098", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nebula", - "displayName": "꙲", - "fid": 1098, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ca7e3d28-a369-48db-c5dd-b1036ada6900/original", - "followers": 1128, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5d27a8fc61d233eb37c0b8509c734a2bcda4e53f", - "etherscanUrl": "https://etherscan.io/address/0x5d27a8fc61d233eb37c0b8509c734a2bcda4e53f", - "xHandle": "wrongnebula", - "xUrl": "https://twitter.com/wrongnebula", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_859268", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "fx1-faucet", - "displayName": "Fx1 Digital Hubs", - "fid": 859268, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/caa81e61-9c76-4f79-7338-b419a2d74300/original", - "followers": 1122, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb11e4a95941393d96b2b13f8de8663c302713e79", - "etherscanUrl": "https://etherscan.io/address/0xb11e4a95941393d96b2b13f8de8663c302713e79", - "xHandle": "fx1_hubs", - "xUrl": "https://twitter.com/fx1_hubs", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_212401", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mesutgulecen", - "displayName": "Mesut", - "fid": 212401, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/166e738f-bc93-4cf9-8fd6-80f32da13c00/original", - "followers": 1119, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf6aecac7906a49761da53094c0dfd6c906e39e18", - "etherscanUrl": "https://etherscan.io/address/0xf6aecac7906a49761da53094c0dfd6c906e39e18", - "xHandle": "mesutgulecen", - "xUrl": "https://twitter.com/mesutgulecen", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_784230", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "tormbreakeer", - "displayName": "tormbreaker 🧬", - "fid": 784230, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/98fd7ba6-fe54-4b09-8035-c26b18405c00/original", - "followers": 1115, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6e934baa19239d0bee3f36785efbb8fcee3b2973", - "etherscanUrl": "https://etherscan.io/address/0x6e934baa19239d0bee3f36785efbb8fcee3b2973", - "xHandle": "flash0599", - "xUrl": "https://twitter.com/flash0599", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_214250", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "xicortm", - "displayName": "Xicor 🎩🔵", - "fid": 214250, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/43db540b-97ba-47d0-593e-efe970c0be00/original", - "followers": 1113, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x76c6ed678c217f78c460302cd40fac593e73edf2", - "etherscanUrl": "https://etherscan.io/address/0x76c6ed678c217f78c460302cd40fac593e73edf2", - "xHandle": "xicortm", - "xUrl": "https://twitter.com/xicortm", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_279711", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "salimmolla", - "displayName": "Salim Molla", - "fid": 279711, - "pfpUrl": "https://i.imgur.com/yRMQyIG.jpg", - "followers": 1110, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4d26d2715d7c9f3fd5c24d3ee5ac39124a069887", - "etherscanUrl": "https://etherscan.io/address/0x4d26d2715d7c9f3fd5c24d3ee5ac39124a069887", - "xHandle": "salimmolla08", - "xUrl": "https://twitter.com/salimmolla08", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_257363", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "charles200", - "displayName": "Charles200", - "fid": 257363, - "pfpUrl": "https://i2mq7oiogcmc5eyo6u4spaj4nho2rcgcfqsqekehwieatnsukmda.arweave.net/RpkPuQ4wmC6TDvU5J4E8ad2oiMIsJQIoh7IICbZUUwY/", - "followers": 1108, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9c1a03887506bd28eda6a9bcdd175ee71ebdb1e8", - "etherscanUrl": "https://etherscan.io/address/0x9c1a03887506bd28eda6a9bcdd175ee71ebdb1e8", - "xHandle": "imelda_100", - "xUrl": "https://twitter.com/imelda_100", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_514113", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "eliasvm.eth", - "displayName": "Elias VM", - "fid": 514113, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c4ae7651-acc0-4fe7-0c0c-a37a3ac3d500/original", - "followers": 1104, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd87efb74597e47fe3b0c8cfd4c1262baafa7b4da", - "etherscanUrl": "https://etherscan.io/address/0xd87efb74597e47fe3b0c8cfd4c1262baafa7b4da", - "xHandle": "_eliasvm", - "xUrl": "https://twitter.com/_eliasvm", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_2770", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "agaperste-", - "displayName": "Jackie | agaperste 🎩", - "fid": 2770, - "pfpUrl": "https://i.imgur.com/qqB7Wuk.jpg", - "followers": 1099, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb10f35351ff21bb81dc02d4fd901ac5ae34e8dc4", - "etherscanUrl": "https://etherscan.io/address/0xb10f35351ff21bb81dc02d4fd901ac5ae34e8dc4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_882532", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "obastee", - "displayName": "OBASTEE CREATIVITY", - "fid": 882532, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/41bfd6b7-4b58-4d47-81a4-d59576322f00/rectcrop3", - "followers": 1091, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7e43d92e6bc0bfdda0da3d8396c16570195e1e66", - "etherscanUrl": "https://etherscan.io/address/0x7e43d92e6bc0bfdda0da3d8396c16570195e1e66", - "xHandle": "obanewa2244", - "xUrl": "https://twitter.com/obanewa2244", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_342592", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "amanullah", - "displayName": "Amanullah", - "fid": 342592, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2ea05593-c6cd-4d75-e558-2731e9a52900/original", - "followers": 1088, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdf5648975108b4fc61e08722d2011c93318d2ca2", - "etherscanUrl": "https://etherscan.io/address/0xdf5648975108b4fc61e08722d2011c93318d2ca2", - "xHandle": "crypto24t", - "xUrl": "https://twitter.com/crypto24t", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_331877", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mbarto", - "displayName": "Mbarto", - "fid": 331877, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/536dd257-a750-4e0e-6783-568a2c2f4100/original", - "followers": 1088, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcd22b8ec5af086ea670f700b039520e30aca2ed0", - "etherscanUrl": "https://etherscan.io/address/0xcd22b8ec5af086ea670f700b039520e30aca2ed0", - "xHandle": "satoshibrt2", - "xUrl": "https://twitter.com/satoshibrt2", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1279", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "hellozeck.eth", - "displayName": "zeck", - "fid": 1279, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2e09facb-7d1e-4e3b-d266-a6bb412c3500/original", - "followers": 1086, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x82eee79c4d54dcfb61ae6590cc0bbdcd01a5e20e", - "etherscanUrl": "https://etherscan.io/address/0x82eee79c4d54dcfb61ae6590cc0bbdcd01a5e20e", - "xHandle": "zeckxyz", - "xUrl": "https://twitter.com/zeckxyz", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1057524", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "imanuelbuchi", - "displayName": "iManuelBuchi", - "fid": 1057524, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2dabad7e-097e-4679-9554-486065f0a700/original", - "followers": 1083, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x25484005832f53c667befd0224a388bbf6010d3f", - "etherscanUrl": "https://etherscan.io/address/0x25484005832f53c667befd0224a388bbf6010d3f", - "xHandle": "imanuelbuchi", - "xUrl": "https://twitter.com/imanuelbuchi", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_599407", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "rachna", - "displayName": "Rachna", - "fid": 599407, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1e704f62-edec-4d69-5cdd-4097ff0bd900/rectcrop3", - "followers": 1072, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4500e809d028d7f302c13962b6b5240c7538fd33", - "etherscanUrl": "https://etherscan.io/address/0x4500e809d028d7f302c13962b6b5240c7538fd33", - "xHandle": "itmerachna", - "xUrl": "https://twitter.com/itmerachna", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_248958", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cryptocrazyboy", - "displayName": "CryptoCrazyBoy_", - "fid": 248958, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/04a1e63b-0e46-47d2-b9bd-c914464a9f00/original", - "followers": 1068, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x52edb5afc9b84dde0ca5c9ab3381308b06973f29", - "etherscanUrl": "https://etherscan.io/address/0x52edb5afc9b84dde0ca5c9ab3381308b06973f29", - "xHandle": "0xrzki", - "xUrl": "https://twitter.com/0xrzki", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_248558", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "rapsh4.eth", - "displayName": "rapsh base.eth", - "fid": 248558, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1f1fe032-23d9-4d88-52a2-e084b75f0d00/original", - "followers": 1060, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdd149027fda1a2a290b891ecbd1559d9acaeb2ce", - "etherscanUrl": "https://etherscan.io/address/0xdd149027fda1a2a290b891ecbd1559d9acaeb2ce", - "xHandle": "rapsh9", - "xUrl": "https://twitter.com/rapsh9", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1344086", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jayewealth001", - "displayName": "jayewealth001", - "fid": 1344086, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f4c10bf1-5dcb-432f-bcca-496c7042e800/original", - "followers": 1057, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x79bd72eb00d1fca38bf2780e78ff3bace0fa3166", - "etherscanUrl": "https://etherscan.io/address/0x79bd72eb00d1fca38bf2780e78ff3bace0fa3166", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_925801", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mehranb", - "displayName": "Mersin", - "fid": 925801, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9f5a7965-f10c-4992-fb48-ad5ce948e800/original", - "followers": 1055, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8143ec413064e9151a9e085a421087f4d9a71800", - "etherscanUrl": "https://etherscan.io/address/0x8143ec413064e9151a9e085a421087f4d9a71800", - "xHandle": "gamenew669748", - "xUrl": "https://twitter.com/gamenew669748", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_261023", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "yans01", - "displayName": "Dian Ulumia🧬", - "fid": 261023, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/99627615-e936-4c87-9f7d-19bba3bbe500/original", - "followers": 1054, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x70fc7e1f0409ba24d82a4db9d42d90a7fa67e400", - "etherscanUrl": "https://etherscan.io/address/0x70fc7e1f0409ba24d82a4db9d42d90a7fa67e400", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_291734", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mobaskol.eth", - "displayName": "mobaskol.base.eth", - "fid": 291734, - "pfpUrl": "https://i.imgur.com/6TW7QQO.jpg", - "followers": 1046, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbd13b79c20aba54508977a1a3468dd702153c253", - "etherscanUrl": "https://etherscan.io/address/0xbd13b79c20aba54508977a1a3468dd702153c253", - "xHandle": "mobaskol", - "xUrl": "https://twitter.com/mobaskol", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_422384", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "millionbit.eth", - "displayName": "million.base.eth", - "fid": 422384, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/72711120-137b-4321-3429-aebe98602700/original", - "followers": 1039, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7e089452fcdf0a00117f6f11aa7990f6d54f3c77", - "etherscanUrl": "https://etherscan.io/address/0x7e089452fcdf0a00117f6f11aa7990f6d54f3c77", - "xHandle": "bithomepage", - "xUrl": "https://twitter.com/bithomepage", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_268756", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "saber66", - "displayName": "Saber Sohrabie", - "fid": 268756, - "pfpUrl": "https://i.imgur.com/o9jC1zZ.jpg", - "followers": 1037, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb9fe3e6ecad643c675eb578b590e1a256c3ce6e4", - "etherscanUrl": "https://etherscan.io/address/0xb9fe3e6ecad643c675eb578b590e1a256c3ce6e4", - "xHandle": "saber6631", - "xUrl": "https://twitter.com/saber6631", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_253385", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "phragg", - "displayName": "phragg", - "fid": 253385, - "pfpUrl": "https://i.imgur.com/GmEWUsi.jpg", - "followers": 1024, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x862bf52be02a2abf96fdaeb22ea9089e821b0591", - "etherscanUrl": "https://etherscan.io/address/0x862bf52be02a2abf96fdaeb22ea9089e821b0591", - "xHandle": "austinkpickett", - "xUrl": "https://twitter.com/austinkpickett", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_421364", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "solcay", - "displayName": "Solcay.base.eth", - "fid": 421364, - "pfpUrl": "https://res.cloudinary.com/base-app/image/upload/f_auto/v1762083227/IMG_2756.jpg.jpg", - "followers": 1023, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7761e32bb054f8cf26bd3180680cd8ebb14df182", - "etherscanUrl": "https://etherscan.io/address/0x7761e32bb054f8cf26bd3180680cd8ebb14df182", - "xHandle": "solcayeth", - "xUrl": "https://twitter.com/solcayeth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1012281", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bradenwolf", - "displayName": "Braden", - "fid": 1012281, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/34b3b41e-6717-4653-ba0f-1d4007824600/original", - "followers": 1022, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfd0725b9fd15b983514b8b99fb70e2ae018c9a8d", - "etherscanUrl": "https://etherscan.io/address/0xfd0725b9fd15b983514b8b99fb70e2ae018c9a8d", - "xHandle": "bradenization", - "xUrl": "https://twitter.com/bradenization", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_7753", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "6666", - "displayName": "6666 🎩🍖", - "fid": 7753, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/98b38c3b-316b-45fc-c74d-913a4983f800/original", - "followers": 1018, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x052f8a8fdcacc47fa5ae7901f00df058f520a5c0", - "etherscanUrl": "https://etherscan.io/address/0x052f8a8fdcacc47fa5ae7901f00df058f520a5c0", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_988485", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "okeaniderya.eth", - "displayName": "derya {means ocean}", - "fid": 988485, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/76d82a82-7ec1-457d-5427-4df024558e00/original", - "followers": 1016, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdfe5264ee30c257f350c52da3fac8b250ff7b1e2", - "etherscanUrl": "https://etherscan.io/address/0xdfe5264ee30c257f350c52da3fac8b250ff7b1e2", - "xHandle": "seaderyastare", - "xUrl": "https://twitter.com/seaderyastare", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_210388", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mananabar", - "displayName": "MetaBar x MananaBar", - "fid": 210388, - "pfpUrl": "https://beb-public.s3.us-west-1.amazonaws.com/blue.jpg", - "followers": 1014, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe90c7578f04bac2a6ff41e2e9e1d65353710efcb", - "etherscanUrl": "https://etherscan.io/address/0xe90c7578f04bac2a6ff41e2e9e1d65353710efcb", - "xHandle": "robzy2030", - "xUrl": "https://twitter.com/robzy2030", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_309250", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "basepool", - "displayName": "BasePool", - "fid": 309250, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6862cfaa-c91d-48c4-1250-b5a360f41100/rectcrop3", - "followers": 1010, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x22e6a141040acf70143c6269d4eb297833fee9a1", - "etherscanUrl": "https://etherscan.io/address/0x22e6a141040acf70143c6269d4eb297833fee9a1", - "xHandle": "basepools", - "xUrl": "https://twitter.com/basepools", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_260777", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "yuu", - "displayName": "Yuu ✨", - "fid": 260777, - "pfpUrl": "https://i.imgur.com/gMiFmDx.jpg", - "followers": 1006, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf24c0df46a5d231bfa6743341c8a3e37e37347e9", - "etherscanUrl": "https://etherscan.io/address/0xf24c0df46a5d231bfa6743341c8a3e37e37347e9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_368483", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cgardens710", - "displayName": "Cgardens710.base.eth", - "fid": 368483, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/246f3738-4291-4a9b-5f5f-90c12b6b9500/original", - "followers": 996, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x42a0af6d78aefcc16d95edfe5afb5485c963a8bb", - "etherscanUrl": "https://etherscan.io/address/0x42a0af6d78aefcc16d95edfe5afb5485c963a8bb", - "xHandle": "cgardens710", - "xUrl": "https://twitter.com/cgardens710", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_831549", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "lib12", - "displayName": "Xeav", - "fid": 831549, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/cec8c59c-303b-4875-3287-096ff3dc0d00/original", - "followers": 988, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x78c825b3bbd9c08d0809c327ab042764c4d327c5", - "etherscanUrl": "https://etherscan.io/address/0x78c825b3bbd9c08d0809c327ab042764c4d327c5", - "xHandle": "libekfi", - "xUrl": "https://twitter.com/libekfi", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_334357", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nonamei", - "displayName": "Mich🥜👆👾🐿️ 🧬", - "fid": 334357, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/01308fd4-8cdc-4ffc-a3ba-4433ce9b6f00/rectcrop3", - "followers": 979, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc5377f18670a7715d3e03f0e33600c97d1836a63", - "etherscanUrl": "https://etherscan.io/address/0xc5377f18670a7715d3e03f0e33600c97d1836a63", - "xHandle": "stevme1", - "xUrl": "https://twitter.com/stevme1", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_15089", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bheghe", - "displayName": "Wobble.base.eth", - "fid": 15089, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/718b7600-daab-4dd3-81d3-f89e15fd7600/original", - "followers": 979, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7e26ce06f33769092a03dcd50be89fedbccc68b8", - "etherscanUrl": "https://etherscan.io/address/0x7e26ce06f33769092a03dcd50be89fedbccc68b8", - "xHandle": "new_bhe", - "xUrl": "https://twitter.com/new_bhe", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_680485", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dhen", - "displayName": "Ndhen19 ", - "fid": 680485, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/190c74fd-8093-44cf-fde0-c667a35a6700/rectcrop3", - "followers": 976, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa6779a0c4c6785ca341b5a908d987e29827ec224", - "etherscanUrl": "https://etherscan.io/address/0xa6779a0c4c6785ca341b5a908d987e29827ec224", - "xHandle": "iketkidul", - "xUrl": "https://twitter.com/iketkidul", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_382192", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bloob", - "displayName": "Bloob", - "fid": 382192, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/076dbb6b-461f-47c7-a86b-1d3acae04400/original", - "followers": 967, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xafd96ffb55452ded4dc2354873f3980258e1e547", - "etherscanUrl": "https://etherscan.io/address/0xafd96ffb55452ded4dc2354873f3980258e1e547", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_948843", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bossboss", - "displayName": "BossBoss", - "fid": 948843, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4892b573-843e-4e20-30a0-6d708680ef00/original", - "followers": 965, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe8f489ccb06648c2a64975b73903d9129564538f", - "etherscanUrl": "https://etherscan.io/address/0xe8f489ccb06648c2a64975b73903d9129564538f", - "xHandle": "tizisdaillest", - "xUrl": "https://twitter.com/tizisdaillest", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_302398", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "vnbc", - "displayName": "VNBC | base.eth", - "fid": 302398, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1f6d8119-3a7a-4b85-7cf3-067e81715e00/original", - "followers": 965, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4f15b3528dcebff7fea2cfc088075a1c260d2bce", - "etherscanUrl": "https://etherscan.io/address/0x4f15b3528dcebff7fea2cfc088075a1c260d2bce", - "xHandle": "duyhuanh", - "xUrl": "https://twitter.com/duyhuanh", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_301313", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "fidi", - "displayName": "fidbase", - "fid": 301313, - "pfpUrl": "https://tba-mobile.mypinata.cloud/ipfs/QmQ3mKWGYxKitSvXtcFv3SM6e4sQH9GoYkZAq2nSpvZGTA?pinataGatewayToken=3nq0UVhtd3rYmgYDdb1I9qv7rHsw-_DzwdWkZPRQ-QW1avFI9dCS8knaSfq_R5_q", - "followers": 959, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x39f3bb4dc08e40d4d0039e137634ce223b70cc9b", - "etherscanUrl": "https://etherscan.io/address/0x39f3bb4dc08e40d4d0039e137634ce223b70cc9b", - "xHandle": "fidbase", - "xUrl": "https://twitter.com/fidbase", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_429293", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "arash865", - "displayName": "Arash_Shanto⚡", - "fid": 429293, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/91ae45c6-3607-49dc-9bc1-899b8203bb00/original", - "followers": 959, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd4889b4db7e633a1c916f90f3f341835bd9a4035", - "etherscanUrl": "https://etherscan.io/address/0xd4889b4db7e633a1c916f90f3f341835bd9a4035", - "xHandle": "arash_shanto58", - "xUrl": "https://twitter.com/arash_shanto58", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1035745", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "aravindbetachain", - "displayName": "DOGBITCAT", - "fid": 1035745, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5898dc4f-d73a-452e-e6fd-15397d5af200/original", - "followers": 958, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb3e8bc46cd3cf5e43a08a2db7ca05c0695d89c90", - "etherscanUrl": "https://etherscan.io/address/0xb3e8bc46cd3cf5e43a08a2db7ca05c0695d89c90", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_191132", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kimmuchi", - "displayName": "hoangmi", - "fid": 191132, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d6a2c309-87b7-4d13-f854-e26ca5ffec00/rectcrop3", - "followers": 933, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x44d5367631d463ae8be89dde6702efa173532ff4", - "etherscanUrl": "https://etherscan.io/address/0x44d5367631d463ae8be89dde6702efa173532ff4", - "xHandle": "hoangmi0123", - "xUrl": "https://twitter.com/hoangmi0123", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_2275", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cryptoinfluence.eth", - "displayName": "cryptoinfluence.base.eth", - "fid": 2275, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b16ce334-8bbf-4f49-1778-f1e5e4407a00/rectcrop3", - "followers": 931, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x169302fc98fb898fffdfc60313f1c0341499c6bc", - "etherscanUrl": "https://etherscan.io/address/0x169302fc98fb898fffdfc60313f1c0341499c6bc", - "xHandle": "cryptoinfluence", - "xUrl": "https://twitter.com/cryptoinfluence", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_900274", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ghostsama", - "displayName": "Sama 🤍✨", - "fid": 900274, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/db1f0a04-850e-4963-3726-92e798501600/original", - "followers": 926, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x610ffce6c9f641b2065e978d036a846da5a00454", - "etherscanUrl": "https://etherscan.io/address/0x610ffce6c9f641b2065e978d036a846da5a00454", - "xHandle": "1ghostsama", - "xUrl": "https://twitter.com/1ghostsama", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_903908", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "spawniz", - "displayName": "Spawniz", - "fid": 903908, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/86bf54ae-c735-4490-a283-cc768cb38900/original", - "followers": 925, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x596a08a5ba8c7fec6105f192d64305f4fc5b08ff", - "etherscanUrl": "https://etherscan.io/address/0x596a08a5ba8c7fec6105f192d64305f4fc5b08ff", - "xHandle": "spawnizz", - "xUrl": "https://twitter.com/spawnizz", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1051552", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "melaningenie", - "displayName": "Princess Porcupine🦋", - "fid": 1051552, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/76b39334-e192-4c6a-6433-67a5d2fcfa00/original", - "followers": 925, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x59e5b688d08ae5d89fb8c8792b78df605a4c2e5d", - "etherscanUrl": "https://etherscan.io/address/0x59e5b688d08ae5d89fb8c8792b78df605a4c2e5d", - "xHandle": "idunnomannxo", - "xUrl": "https://twitter.com/idunnomannxo", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_482920", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nomadicwolf.eth", - "displayName": "Irfan Khan Wolf World", - "fid": 482920, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7ce37153-95df-458c-aeea-e78b8fea1500/rectcrop3", - "followers": 924, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x303a49a0b1fb041d9cde329325461777efbac701", - "etherscanUrl": "https://etherscan.io/address/0x303a49a0b1fb041d9cde329325461777efbac701", - "xHandle": "hasanalamattar", - "xUrl": "https://twitter.com/hasanalamattar", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_391862", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "burakxm8", - "displayName": "Burak", - "fid": 391862, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/35509019-173a-426d-5617-1c18f3abf400/original", - "followers": 917, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x95539522c83143d2e57d3e1add6463e2e60131fc", - "etherscanUrl": "https://etherscan.io/address/0x95539522c83143d2e57d3e1add6463e2e60131fc", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_802090", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "lopezonchain.eth", - "displayName": "Lopez", - "fid": 802090, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/84c8a55f-d367-4c43-5b5d-6d43dbc5b700/rectcrop3", - "followers": 913, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x470b65e9ea7844182fe80cd07c818ffc36664be4", - "etherscanUrl": "https://etherscan.io/address/0x470b65e9ea7844182fe80cd07c818ffc36664be4", - "xHandle": "lopezonchain", - "xUrl": "https://twitter.com/lopezonchain", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_7513", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "alyonafuria", - "displayName": "alyonafuria", - "fid": 7513, - "pfpUrl": "https://p765cpbvm0.execute-api.eu-central-1.amazonaws.com/p1/renderer/Minteeble/chain/base/collection/6e0b39a5-8569-4e67-b330-d352593c9629/image/3393.png", - "followers": 899, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x68f53005e7ee9f707f6d9861306fc4a0e91f12bd", - "etherscanUrl": "https://etherscan.io/address/0x68f53005e7ee9f707f6d9861306fc4a0e91f12bd", - "xHandle": "dranikipunk", - "xUrl": "https://twitter.com/dranikipunk", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_321795", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "classeart.eth", - "displayName": "Classe 🎩", - "fid": 321795, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3f40049a-cc75-494f-d3f0-e7ffc0602400/original", - "followers": 893, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4375fdbb0183362620daa629d9f830fb75c012ee", - "etherscanUrl": "https://etherscan.io/address/0x4375fdbb0183362620daa629d9f830fb75c012ee", - "xHandle": "claes_pancakes", - "xUrl": "https://twitter.com/claes_pancakes", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_17795", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "zelda", - "displayName": "ZELDA ", - "fid": 17795, - "pfpUrl": "https://i.imgur.com/D90Doa5.jpg", - "followers": 881, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe6a8a0b43efea3df38bfb2098483fcbd816110e2", - "etherscanUrl": "https://etherscan.io/address/0xe6a8a0b43efea3df38bfb2098483fcbd816110e2", - "xHandle": "zelda0xx", - "xUrl": "https://twitter.com/zelda0xx", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_21303", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ch0mx2.eth", - "displayName": "base.ch0mx2", - "fid": 21303, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/47a0b569-91cb-451c-017b-78656c0c3100/original", - "followers": 879, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x09623519507ca933b6867dd3dfb5fb5acce72f02", - "etherscanUrl": "https://etherscan.io/address/0x09623519507ca933b6867dd3dfb5fb5acce72f02", - "xHandle": "ch0mx2eth", - "xUrl": "https://twitter.com/ch0mx2eth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_257076", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "hunghung", - "displayName": "Hunghung", - "fid": 257076, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/48d48746-d207-4f37-32e9-667418877b00/original", - "followers": 872, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc11e939f32d60fbb602be14ced1e34c13cc31baf", - "etherscanUrl": "https://etherscan.io/address/0xc11e939f32d60fbb602be14ced1e34c13cc31baf", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_523201", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sirkarga", - "displayName": "sirkarga.base.eth", - "fid": 523201, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2b2e20b8-c881-4aef-ab46-eb31b5298c00/original", - "followers": 869, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbd968e23580c7094d2c97fd2c0d6f6340dd3a48a", - "etherscanUrl": "https://etherscan.io/address/0xbd968e23580c7094d2c97fd2c0d6f6340dd3a48a", - "xHandle": "sir_karga", - "xUrl": "https://twitter.com/sir_karga", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_396057", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mannaushack", - "displayName": "Mannaushack 🧬", - "fid": 396057, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/be6bd6cc-5c3f-42fc-d7a1-448d5de0b900/original", - "followers": 866, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5b7a81ca93020988166f903194f8181f2d364c66", - "etherscanUrl": "https://etherscan.io/address/0x5b7a81ca93020988166f903194f8181f2d364c66", - "xHandle": "ohn363176", - "xUrl": "https://twitter.com/ohn363176", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_307738", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kaya2732", - "displayName": "Yakup Kaya", - "fid": 307738, - "pfpUrl": "https://i.imgur.com/9Adue7W.jpg", - "followers": 866, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x612a748c057bd9113f04a4c007e622ba876b3858", - "etherscanUrl": "https://etherscan.io/address/0x612a748c057bd9113f04a4c007e622ba876b3858", - "xHandle": "felsen533", - "xUrl": "https://twitter.com/felsen533", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_246414", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jialin", - "displayName": "Jialin 🎩", - "fid": 246414, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/afafc845-a1b6-4598-7f14-6083a5582f00/original", - "followers": 859, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5f220b30cb4000694cc91bce040e873e0905f0dc", - "etherscanUrl": "https://etherscan.io/address/0x5f220b30cb4000694cc91bce040e873e0905f0dc", - "xHandle": "0xjialin", - "xUrl": "https://twitter.com/0xjialin", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_881461", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "web3clown", - "displayName": "Web3clown_🤡", - "fid": 881461, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/21885e30-eb1a-4e53-efe7-1ba1e7cd5f00/rectcrop3", - "followers": 852, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2ebdf314a8e20b02d9b5f437ae86bb8b6996c9ba", - "etherscanUrl": "https://etherscan.io/address/0x2ebdf314a8e20b02d9b5f437ae86bb8b6996c9ba", - "xHandle": "almightyalani", - "xUrl": "https://twitter.com/almightyalani", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1009822", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0x0embryo.eth", - "displayName": "Embryo_dolphin.base.eth", - "fid": 1009822, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/8aed31ed-fa20-4458-1456-1dbd6d4cd600/original", - "followers": 852, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x47b31555b97479d9c307f7d318f8852bf30a0f85", - "etherscanUrl": "https://etherscan.io/address/0x47b31555b97479d9c307f7d318f8852bf30a0f85", - "xHandle": "embryo_eth", - "xUrl": "https://twitter.com/embryo_eth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_416057", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0xnisya", - "displayName": "Nadya Romana", - "fid": 416057, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ec4e9403-457d-461d-a79f-a1dc9f0db200/original", - "followers": 849, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x15c983e1a6a491353d1fd7c9ed99b2f61e214223", - "etherscanUrl": "https://etherscan.io/address/0x15c983e1a6a491353d1fd7c9ed99b2f61e214223", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_432783", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "huanghui", - "displayName": "JACK(互关)", - "fid": 432783, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3c5d1139-1480-4e1c-f38d-036c24208a00/original", - "followers": 833, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xed4e3a8aedcd0e42b8fa449456eab37356d304fc", - "etherscanUrl": "https://etherscan.io/address/0xed4e3a8aedcd0e42b8fa449456eab37356d304fc", - "xHandle": "huanghu41738753", - "xUrl": "https://twitter.com/huanghu41738753", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_188147", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "scvlk", - "displayName": "Valik", - "fid": 188147, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/73beb781-8142-481d-2626-d7463a703f00/original", - "followers": 829, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4a1d2a4e6ea90c1b695b4023fe040531e9fb1f98", - "etherscanUrl": "https://etherscan.io/address/0x4a1d2a4e6ea90c1b695b4023fe040531e9fb1f98", - "xHandle": "ingvalik", - "xUrl": "https://twitter.com/ingvalik", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1153754", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mr-r0b0t", - "displayName": "mr-r0b0t.farcaster.eth", - "fid": 1153754, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7a187ddf-e756-454e-3cf2-0f677b0d8200/original", - "followers": 816, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x86e36c9ba3c6a2542fd761bc2b4fd61a110ea6cd", - "etherscanUrl": "https://etherscan.io/address/0x86e36c9ba3c6a2542fd761bc2b4fd61a110ea6cd", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_410368", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "toms", - "displayName": "Toms 🎩", - "fid": 410368, - "pfpUrl": "https://i.imgur.com/cnklnz4.jpg", - "followers": 816, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x57893db4c6e78fb360f774670fa8582cc1c51777", - "etherscanUrl": "https://etherscan.io/address/0x57893db4c6e78fb360f774670fa8582cc1c51777", - "xHandle": "ronytokaeh", - "xUrl": "https://twitter.com/ronytokaeh", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_559100", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bajie888", - "displayName": "sunjie", - "fid": 559100, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b22163df-3813-4802-c47b-a2758ea54d00/original", - "followers": 812, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6dc2f543a5eaa736067b3b2c56d4d7f133d88634", - "etherscanUrl": "https://etherscan.io/address/0x6dc2f543a5eaa736067b3b2c56d4d7f133d88634", - "xHandle": "sunhaojie88", - "xUrl": "https://twitter.com/sunhaojie88", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_279015", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "lsliton13", - "displayName": "Ls Liton ", - "fid": 279015, - "pfpUrl": "https://i.imgur.com/8KlOgW6.jpg", - "followers": 809, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0ad003e52b3fd85bab5f2fd04a6039baf1128c02", - "etherscanUrl": "https://etherscan.io/address/0x0ad003e52b3fd85bab5f2fd04a6039baf1128c02", - "xHandle": "lsliton131", - "xUrl": "https://twitter.com/lsliton131", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_233898", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kennkenn32", - "displayName": "FarCastrator", - "fid": 233898, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6f948c36-28f6-4cd0-438c-df97d427f500/original", - "followers": 803, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xedbc1feaf8e6437c47975abe452debcf5ed8217a", - "etherscanUrl": "https://etherscan.io/address/0xedbc1feaf8e6437c47975abe452debcf5ed8217a", - "xHandle": "jason_rt89", - "xUrl": "https://twitter.com/jason_rt89", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_305972", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cozycliff", - "displayName": "base.eth", - "fid": 305972, - "pfpUrl": "https://ipfs.decentralized-content.com/ipfs/bafybeie73ub2xxin6r56uw7cd5fl3zgoklbfxwrfjrizwr5ygjt6rhwe4i", - "followers": 803, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3a239489ba260c7ae675b267d5adcb7525c287f3", - "etherscanUrl": "https://etherscan.io/address/0x3a239489ba260c7ae675b267d5adcb7525c287f3", - "xHandle": "pumpfunfren", - "xUrl": "https://twitter.com/pumpfunfren", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1134743", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0xbrendan.eth", - "displayName": "Brendan", - "fid": 1134743, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/577b4c97-280f-4975-ae93-350b22e9cb00/original", - "followers": 795, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x694c033bd2527a5d073582d1e53a92863424350a", - "etherscanUrl": "https://etherscan.io/address/0x694c033bd2527a5d073582d1e53a92863424350a", - "xHandle": "0xbrendaneth", - "xUrl": "https://twitter.com/0xbrendaneth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_294149", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "vitot212", - "displayName": "Vitot base.eth 🧬", - "fid": 294149, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9688fbb5-24b9-4db1-80af-2577b6c7cd00/original", - "followers": 791, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x521ff78d2c02e0d8f478c32cf6bb9876f825a94f", - "etherscanUrl": "https://etherscan.io/address/0x521ff78d2c02e0d8f478c32cf6bb9876f825a94f", - "xHandle": "loiska041286", - "xUrl": "https://twitter.com/loiska041286", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_465576", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "keziii", - "displayName": "keziii.eth", - "fid": 465576, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2cad18bd-7cb2-4330-2665-c107b1150100/original", - "followers": 791, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc06edb945b0b028e72bde38f27815f5fccfab5ae", - "etherscanUrl": "https://etherscan.io/address/0xc06edb945b0b028e72bde38f27815f5fccfab5ae", - "xHandle": "kaffinn", - "xUrl": "https://twitter.com/kaffinn", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1510", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "whenlambo", - "displayName": "WhenLambo🎩", - "fid": 1510, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/aae9980d-025b-447b-3f33-31e678bcb200/original", - "followers": 763, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2ad62b2daf76d3cbeaa6b6ac005b9ece405cb906", - "etherscanUrl": "https://etherscan.io/address/0x2ad62b2daf76d3cbeaa6b6ac005b9ece405cb906", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_235872", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "brizpaint.eth", - "displayName": "Brkz", - "fid": 235872, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/db1e2639-b572-48ce-b496-5b26b50d3200/original", - "followers": 762, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf982c7702ddc45dc37c46c6cd6613780f7aa26e3", - "etherscanUrl": "https://etherscan.io/address/0xf982c7702ddc45dc37c46c6cd6613780f7aa26e3", - "xHandle": "0xstarcheese", - "xUrl": "https://twitter.com/0xstarcheese", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_613345", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "victhorious", - "displayName": "Victhorious ", - "fid": 613345, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0ad162a9-e27c-4e6a-7190-c397f0faa200/rectcrop3", - "followers": 756, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6fab902ff33350ae6ecaa05d63e665521974ebb9", - "etherscanUrl": "https://etherscan.io/address/0x6fab902ff33350ae6ecaa05d63e665521974ebb9", - "xHandle": "victhoriou98702", - "xUrl": "https://twitter.com/victhoriou98702", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_270119", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "yaelahdredd", - "displayName": "Suri name", - "fid": 270119, - "pfpUrl": "https://i.imgur.com/HzIysoB.jpg", - "followers": 753, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb0446b010f840551b2707986b484062e05f5f2c0", - "etherscanUrl": "https://etherscan.io/address/0xb0446b010f840551b2707986b484062e05f5f2c0", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1094600", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "famez", - "displayName": "Famez", - "fid": 1094600, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ae0eb39c-3114-4fe5-7bc0-47a86cb8ea00/original", - "followers": 752, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa0887e35363b883b5bde9d59543a772c66b8b695", - "etherscanUrl": "https://etherscan.io/address/0xa0887e35363b883b5bde9d59543a772c66b8b695", - "xHandle": "heisfamez", - "xUrl": "https://twitter.com/heisfamez", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1044514", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cryptobaddie", - "displayName": "Joy", - "fid": 1044514, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1e43f678-f7a8-4797-9107-819179276600/original", - "followers": 752, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8f69c8eb92ed068aa577ce1847d568b39b0d9ebf", - "etherscanUrl": "https://etherscan.io/address/0x8f69c8eb92ed068aa577ce1847d568b39b0d9ebf", - "xHandle": "cryptobaddie___", - "xUrl": "https://twitter.com/cryptobaddie___", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_888823", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sethnuno00", - "displayName": "Luís Fernandes", - "fid": 888823, - "pfpUrl": "https://api.npc.nexus/v1/storage/buckets/6550e3a8c8d1568fe219/files/888823/preview?project=npcnexus&date=5867388", - "followers": 750, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x55baf79137628aad1c502a8caba7d67df38cd7dc", - "etherscanUrl": "https://etherscan.io/address/0x55baf79137628aad1c502a8caba7d67df38cd7dc", - "xHandle": "nunoseth25", - "xUrl": "https://twitter.com/nunoseth25", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_253373", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "!253373", - "displayName": "Iagoribeiro32", - "fid": 253373, - "pfpUrl": "https://arweave.net/OX0HWun1T1PzxlK_qsbmOhzUm0-3CAWfmFSG_EKIC3c/", - "followers": 747, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6cebbedc781f24720d91747e1dd62ef926848bed", - "etherscanUrl": "https://etherscan.io/address/0x6cebbedc781f24720d91747e1dd62ef926848bed", - "xHandle": "iagoribeiro3400", - "xUrl": "https://twitter.com/iagoribeiro3400", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_428515", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mohamedadam", - "displayName": "Mohamed Adam", - "fid": 428515, - "pfpUrl": "https://beb-public.s3.us-west-1.amazonaws.com/blue.jpg", - "followers": 743, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x491d1cb0fe5cb997961b93db53eb6f57e340db7e", - "etherscanUrl": "https://etherscan.io/address/0x491d1cb0fe5cb997961b93db53eb6f57e340db7e", - "xHandle": "mohamed99798592", - "xUrl": "https://twitter.com/mohamed99798592", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_188723", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "woodybuzz", - "displayName": "woody", - "fid": 188723, - "pfpUrl": "https://i.imgur.com/cLnggmn.jpg", - "followers": 742, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9d2a30d01980401573f21eb82ea6d0121b849cf0", - "etherscanUrl": "https://etherscan.io/address/0x9d2a30d01980401573f21eb82ea6d0121b849cf0", - "xHandle": "woodyybuzz8", - "xUrl": "https://twitter.com/woodyybuzz8", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_211120", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kasutjepang", - "displayName": "Anca 🎩", - "fid": 211120, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/780eeb86-e9bf-4bbc-bd8d-e4e4b6499500/original", - "followers": 733, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1494fe694582addadaa4901110824d955f7e6005", - "etherscanUrl": "https://etherscan.io/address/0x1494fe694582addadaa4901110824d955f7e6005", - "xHandle": "moonesown", - "xUrl": "https://twitter.com/moonesown", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_459422", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "whalle89", - "displayName": "Whalle89☑️", - "fid": 459422, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/381d091d-530e-4d90-1ff5-b7d17b7f3500/original", - "followers": 730, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x807f6b351ecb861bf1eb92d1cbc42187f0be8c5b", - "etherscanUrl": "https://etherscan.io/address/0x807f6b351ecb861bf1eb92d1cbc42187f0be8c5b", - "xHandle": "whallekoe", - "xUrl": "https://twitter.com/whallekoe", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_847065", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "uforgottenkai", - "displayName": "UnforgottenKai", - "fid": 847065, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2f6ae88a-2289-4ea7-612f-b44f70d36000/original", - "followers": 725, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x063d4bdf79780b423f01ffaf57d48433c1e994bd", - "etherscanUrl": "https://etherscan.io/address/0x063d4bdf79780b423f01ffaf57d48433c1e994bd", - "xHandle": "unforgottenkaii", - "xUrl": "https://twitter.com/unforgottenkaii", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_265921", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "aldoendaray", - "displayName": "aldoendaray.base.eth", - "fid": 265921, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e8bec14d-4bf1-42f8-89f8-1da020789200/original", - "followers": 725, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6514ebd5fcac0cd37bbda53b6f972e8335a38d5d", - "etherscanUrl": "https://etherscan.io/address/0x6514ebd5fcac0cd37bbda53b6f972e8335a38d5d", - "xHandle": "endaaldo99", - "xUrl": "https://twitter.com/endaaldo99", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_330134", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "pikabu123", - "displayName": "Pikabu123 ⛓️🍖🎭⚡", - "fid": 330134, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/30c03fae-b10b-4a3c-f054-2986d91b4200/rectcrop3", - "followers": 721, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6cdd8ac8fa6a9276c5465fdbfac29c205fb28183", - "etherscanUrl": "https://etherscan.io/address/0x6cdd8ac8fa6a9276c5465fdbfac29c205fb28183", - "xHandle": "abdul88992729", - "xUrl": "https://twitter.com/abdul88992729", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1095717", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ephorie", - "displayName": "DropSeeker.base.eth", - "fid": 1095717, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/17345bfa-d8d7-4bc6-b953-714f83a38300/original", - "followers": 715, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd7576733e2fba37f616613b0813c75519e67ec3e", - "etherscanUrl": "https://etherscan.io/address/0xd7576733e2fba37f616613b0813c75519e67ec3e", - "xHandle": "d_euforie", - "xUrl": "https://twitter.com/d_euforie", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_637563", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "serhio91", - "displayName": "Serhii Vlasiuk", - "fid": 637563, - "pfpUrl": "https://beb-public.s3.us-west-1.amazonaws.com/black.jpg", - "followers": 715, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa1f82409992616619cb57efb4fe04a22a6d02cd9", - "etherscanUrl": "https://etherscan.io/address/0xa1f82409992616619cb57efb4fe04a22a6d02cd9", - "xHandle": "sergio91436017", - "xUrl": "https://twitter.com/sergio91436017", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_254395", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "xxone", - "displayName": "xxone.eth 🧬", - "fid": 254395, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/99e67912-8c18-4cf1-d2a9-a73c60e71400/original", - "followers": 709, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd11f5b5c547633f9a0d1a8c92b13f1a76041e316", - "etherscanUrl": "https://etherscan.io/address/0xd11f5b5c547633f9a0d1a8c92b13f1a76041e316", - "xHandle": "ptesnet", - "xUrl": "https://twitter.com/ptesnet", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_314062", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "pktdat.eth", - "displayName": "PKTDat", - "fid": 314062, - "pfpUrl": "https://ipfs.decentralized-content.com/ipfs/QmYd2J36s7y5gZaY227PAZ9UBqfXsyo9ZbSM1rRsDgZU4B/244.webp", - "followers": 708, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2585247ddeb117710094111906aa2d1ff223a032", - "etherscanUrl": "https://etherscan.io/address/0x2585247ddeb117710094111906aa2d1ff223a032", - "xHandle": "pktdat", - "xUrl": "https://twitter.com/pktdat", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_303199", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "lsliton131", - "displayName": "Islamic Post", - "fid": 303199, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ace3af97-2ead-4d95-11f3-b638fc043e00/rectcrop3", - "followers": 707, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x67c8bc26811effd5a8bf5b41cbde57c6acf2a779", - "etherscanUrl": "https://etherscan.io/address/0x67c8bc26811effd5a8bf5b41cbde57c6acf2a779", - "xHandle": "anhamoni12", - "xUrl": "https://twitter.com/anhamoni12", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_975092", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "basedguymeme", - "displayName": "Just a based guy", - "fid": 975092, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3f88af20-7d88-484e-d739-e11c702b1c00/rectcrop3", - "followers": 704, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x07521a5bdee07100a574bc82ab4cd54ad75855ba", - "etherscanUrl": "https://etherscan.io/address/0x07521a5bdee07100a574bc82ab4cd54ad75855ba", - "xHandle": "basedguymeme", - "xUrl": "https://twitter.com/basedguymeme", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_295733", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "gregarious", - "displayName": "Gregarious", - "fid": 295733, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/46103934-4faf-4c3e-4c9a-d38911837a00/rectcrop3", - "followers": 703, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8250100587b3e415771025fc26d3f281a320dfaa", - "etherscanUrl": "https://etherscan.io/address/0x8250100587b3e415771025fc26d3f281a320dfaa", - "xHandle": "gregarious", - "xUrl": "https://twitter.com/gregarious", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_500954", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kaiser75", - "displayName": "KAISER_KABIR", - "fid": 500954, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f3c44090-68e3-4baa-ed0c-fe185cc23300/rectcrop3", - "followers": 701, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x110eda6baa872a97399254e5a1e654593e591ff7", - "etherscanUrl": "https://etherscan.io/address/0x110eda6baa872a97399254e5a1e654593e591ff7", - "xHandle": "kaiserkabi63601", - "xUrl": "https://twitter.com/kaiserkabi63601", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_261532", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "indexyz", - "displayName": "index🎩", - "fid": 261532, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ff4d7c85-6452-480a-a5c0-f711a59b2000/rectcrop3", - "followers": 699, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcb7e07b7a78bd620a581b4224915b39384e30405", - "etherscanUrl": "https://etherscan.io/address/0xcb7e07b7a78bd620a581b4224915b39384e30405", - "xHandle": "cyber_lii", - "xUrl": "https://twitter.com/cyber_lii", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1071109", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "raples", - "displayName": "Raples 🟦", - "fid": 1071109, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3321b371-d649-4698-3e9c-3da46d455700/original", - "followers": 697, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfd7cc01fa99c57e840fd55fcde340ebb56d35277", - "etherscanUrl": "https://etherscan.io/address/0xfd7cc01fa99c57e840fd55fcde340ebb56d35277", - "xHandle": "cast_69k", - "xUrl": "https://twitter.com/cast_69k", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_254812", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "naura", - "displayName": "Naurachan", - "fid": 254812, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/bf60bedc-e1ed-4c93-9cdb-26bcc098cf00/original", - "followers": 696, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcdf959365fc0d2e804f928bab31c66092d3dcd1f", - "etherscanUrl": "https://etherscan.io/address/0xcdf959365fc0d2e804f928bab31c66092d3dcd1f", - "xHandle": "canendu", - "xUrl": "https://twitter.com/canendu", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_327851", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "atakco", - "displayName": "FAKE Drop", - "fid": 327851, - "pfpUrl": "https://i.imgur.com/rQS6Nlg.jpg", - "followers": 695, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x61e0880fee21c516748177ce17c186a6ed16cd90", - "etherscanUrl": "https://etherscan.io/address/0x61e0880fee21c516748177ce17c186a6ed16cd90", - "xHandle": "igede_wl66a", - "xUrl": "https://twitter.com/igede_wl66a", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1328775", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "byrneybabes.base.eth", - "displayName": "Amy byrne", - "fid": 1328775, - "pfpUrl": "https://res.cloudinary.com/base-app/image/upload/f_auto/v1763144927/5db9d865-da49-40d3-a3d9-7b2d95264003.heic", - "followers": 680, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc299f390a84eec4756d27c187116f539005942f6", - "etherscanUrl": "https://etherscan.io/address/0xc299f390a84eec4756d27c187116f539005942f6", - "xHandle": "peachy_fyr", - "xUrl": "https://twitter.com/peachy_fyr", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_252259", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "skygims", - "displayName": "skygims.base.eth", - "fid": 252259, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/98ea859a-47bd-43c9-8e20-165b4be95600/original", - "followers": 679, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5bface33343c354d4f577f4ebe29e20b1c691ba9", - "etherscanUrl": "https://etherscan.io/address/0x5bface33343c354d4f577f4ebe29e20b1c691ba9", - "xHandle": "skygims", - "xUrl": "https://twitter.com/skygims", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_19098", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "romio.eth", - "displayName": "Sanjib 🔵 🎩", - "fid": 19098, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/941f6a9e-1362-4d51-f443-f08580158200/original", - "followers": 672, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x68d4245114cc22a537c39143d032a646e77032e8", - "etherscanUrl": "https://etherscan.io/address/0x68d4245114cc22a537c39143d032a646e77032e8", - "xHandle": "basaksanjib1992", - "xUrl": "https://twitter.com/basaksanjib1992", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_313461", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0xsid", - "displayName": "Sid 🎩", - "fid": 313461, - "pfpUrl": "https://i.imgur.com/hfgltuV.png", - "followers": 671, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcf50b513cc0f6770564cb0d294f155243dd24285", - "etherscanUrl": "https://etherscan.io/address/0xcf50b513cc0f6770564cb0d294f155243dd24285", - "xHandle": "0xsidlikespizza", - "xUrl": "https://twitter.com/0xsidlikespizza", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_347638", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "naufaljon", - "displayName": "nathhan", - "fid": 347638, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3ae29f73-5a16-4393-cc56-1fc900455900/original", - "followers": 662, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0aded78d1320cb28870c4401adc3614427c99ba7", - "etherscanUrl": "https://etherscan.io/address/0x0aded78d1320cb28870c4401adc3614427c99ba7", - "xHandle": "naufaljonzora", - "xUrl": "https://twitter.com/naufaljonzora", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_20435", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "valent1ne", - "displayName": "VALent1NE", - "fid": 20435, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0e845ddd-5f0f-433c-1c23-f7affd106a00/rectcrop3", - "followers": 660, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7898a24962078105f14f45f61de71edcbc3892ff", - "etherscanUrl": "https://etherscan.io/address/0x7898a24962078105f14f45f61de71edcbc3892ff", - "xHandle": "valent1ne_eth_", - "xUrl": "https://twitter.com/valent1ne_eth_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_292215", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "rean", - "displayName": "Rehan", - "fid": 292215, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/8ac54b16-20cf-4e1a-fb8f-7f3ebe1c2600/original", - "followers": 660, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2c22005be108d407eaea058351190a8cef67141f", - "etherscanUrl": "https://etherscan.io/address/0x2c22005be108d407eaea058351190a8cef67141f", - "xHandle": "xoxoapee", - "xUrl": "https://twitter.com/xoxoapee", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_471872", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "babuy", - "displayName": "Skyeth 🧬", - "fid": 471872, - "pfpUrl": "https://i.imgur.com/VvCtQvy.jpg", - "followers": 659, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1101ca6276bcea1665705bee0593a9ecd455a96d", - "etherscanUrl": "https://etherscan.io/address/0x1101ca6276bcea1665705bee0593a9ecd455a96d", - "xHandle": "sky_eth77", - "xUrl": "https://twitter.com/sky_eth77", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_411541", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dieznutz83", - "displayName": "Dieznutz83 🔵", - "fid": 411541, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7102836c-cb0a-431d-68f8-8e2c87fc9800/original", - "followers": 657, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4b72be51725686bde5763aacbe9c73a6780a4317", - "etherscanUrl": "https://etherscan.io/address/0x4b72be51725686bde5763aacbe9c73a6780a4317", - "xHandle": "dieznutz83", - "xUrl": "https://twitter.com/dieznutz83", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_279380", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "taiwotona", - "displayName": "pqrsixnine.moca", - "fid": 279380, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/282e876c-0407-4680-7a4d-5eea25290100/original", - "followers": 649, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7bb4aa3d1a1091f83af89c13b030ab2800b6b0db", - "etherscanUrl": "https://etherscan.io/address/0x7bb4aa3d1a1091f83af89c13b030ab2800b6b0db", - "xHandle": "tona_taiwo", - "xUrl": "https://twitter.com/tona_taiwo", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_899401", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "hortyspace917", - "displayName": "vonguard🧬", - "fid": 899401, - "pfpUrl": "https://res.cloudinary.com/base-app/image/upload/f_auto/v1759038490/image_uploads/356073e5-ff84-4260-bbf8-43b51c80e64c.jpg", - "followers": 649, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4d09d71195cf6c155c9a1a14dcc1d568d9037878", - "etherscanUrl": "https://etherscan.io/address/0x4d09d71195cf6c155c9a1a14dcc1d568d9037878", - "xHandle": "harrisonhensha", - "xUrl": "https://twitter.com/harrisonhensha", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_383144", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bfix.eth", - "displayName": "bfix.eth🎩🍖", - "fid": 383144, - "pfpUrl": "https://ipfs.decentralized-content.com/ipfs/bafybeihlond74ij2vbzyuagma2uxtv2b7e4nmty6ujxbapqopsarzy3yo4/631.png", - "followers": 641, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xae1114e5f1c9db6252fb19601d903dd9841d988d", - "etherscanUrl": "https://etherscan.io/address/0xae1114e5f1c9db6252fb19601d903dd9841d988d", - "xHandle": "bigdfix", - "xUrl": "https://twitter.com/bigdfix", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1115503", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "johnbosco2006", - "displayName": "shinkaffi", - "fid": 1115503, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f4d3e8b6-d203-4057-5d1c-fd54cf268c00/original", - "followers": 639, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1f1de378c7752fe6c4109d41a0dde218337138ee", - "etherscanUrl": "https://etherscan.io/address/0x1f1de378c7752fe6c4109d41a0dde218337138ee", - "xHandle": "enenetuei_john", - "xUrl": "https://twitter.com/enenetuei_john", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_895977", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "lordsirius", - "displayName": "LordSirius", - "fid": 895977, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c5a577a8-87a3-4c14-edfb-f4270f12b000/original", - "followers": 636, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x46ff51a6bd61bcfe38a3ebf6557259ab4f374a23", - "etherscanUrl": "https://etherscan.io/address/0x46ff51a6bd61bcfe38a3ebf6557259ab4f374a23", - "xHandle": "l0rds1rius", - "xUrl": "https://twitter.com/l0rds1rius", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_737077", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "robinhood01", - "displayName": "abella.", - "fid": 737077, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6fa98038-eeb6-43ff-e855-22cf9cd37e00/original", - "followers": 627, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x091c3c64da534e7daed90ce850be13571dc5baf4", - "etherscanUrl": "https://etherscan.io/address/0x091c3c64da534e7daed90ce850be13571dc5baf4", - "xHandle": "024trumpet", - "xUrl": "https://twitter.com/024trumpet", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1049927", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cartoonmeseries", - "displayName": "RealRobWood.xyz", - "fid": 1049927, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3f4678b4-5f95-492c-afab-8eae5a982900/original", - "followers": 624, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4ba58317b1dd49a050668e7fcbf3569fba6b97b9", - "etherscanUrl": "https://etherscan.io/address/0x4ba58317b1dd49a050668e7fcbf3569fba6b97b9", - "xHandle": "realrobwoodxyz", - "xUrl": "https://twitter.com/realrobwoodxyz", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_682620", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "saiful29", - "displayName": "SAIFUL ISLAM", - "fid": 682620, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3e2943b9-92df-4afe-9827-ebee74679a00/original", - "followers": 622, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdfda5ed13703f94dbefb7356184cdb70c7dddcd6", - "etherscanUrl": "https://etherscan.io/address/0xdfda5ed13703f94dbefb7356184cdb70c7dddcd6", - "xHandle": "saydur231", - "xUrl": "https://twitter.com/saydur231", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_559885", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dillirajagiri", - "displayName": "Dilli Rajagiri ", - "fid": 559885, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9038bebd-8ba3-4c88-969d-bc0a114c1c00/rectcrop3", - "followers": 619, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x45069e40188b0a315b6e06ffb97baef9c37c214b", - "etherscanUrl": "https://etherscan.io/address/0x45069e40188b0a315b6e06ffb97baef9c37c214b", - "xHandle": "rajagiri_dilli", - "xUrl": "https://twitter.com/rajagiri_dilli", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_12034", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jennifertran", - "displayName": "Jennifer Tran", - "fid": 12034, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/77f0650e-118f-4d7d-0476-d4d00639b600/original", - "followers": 619, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4f1a2cd5b57f7504c9dd69a2089cb294934e649d", - "etherscanUrl": "https://etherscan.io/address/0x4f1a2cd5b57f7504c9dd69a2089cb294934e649d", - "xHandle": "jkim_tran", - "xUrl": "https://twitter.com/jkim_tran", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_476552", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "frogish", - "displayName": "Frogish", - "fid": 476552, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6001f839-49d3-4d0b-ad3a-37c6fa181300/rectcrop3", - "followers": 618, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4e7987e413caaf8dbca3cf8d4a372cc260385078", - "etherscanUrl": "https://etherscan.io/address/0x4e7987e413caaf8dbca3cf8d4a372cc260385078", - "xHandle": "frogish2025", - "xUrl": "https://twitter.com/frogish2025", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_253895", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sirpent", - "displayName": "Pent", - "fid": 253895, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ff208b93-8cd6-48de-e367-00741df03f00/original", - "followers": 617, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbccb437572030bbf9f18c196bdb58cc2a92efca3", - "etherscanUrl": "https://etherscan.io/address/0xbccb437572030bbf9f18c196bdb58cc2a92efca3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_420816", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "yigitm", - "displayName": "0xyigit.base.eth", - "fid": 420816, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/43c5b974-4342-410d-7147-48f98d94c100/original", - "followers": 607, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa69a63496296e6b04777ad41f6d3605bbeb574d1", - "etherscanUrl": "https://etherscan.io/address/0xa69a63496296e6b04777ad41f6d3605bbeb574d1", - "xHandle": "yigitm44", - "xUrl": "https://twitter.com/yigitm44", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_252942", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ariwibowo", - "displayName": "Pendekar212", - "fid": 252942, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/05d1745f-c244-4db4-3073-370665021200/original", - "followers": 605, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf9450615b4aed29060d5125a5b3dd198ce1b6cc8", - "etherscanUrl": "https://etherscan.io/address/0xf9450615b4aed29060d5125a5b3dd198ce1b6cc8", - "xHandle": "koweasu05", - "xUrl": "https://twitter.com/koweasu05", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1310854", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "token-mara1", - "displayName": "Movies-Onchain.Base🟦", - "fid": 1310854, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d2cb1440-05b8-4e0e-b4f5-fe225b16a900/original", - "followers": 604, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0f801a683fe74551329544b86e648c476cb4439f", - "etherscanUrl": "https://etherscan.io/address/0x0f801a683fe74551329544b86e648c476cb4439f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1113654", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "firmjeff", - "displayName": "Jeffrey (Computer Operator)", - "fid": 1113654, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/cc49a4b0-7d3a-4e1e-e294-6754aad60c00/original", - "followers": 602, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7f48ae30095013130bf181773a2c360722c29242", - "etherscanUrl": "https://etherscan.io/address/0x7f48ae30095013130bf181773a2c360722c29242", - "xHandle": "thefirmjeff", - "xUrl": "https://twitter.com/thefirmjeff", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_260571", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "arshaka", - "displayName": "Arshaka Virendra 🎩🔄 👾", - "fid": 260571, - "pfpUrl": "https://i.imgur.com/8OuTBLL.jpg", - "followers": 598, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xaf58b86c9b5d5bb0323adfbe3fd6670d48e7e717", - "etherscanUrl": "https://etherscan.io/address/0xaf58b86c9b5d5bb0323adfbe3fd6670d48e7e717", - "xHandle": "arshaka66", - "xUrl": "https://twitter.com/arshaka66", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_483197", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "semijon", - "displayName": "🥜SeⓂ️ira 📺", - "fid": 483197, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6d00878d-f866-41af-842e-decde6da5300/rectcrop3", - "followers": 597, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x46b6063453a1f197e86ac646c583be3b31b7aa13", - "etherscanUrl": "https://etherscan.io/address/0x46b6063453a1f197e86ac646c583be3b31b7aa13", - "xHandle": "semi4540406736", - "xUrl": "https://twitter.com/semi4540406736", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_530090", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "andreapn.eth", - "displayName": "andreapn.base.eth", - "fid": 530090, - "pfpUrl": "https://res.cloudinary.com/base-app/image/upload/f_auto/v1758205323/image_uploads/1f9f014f-41e4-4ce6-8f49-85082e83e70e.jpg", - "followers": 589, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4c37a5ff1096f1d5da7fa6f77a053139e8a2ba47", - "etherscanUrl": "https://etherscan.io/address/0x4c37a5ff1096f1d5da7fa6f77a053139e8a2ba47", - "xHandle": "andreapn_", - "xUrl": "https://twitter.com/andreapn_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_275597", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cnnid69", - "displayName": "ainurrof1", - "fid": 275597, - "pfpUrl": "https://i.imgur.com/o3fYTSF.jpg", - "followers": 587, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4e298dffa818ba95ec1dde1eace83bc1d844ad51", - "etherscanUrl": "https://etherscan.io/address/0x4e298dffa818ba95ec1dde1eace83bc1d844ad51", - "xHandle": "kaaddos22", - "xUrl": "https://twitter.com/kaaddos22", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_250760", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "obbyrobs", - "displayName": "obbyrobs.base.eth🐹🔵", - "fid": 250760, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/8708a9e9-d445-4364-6ad4-b2c5efbaab00/original", - "followers": 576, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd128db97095c59591621748439e42e6ad3eedb2d", - "etherscanUrl": "https://etherscan.io/address/0xd128db97095c59591621748439e42e6ad3eedb2d", - "xHandle": "xaxurura", - "xUrl": "https://twitter.com/xaxurura", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_191231", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ruthn", - "displayName": "Ruthn", - "fid": 191231, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9fefbd69-a757-4915-838c-643379d5dc00/original", - "followers": 575, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7e179e77a0c7ba04ac0d02e136c9d6516660c80e", - "etherscanUrl": "https://etherscan.io/address/0x7e179e77a0c7ba04ac0d02e136c9d6516660c80e", - "xHandle": "goodp1ne", - "xUrl": "https://twitter.com/goodp1ne", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_252262", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "lucx", - "displayName": "Lucwishmeluc", - "fid": 252262, - "pfpUrl": "https://i.imgur.com/5EPt7XQ.jpg", - "followers": 574, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x273e2034e6f71646b72209fa9a5e827a725a4911", - "etherscanUrl": "https://etherscan.io/address/0x273e2034e6f71646b72209fa9a5e827a725a4911", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_20086", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "punkrock", - "displayName": "punkrock77.eth🎱🐉⚔", - "fid": 20086, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4dc75321-bdce-44f6-6b4d-ecb098177600/original", - "followers": 573, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd33a0f908d3f88f19f20840c12bfa47537bb3555", - "etherscanUrl": "https://etherscan.io/address/0xd33a0f908d3f88f19f20840c12bfa47537bb3555", - "xHandle": "callme_punkrock", - "xUrl": "https://twitter.com/callme_punkrock", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1049293", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "tandeptrai21", - "displayName": "Hoàng Nhật Tân", - "fid": 1049293, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3dca4a22-7e8a-4a51-6006-8e3120593800/rectcrop3", - "followers": 573, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3f0198857089a0badc8aeff87e74f47f6e6923df", - "etherscanUrl": "https://etherscan.io/address/0x3f0198857089a0badc8aeff87e74f47f6e6923df", - "xHandle": "hoangtan99", - "xUrl": "https://twitter.com/hoangtan99", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_300245", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "fefex", - "displayName": "iZiKi🔵🎩", - "fid": 300245, - "pfpUrl": "https://i.imgur.com/FAon8So.jpg", - "followers": 571, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x85f1a63a2b7259dd4fdc84e146f006faa2d40819", - "etherscanUrl": "https://etherscan.io/address/0x85f1a63a2b7259dd4fdc84e146f006faa2d40819", - "xHandle": "sarc_gg", - "xUrl": "https://twitter.com/sarc_gg", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1344157", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "shubs", - "displayName": ".Shubs", - "fid": 1344157, - "pfpUrl": "https://beb-public.s3.us-west-1.amazonaws.com/black.jpg", - "followers": 567, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3011e765574d1c7c53f0b2a01b18bb3b40af7452", - "etherscanUrl": "https://etherscan.io/address/0x3011e765574d1c7c53f0b2a01b18bb3b40af7452", - "xHandle": "motunrayokejii", - "xUrl": "https://twitter.com/motunrayokejii", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_716335", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "00999", - "displayName": "aijaz ahmed", - "fid": 716335, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4c5c25fb-dbd4-42ac-c021-d09a7e2ba200/rectcrop3", - "followers": 566, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x21b2850c3973cde3d2302231572efef5d206bcdd", - "etherscanUrl": "https://etherscan.io/address/0x21b2850c3973cde3d2302231572efef5d206bcdd", - "xHandle": "engnraijaz", - "xUrl": "https://twitter.com/engnraijaz", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_292004", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "hajicool", - "displayName": "Zazy 🌹🌹🌹🌹", - "fid": 292004, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/55150a4c-7843-45fe-d05a-03a1d630bd00/rectcrop3", - "followers": 564, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xee399b329030b8037ff5f5a41c567a9a37d4caea", - "etherscanUrl": "https://etherscan.io/address/0xee399b329030b8037ff5f5a41c567a9a37d4caea", - "xHandle": "hajisocool", - "xUrl": "https://twitter.com/hajisocool", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_868582", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "solomid94", - "displayName": "Naji Huzairin", - "fid": 868582, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9ee33fef-b6ab-4552-3ded-73c37b80b100/rectcrop3", - "followers": 559, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2b6421df4926be5dd6706d3604d659ccda55ff26", - "etherscanUrl": "https://etherscan.io/address/0x2b6421df4926be5dd6706d3604d659ccda55ff26", - "xHandle": "simianliner", - "xUrl": "https://twitter.com/simianliner", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_496999", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "rajanasuti", - "displayName": "Omnibase.L2", - "fid": 496999, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/25532579-c419-46a8-234a-9f2f6e2b9000/original", - "followers": 558, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2348ec63372c35c41be8ad5634d22e6b4e3557e4", - "etherscanUrl": "https://etherscan.io/address/0x2348ec63372c35c41be8ad5634d22e6b4e3557e4", - "xHandle": "qbxowo", - "xUrl": "https://twitter.com/qbxowo", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_894317", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "fmmm10qe0", - "displayName": "Raheleh 🧬🔵✨️", - "fid": 894317, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/cb2ef7b9-f028-481b-1cf2-401ab3f13b00/rectcrop3", - "followers": 551, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdfcfc3115240f5b0e4a5cdfbe4caa5646dee6f79", - "etherscanUrl": "https://etherscan.io/address/0xdfcfc3115240f5b0e4a5cdfbe4caa5646dee6f79", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_358939", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "godsticky.eth", - "displayName": "sticky 🐉", - "fid": 358939, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b8ff6046-e9e3-47f0-1755-63ceaec0a100/original", - "followers": 548, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4e678d4b2ec0d93e43f9b0eb0785da8269be9a5e", - "etherscanUrl": "https://etherscan.io/address/0x4e678d4b2ec0d93e43f9b0eb0785da8269be9a5e", - "xHandle": "alxstai", - "xUrl": "https://twitter.com/alxstai", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_326807", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "exellance", - "displayName": "Cüneyt Yarçın", - "fid": 326807, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b30e729c-cc9a-4c1d-b72f-b69c6afff700/original", - "followers": 548, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x35c52123023eb225aa12b4e98aa6838dcf21d85f", - "etherscanUrl": "https://etherscan.io/address/0x35c52123023eb225aa12b4e98aa6838dcf21d85f", - "xHandle": "cuneyt_yarc", - "xUrl": "https://twitter.com/cuneyt_yarc", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1015735", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "theneetguy.eth", - "displayName": "The Neet Guy", - "fid": 1015735, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/820c1a61-94b0-4ad0-b74f-eb2066a82c00/original", - "followers": 547, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9db704bae6a8f4582cd79b3a6eb88211aad936b3", - "etherscanUrl": "https://etherscan.io/address/0x9db704bae6a8f4582cd79b3a6eb88211aad936b3", - "xHandle": "theneetguy", - "xUrl": "https://twitter.com/theneetguy", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_256068", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jarullyzard", - "displayName": "Jarule.base.eth 🐹", - "fid": 256068, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4c8d172c-fc06-47de-52d0-17ac3de14000/original", - "followers": 546, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe39c2c8986e5d2ecc0d51baa33198853ae5a8062", - "etherscanUrl": "https://etherscan.io/address/0xe39c2c8986e5d2ecc0d51baa33198853ae5a8062", - "xHandle": "bilintikcrew", - "xUrl": "https://twitter.com/bilintikcrew", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_266121", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "vharozes", - "displayName": "Vharosdiana base.eth", - "fid": 266121, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/69d3019d-a952-4027-f0dc-d45cce99e500/original", - "followers": 545, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9e73c24af3a955a52a9a95116ba40a7c3bc8321f", - "etherscanUrl": "https://etherscan.io/address/0x9e73c24af3a955a52a9a95116ba40a7c3bc8321f", - "xHandle": "hernirepina", - "xUrl": "https://twitter.com/hernirepina", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_19327", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "timmykwesi.eth", - "displayName": "Timmy🎩🐹⬆🕵️‍♂️", - "fid": 19327, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/12ee8741-4e06-4d97-bc88-8e7288736200/original", - "followers": 545, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe3185fd2e5010271a0e866be070addc073713734", - "etherscanUrl": "https://etherscan.io/address/0xe3185fd2e5010271a0e866be070addc073713734", - "xHandle": "timmycwesi", - "xUrl": "https://twitter.com/timmycwesi", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_203988", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "shuaixiaohuo", - "displayName": "Xiaoxiaoniao", - "fid": 203988, - "pfpUrl": "https://i.imgur.com/two8Idi.jpg", - "followers": 544, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa244487be81c89c0d82000e88af2fc783384597f", - "etherscanUrl": "https://etherscan.io/address/0xa244487be81c89c0d82000e88af2fc783384597f", - "xHandle": "pycaq", - "xUrl": "https://twitter.com/pycaq", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_917404", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "otorolabs", - "displayName": "Otoro", - "fid": 917404, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/180bb96f-eb03-4382-7fb8-2c3e791eb200/original", - "followers": 543, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf7e61417b1613da1fcf8bea19ebfa6a229668a5d", - "etherscanUrl": "https://etherscan.io/address/0xf7e61417b1613da1fcf8bea19ebfa6a229668a5d", - "xHandle": "239ailabs", - "xUrl": "https://twitter.com/239ailabs", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_258648", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "arissp", - "displayName": "Aris.base.eth", - "fid": 258648, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4d70bee4-c1dc-4546-7801-97d333cc5400/rectcrop3", - "followers": 533, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x098d70be405dd3e5ae63a40ca26928aadcf838bb", - "etherscanUrl": "https://etherscan.io/address/0x098d70be405dd3e5ae63a40ca26928aadcf838bb", - "xHandle": "arissp07", - "xUrl": "https://twitter.com/arissp07", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_260670", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mmarifrzk", - "displayName": "Ngotskuy", - "fid": 260670, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f5577403-0656-4871-12aa-2d4ab14e0400/original", - "followers": 533, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc9ed1aaf1d6f7354758c90e149205588189ecc34", - "etherscanUrl": "https://etherscan.io/address/0xc9ed1aaf1d6f7354758c90e149205588189ecc34", - "xHandle": "angots666", - "xUrl": "https://twitter.com/angots666", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_256783", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "donny2704", - "displayName": "Donny Saputra", - "fid": 256783, - "pfpUrl": "https://i.imgur.com/4w9mfkq.jpg", - "followers": 531, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfee6fed4f67a765ca454fa006f65a2aeb83e53fa", - "etherscanUrl": "https://etherscan.io/address/0xfee6fed4f67a765ca454fa006f65a2aeb83e53fa", - "xHandle": "donny2704", - "xUrl": "https://twitter.com/donny2704", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1090713", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0xbigb.eth", - "displayName": "大B哥|🟣等福报中", - "fid": 1090713, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b23673ab-4fba-42b3-0659-2e43145d7800/original", - "followers": 525, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0809d0ecc6353228b47f0c801b20b61b2d01db80", - "etherscanUrl": "https://etherscan.io/address/0x0809d0ecc6353228b47f0c801b20b61b2d01db80", - "xHandle": "sillydragon4488", - "xUrl": "https://twitter.com/sillydragon4488", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_257033", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jazy", - "displayName": "Jazz.eth", - "fid": 257033, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/41aaf364-067b-469e-704d-c0c8955fb900/original", - "followers": 522, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfb13a89515102abf2e66f6682f91f65d43dc5334", - "etherscanUrl": "https://etherscan.io/address/0xfb13a89515102abf2e66f6682f91f65d43dc5334", - "xHandle": "davidtrumpeth", - "xUrl": "https://twitter.com/davidtrumpeth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1117360", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "shifat007", - "displayName": "SHIFAT 🧬", - "fid": 1117360, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7f33282f-edc1-4e05-5a56-bb000892a400/original", - "followers": 521, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4d2fff566f7c2f6c620bb3826f65ec7d4c970190", - "etherscanUrl": "https://etherscan.io/address/0x4d2fff566f7c2f6c620bb3826f65ec7d4c970190", - "xHandle": "shifat01644", - "xUrl": "https://twitter.com/shifat01644", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_11561", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "starcraft2.eth", - "displayName": "starcraft2.hl", - "fid": 11561, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e349caa8-89e3-4bf0-5c13-29cc02017900/original", - "followers": 521, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5a280de2ab7236bc9612b2977bc9cab4406ffec9", - "etherscanUrl": "https://etherscan.io/address/0x5a280de2ab7236bc9612b2977bc9cab4406ffec9", - "xHandle": "user_starcraft2", - "xUrl": "https://twitter.com/user_starcraft2", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1113286", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "basebiggy", - "displayName": "BaseBiggy", - "fid": 1113286, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/fc3c9081-bdb3-4169-7959-e99f64a3d200/original", - "followers": 520, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6490e0c69477613dbe1c4a97b2ee082b62566337", - "etherscanUrl": "https://etherscan.io/address/0x6490e0c69477613dbe1c4a97b2ee082b62566337", - "xHandle": "basebiggy", - "xUrl": "https://twitter.com/basebiggy", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_276902", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "drizky", - "displayName": "AlfarizkyDafka", - "fid": 276902, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ee26d9b8-8f12-4c2e-cab2-e50c16a48a00/rectcrop3", - "followers": 516, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf748d693b2ec184c297984ac77e3a53d0be2435d", - "etherscanUrl": "https://etherscan.io/address/0xf748d693b2ec184c297984ac77e3a53d0be2435d", - "xHandle": "tsaputra77", - "xUrl": "https://twitter.com/tsaputra77", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_305948", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dnzcrypto", - "displayName": "0xDanssky", - "fid": 305948, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5b045a61-610b-4e2b-3c81-8a8410363a00/rectcrop3", - "followers": 514, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x61cf19b61339a25b319f5e47d86e92489f131fcd", - "etherscanUrl": "https://etherscan.io/address/0x61cf19b61339a25b319f5e47d86e92489f131fcd", - "xHandle": "saymajime", - "xUrl": "https://twitter.com/saymajime", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_217630", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sashanka.eth", - "displayName": "SASHANKA SEKHAR DEY", - "fid": 217630, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/af99bdcb-9257-4116-3ef1-61887b836b00/original", - "followers": 513, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x597cfd2099b106bd79ac57ce1ccd8a5b6cc40803", - "etherscanUrl": "https://etherscan.io/address/0x597cfd2099b106bd79ac57ce1ccd8a5b6cc40803", - "xHandle": "sashankadey45", - "xUrl": "https://twitter.com/sashankadey45", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_249478", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "maximleader.eth", - "displayName": "Maximbase", - "fid": 249478, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/112e3a72-3795-4ec1-6ae7-bef55ee1d600/original", - "followers": 513, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1a86f38187e8dbd7eab3e0bbdc98d550916f545a", - "etherscanUrl": "https://etherscan.io/address/0x1a86f38187e8dbd7eab3e0bbdc98d550916f545a", - "xHandle": "maximbase", - "xUrl": "https://twitter.com/maximbase", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_575438", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "lithium34", - "displayName": "Fatih 🟦 base.eth", - "fid": 575438, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c8a6cb69-4bca-4811-dcca-4f0548db3300/original", - "followers": 507, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x65fa778f241124b4fe583893b2a857f64e287c50", - "etherscanUrl": "https://etherscan.io/address/0x65fa778f241124b4fe583893b2a857f64e287c50", - "xHandle": "fatih8282174527", - "xUrl": "https://twitter.com/fatih8282174527", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_299675", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "almuksi", - "displayName": "al_si.base 🟦", - "fid": 299675, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b9375cfd-91bf-4c08-5d00-f9bf91e58400/rectcrop3", - "followers": 500, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xee80bde1a41117af469a70638286059b4ae9e39c", - "etherscanUrl": "https://etherscan.io/address/0xee80bde1a41117af469a70638286059b4ae9e39c", - "xHandle": "uciuchiha", - "xUrl": "https://twitter.com/uciuchiha", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_217269", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "2toneartist", - "displayName": "2Tone", - "fid": 217269, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/66be1e6b-58db-42c6-2873-1284f8e5a500/original", - "followers": 498, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe73e7c78f181497a3ba705c71b1630289d7387bb", - "etherscanUrl": "https://etherscan.io/address/0xe73e7c78f181497a3ba705c71b1630289d7387bb", - "xHandle": "2tone_artist", - "xUrl": "https://twitter.com/2tone_artist", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_896968", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "truemarkets", - "displayName": "Truemarkets", - "fid": 896968, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/480a46f2-138e-4ce9-0b19-aa62e52b3c00/rectcrop3", - "followers": 498, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x497aa315434ac7060ba0aac0b829357de52a0ff8", - "etherscanUrl": "https://etherscan.io/address/0x497aa315434ac7060ba0aac0b829357de52a0ff8", - "xHandle": "truemarketsorg", - "xUrl": "https://twitter.com/truemarketsorg", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_533626", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nomankhan797e", - "displayName": "Noman Khan \"base.eth\"", - "fid": 533626, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7ec6b56b-9182-4780-81a0-7ec36f7e9700/rectcrop3", - "followers": 496, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x310613403e3e188ba25a7c3940d07baae5547605", - "etherscanUrl": "https://etherscan.io/address/0x310613403e3e188ba25a7c3940d07baae5547605", - "xHandle": "nomankhan797g", - "xUrl": "https://twitter.com/nomankhan797g", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_14642", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "frenchy.eth", - "displayName": "Mazzy", - "fid": 14642, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ec5a40f6-d894-4bca-fabc-af4e6b8a9500/original", - "followers": 495, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5f57904e95d17dfb405160adbac3fda5e2cbb8b2", - "etherscanUrl": "https://etherscan.io/address/0x5f57904e95d17dfb405160adbac3fda5e2cbb8b2", - "xHandle": "mazzydoteth", - "xUrl": "https://twitter.com/mazzydoteth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_3560", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "rickcrosschain", - "displayName": "Rick Crosschain", - "fid": 3560, - "pfpUrl": "https://i.imgur.com/U7CFZ5i.jpg", - "followers": 491, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xeb00bad4ad2f1b7caa6adbc968c1193c0435dc28", - "etherscanUrl": "https://etherscan.io/address/0xeb00bad4ad2f1b7caa6adbc968c1193c0435dc28", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_251993", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "quzzlpu20", - "displayName": "AgusEpendi 🎩 🍕", - "fid": 251993, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/99997a43-3399-4030-cf37-e0b689b37b00/original", - "followers": 491, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf54a9262841817f3e6eebbbdfe46c2993229c735", - "etherscanUrl": "https://etherscan.io/address/0xf54a9262841817f3e6eebbbdfe46c2993229c735", - "xHandle": "adelia100599", - "xUrl": "https://twitter.com/adelia100599", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_2880", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0xknight", - "displayName": "0x_Knight 🎩", - "fid": 2880, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5955131e-6748-4082-1250-cc6658c37200/original", - "followers": 490, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x384e8b96cd1a72ccd11b445af8a567743d0c62c1", - "etherscanUrl": "https://etherscan.io/address/0x384e8b96cd1a72ccd11b445af8a567743d0c62c1", - "xHandle": "0x_knight", - "xUrl": "https://twitter.com/0x_knight", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_567960", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "roshansingh", - "displayName": "RoshanSingh", - "fid": 567960, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f0dd91d2-97b3-42b2-f348-8cdb33697700/original", - "followers": 488, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbc9a0cbff6aa98b775fc1dfc683139b287a7b399", - "etherscanUrl": "https://etherscan.io/address/0xbc9a0cbff6aa98b775fc1dfc683139b287a7b399", - "xHandle": "technicalrosha6", - "xUrl": "https://twitter.com/technicalrosha6", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_612957", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "themetadegen", - "displayName": "themetadegen.base.eth", - "fid": 612957, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/47663f55-69b6-40ea-3b0f-14d5bf66ed00/original", - "followers": 487, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8a60b22308eac879052ef9ac33cb6f74a364a7f7", - "etherscanUrl": "https://etherscan.io/address/0x8a60b22308eac879052ef9ac33cb6f74a364a7f7", - "xHandle": "_themetadegen", - "xUrl": "https://twitter.com/_themetadegen", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_274116", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "eraa", - "displayName": "Eraa", - "fid": 274116, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ab706fdc-8e16-4079-a4e3-4e58446e2300/original", - "followers": 487, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0e2865cf89ec947da933661e6e52b01760f3d4ee", - "etherscanUrl": "https://etherscan.io/address/0x0e2865cf89ec947da933661e6e52b01760f3d4ee", - "xHandle": "akun08383", - "xUrl": "https://twitter.com/akun08383", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1331904", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "unddupnext", - "displayName": "Undd zerozeroseven 🦅🦅", - "fid": 1331904, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/294f7323-0fcd-4640-c8de-d6f9fa585800/original", - "followers": 487, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc0260d5a0098b02edac6dc6e2b32b12b42576e99", - "etherscanUrl": "https://etherscan.io/address/0xc0260d5a0098b02edac6dc6e2b32b12b42576e99", - "xHandle": "undd2060", - "xUrl": "https://twitter.com/undd2060", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_297212", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ferdj", - "displayName": "Fer", - "fid": 297212, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/834fa91a-12ab-4b82-3d3f-23ce1316a800/rectcrop3", - "followers": 486, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x777394a03b62c802edac4168501a96c357e9750c", - "etherscanUrl": "https://etherscan.io/address/0x777394a03b62c802edac4168501a96c357e9750c", - "xHandle": "ferdjpoker", - "xUrl": "https://twitter.com/ferdjpoker", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_568111", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "vineethsingh", - "displayName": "VineethSingh", - "fid": 568111, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/cdff9e87-dce2-4224-f38a-33298b482400/original", - "followers": 484, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9b68e3a396ab7a275ca388ca349d4950105df552", - "etherscanUrl": "https://etherscan.io/address/0x9b68e3a396ab7a275ca388ca349d4950105df552", - "xHandle": "vineethsingh785", - "xUrl": "https://twitter.com/vineethsingh785", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1025388", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "vipulpapriwal", - "displayName": "Vipul Papriwal", - "fid": 1025388, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b3f80f7f-5eab-48be-88c0-a0dc27b4c200/rectcrop3", - "followers": 477, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x921bc01f8b105b831f01ec6619ff915b41de9fa8", - "etherscanUrl": "https://etherscan.io/address/0x921bc01f8b105b831f01ec6619ff915b41de9fa8", - "xHandle": "vipulpapriwal", - "xUrl": "https://twitter.com/vipulpapriwal", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_346145", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "demmich", - "displayName": "Demmich", - "fid": 346145, - "pfpUrl": "https://i.imgur.com/Ud3ttUP.jpg", - "followers": 475, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1a4bc81a5ed2f8bf84863b2a08a8aa5eeb5da36b", - "etherscanUrl": "https://etherscan.io/address/0x1a4bc81a5ed2f8bf84863b2a08a8aa5eeb5da36b", - "xHandle": "0xdemmich", - "xUrl": "https://twitter.com/0xdemmich", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_559", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "vinam", - "displayName": "Vinam Suri", - "fid": 559, - "pfpUrl": "https://i.imgur.com/usCWmEr.jpg", - "followers": 475, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd93af9f2b80ee699de85495ac45a453cd0ada586", - "etherscanUrl": "https://etherscan.io/address/0xd93af9f2b80ee699de85495ac45a453cd0ada586", - "xHandle": "surivinam", - "xUrl": "https://twitter.com/surivinam", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_889982", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "virility", - "displayName": "Shillian Wakespeare", - "fid": 889982, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f759ca13-34a8-4595-afd4-53706736a300/original", - "followers": 473, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x567d1518fb0c812905d967da7071aa649904a877", - "etherscanUrl": "https://etherscan.io/address/0x567d1518fb0c812905d967da7071aa649904a877", - "xHandle": "noomnole", - "xUrl": "https://twitter.com/noomnole", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_19391", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "imaan", - "displayName": "0ximan.base.eth🧬", - "fid": 19391, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f5fad33c-7f14-4f96-b917-6a3a535e0800/original", - "followers": 471, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x75baa79bba289c00d9ab083f12f6cd1675885eb3", - "etherscanUrl": "https://etherscan.io/address/0x75baa79bba289c00d9ab083f12f6cd1675885eb3", - "xHandle": "imanpjnir", - "xUrl": "https://twitter.com/imanpjnir", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_404703", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jawarpe", - "displayName": "jAWarpé", - "fid": 404703, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/07c2dad9-3eee-441e-2583-6824e911c400/original", - "followers": 465, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf0807da85b2af462edd8a1855f4d301fba1f74ae", - "etherscanUrl": "https://etherscan.io/address/0xf0807da85b2af462edd8a1855f4d301fba1f74ae", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_14662", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "stupidonny27", - "displayName": "Donny 🧬", - "fid": 14662, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a42aa1ff-51b5-4c64-cc65-73d2acca4200/rectcrop3", - "followers": 460, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb10cc873c90afe981ddf7f647e783fb2c5952014", - "etherscanUrl": "https://etherscan.io/address/0xb10cc873c90afe981ddf7f647e783fb2c5952014", - "xHandle": "dondonkuy", - "xUrl": "https://twitter.com/dondonkuy", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_438344", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dragunov", - "displayName": "DR4GUNOV🐹🎭🙈👾", - "fid": 438344, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/69c9ea1c-7e27-47c8-d076-0c273b717700/original", - "followers": 459, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0d04d2a80c33334c9bd02fcbe5f9f8b7ff1fc66f", - "etherscanUrl": "https://etherscan.io/address/0x0d04d2a80c33334c9bd02fcbe5f9f8b7ff1fc66f", - "xHandle": "holoholo184797", - "xUrl": "https://twitter.com/holoholo184797", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1408120", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "elonboss", - "displayName": "Elonboss", - "fid": 1408120, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d59c050c-5ea1-4f97-6fe7-6750250bc200/original", - "followers": 457, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x23b400a8d9c8133cc7a86c90f871848fa5f18d05", - "etherscanUrl": "https://etherscan.io/address/0x23b400a8d9c8133cc7a86c90f871848fa5f18d05", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_264455", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "patron", - "displayName": "Alex El Patron", - "fid": 264455, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7d9bf4f1-f7ba-4d56-7482-4bb1cb96ee00/rectcrop3", - "followers": 455, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb00652fe6e3be1a7bd29ea9df2c7aca46bb8c55c", - "etherscanUrl": "https://etherscan.io/address/0xb00652fe6e3be1a7bd29ea9df2c7aca46bb8c55c", - "xHandle": "defiturkiye", - "xUrl": "https://twitter.com/defiturkiye", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_189532", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0xpeter", - "displayName": "Peter", - "fid": 189532, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f64d786b-d6bf-4c12-45c7-d49858b91200/rectcrop3", - "followers": 447, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf0a412aa99b74704b8adc7f8731bbda6591ebf43", - "etherscanUrl": "https://etherscan.io/address/0xf0a412aa99b74704b8adc7f8731bbda6591ebf43", - "xHandle": "0xpeternguyen", - "xUrl": "https://twitter.com/0xpeternguyen", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_274278", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "babycow", - "displayName": "Jennifer", - "fid": 274278, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3ec3f905-da64-4004-d461-2e1d9687fe00/original", - "followers": 441, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x761beda5b1a96710836919510677dff6c462591c", - "etherscanUrl": "https://etherscan.io/address/0x761beda5b1a96710836919510677dff6c462591c", - "xHandle": "btcbabycow", - "xUrl": "https://twitter.com/btcbabycow", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_560539", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jawadms", - "displayName": "Shuaibu Muhammed Jawad ", - "fid": 560539, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/be3454a4-a6f6-4d67-d3a0-5df699b1a300/rectcrop3", - "followers": 438, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd3cbb7b33de65d61f0b0851eff7ddea39fb74db7", - "etherscanUrl": "https://etherscan.io/address/0xd3cbb7b33de65d61f0b0851eff7ddea39fb74db7", - "xHandle": "jawadms", - "xUrl": "https://twitter.com/jawadms", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1056890", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "xorloveski49", - "displayName": "xorloveski49", - "fid": 1056890, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ceb71c19-0c90-41d9-b741-228b5b3e6900/original", - "followers": 433, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x202cab0460a9cc210dd6312a7e528380d2fe4036", - "etherscanUrl": "https://etherscan.io/address/0x202cab0460a9cc210dd6312a7e528380d2fe4036", - "xHandle": "xorloveski", - "xUrl": "https://twitter.com/xorloveski", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_982091", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ladylight", - "displayName": "Lady Light", - "fid": 982091, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f77851a0-559c-4765-abff-89a1aa98ad00/rectcrop3", - "followers": 428, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbdbc40fcbfb1bf72c4c936c503b5c2e61b67bc88", - "etherscanUrl": "https://etherscan.io/address/0xbdbc40fcbfb1bf72c4c936c503b5c2e61b67bc88", - "xHandle": "lady_light_lsk", - "xUrl": "https://twitter.com/lady_light_lsk", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_579082", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "enerdgiua", - "displayName": "Ihor Grischenko", - "fid": 579082, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/cd8e6632-32b3-4b97-a124-210a3c2b3200/rectcrop3", - "followers": 428, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd6e0aa21f60a7f7507d56b0b1f02187034fef696", - "etherscanUrl": "https://etherscan.io/address/0xd6e0aa21f60a7f7507d56b0b1f02187034fef696", - "xHandle": "nouxau83", - "xUrl": "https://twitter.com/nouxau83", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_266713", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bayz19", - "displayName": "Bayz19", - "fid": 266713, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d9d76912-326c-4663-715c-dfe6586f3900/original", - "followers": 427, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x65f941d86d5dbd8aa8087b16d6ac0ef77f35c44c", - "etherscanUrl": "https://etherscan.io/address/0x65f941d86d5dbd8aa8087b16d6ac0ef77f35c44c", - "xHandle": "bayushrl19", - "xUrl": "https://twitter.com/bayushrl19", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_434709", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "drizz21", - "displayName": "yzyy", - "fid": 434709, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5a2717bd-8a5e-4596-12ba-67e920d4f600/original", - "followers": 423, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc6742307b3d09b06e9ce719427a95650f0d6b0b4", - "etherscanUrl": "https://etherscan.io/address/0xc6742307b3d09b06e9ce719427a95650f0d6b0b4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_561399", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kidmyr.eth", - "displayName": "Kidmyr", - "fid": 561399, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9832e64f-f7bc-4813-b67f-9a65606e5900/original", - "followers": 422, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xee2bb96a633e50de9cbc8b4229b07cef9a008852", - "etherscanUrl": "https://etherscan.io/address/0xee2bb96a633e50de9cbc8b4229b07cef9a008852", - "xHandle": "k1dmyr", - "xUrl": "https://twitter.com/k1dmyr", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_317304", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "airdropsilents", - "displayName": "Hanangelia 🍖x100 🍡🎭", - "fid": 317304, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/27c151a7-90ef-49dd-4989-5290d34edf00/rectcrop3", - "followers": 420, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6eb789ac6fc408b7d452cf31145a5febcfde626e", - "etherscanUrl": "https://etherscan.io/address/0x6eb789ac6fc408b7d452cf31145a5febcfde626e", - "xHandle": "airdropsilents", - "xUrl": "https://twitter.com/airdropsilents", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1050419", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cryptolust", - "displayName": "Cryptolust", - "fid": 1050419, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/cbe01177-8a16-4e11-0392-1f7419050300/original", - "followers": 419, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb30b659406f00af4ba486a084002a1eeaf8c3270", - "etherscanUrl": "https://etherscan.io/address/0xb30b659406f00af4ba486a084002a1eeaf8c3270", - "xHandle": "cryptolust_nft", - "xUrl": "https://twitter.com/cryptolust_nft", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_246169", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "shutdown", - "displayName": "Shutdown ", - "fid": 246169, - "pfpUrl": "https://i.imgur.com/Ia410UN.jpg", - "followers": 416, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0f9f75b72bc97f5c6aaf362b6d6a967b50fecd4e", - "etherscanUrl": "https://etherscan.io/address/0x0f9f75b72bc97f5c6aaf362b6d6a967b50fecd4e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_334951", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "airdropusa1", - "displayName": "Md Moniruzzaman (Nishan)", - "fid": 334951, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d2ef8c02-b247-4c7f-9f4b-1bcb27a01800/original", - "followers": 416, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x07b3dfb9ff621c1dddf49681192f2b47b706a26e", - "etherscanUrl": "https://etherscan.io/address/0x07b3dfb9ff621c1dddf49681192f2b47b706a26e", - "xHandle": "mdmonir56518446", - "xUrl": "https://twitter.com/mdmonir56518446", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_17287", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "based", - "displayName": "based", - "fid": 17287, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/015fa50e-663f-41c7-e7ee-067e32d8d500/original", - "followers": 414, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x093679494ae099fb4074ec0528a7ccd076a9d0a4", - "etherscanUrl": "https://etherscan.io/address/0x093679494ae099fb4074ec0528a7ccd076a9d0a4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_493537", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "grif", - "displayName": "Grif", - "fid": 493537, - "pfpUrl": "https://res.cloudinary.com/base-app/image/upload/f_m3u8,f_png/v1757513217/video_uploads/1a2c0bcd-6efa-4420-ac52-8423d451b9e1.png", - "followers": 409, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x511227c444d741779414b6f35782da01cc88414c", - "etherscanUrl": "https://etherscan.io/address/0x511227c444d741779414b6f35782da01cc88414c", - "xHandle": "grif_gg", - "xUrl": "https://twitter.com/grif_gg", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1084542", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "inspectorredflag", - "displayName": "Inspectorredflag.base.eth", - "fid": 1084542, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3316d197-e25e-4b82-2d80-e854e21f4400/original", - "followers": 400, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf5cb08926a1bb87c61b9149334e20c2ca6df0c19", - "etherscanUrl": "https://etherscan.io/address/0xf5cb08926a1bb87c61b9149334e20c2ca6df0c19", - "xHandle": "dt_talley", - "xUrl": "https://twitter.com/dt_talley", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_241491", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "m231", - "displayName": "MG", - "fid": 241491, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/330c81a2-a3a3-462a-ad66-f0048a776d00/original", - "followers": 399, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xaf94455caceae382f9e951f503080b5decd6dde1", - "etherscanUrl": "https://etherscan.io/address/0xaf94455caceae382f9e951f503080b5decd6dde1", - "xHandle": "m2310g", - "xUrl": "https://twitter.com/m2310g", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_492595", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "adewale118", - "displayName": "RILIWAN KAZEEM🎩", - "fid": 492595, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ab2f2185-f027-453a-f966-a732a5f19800/rectcrop3", - "followers": 399, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9a7f8eedc3552be192f4644502b3c55f1879921d", - "etherscanUrl": "https://etherscan.io/address/0x9a7f8eedc3552be192f4644502b3c55f1879921d", - "xHandle": "kazeemriliwana1", - "xUrl": "https://twitter.com/kazeemriliwana1", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_375659", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "livenlearn47", - "displayName": "baselife.base.eth🎩", - "fid": 375659, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3f46971b-44a3-4218-91ee-52a9e865b900/original", - "followers": 399, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9811b3b6b7f3a0b171c15e2c3bf2fb41eea85638", - "etherscanUrl": "https://etherscan.io/address/0x9811b3b6b7f3a0b171c15e2c3bf2fb41eea85638", - "xHandle": "livenlearn47", - "xUrl": "https://twitter.com/livenlearn47", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_20313", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "orcndwarf.eth", - "displayName": "Orcndwarf 🌈", - "fid": 20313, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0841999e-32c4-4915-6395-bee536e32500/original", - "followers": 397, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2efccaff5db9b8ca241288a5eeb84cd9ffda7341", - "etherscanUrl": "https://etherscan.io/address/0x2efccaff5db9b8ca241288a5eeb84cd9ffda7341", - "xHandle": "mattrom", - "xUrl": "https://twitter.com/mattrom", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_237602", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "drophyte", - "displayName": "drophyte", - "fid": 237602, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6df65952-13b2-4a00-3adf-9d02c5c20900/original", - "followers": 397, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x26cd3b3f82cf9980ebe3ca57f60b4416e091c360", - "etherscanUrl": "https://etherscan.io/address/0x26cd3b3f82cf9980ebe3ca57f60b4416e091c360", - "xHandle": "drophyte", - "xUrl": "https://twitter.com/drophyte", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_342036", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "michaelgee", - "displayName": "Michaela 🎩🎭⚡", - "fid": 342036, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1afc1cd2-86a4-4fd0-1e13-44cd7665e800/rectcrop3", - "followers": 388, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa894e0c3e28298a41e7253105e2f6ebc483ef0d5", - "etherscanUrl": "https://etherscan.io/address/0xa894e0c3e28298a41e7253105e2f6ebc483ef0d5", - "xHandle": "oxmomonoskie", - "xUrl": "https://twitter.com/oxmomonoskie", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_231652", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "2blunt4u", - "displayName": "2Blunt4u", - "fid": 231652, - "pfpUrl": "https://i.imgur.com/X9kgTPi.jpg", - "followers": 386, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9e8d1624f68852bfc725f095c9cfe66e523656c6", - "etherscanUrl": "https://etherscan.io/address/0x9e8d1624f68852bfc725f095c9cfe66e523656c6", - "xHandle": "2_blunt4u", - "xUrl": "https://twitter.com/2_blunt4u", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1077510", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "gon0x.eth", - "displayName": "Gon", - "fid": 1077510, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/379eb665-8c25-4c62-4d83-060429575200/original", - "followers": 385, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc002ad4e9b1dab521216be7a8f5936f2f363cbcc", - "etherscanUrl": "https://etherscan.io/address/0xc002ad4e9b1dab521216be7a8f5936f2f363cbcc", - "xHandle": "gon0x_", - "xUrl": "https://twitter.com/gon0x_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_455653", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cashcation.eth", - "displayName": "takeshi", - "fid": 455653, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ed5504f6-dab8-45ff-4f3d-0e1a1201bf00/original", - "followers": 384, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x28dc4bcd9eec159b55296e8e0804b13deebf479b", - "etherscanUrl": "https://etherscan.io/address/0x28dc4bcd9eec159b55296e8e0804b13deebf479b", - "xHandle": "r4dyrfzac5hzzh1", - "xUrl": "https://twitter.com/r4dyrfzac5hzzh1", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_257004", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "duitghoib", - "displayName": "Jacksaw.base.eth", - "fid": 257004, - "pfpUrl": "https://ipfs.decentralized-content.com/ipfs/bafybeiaryrtxzhaqorhkmrtxvw2fb7fxyd23t6eqhisbs66bh2srkuwnsu", - "followers": 383, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6ceb065b822dc276db14fb213662b89b1e8eb43e", - "etherscanUrl": "https://etherscan.io/address/0x6ceb065b822dc276db14fb213662b89b1e8eb43e", - "xHandle": "shibaswhap", - "xUrl": "https://twitter.com/shibaswhap", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1188728", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "arthustle", - "displayName": "Arthustle", - "fid": 1188728, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f9757828-e0ee-4cff-45ff-35b02eddb600/original", - "followers": 382, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x17483352a4f376d2a5e99fac4f3d52a4009d4ac0", - "etherscanUrl": "https://etherscan.io/address/0x17483352a4f376d2a5e99fac4f3d52a4009d4ac0", - "xHandle": "arthustlex", - "xUrl": "https://twitter.com/arthustlex", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1078184", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "yohaneswaleho", - "displayName": "Yohanes", - "fid": 1078184, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0a88ec0d-92ad-404b-dce0-731aa8351f00/original", - "followers": 382, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdca79950402e3c60f9938930500861e3b7443342", - "etherscanUrl": "https://etherscan.io/address/0xdca79950402e3c60f9938930500861e3b7443342", - "xHandle": "robothnezz", - "xUrl": "https://twitter.com/robothnezz", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_495510", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "pallabrout", - "displayName": "Pallab Kumar Rout", - "fid": 495510, - "pfpUrl": "https://ipfs.decentralized-content.com/ipfs/bafybeidv4j2lqgrxvgbaxd2zsxswqag2xbccnj6y3rixmpzlxlxiramsy4", - "followers": 381, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc7dfeed9f265fc5519611a4a18ef2683f3dc7857", - "etherscanUrl": "https://etherscan.io/address/0xc7dfeed9f265fc5519611a4a18ef2683f3dc7857", - "xHandle": "pallavrout", - "xUrl": "https://twitter.com/pallavrout", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_417233", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "rseal", - "displayName": "rseal", - "fid": 417233, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d431f6b7-c106-4e62-1671-a39b9af36000/original", - "followers": 380, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x70b45589630ed87f69e2848dc314dbab754edd35", - "etherscanUrl": "https://etherscan.io/address/0x70b45589630ed87f69e2848dc314dbab754edd35", - "xHandle": "gamefinanceru", - "xUrl": "https://twitter.com/gamefinanceru", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_20702", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nowheresafe", - "displayName": "Nowhere_safe", - "fid": 20702, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e5232206-757e-4e8f-f74a-0414355cae00/original", - "followers": 379, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x31ecee563b6259d308f830be559bbf663adddf6e", - "etherscanUrl": "https://etherscan.io/address/0x31ecee563b6259d308f830be559bbf663adddf6e", - "xHandle": "nowhere_safe", - "xUrl": "https://twitter.com/nowhere_safe", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_561059", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mohan22333", - "displayName": "Surala Mohanrao", - "fid": 561059, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d5f308b3-5ad3-4171-9fd8-b9d1f8a71800/rectcrop3", - "followers": 376, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe2d391dc01489c919068456329a848bc244e586f", - "etherscanUrl": "https://etherscan.io/address/0xe2d391dc01489c919068456329a848bc244e586f", - "xHandle": "prasadtech9090", - "xUrl": "https://twitter.com/prasadtech9090", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_329223", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "demonstarieze", - "displayName": "Demonstarieze✈️", - "fid": 329223, - "pfpUrl": "https://res.cloudinary.com/base-app/image/upload/f_auto/v1763291483/5ce8966c-2852-464f-8c91-b91bbe1f8b99.jpg", - "followers": 374, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3f383173eb4a3d9b3f7faa7befbc7ea75d07ef66", - "etherscanUrl": "https://etherscan.io/address/0x3f383173eb4a3d9b3f7faa7befbc7ea75d07ef66", - "xHandle": "lordmorbido", - "xUrl": "https://twitter.com/lordmorbido", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1078302", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "alary-creates", - "displayName": "Real Soul", - "fid": 1078302, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/16902bbf-fdde-475f-77f7-b1f13a3d9000/original", - "followers": 372, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5da5b333e10a5271b673d01defad9e272446e45f", - "etherscanUrl": "https://etherscan.io/address/0x5da5b333e10a5271b673d01defad9e272446e45f", - "xHandle": "infant_cthulhu", - "xUrl": "https://twitter.com/infant_cthulhu", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1021902", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "lordkolie", - "displayName": "Kolie.base.eth", - "fid": 1021902, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/8f150fd6-0593-4074-5b63-3e731deee800/original", - "followers": 371, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf9fef029521c7f2f7917f5f4a4cb3320d381e9e2", - "etherscanUrl": "https://etherscan.io/address/0xf9fef029521c7f2f7917f5f4a4cb3320d381e9e2", - "xHandle": "lordkolie", - "xUrl": "https://twitter.com/lordkolie", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_498678", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "disciple", - "displayName": "Disciple", - "fid": 498678, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/47d49b91-2f2e-4064-047e-13bb202ba100/rectcrop3", - "followers": 365, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8235f407480884ea12db9c6016b65466c47450e1", - "etherscanUrl": "https://etherscan.io/address/0x8235f407480884ea12db9c6016b65466c47450e1", - "xHandle": "mathburn666", - "xUrl": "https://twitter.com/mathburn666", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_315212", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "oxsirkrobinkhan", - "displayName": "Imtiaz.btc", - "fid": 315212, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b248e49f-0aaa-4d70-bf90-170e8b80ea00/rectcrop3", - "followers": 363, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfcdd8d7ab341300379b9d3954c8c4daeaf39b70a", - "etherscanUrl": "https://etherscan.io/address/0xfcdd8d7ab341300379b9d3954c8c4daeaf39b70a", - "xHandle": "imtiaz_btc", - "xUrl": "https://twitter.com/imtiaz_btc", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1045855", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "seahawksquawk", - "displayName": "Nick", - "fid": 1045855, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2720ae56-1ef2-43c0-23ab-c38a35ea3300/rectcrop3", - "followers": 361, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc81178cbf294124c34e0e58dca6587be7e9c8732", - "etherscanUrl": "https://etherscan.io/address/0xc81178cbf294124c34e0e58dca6587be7e9c8732", - "xHandle": "cryptoslatestn1", - "xUrl": "https://twitter.com/cryptoslatestn1", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_243949", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "oxxyy13", - "displayName": "Oxxyy", - "fid": 243949, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/34c4302d-803c-45b6-eae9-8404d8418700/original", - "followers": 361, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc451a6b34b7ecef9f8cca2648cd6c620a6f361c8", - "etherscanUrl": "https://etherscan.io/address/0xc451a6b34b7ecef9f8cca2648cd6c620a6f361c8", - "xHandle": "oxxyy13", - "xUrl": "https://twitter.com/oxxyy13", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1314539", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "encodedchat", - "displayName": "Encoded Chat", - "fid": 1314539, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9f849bf5-fae4-4621-183d-a4d0dbd3b800/original", - "followers": 359, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0fd38b5d9e0c0ddedceadf8c546031a01a3fbb73", - "etherscanUrl": "https://etherscan.io/address/0x0fd38b5d9e0c0ddedceadf8c546031a01a3fbb73", - "xHandle": "encodedchat", - "xUrl": "https://twitter.com/encodedchat", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_689010", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "borisych", - "displayName": "Borys", - "fid": 689010, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ca6005dd-9232-4ba4-257d-bcc6fa4e0500/rectcrop3", - "followers": 358, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5445b7ed541cfb79f7b6c73ce1c228e85b011f9a", - "etherscanUrl": "https://etherscan.io/address/0x5445b7ed541cfb79f7b6c73ce1c228e85b011f9a", - "xHandle": "borys3833296563", - "xUrl": "https://twitter.com/borys3833296563", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_649105", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sorenex", - "displayName": "Soren Wilder", - "fid": 649105, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/40fb491a-1412-4aa9-f461-a552101bb400/rectcrop3", - "followers": 358, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf886f145e223ef7bb12c611949c8b2d86871cbb4", - "etherscanUrl": "https://etherscan.io/address/0xf886f145e223ef7bb12c611949c8b2d86871cbb4", - "xHandle": "elizaveta616326", - "xUrl": "https://twitter.com/elizaveta616326", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_10197", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "digidigit.eth", - "displayName": "digidigit", - "fid": 10197, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/63a27022-d044-470b-8524-ca06730c5f00/original", - "followers": 357, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9f0227cf46f44bfa68bfcf2d7fa8234eb36863ed", - "etherscanUrl": "https://etherscan.io/address/0x9f0227cf46f44bfa68bfcf2d7fa8234eb36863ed", - "xHandle": "digi_digit", - "xUrl": "https://twitter.com/digi_digit", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_255044", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "arbik88", - "displayName": "Nonton Bareng", - "fid": 255044, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9f26f91e-d66a-49c6-fa65-d309ba8a4a00/rectcrop3", - "followers": 355, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x69f7991249e68df64fbc7722eab4d87fbf581ad6", - "etherscanUrl": "https://etherscan.io/address/0x69f7991249e68df64fbc7722eab4d87fbf581ad6", - "xHandle": "ryoozjin", - "xUrl": "https://twitter.com/ryoozjin", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_651924", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "volodymyr2202", - "displayName": "Vladymyr", - "fid": 651924, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/02d132fd-5225-455e-3348-dd4e8f476200/original", - "followers": 355, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf7fb4b7c5ba155ab889dbd1d28c8b594169abfc1", - "etherscanUrl": "https://etherscan.io/address/0xf7fb4b7c5ba155ab889dbd1d28c8b594169abfc1", - "xHandle": "bitcoin_sky_80", - "xUrl": "https://twitter.com/bitcoin_sky_80", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_258804", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "doppelhq.eth", - "displayName": "Halima Virus", - "fid": 258804, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/445cef2f-689c-4e6a-9a57-206a07e91900/original", - "followers": 354, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfc4842d215753b38c66f0f903dc71f4c40d11ec7", - "etherscanUrl": "https://etherscan.io/address/0xfc4842d215753b38c66f0f903dc71f4c40d11ec7", - "xHandle": "halimavirus", - "xUrl": "https://twitter.com/halimavirus", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_189836", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "vstation", - "displayName": "Vstation", - "fid": 189836, - "pfpUrl": "https://i.imgur.com/XAlGwSR.jpg", - "followers": 353, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9c9304de81387798093f9d2484ec37288e99182b", - "etherscanUrl": "https://etherscan.io/address/0x9c9304de81387798093f9d2484ec37288e99182b", - "xHandle": "_tnthinh_", - "xUrl": "https://twitter.com/_tnthinh_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_254277", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "pools", - "displayName": "pools base.eth", - "fid": 254277, - "pfpUrl": "https://tba-mobile.mypinata.cloud/ipfs/QmVt5BveaHHeSqbrMxdC9cAQQT1pgVuq3cX1jDktVJYUrU?pinataGatewayToken=3nq0UVhtd3rYmgYDdb1I9qv7rHsw-_DzwdWkZPRQ-QW1avFI9dCS8knaSfq_R5_q", - "followers": 352, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x44783f4b4fd44de0fdc611807eca7190c0144c96", - "etherscanUrl": "https://etherscan.io/address/0x44783f4b4fd44de0fdc611807eca7190c0144c96", - "xHandle": "jelek9898", - "xUrl": "https://twitter.com/jelek9898", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_411655", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0xfaheng", - "displayName": "Neo-法恒", - "fid": 411655, - "pfpUrl": "https://i.imgur.com/iDslyCV.jpg", - "followers": 346, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc5e48984c2e569993dcb68ecd84bc328024aa1f2", - "etherscanUrl": "https://etherscan.io/address/0xc5e48984c2e569993dcb68ecd84bc328024aa1f2", - "xHandle": "0xfaheng", - "xUrl": "https://twitter.com/0xfaheng", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_377018", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "whirlwind", - "displayName": "Sash", - "fid": 377018, - "pfpUrl": "https://i.imgur.com/rHt9caC.jpg", - "followers": 342, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x36b20d3f0bd86e6f7370b7bbd1c2ddc132f3e914", - "etherscanUrl": "https://etherscan.io/address/0x36b20d3f0bd86e6f7370b7bbd1c2ddc132f3e914", - "xHandle": "crypto_sanige", - "xUrl": "https://twitter.com/crypto_sanige", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_360435", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "earlyxxone.eth", - "displayName": "earlyxxone", - "fid": 360435, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a086ccaf-fed5-44ce-e5b6-829df3402200/original", - "followers": 342, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x14dc46a8a7fbfaf030e6cddd5b57f3e06dffd029", - "etherscanUrl": "https://etherscan.io/address/0x14dc46a8a7fbfaf030e6cddd5b57f3e06dffd029", - "xHandle": "earlyxxone", - "xUrl": "https://twitter.com/earlyxxone", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_15295", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dilipkumar", - "displayName": "DilipKumar.base.eth", - "fid": 15295, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/87bd70bd-a0f8-4c33-2192-bcf0bbe07800/original", - "followers": 339, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x042d120e37ce5374d7e2f54fe7c1e71ccfb7dbdd", - "etherscanUrl": "https://etherscan.io/address/0x042d120e37ce5374d7e2f54fe7c1e71ccfb7dbdd", - "xHandle": "dilipkumarbd", - "xUrl": "https://twitter.com/dilipkumarbd", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_565990", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "derusxbt.eth", - "displayName": "Dee", - "fid": 565990, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/60d18e2f-a387-4a0e-1526-344ba1b74400/original", - "followers": 338, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf6f089d4cfca6200c1d00deb756300303915c136", - "etherscanUrl": "https://etherscan.io/address/0xf6f089d4cfca6200c1d00deb756300303915c136", - "xHandle": "derusxbt", - "xUrl": "https://twitter.com/derusxbt", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1146559", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "grinluxury", - "displayName": "Yakubu Yunusa", - "fid": 1146559, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/17a2ca8d-52d4-4012-60db-5d119cba8700/original", - "followers": 336, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x30df66fc5651ba799d471d8a7287a1639ef4d68c", - "etherscanUrl": "https://etherscan.io/address/0x30df66fc5651ba799d471d8a7287a1639ef4d68c", - "xHandle": "grincurrencey", - "xUrl": "https://twitter.com/grincurrencey", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_256531", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0xsyks", - "displayName": "⌐◨-◨ 𓆏👾😈", - "fid": 256531, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/93799aa7-38b4-427b-be7d-ea5ec1a5c700/original", - "followers": 336, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5ff387dd379a0578bbf8c500401bf571060344a5", - "etherscanUrl": "https://etherscan.io/address/0x5ff387dd379a0578bbf8c500401bf571060344a5", - "xHandle": "syks_x", - "xUrl": "https://twitter.com/syks_x", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_247921", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bebankless", - "displayName": "Kobie", - "fid": 247921, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b0bed009-a175-40aa-3f7d-994679bf5100/original", - "followers": 335, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xaa52c4e0868ac33fda141563e1bc244ceb32c84a", - "etherscanUrl": "https://etherscan.io/address/0xaa52c4e0868ac33fda141563e1bc244ceb32c84a", - "xHandle": "bebankless", - "xUrl": "https://twitter.com/bebankless", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_387406", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "demarco", - "displayName": "demarco 🎩😼🕶", - "fid": 387406, - "pfpUrl": "https://i.imgur.com/LXrwo8P.jpg", - "followers": 332, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf8ef8c1145d31417d8593cb94f9dd85adef19630", - "etherscanUrl": "https://etherscan.io/address/0xf8ef8c1145d31417d8593cb94f9dd85adef19630", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_261036", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "natz", - "displayName": "Natz 🔵", - "fid": 261036, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/62ae66d4-fd6a-4dba-e0f2-33b18e45d000/rectcrop3", - "followers": 330, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1996407c670f1d616322dce3a8bbbe126b809e7b", - "etherscanUrl": "https://etherscan.io/address/0x1996407c670f1d616322dce3a8bbbe126b809e7b", - "xHandle": "natzcrypto", - "xUrl": "https://twitter.com/natzcrypto", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_16382", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "lass", - "displayName": "lass", - "fid": 16382, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/300dbc1c-42f9-468a-dfbe-192aaae2b300/original", - "followers": 328, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcac7e9a834c85df04a91411927166082bfd78c8b", - "etherscanUrl": "https://etherscan.io/address/0xcac7e9a834c85df04a91411927166082bfd78c8b", - "xHandle": "lassandfriends", - "xUrl": "https://twitter.com/lassandfriends", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_317583", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "omoko2024", - "displayName": "Omoko Justice Odafe", - "fid": 317583, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/92eaebeb-8361-4486-764b-4182a38deb00/original", - "followers": 327, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe73a74eb6c903819ee3e8a2b99e25c59469c6792", - "etherscanUrl": "https://etherscan.io/address/0xe73a74eb6c903819ee3e8a2b99e25c59469c6792", - "xHandle": "jakodice1990", - "xUrl": "https://twitter.com/jakodice1990", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_973228", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "s0lverr", - "displayName": "s0lverr", - "fid": 973228, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/35eed58c-562d-4d66-d706-2dfab02bf700/original", - "followers": 326, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7ce34d81517e6a48a5ad31a332a07e75c87864f9", - "etherscanUrl": "https://etherscan.io/address/0x7ce34d81517e6a48a5ad31a332a07e75c87864f9", - "xHandle": "s0lverr_", - "xUrl": "https://twitter.com/s0lverr_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_326398", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "anwarnugr", - "displayName": "0xAnwarnugr", - "fid": 326398, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6a45eeee-4ce8-4bf2-bdee-fee3870b3400/original", - "followers": 326, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf6c8a84040eaab7de4dc1854a6ee112949505a4c", - "etherscanUrl": "https://etherscan.io/address/0xf6c8a84040eaab7de4dc1854a6ee112949505a4c", - "xHandle": "anwarnugroho11", - "xUrl": "https://twitter.com/anwarnugroho11", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_2598", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "danner", - "displayName": "danner.eth✨", - "fid": 2598, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9a133a50-1cb9-4c61-bb04-a0432d452d00/rectcrop3", - "followers": 325, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x64dcf1994356eaa03b141b8659f98a457b30307a", - "etherscanUrl": "https://etherscan.io/address/0x64dcf1994356eaa03b141b8659f98a457b30307a", - "xHandle": "drakedanner", - "xUrl": "https://twitter.com/drakedanner", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_381949", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "tomatoxyz", - "displayName": "tomato", - "fid": 381949, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4ac63710-4985-42cb-8ff3-294ea8337400/original", - "followers": 325, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x39dcc2ee514ed87bb56b0e08bba99454bd8a71b0", - "etherscanUrl": "https://etherscan.io/address/0x39dcc2ee514ed87bb56b0e08bba99454bd8a71b0", - "xHandle": "abcxyz_tomato", - "xUrl": "https://twitter.com/abcxyz_tomato", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_214888", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kyliekay21", - "displayName": "Kylie Kay Ⓜ️", - "fid": 214888, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/89ca2381-28d7-4dce-5196-458daf755f00/rectcrop3", - "followers": 324, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4e7dce1d4413afdaddd3108aee9cb098f044015e", - "etherscanUrl": "https://etherscan.io/address/0x4e7dce1d4413afdaddd3108aee9cb098f044015e", - "xHandle": "herritykylie", - "xUrl": "https://twitter.com/herritykylie", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_994400", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "angeldeicide", - "displayName": "ANGEL DEICIDE ☥", - "fid": 994400, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3b45447e-f919-48e8-b854-89039092ed00/rectcrop3", - "followers": 323, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xac97615633d658975b450d2335ac30d0840f8f17", - "etherscanUrl": "https://etherscan.io/address/0xac97615633d658975b450d2335ac30d0840f8f17", - "xHandle": "ang3ld3icid3", - "xUrl": "https://twitter.com/ang3ld3icid3", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_411935", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jshears83", - "displayName": "Joshua Shears", - "fid": 411935, - "pfpUrl": "https://i.imgur.com/jKlaDVJ.jpg", - "followers": 323, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbf7dbd0313c9c185292feaf528a977bb7954062c", - "etherscanUrl": "https://etherscan.io/address/0xbf7dbd0313c9c185292feaf528a977bb7954062c", - "xHandle": "joshuashears6", - "xUrl": "https://twitter.com/joshuashears6", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_883153", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "1hz", - "displayName": "1hz.btc", - "fid": 883153, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9eb3e2d3-f1cf-434a-86c4-a685b03fd300/original", - "followers": 320, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9641297298c883b57b4dd2cfe07a37c80c858292", - "etherscanUrl": "https://etherscan.io/address/0x9641297298c883b57b4dd2cfe07a37c80c858292", - "xHandle": "habeebpootchai", - "xUrl": "https://twitter.com/habeebpootchai", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_244946", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "web3-lord", - "displayName": "Web3_Lord | Potato", - "fid": 244946, - "pfpUrl": "https://i.imgur.com/Pnv2OFb.jpg", - "followers": 316, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfcf294004bc6a675bd8c51bd2db2fa8c4e002eae", - "etherscanUrl": "https://etherscan.io/address/0xfcf294004bc6a675bd8c51bd2db2fa8c4e002eae", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1059183", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mdyasine", - "displayName": "MD Yasine", - "fid": 1059183, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5567fc3e-c6a7-4b6d-b410-a5c46554ab00/original", - "followers": 312, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb808f242eeeeb76fd111502093d224a2d14b0934", - "etherscanUrl": "https://etherscan.io/address/0xb808f242eeeeb76fd111502093d224a2d14b0934", - "xHandle": "mdyaine903", - "xUrl": "https://twitter.com/mdyaine903", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_295626", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "naila23", - "displayName": "Kirana.eth", - "fid": 295626, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c994698d-6685-421d-4536-6228a2ee7600/original", - "followers": 312, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9c7316193b057180f3c5a72ecd36ad52d0103112", - "etherscanUrl": "https://etherscan.io/address/0x9c7316193b057180f3c5a72ecd36ad52d0103112", - "xHandle": "hasanba74226250", - "xUrl": "https://twitter.com/hasanba74226250", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_442312", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jeykall", - "displayName": "jeykallETH", - "fid": 442312, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9686f623-e2a0-4ede-7c0d-bb7198791d00/rectcrop3", - "followers": 310, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd08a2e07e002f4a2e006815c0c70602ad86b0158", - "etherscanUrl": "https://etherscan.io/address/0xd08a2e07e002f4a2e006815c0c70602ad86b0158", - "xHandle": "jeykall777", - "xUrl": "https://twitter.com/jeykall777", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_257285", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "axellab", - "displayName": "Appsara", - "fid": 257285, - "pfpUrl": "https://imagedelivery.net/g4iQ0bIzMZrjFMgjAnSGfw/49e85544-51a9-4399-ddf3-d24b9d309900/public", - "followers": 307, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x31f6ccd1f6aff5b3787733575e28a23c32f0296c", - "etherscanUrl": "https://etherscan.io/address/0x31f6ccd1f6aff5b3787733575e28a23c32f0296c", - "xHandle": "tamamus12", - "xUrl": "https://twitter.com/tamamus12", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_314005", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "syrx", - "displayName": "xsyr Ⓜ️🔮🍖🎩 🧬", - "fid": 314005, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4695b155-fe1a-4fb1-94ab-bde307f1eb00/original", - "followers": 307, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x120ca36128daa58182a13d2b448c3da0ff3e8001", - "etherscanUrl": "https://etherscan.io/address/0x120ca36128daa58182a13d2b448c3da0ff3e8001", - "xHandle": "sundanise3", - "xUrl": "https://twitter.com/sundanise3", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_878324", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ninhle", - "displayName": "NinhLe", - "fid": 878324, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/cce64128-7cc7-4ec5-8c78-f366788ebb00/original", - "followers": 307, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x242219c624e4e4f639aa55809dcb7bd1be365afa", - "etherscanUrl": "https://etherscan.io/address/0x242219c624e4e4f639aa55809dcb7bd1be365afa", - "xHandle": "ninhle298", - "xUrl": "https://twitter.com/ninhle298", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_288773", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "apelord", - "displayName": "KekeisusFarcaster", - "fid": 288773, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f649fe1f-7e7b-4ad9-fe41-9f75e3292c00/original", - "followers": 305, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd06abc1613e4bd4c1026380ab91fbb95edfb4a3f", - "etherscanUrl": "https://etherscan.io/address/0xd06abc1613e4bd4c1026380ab91fbb95edfb4a3f", - "xHandle": "0xbasedegen", - "xUrl": "https://twitter.com/0xbasedegen", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_517178", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "soitienao", - "displayName": "Soi 🔵 🎩", - "fid": 517178, - "pfpUrl": "https://ipfs.decentralized-content.com/ipfs/bafybeig7k4bnqm5glrsx7tnxzqx5ekdwchkxpquyq6gtp4p2edv4ha2vdi", - "followers": 305, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x23e003f45c70f558df1e589de73573d5a0405369", - "etherscanUrl": "https://etherscan.io/address/0x23e003f45c70f558df1e589de73573d5a0405369", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_669437", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "zam365", - "displayName": "Emmanuel Davou Zam", - "fid": 669437, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0ea8fb7f-2f79-4950-cbe5-59be276c0400/rectcrop3", - "followers": 304, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9002bd806e55d373bf08eeadd579bdb122ed7ac8", - "etherscanUrl": "https://etherscan.io/address/0x9002bd806e55d373bf08eeadd579bdb122ed7ac8", - "xHandle": "zamemmanuel2", - "xUrl": "https://twitter.com/zamemmanuel2", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_408711", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "rayblanco.eth", - "displayName": "Hodlin", - "fid": 408711, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/358b6b7f-d2c4-497b-64b5-7146382e9400/original", - "followers": 304, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbff8c6c34f1efacf6844350de907cca6f07c76b8", - "etherscanUrl": "https://etherscan.io/address/0xbff8c6c34f1efacf6844350de907cca6f07c76b8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_479608", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "laprade", - "displayName": "laprade", - "fid": 479608, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d45cbe14-8fff-46a8-4f32-da7605ea2600/original", - "followers": 303, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x15d49914cbee10fd64b7e825c750771deeb2f5e4", - "etherscanUrl": "https://etherscan.io/address/0x15d49914cbee10fd64b7e825c750771deeb2f5e4", - "xHandle": "alexlaprade", - "xUrl": "https://twitter.com/alexlaprade", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_881589", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "donzoq", - "displayName": "Crypto Events ¶", - "fid": 881589, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/76863f80-af19-44c1-1c60-5fc099c94100/rectcrop3", - "followers": 303, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x06c9f8611fa5a1d3ee56bd41947e1c302bf9313e", - "etherscanUrl": "https://etherscan.io/address/0x06c9f8611fa5a1d3ee56bd41947e1c302bf9313e", - "xHandle": "blockchain795", - "xUrl": "https://twitter.com/blockchain795", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_549749", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "hybee123", - "displayName": "Adebayo Ibrahim Abayomi", - "fid": 549749, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7b0f81ed-a353-4104-ae9d-41f010176200/rectcrop3", - "followers": 302, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2ec8d78d1802522b947de5da8b43823d1493d247", - "etherscanUrl": "https://etherscan.io/address/0x2ec8d78d1802522b947de5da8b43823d1493d247", - "xHandle": "wiseonetoons", - "xUrl": "https://twitter.com/wiseonetoons", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_253701", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "hope2049", - "displayName": "Farcaster🍭 Lollipop 🍭", - "fid": 253701, - "pfpUrl": "https://arweave.net/aiTwqdiAGYEdukXHEF_4Y711uYXzn_5xuPcyErb89DA/", - "followers": 298, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x67e13ca099cf4e685201554e0ee51f18ed9e958b", - "etherscanUrl": "https://etherscan.io/address/0x67e13ca099cf4e685201554e0ee51f18ed9e958b", - "xHandle": "blacknoirx20", - "xUrl": "https://twitter.com/blacknoirx20", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_305715", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jiko", - "displayName": "riya Akhter🔥", - "fid": 305715, - "pfpUrl": "https://tba-mobile.mypinata.cloud/ipfs/QmZwxDy3Ec1GkGjUY9LfAg4RJR37jbzFjZDa4fvYExaSNH?pinataGatewayToken=3nq0UVhtd3rYmgYDdb1I9qv7rHsw-_DzwdWkZPRQ-QW1avFI9dCS8knaSfq_R5_q", - "followers": 298, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7d7a26e41d6dfc9376ffdf4afa6f86e2029a07f1", - "etherscanUrl": "https://etherscan.io/address/0x7d7a26e41d6dfc9376ffdf4afa6f86e2029a07f1", - "xHandle": "sagorsajid84499", - "xUrl": "https://twitter.com/sagorsajid84499", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_473626", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "gabrululu", - "displayName": "Jennifer Gabriela ", - "fid": 473626, - "pfpUrl": "https://i.imgur.com/m4tMe3U.jpg", - "followers": 295, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe374cd7cdacbddc9f28ebb3256558294ebd0a4eb", - "etherscanUrl": "https://etherscan.io/address/0xe374cd7cdacbddc9f28ebb3256558294ebd0a4eb", - "xHandle": "gabrululu", - "xUrl": "https://twitter.com/gabrululu", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_745795", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "7siyuan0202", - "displayName": "7siyuan0202", - "fid": 745795, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/600d1a4a-1da1-4afe-6aad-6a5e96166100/rectcrop3", - "followers": 293, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdbdeedf38028a718ac719164bb2f472f18695bb4", - "etherscanUrl": "https://etherscan.io/address/0xdbdeedf38028a718ac719164bb2f472f18695bb4", - "xHandle": "siyuan829269563", - "xUrl": "https://twitter.com/siyuan829269563", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_855085", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0xalyson", - "displayName": "Alyson 🎒", - "fid": 855085, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/19adf03e-8659-4b3d-d51b-30d79a083100/original", - "followers": 292, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x625e53555890a2790108d98246891d83b0699c24", - "etherscanUrl": "https://etherscan.io/address/0x625e53555890a2790108d98246891d83b0699c24", - "xHandle": "0xalyson", - "xUrl": "https://twitter.com/0xalyson", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_256019", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sulistiyani12", - "displayName": "Sulist", - "fid": 256019, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/703329a6-c511-485c-99f8-08df483ff900/original", - "followers": 292, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x17e965883eca2ba5efd95893b0421e0bd885d7a2", - "etherscanUrl": "https://etherscan.io/address/0x17e965883eca2ba5efd95893b0421e0bd885d7a2", - "xHandle": "shilvyvi", - "xUrl": "https://twitter.com/shilvyvi", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_871430", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "urbanwanderer", - "displayName": "Billy", - "fid": 871430, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/10051305-f0ce-43af-c280-d674699b2700/rectcrop3", - "followers": 286, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xda82e5ed55b9912b259a69369eecc3e7bfe32cf1", - "etherscanUrl": "https://etherscan.io/address/0xda82e5ed55b9912b259a69369eecc3e7bfe32cf1", - "xHandle": "xieyi681", - "xUrl": "https://twitter.com/xieyi681", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_687238", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "tgstarlight", - "displayName": "Pk Alio (Onchain)⚡️", - "fid": 687238, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f5d15bed-f505-43cf-cc64-f16a268f2c00/original", - "followers": 286, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfbcfc9ee0a973b9064ef9c0e28d2b5548110c8dc", - "etherscanUrl": "https://etherscan.io/address/0xfbcfc9ee0a973b9064ef9c0e28d2b5548110c8dc", - "xHandle": "tgstarlight", - "xUrl": "https://twitter.com/tgstarlight", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_794785", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "muskan783", - "displayName": "Okehd", - "fid": 794785, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6e0a6b61-64ed-4cd1-1e44-24f961c53900/rectcrop3", - "followers": 284, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd59a3184da914c0a16477609dbc3f7866e553631", - "etherscanUrl": "https://etherscan.io/address/0xd59a3184da914c0a16477609dbc3f7866e553631", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_263797", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "chemicae", - "displayName": "Manuel", - "fid": 263797, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c79a6abf-6101-4142-9351-dfc938f8e400/original", - "followers": 281, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x87cc3ded6a7f8d366f12320a2e65fe926e975d4d", - "etherscanUrl": "https://etherscan.io/address/0x87cc3ded6a7f8d366f12320a2e65fe926e975d4d", - "xHandle": "obiekweobinna10", - "xUrl": "https://twitter.com/obiekweobinna10", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_251595", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "peko", - "displayName": "Adit", - "fid": 251595, - "pfpUrl": "https://i.imgur.com/nu0ASH2.jpg", - "followers": 279, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x38b917c2a727a6f00d2375c3c8c02436cfcd0619", - "etherscanUrl": "https://etherscan.io/address/0x38b917c2a727a6f00d2375c3c8c02436cfcd0619", - "xHandle": "adaterus51", - "xUrl": "https://twitter.com/adaterus51", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_403674", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "naser", - "displayName": "Maryam 3090", - "fid": 403674, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/77ceec7a-4fe0-4f81-170f-008b058fe000/original", - "followers": 277, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x57a2b4c5792a1c2fc9d217c23bcbbe9a12d3d0dc", - "etherscanUrl": "https://etherscan.io/address/0x57a2b4c5792a1c2fc9d217c23bcbbe9a12d3d0dc", - "xHandle": "maryam637675432", - "xUrl": "https://twitter.com/maryam637675432", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_307747", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "penjahatklausa", - "displayName": "Firdausa Agri", - "fid": 307747, - "pfpUrl": "https://i.imgur.com/WtdR6Rm.jpg", - "followers": 276, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8a8ea0ba1b0bfba12293a63824bfbaa7a52959cb", - "etherscanUrl": "https://etherscan.io/address/0x8a8ea0ba1b0bfba12293a63824bfbaa7a52959cb", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1112512", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "hemanthpnft", - "displayName": "Hemanthpnft", - "fid": 1112512, - "pfpUrl": "https://res.cloudinary.com/base-app/image/upload/f_auto/v1762066304/1000013518.jpg.jpg", - "followers": 273, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9f5fa842c65a110e77dacb729cc04cf4952d6a50", - "etherscanUrl": "https://etherscan.io/address/0x9f5fa842c65a110e77dacb729cc04cf4952d6a50", - "xHandle": "hemanthp_nft", - "xUrl": "https://twitter.com/hemanthp_nft", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_195881", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "capybara", - "displayName": "Capybara", - "fid": 195881, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/40727d2e-9ca8-49a7-0490-32cce493e900/original", - "followers": 272, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa1742fc31f1a58d8502947ac97897257afe02dd8", - "etherscanUrl": "https://etherscan.io/address/0xa1742fc31f1a58d8502947ac97897257afe02dd8", - "xHandle": "funofcapy", - "xUrl": "https://twitter.com/funofcapy", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_749008", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "16macyt", - "displayName": "16macyt 🎭", - "fid": 749008, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1f60e6b3-62ec-46d1-1761-d6bb5c405700/rectcrop3", - "followers": 272, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x23de7d75f7be464885527c61d17b55b8aaf84191", - "etherscanUrl": "https://etherscan.io/address/0x23de7d75f7be464885527c61d17b55b8aaf84191", - "xHandle": "cameronannn", - "xUrl": "https://twitter.com/cameronannn", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_300187", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "anticrash", - "displayName": "anticrash 🔺", - "fid": 300187, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/17eb4cea-d490-4a93-c74c-9fd2f1f9bf00/original", - "followers": 271, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1449f831c45e0cc08c18f73e71ad3de6b00cbc31", - "etherscanUrl": "https://etherscan.io/address/0x1449f831c45e0cc08c18f73e71ad3de6b00cbc31", - "xHandle": "anticrash", - "xUrl": "https://twitter.com/anticrash", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_446441", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "yaboyolgreg", - "displayName": "Yaboyolgreg", - "fid": 446441, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9721a793-8712-4e9f-6c93-11d0ab492f00/original", - "followers": 269, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x09116862e7df2c614da9bcf428f3fd8751962782", - "etherscanUrl": "https://etherscan.io/address/0x09116862e7df2c614da9bcf428f3fd8751962782", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1060603", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "pyueegyi81-eth", - "displayName": "zoranounsdao", - "fid": 1060603, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d308a3a8-a6c3-4238-8560-1389f3762500/original", - "followers": 265, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbfebb921b8d983943c1a1bcb6bd78907c0b94b14", - "etherscanUrl": "https://etherscan.io/address/0xbfebb921b8d983943c1a1bcb6bd78907c0b94b14", - "xHandle": "pyueegyi", - "xUrl": "https://twitter.com/pyueegyi", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_742266", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "10xinsenqy191", - "displayName": "10xinsenqy191 🎭", - "fid": 742266, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/05aa7bfe-25f0-4735-cee1-9ee1f23b3c00/rectcrop3", - "followers": 264, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1c21aba1614d9874b6df926f2f5f1be39ee58d3e", - "etherscanUrl": "https://etherscan.io/address/0x1c21aba1614d9874b6df926f2f5f1be39ee58d3e", - "xHandle": "luck06858092738", - "xUrl": "https://twitter.com/luck06858092738", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_368589", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "thi3ns0n", - "displayName": "Thiên Sơn / Base.eth", - "fid": 368589, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3367a1bb-0d69-4f92-a66a-9c6513f18000/original", - "followers": 260, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2231450c9a28b4f784c6cacb3dc23d12738aa609", - "etherscanUrl": "https://etherscan.io/address/0x2231450c9a28b4f784c6cacb3dc23d12738aa609", - "xHandle": "immthi3ns0n", - "xUrl": "https://twitter.com/immthi3ns0n", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_648196", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "raketaa", - "displayName": "Roman", - "fid": 648196, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/36ba0e95-3ff7-49c9-be21-3b479241cd00/original", - "followers": 258, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa070846b1d237a24dfa45a692b3080584fbb2cf2", - "etherscanUrl": "https://etherscan.io/address/0xa070846b1d237a24dfa45a692b3080584fbb2cf2", - "xHandle": "roma_raketa", - "xUrl": "https://twitter.com/roma_raketa", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_250200", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "chiagozie.eth", - "displayName": "Chiagozie", - "fid": 250200, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/fd2cdec0-7149-450f-4f45-509a578b7c00/rectcrop3", - "followers": 255, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb888e768a4c11fbb21cf0bb2fc73ac69e044e8b4", - "etherscanUrl": "https://etherscan.io/address/0xb888e768a4c11fbb21cf0bb2fc73ac69e044e8b4", - "xHandle": "chiagozie50", - "xUrl": "https://twitter.com/chiagozie50", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_887565", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "gondoruo", - "displayName": "Gondoruwo", - "fid": 887565, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/862268af-a404-4214-09a7-b4d071e42a00/rectcrop3", - "followers": 255, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1cd9c5601a6ca671209ed6ed978402683ba4bc79", - "etherscanUrl": "https://etherscan.io/address/0x1cd9c5601a6ca671209ed6ed978402683ba4bc79", - "xHandle": "alawiyah59083", - "xUrl": "https://twitter.com/alawiyah59083", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1042073", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "zurich755", - "displayName": "ZurichArt", - "fid": 1042073, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/786ab8cd-6c6e-4093-0762-27a25802e400/original", - "followers": 255, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1e88a6a05132c2047665484bb6f40c9b1e1d3ef7", - "etherscanUrl": "https://etherscan.io/address/0x1e88a6a05132c2047665484bb6f40c9b1e1d3ef7", - "xHandle": "zurich7557", - "xUrl": "https://twitter.com/zurich7557", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_711889", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "902717iskander", - "displayName": "Iskanderkill", - "fid": 711889, - "pfpUrl": "https://ipfs.decentralized-content.com/ipfs/QmUFVix41rraSFJuL49VW1mAzMiJWyz9BccAYEkv2nmCZX/124.webp", - "followers": 251, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5d9e43140e7e87c9facec21b1d4661b73cd36088", - "etherscanUrl": "https://etherscan.io/address/0x5d9e43140e7e87c9facec21b1d4661b73cd36088", - "xHandle": "patriciaki21018", - "xUrl": "https://twitter.com/patriciaki21018", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_241702", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "naufaljr19", - "displayName": "Fal", - "fid": 241702, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a50b9def-10f6-44f2-e9fc-a2d2f011a300/original", - "followers": 246, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8bf10ccb54eafe14eb261db0f61804db405c965f", - "etherscanUrl": "https://etherscan.io/address/0x8bf10ccb54eafe14eb261db0f61804db405c965f", - "xHandle": "fal19jr", - "xUrl": "https://twitter.com/fal19jr", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1143161", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nikkiwordsmith", - "displayName": "Nikki Wordsmith", - "fid": 1143161, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e0382cc6-f459-4f6f-f36d-859b5d1a9200/original", - "followers": 245, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9b3ea00b357a7abcf99f8c63fc21884e29fc7df0", - "etherscanUrl": "https://etherscan.io/address/0x9b3ea00b357a7abcf99f8c63fc21884e29fc7df0", - "xHandle": "nikkiwordsmith", - "xUrl": "https://twitter.com/nikkiwordsmith", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_455866", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "warpedone", - "displayName": "WarpedOne", - "fid": 455866, - "pfpUrl": "https://i.imgur.com/OdlzhTc.jpg", - "followers": 244, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xef68a8f68f387377c71f6a86b7e7f98b8413471d", - "etherscanUrl": "https://etherscan.io/address/0xef68a8f68f387377c71f6a86b7e7f98b8413471d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_326992", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "hilmitsn", - "displayName": "hilmitsn.base.eth", - "fid": 326992, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/df172eaa-a797-4343-ff6d-ebdb1555db00/original", - "followers": 243, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x70a1c9f348856f2ed142b4972d61d3f8a1578416", - "etherscanUrl": "https://etherscan.io/address/0x70a1c9f348856f2ed142b4972d61d3f8a1578416", - "xHandle": "toprakuzey51", - "xUrl": "https://twitter.com/toprakuzey51", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_510460", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ionrod", - "displayName": "Rod Mamin", - "fid": 510460, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f0e2e606-0da3-4c31-47b2-6a3a7b0e1700/rectcrop3", - "followers": 243, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf8fc4795aca76b8e16c564e7ac0db9a1cff477f1", - "etherscanUrl": "https://etherscan.io/address/0xf8fc4795aca76b8e16c564e7ac0db9a1cff477f1", - "xHandle": "0xionrod", - "xUrl": "https://twitter.com/0xionrod", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1096717", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "samo-miyagi", - "displayName": "SAMO-Miyagi", - "fid": 1096717, - "pfpUrl": "https://res.cloudinary.com/base-app/image/upload/f_auto/v1761782431/IMG_6280.jpg.jpg", - "followers": 241, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa33f2243dbf978e4f7e4e4c24ed194bd2ed209e5", - "etherscanUrl": "https://etherscan.io/address/0xa33f2243dbf978e4f7e4e4c24ed194bd2ed209e5", - "xHandle": "_shanefrederick", - "xUrl": "https://twitter.com/_shanefrederick", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_708818", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dimashilo", - "displayName": "DimaShilo", - "fid": 708818, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3e733338-36ce-4a38-8a51-0b38aa2e5200/rectcrop3", - "followers": 239, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xde3ad1cb1d72149a30f468225eb03529754467ad", - "etherscanUrl": "https://etherscan.io/address/0xde3ad1cb1d72149a30f468225eb03529754467ad", - "xHandle": "gigabvait31701", - "xUrl": "https://twitter.com/gigabvait31701", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_299508", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bodega", - "displayName": "Bodega", - "fid": 299508, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c3cc5a19-af7b-4cce-4a73-7605b5358100/original", - "followers": 237, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x54da363e42e0d4b2de429a108d819cb96918e1cb", - "etherscanUrl": "https://etherscan.io/address/0x54da363e42e0d4b2de429a108d819cb96918e1cb", - "xHandle": "debodegacat", - "xUrl": "https://twitter.com/debodegacat", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_711822", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "juissynft", - "displayName": "Priya", - "fid": 711822, - "pfpUrl": "https://res.cloudinary.com/base-app/image/upload/f_auto/v1763057447/882ab86b-7752-4665-99fa-e90a01b3d037.jpg", - "followers": 237, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x495ee34015e8639ecf1197b4f650e9f25c8fddb7", - "etherscanUrl": "https://etherscan.io/address/0x495ee34015e8639ecf1197b4f650e9f25c8fddb7", - "xHandle": "prasadysrcp", - "xUrl": "https://twitter.com/prasadysrcp", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_749528", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "1sharp8", - "displayName": "1sharp8 🎭", - "fid": 749528, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/08f1c65e-72d1-4bb1-dd6a-a9c8e5894600/rectcrop3", - "followers": 234, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x17e3c1372f29ccbce035f90038d65d199657a98d", - "etherscanUrl": "https://etherscan.io/address/0x17e3c1372f29ccbce035f90038d65d199657a98d", - "xHandle": "kalani2unc", - "xUrl": "https://twitter.com/kalani2unc", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1379545", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "seershub", - "displayName": "seershub.base.eth", - "fid": 1379545, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6277a4f5-f930-4518-4be2-a0f19e76b500/original", - "followers": 231, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x95ac688bd952d5708466463255dcbec10d50ca74", - "etherscanUrl": "https://etherscan.io/address/0x95ac688bd952d5708466463255dcbec10d50ca74", - "xHandle": "seershub", - "xUrl": "https://twitter.com/seershub", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_884823", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sarvagna", - "displayName": "Sarvagna Kadiya", - "fid": 884823, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1a5264a6-7e9a-4f7a-66b8-20584f6a4300/original", - "followers": 230, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc9b06e8122f0c51eb69ea713e27da922ca7c458e", - "etherscanUrl": "https://etherscan.io/address/0xc9b06e8122f0c51eb69ea713e27da922ca7c458e", - "xHandle": "sarvagnakadiya", - "xUrl": "https://twitter.com/sarvagnakadiya", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_449598", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "khangln", - "displayName": "Khangln.base.eth", - "fid": 449598, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7dd7ab5e-1874-41bf-429f-69775ea61500/original", - "followers": 229, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xac9ae0541d85fcbe093cea88b473cfe4c3334157", - "etherscanUrl": "https://etherscan.io/address/0xac9ae0541d85fcbe093cea88b473cfe4c3334157", - "xHandle": "giangtruong1997", - "xUrl": "https://twitter.com/giangtruong1997", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1073937", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "capminal.eth", - "displayName": "Capminal", - "fid": 1073937, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/694ecc01-f03f-4131-92e8-56dfbf512200/original", - "followers": 229, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7e770a61fe356640287cb3dec48f3334525ac333", - "etherscanUrl": "https://etherscan.io/address/0x7e770a61fe356640287cb3dec48f3334525ac333", - "xHandle": "capminal", - "xUrl": "https://twitter.com/capminal", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1062670", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mjbiplob", - "displayName": "Mj Biplob", - "fid": 1062670, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ecce54d0-8734-4bd2-b257-10c254a49a00/original", - "followers": 228, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x91bc6fc9026c990adcd4999f1a0bfea79a7a7183", - "etherscanUrl": "https://etherscan.io/address/0x91bc6fc9026c990adcd4999f1a0bfea79a7a7183", - "xHandle": "mjbiplob16", - "xUrl": "https://twitter.com/mjbiplob16", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1087036", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "theprime1004", - "displayName": "theprime1004", - "fid": 1087036, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b973a257-524f-4958-2f5e-25e7244dab00/original", - "followers": 227, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe31f845b4d04b304db6da6ab56101688d799d78d", - "etherscanUrl": "https://etherscan.io/address/0xe31f845b4d04b304db6da6ab56101688d799d78d", - "xHandle": "theprime1004", - "xUrl": "https://twitter.com/theprime1004", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_242997", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "xlaw", - "displayName": "Xlaw.base.eth", - "fid": 242997, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/8acf0620-e38c-43c9-88f0-89c260567e00/original", - "followers": 225, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x01f6b52b04d92c255ba6f4778f19639956112603", - "etherscanUrl": "https://etherscan.io/address/0x01f6b52b04d92c255ba6f4778f19639956112603", - "xHandle": "exlawbond", - "xUrl": "https://twitter.com/exlawbond", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1858", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "zack", - "displayName": "Zack", - "fid": 1858, - "pfpUrl": "https://lh3.googleusercontent.com/R7KybJX8D9BREcrzGp7ZCLW8XSmMcs9pstee141R7GqLW7yt_cNU8JChUsIVuagoWQ4XY98FVz7ug1IWWN5s3xUPGyvPC33JkVEzlFE", - "followers": 225, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x927d243b15cc3ca5842a86bbd146e2d3982c11a5", - "etherscanUrl": "https://etherscan.io/address/0x927d243b15cc3ca5842a86bbd146e2d3982c11a5", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_384287", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kester1", - "displayName": "Kester1 base.eth", - "fid": 384287, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3cd94ebd-72e2-4fe6-cf39-8dfdec006200/rectcrop3", - "followers": 224, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x377c43bc804e3bb37fe91d1671f3cf25ccb05fc7", - "etherscanUrl": "https://etherscan.io/address/0x377c43bc804e3bb37fe91d1671f3cf25ccb05fc7", - "xHandle": "william213620", - "xUrl": "https://twitter.com/william213620", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_449975", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "stranger4599.eth", - "displayName": "Stranger4599", - "fid": 449975, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/61d30f13-1f63-4fe0-7cc9-197f27794400/original", - "followers": 224, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x68b8dd3d7d5cedb72b40c4cf3152a175990d4599", - "etherscanUrl": "https://etherscan.io/address/0x68b8dd3d7d5cedb72b40c4cf3152a175990d4599", - "xHandle": "stranger4599", - "xUrl": "https://twitter.com/stranger4599", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_403173", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "rubleonacorn", - "displayName": "E.A", - "fid": 403173, - "pfpUrl": "https://i.imgur.com/t6iJeS2.jpg", - "followers": 223, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe31402888c283689f91f6bd2caffe21c0d1c45bb", - "etherscanUrl": "https://etherscan.io/address/0xe31402888c283689f91f6bd2caffe21c0d1c45bb", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_462945", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "skibijb", - "displayName": "Jean baptiste mohr.base.eth", - "fid": 462945, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ea79a6d9-9fad-43e7-6d07-d008c6d33900/rectcrop3", - "followers": 223, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x71f089c439d96530989643d130099d12b8f895b5", - "etherscanUrl": "https://etherscan.io/address/0x71f089c439d96530989643d130099d12b8f895b5", - "xHandle": "jbmohrskibijb", - "xUrl": "https://twitter.com/jbmohrskibijb", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_547444", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "fiatclone", - "displayName": "🎭Ⓜ️🍖🎩🔑 🔥✨", - "fid": 547444, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7bd1aea3-675a-499a-a497-d7eebb0cfc00/original", - "followers": 222, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb02b4dd2fb41e45608c68cccf89aab1fbeda1642", - "etherscanUrl": "https://etherscan.io/address/0xb02b4dd2fb41e45608c68cccf89aab1fbeda1642", - "xHandle": "fiatclone", - "xUrl": "https://twitter.com/fiatclone", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_18418", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "d20", - "displayName": "Jack Fitzpatrick", - "fid": 18418, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/992778dc-97a8-4215-be18-24bdb836e400/original", - "followers": 220, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x63adde7442810ca309e7caf18ffc6f2c59b10c87", - "etherscanUrl": "https://etherscan.io/address/0x63adde7442810ca309e7caf18ffc6f2c59b10c87", - "xHandle": "d20_eth", - "xUrl": "https://twitter.com/d20_eth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_240257", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "futuregeegee", - "displayName": "Gee Gee 🎩🍖", - "fid": 240257, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f8f6fd8d-e7ea-428e-d7fb-d6aa9f93e100/original", - "followers": 219, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe115d7417cb1bdc0582dcbe525d84acfba0eba1a", - "etherscanUrl": "https://etherscan.io/address/0xe115d7417cb1bdc0582dcbe525d84acfba0eba1a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1023081", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "hexate", - "displayName": "Hex", - "fid": 1023081, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d9d22d14-35cf-46f3-d05b-d149eba59d00/rectcrop3", - "followers": 219, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd93915dca6224295ee5f9808e874581b93bb2d06", - "etherscanUrl": "https://etherscan.io/address/0xd93915dca6224295ee5f9808e874581b93bb2d06", - "xHandle": "nymonanym", - "xUrl": "https://twitter.com/nymonanym", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_432054", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bolsaverse.eth", - "displayName": "Bolsa Verse", - "fid": 432054, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1de67322-2d0c-4d7c-cf58-f42d17b71200/original", - "followers": 218, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd5434843c9d1daa5c00f3501f494ff085e3e5c6d", - "etherscanUrl": "https://etherscan.io/address/0xd5434843c9d1daa5c00f3501f494ff085e3e5c6d", - "xHandle": "bolsaverse", - "xUrl": "https://twitter.com/bolsaverse", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_517651", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jpcast", - "displayName": "Jpown 🎩🔵👾🙈", - "fid": 517651, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f7046de3-6328-418a-280c-d11c0783bb00/rectcrop3", - "followers": 214, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x946aa3e335bdb1a4809c63bc17aab56a59b4a901", - "etherscanUrl": "https://etherscan.io/address/0x946aa3e335bdb1a4809c63bc17aab56a59b4a901", - "xHandle": "jpown1", - "xUrl": "https://twitter.com/jpown1", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_340091", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "wenn", - "displayName": "wen", - "fid": 340091, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/503f7dfd-3383-45b9-ac46-99f09b44a500/original", - "followers": 214, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb4dd124ce03b12a52176cb340dd9d9380fdf3a37", - "etherscanUrl": "https://etherscan.io/address/0xb4dd124ce03b12a52176cb340dd9d9380fdf3a37", - "xHandle": "44xumut", - "xUrl": "https://twitter.com/44xumut", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_201224", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kenzodadon", - "displayName": "kenzodadon84 🧬", - "fid": 201224, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c610ad24-9ce3-4c9b-c124-5edf33403800/original", - "followers": 213, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x426774a6b350101696db40ae7842361cd8c87950", - "etherscanUrl": "https://etherscan.io/address/0x426774a6b350101696db40ae7842361cd8c87950", - "xHandle": "kenzodadon3", - "xUrl": "https://twitter.com/kenzodadon3", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_288019", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dewikagura", - "displayName": "dewikagura.base.eth", - "fid": 288019, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6024d3a5-d5d6-4af8-3c34-afea521b2300/original", - "followers": 212, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc224249678bab5b97aaf782566ba321eea1e3368", - "etherscanUrl": "https://etherscan.io/address/0xc224249678bab5b97aaf782566ba321eea1e3368", - "xHandle": "kzomedo", - "xUrl": "https://twitter.com/kzomedo", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_333175", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "paypay", - "displayName": "paypay", - "fid": 333175, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e5119e4e-b44a-4579-aa71-d08c25cc1300/rectcrop3", - "followers": 209, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa1529b1f06b4c2ddd374bade8880c3a90bebbe43", - "etherscanUrl": "https://etherscan.io/address/0xa1529b1f06b4c2ddd374bade8880c3a90bebbe43", - "xHandle": "jepewong", - "xUrl": "https://twitter.com/jepewong", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_20552", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "zak3939", - "displayName": "zak3939 ↑", - "fid": 20552, - "pfpUrl": "https://i.imgur.com/AAAQU6K.jpg", - "followers": 208, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5037e7747faa78fc0ecf8dfc526dcd19f73076ce", - "etherscanUrl": "https://etherscan.io/address/0x5037e7747faa78fc0ecf8dfc526dcd19f73076ce", - "xHandle": "zack_3939", - "xUrl": "https://twitter.com/zack_3939", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_369992", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ngocthach81", - "displayName": "Lê Ngọc Thạch", - "fid": 369992, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3d8b1c2c-f510-4aee-b428-15a41e551d00/original", - "followers": 207, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xed3fd648dbc2feedbe024771cabe9d1497babf04", - "etherscanUrl": "https://etherscan.io/address/0xed3fd648dbc2feedbe024771cabe9d1497babf04", - "xHandle": "thach8182019", - "xUrl": "https://twitter.com/thach8182019", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1060647", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "deboy", - "displayName": "De Boy", - "fid": 1060647, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d15f0092-506a-4ed9-ce97-760b143aeb00/original", - "followers": 206, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe7515471af0e998db0c4cdce024e0185f2eda9b8", - "etherscanUrl": "https://etherscan.io/address/0xe7515471af0e998db0c4cdce024e0185f2eda9b8", - "xHandle": "ayanokojisa", - "xUrl": "https://twitter.com/ayanokojisa", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_198875", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "akbanks", - "displayName": "Akindayo", - "fid": 198875, - "pfpUrl": "https://ipfs.decentralized-content.com/ipfs/QmYDVrQBdvG5xh9GfpvdiaVDskp9w5KtrwhtyjixjRRDNW/1310.png", - "followers": 206, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb21be27a6a222a69d7391cddefb192c7d1cffd34", - "etherscanUrl": "https://etherscan.io/address/0xb21be27a6a222a69d7391cddefb192c7d1cffd34", - "xHandle": "akbanksdayo", - "xUrl": "https://twitter.com/akbanksdayo", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_977328", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "evgeny028", - "displayName": "Evgeny", - "fid": 977328, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b566b177-f42b-4f66-d928-331bc3be3000/rectcrop3", - "followers": 203, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2a7160286a6513d01fb46dd68f59b7953b93878f", - "etherscanUrl": "https://etherscan.io/address/0x2a7160286a6513d01fb46dd68f59b7953b93878f", - "xHandle": "evgeny956138075", - "xUrl": "https://twitter.com/evgeny956138075", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1032105", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "davthebullishman", - "displayName": "Bullishexplorer", - "fid": 1032105, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/78f44731-78b3-4577-b8f7-6165d5b92100/rectcrop3", - "followers": 203, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1bf97552876bd365df78218ea058dc3459a331e6", - "etherscanUrl": "https://etherscan.io/address/0x1bf97552876bd365df78218ea058dc3459a331e6", - "xHandle": "waverdav", - "xUrl": "https://twitter.com/waverdav", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_13172", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dewd.eth", - "displayName": "Derrick", - "fid": 13172, - "pfpUrl": "https://i.imgur.com/7LXEy2m.jpg", - "followers": 202, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9ad4bebee68f657490649ea722b18087a10bfc5d", - "etherscanUrl": "https://etherscan.io/address/0x9ad4bebee68f657490649ea722b18087a10bfc5d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_733390", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "manjulaparmar", - "displayName": "Manjula Parmar", - "fid": 733390, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/40be4c7d-b738-4528-e160-6754b92e7f00/rectcrop3", - "followers": 199, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1438cda2b2c9868a3b94c8f3c91da2653799d5ed", - "etherscanUrl": "https://etherscan.io/address/0x1438cda2b2c9868a3b94c8f3c91da2653799d5ed", - "xHandle": "ac2_airdrop", - "xUrl": "https://twitter.com/ac2_airdrop", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_373120", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "maenswirony", - "displayName": "Maenswirony", - "fid": 373120, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4a7456df-a7bf-4daa-9a37-d29c3a43b900/original", - "followers": 199, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x28537de056f1028cbd2a36cb1e56e6992b6bb837", - "etherscanUrl": "https://etherscan.io/address/0x28537de056f1028cbd2a36cb1e56e6992b6bb837", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_308688", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nubplus", - "displayName": "nubplus", - "fid": 308688, - "pfpUrl": "https://i.imgur.com/bxwudbt.jpg", - "followers": 197, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x42a1e29826a2acaaa6d3e6c41b76c3a1061797b7", - "etherscanUrl": "https://etherscan.io/address/0x42a1e29826a2acaaa6d3e6c41b76c3a1061797b7", - "xHandle": "nubplus", - "xUrl": "https://twitter.com/nubplus", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1060895", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "6555555.eth", - "displayName": "XRAYZ 🧬", - "fid": 1060895, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d8c02533-4123-4671-b902-0df6ae87e500/original", - "followers": 196, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7b67a29d1e9fd5105bd75968d4df22b6c543a444", - "etherscanUrl": "https://etherscan.io/address/0x7b67a29d1e9fd5105bd75968d4df22b6c543a444", - "xHandle": "bityox", - "xUrl": "https://twitter.com/bityox", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_551429", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "shamir677", - "displayName": "99Land Nft", - "fid": 551429, - "pfpUrl": "https://ipfs.decentralized-content.com/ipfs/bafybeidgy7bgx23ao3dbfplhjybr2bgfdjibtferu6c3rcbqzaptzxtrem", - "followers": 192, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x87309e5b2af8bdbcec7a437a82a20045a3286347", - "etherscanUrl": "https://etherscan.io/address/0x87309e5b2af8bdbcec7a437a82a20045a3286347", - "xHandle": "shamshadfri", - "xUrl": "https://twitter.com/shamshadfri", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_963470", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "wawiks", - "displayName": "wawiks.base.eth", - "fid": 963470, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9d075c1f-a012-4f60-5432-7b47e2d16000/original", - "followers": 190, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8166a1f8d01cc65ce44dd56ace99db3fcd401269", - "etherscanUrl": "https://etherscan.io/address/0x8166a1f8d01cc65ce44dd56ace99db3fcd401269", - "xHandle": "ronewwwa", - "xUrl": "https://twitter.com/ronewwwa", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_848616", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nkechinyere", - "displayName": "Nkechinyere", - "fid": 848616, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7ec26008-8787-4cc3-2b74-fe9c78b3e600/rectcrop3", - "followers": 190, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x008c44cc90a40b701729e3cb9b43a31d810ed535", - "etherscanUrl": "https://etherscan.io/address/0x008c44cc90a40b701729e3cb9b43a31d810ed535", - "xHandle": "nkechinyerenm", - "xUrl": "https://twitter.com/nkechinyerenm", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1398788", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "wilvfred", - "displayName": "wilvfred", - "fid": 1398788, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/dfe67c89-720e-475e-ec23-1d3106532a00/original", - "followers": 187, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x720acb3662560edf64c38162b76ec69a1e77fc4a", - "etherscanUrl": "https://etherscan.io/address/0x720acb3662560edf64c38162b76ec69a1e77fc4a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1035556", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "arakash11", - "displayName": "MD. AKASH ALI 🐱", - "fid": 1035556, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/cd5621af-86fc-4d54-5b65-9f2e3d7d4a00/original", - "followers": 187, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc7cff1a95600d6a14f546964ba7d0016f2576806", - "etherscanUrl": "https://etherscan.io/address/0xc7cff1a95600d6a14f546964ba7d0016f2576806", - "xHandle": "akashkhan9091", - "xUrl": "https://twitter.com/akashkhan9091", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_488234", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "hadhad", - "displayName": "Hadikhodaparast \"base.eth\"", - "fid": 488234, - "pfpUrl": "https://tba-mobile.mypinata.cloud/ipfs/QmR1uHEezrMivKw8muaofELajxHAkVxf2daurJ5iM3fZCn?pinataGatewayToken=3nq0UVhtd3rYmgYDdb1I9qv7rHsw-_DzwdWkZPRQ-QW1avFI9dCS8knaSfq_R5_q", - "followers": 187, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x825dd7750cd173b1e534af4b1bac36ecba0e8bc1", - "etherscanUrl": "https://etherscan.io/address/0x825dd7750cd173b1e534af4b1bac36ecba0e8bc1", - "xHandle": "hadikhodaparast", - "xUrl": "https://twitter.com/hadikhodaparast", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_444661", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "twarpcast", - "displayName": "themo.base.eth", - "fid": 444661, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2678bc32-02cc-4b2d-1429-eae6a0448900/rectcrop3", - "followers": 185, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb6b3e63863e509029ead0aecd5b57c64c43d3159", - "etherscanUrl": "https://etherscan.io/address/0xb6b3e63863e509029ead0aecd5b57c64c43d3159", - "xHandle": "oxlayersunda", - "xUrl": "https://twitter.com/oxlayersunda", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_546746", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "shreemanta", - "displayName": "Shreemanta Barman", - "fid": 546746, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/fd772212-5b9b-470d-411f-3cf84c753500/rectcrop3", - "followers": 184, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8e33cfb186e8fc4d1ab394bf26ba2fd164c44fe3", - "etherscanUrl": "https://etherscan.io/address/0x8e33cfb186e8fc4d1ab394bf26ba2fd164c44fe3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1018550", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "gadoo", - "displayName": "Profyeboah", - "fid": 1018550, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6001120c-b8da-469c-ec2a-d12321c5a400/rectcrop3", - "followers": 183, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbb226444e314165053af62715ad1da898db00493", - "etherscanUrl": "https://etherscan.io/address/0xbb226444e314165053af62715ad1da898db00493", - "xHandle": "profyeboah", - "xUrl": "https://twitter.com/profyeboah", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1066021", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dhevrir", - "displayName": "dhevri.base.eth", - "fid": 1066021, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1e47e912-1565-428c-bc3f-4417d3da4000/original", - "followers": 179, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1d06a65db6a16ce84c9d570e1efc03c196a1a52f", - "etherscanUrl": "https://etherscan.io/address/0x1d06a65db6a16ce84c9d570e1efc03c196a1a52f", - "xHandle": "dhevrir", - "xUrl": "https://twitter.com/dhevrir", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_365859", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kriptonesia", - "displayName": "Gundam.eth", - "fid": 365859, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/cd8e224c-4833-4936-714c-3aadb0072d00/original", - "followers": 179, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xabb7ecaef9a7def899372f95fcdc134df090c2d0", - "etherscanUrl": "https://etherscan.io/address/0xabb7ecaef9a7def899372f95fcdc134df090c2d0", - "xHandle": "ahkmaheswari", - "xUrl": "https://twitter.com/ahkmaheswari", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_328942", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "andymage", - "displayName": "AndyMage", - "fid": 328942, - "pfpUrl": "https://i.imgur.com/sY2ZDPZ.jpg", - "followers": 179, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0bad62a71592f458740494f14e82bef6cc1bf4df", - "etherscanUrl": "https://etherscan.io/address/0x0bad62a71592f458740494f14e82bef6cc1bf4df", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_293195", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "shuvankarsantra", - "displayName": "Shuvankar Santra", - "fid": 293195, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/dbf7860a-9761-466d-9c4d-ef4bf6aaf000/original", - "followers": 177, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0066ce704b6f34deae3deeec93360a3c125680d5", - "etherscanUrl": "https://etherscan.io/address/0x0066ce704b6f34deae3deeec93360a3c125680d5", - "xHandle": "shuvankarsantr1", - "xUrl": "https://twitter.com/shuvankarsantr1", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_380924", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "trading23", - "displayName": "Trading23", - "fid": 380924, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/60cafa69-5040-4e1d-3eac-0735f7caa000/original", - "followers": 170, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5f6687a6c5ca824627c399e98173042119aba8ac", - "etherscanUrl": "https://etherscan.io/address/0x5f6687a6c5ca824627c399e98173042119aba8ac", - "xHandle": "fernandosimonlu", - "xUrl": "https://twitter.com/fernandosimonlu", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_581516", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "aahad7220", - "displayName": "Ahadul Islam", - "fid": 581516, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b4422926-384e-4c33-c9cc-2310bb81bd00/rectcrop3", - "followers": 169, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xba4c49d4a5f41605419d75fa0aa5ff68df704d56", - "etherscanUrl": "https://etherscan.io/address/0xba4c49d4a5f41605419d75fa0aa5ff68df704d56", - "xHandle": "leocasta1994", - "xUrl": "https://twitter.com/leocasta1994", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_360192", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "checkfit", - "displayName": "Pranav", - "fid": 360192, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2658b767-79d5-4a62-6f44-912729a3e000/original", - "followers": 168, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x52711ac7e3b246c3ba4112e30b49603660d8f25a", - "etherscanUrl": "https://etherscan.io/address/0x52711ac7e3b246c3ba4112e30b49603660d8f25a", - "xHandle": "checkfit", - "xUrl": "https://twitter.com/checkfit", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_637264", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "caiuses", - "displayName": "Caius Raven", - "fid": 637264, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ad112220-4f28-4b7a-25e2-5a84accd6100/rectcrop3", - "followers": 167, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x943aa976fd98d0cd20c90dcedcaf15b27724d607", - "etherscanUrl": "https://etherscan.io/address/0x943aa976fd98d0cd20c90dcedcaf15b27724d607", - "xHandle": "carmen1797453", - "xUrl": "https://twitter.com/carmen1797453", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_613561", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "floridamike", - "displayName": "Floridamike.eth🐒", - "fid": 613561, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/16465b65-6e0e-4823-d257-64cf0ff88e00/original", - "followers": 167, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf6b6257de91a1185660debaa15dc177c9f8d6354", - "etherscanUrl": "https://etherscan.io/address/0xf6b6257de91a1185660debaa15dc177c9f8d6354", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_246684", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "gokk", - "displayName": "Gok", - "fid": 246684, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/619d1407-fa69-48ec-5cba-83fd44aba300/original", - "followers": 166, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1522917481d65838f46f4492742ac1a287c47539", - "etherscanUrl": "https://etherscan.io/address/0x1522917481d65838f46f4492742ac1a287c47539", - "xHandle": "gokcrypto22", - "xUrl": "https://twitter.com/gokcrypto22", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_267849", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "debtcollector", - "displayName": "DebtCollector", - "fid": 267849, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/79abc2c4-e779-4be0-093c-aab211641c00/rectcrop3", - "followers": 165, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf0628d2b6beb98dfa1c864677fab9250bb64494a", - "etherscanUrl": "https://etherscan.io/address/0xf0628d2b6beb98dfa1c864677fab9250bb64494a", - "xHandle": "0xdecr", - "xUrl": "https://twitter.com/0xdecr", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_338889", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "radradosny", - "displayName": "Rad", - "fid": 338889, - "pfpUrl": "https://i.imgur.com/aMA64Ie.jpg", - "followers": 162, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb03e18ee9972d6fe50a4ad58433a49369232c5df", - "etherscanUrl": "https://etherscan.io/address/0xb03e18ee9972d6fe50a4ad58433a49369232c5df", - "xHandle": "radradosny", - "xUrl": "https://twitter.com/radradosny", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1074473", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "tomaskazlauskas", - "displayName": "productvortex", - "fid": 1074473, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/aa6cd1de-2669-40f5-7fae-31a41bfe2a00/original", - "followers": 162, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9227e298bbe19aca397f08bf884755fd2e76cf05", - "etherscanUrl": "https://etherscan.io/address/0x9227e298bbe19aca397f08bf884755fd2e76cf05", - "xHandle": "tomaskazlauskas", - "xUrl": "https://twitter.com/tomaskazlauskas", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_496685", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sunsub", - "displayName": "Sunsub007", - "fid": 496685, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3c866c8a-3c2a-41f1-baf1-15d1c20ce800/original", - "followers": 161, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf5643aa7f93388914bb1ff1c8870547242294e84", - "etherscanUrl": "https://etherscan.io/address/0xf5643aa7f93388914bb1ff1c8870547242294e84", - "xHandle": "sunsub999", - "xUrl": "https://twitter.com/sunsub999", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1019889", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mediation", - "displayName": "A Osho Mystic Monk Meditation", - "fid": 1019889, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/eed215d4-800e-4198-0101-b52ffa4f4b00/rectcrop3", - "followers": 160, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x855aa7d32b9c5bdf664e1d3ebf264391bf6173ff", - "etherscanUrl": "https://etherscan.io/address/0x855aa7d32b9c5bdf664e1d3ebf264391bf6173ff", - "xHandle": "blue_skygalaxy", - "xUrl": "https://twitter.com/blue_skygalaxy", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_7081", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dasher", - "displayName": "dasher", - "fid": 7081, - "pfpUrl": "https://i.imgur.com/lz2L6sp.jpg", - "followers": 159, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x87772ff7c65b5c93be52e3504670c6407271f829", - "etherscanUrl": "https://etherscan.io/address/0x87772ff7c65b5c93be52e3504670c6407271f829", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_399273", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ikahq", - "displayName": "Ika", - "fid": 399273, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d56f721c-a54d-4dfa-2ca6-d8ba34e8c400/original", - "followers": 158, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1caf2143a1a3785ce43ee56aa51b62d651a91525", - "etherscanUrl": "https://etherscan.io/address/0x1caf2143a1a3785ce43ee56aa51b62d651a91525", - "xHandle": "ika_xbt", - "xUrl": "https://twitter.com/ika_xbt", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1070122", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "t2one", - "displayName": "T2ONE", - "fid": 1070122, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5768e5ab-0047-49e8-c548-a6650ab0bb00/rectcrop3", - "followers": 158, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x208cd9f63399a46ae6501a95bbe78453bf38e08b", - "etherscanUrl": "https://etherscan.io/address/0x208cd9f63399a46ae6501a95bbe78453bf38e08b", - "xHandle": "yulliawen22", - "xUrl": "https://twitter.com/yulliawen22", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_522028", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "artitoyp", - "displayName": "findmax.eth", - "fid": 522028, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/362f29b3-ae4b-42d0-dccd-84517355ec00/rectcrop3", - "followers": 156, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1ccb82e4f59afcffedf3e10ddbd0613d2a83195a", - "etherscanUrl": "https://etherscan.io/address/0x1ccb82e4f59afcffedf3e10ddbd0613d2a83195a", - "xHandle": "nurmayun18", - "xUrl": "https://twitter.com/nurmayun18", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1277961", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "beautyblinks202", - "displayName": "Beautyblinks", - "fid": 1277961, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/13cd6c7b-8fd2-4768-48ca-e32ac3620100/original", - "followers": 155, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5d11dfa219ed6eee4e8ce08cf7036113880b7877", - "etherscanUrl": "https://etherscan.io/address/0x5d11dfa219ed6eee4e8ce08cf7036113880b7877", - "xHandle": "rosefunmilola", - "xUrl": "https://twitter.com/rosefunmilola", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1142631", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cialin", - "displayName": "Cia Lin", - "fid": 1142631, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/13215a65-0e12-46b6-2ed1-1c97fdbaeb00/original", - "followers": 152, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc847fcd86c4439ff0317701db13e240b262cfe19", - "etherscanUrl": "https://etherscan.io/address/0xc847fcd86c4439ff0317701db13e240b262cfe19", - "xHandle": "beautypeaces", - "xUrl": "https://twitter.com/beautypeaces", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_936152", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ramdin", - "displayName": "Ramdin Roham", - "fid": 936152, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e2202127-e088-4d02-dff8-ad029019e200/original", - "followers": 151, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x23f5bbc07a16702aab38c11707b7756c49626f31", - "etherscanUrl": "https://etherscan.io/address/0x23f5bbc07a16702aab38c11707b7756c49626f31", - "xHandle": "123roshansingh1", - "xUrl": "https://twitter.com/123roshansingh1", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1067643", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kataadochii", - "displayName": "kataadochi", - "fid": 1067643, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/47388892-9f07-4625-eebb-85ac09811700/original", - "followers": 150, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x03482e8c00d4ef3ea9717656c4042bb6b9440ad1", - "etherscanUrl": "https://etherscan.io/address/0x03482e8c00d4ef3ea9717656c4042bb6b9440ad1", - "xHandle": "cassanojuns", - "xUrl": "https://twitter.com/cassanojuns", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1448126", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "symslbhry", - "displayName": "symslbhry", - "fid": 1448126, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/76812a45-1705-4a63-6eaf-165d08a6c200/original", - "followers": 148, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x613514f4fd1e2ae23ffa59e879bebc472f12bbd0", - "etherscanUrl": "https://etherscan.io/address/0x613514f4fd1e2ae23ffa59e879bebc472f12bbd0", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_309386", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bobbydigital", - "displayName": "Chris Catalano", - "fid": 309386, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/8f0e98be-5089-4677-b1ee-d4dfd7976700/original", - "followers": 147, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb5639867b0807ce38e1eb534651380ebb09b2c0e", - "etherscanUrl": "https://etherscan.io/address/0xb5639867b0807ce38e1eb534651380ebb09b2c0e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_931532", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "chokdee", - "displayName": "Chokdee", - "fid": 931532, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/16077b00-fc45-4afa-cc5d-96dd91940200/rectcrop3", - "followers": 147, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x967b9ad0882a52ce865716dcec4c77c6beaee636", - "etherscanUrl": "https://etherscan.io/address/0x967b9ad0882a52ce865716dcec4c77c6beaee636", - "xHandle": "suratana12", - "xUrl": "https://twitter.com/suratana12", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1073710", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sharps", - "displayName": "lenidewi 🐉 $MON", - "fid": 1073710, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1a718f57-a5a4-44cc-ffd0-abf45b111600/original", - "followers": 147, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3e8e548634147d4b72c47d609e69d80eabb07a31", - "etherscanUrl": "https://etherscan.io/address/0x3e8e548634147d4b72c47d609e69d80eabb07a31", - "xHandle": "lenidewi2", - "xUrl": "https://twitter.com/lenidewi2", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_969289", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "wizberry303", - "displayName": "BASED BADDIE", - "fid": 969289, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/8a2e2527-6885-4011-b841-2344f2ebf400/original", - "followers": 146, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb03a3e221eeeb63458a31b0fbe1904fcc19de8d8", - "etherscanUrl": "https://etherscan.io/address/0xb03a3e221eeeb63458a31b0fbe1904fcc19de8d8", - "xHandle": "wizberr303", - "xUrl": "https://twitter.com/wizberr303", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1040967", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sicgent", - "displayName": "Sicgent", - "fid": 1040967, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2407949d-6a31-4394-a9ed-0b5c67b25300/original", - "followers": 146, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc172254ad5b72dce2a1b2c93eece9510586f4479", - "etherscanUrl": "https://etherscan.io/address/0xc172254ad5b72dce2a1b2c93eece9510586f4479", - "xHandle": "sicgent", - "xUrl": "https://twitter.com/sicgent", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_248912", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "drmoonhatt4n", - "displayName": "Dr.Moonhatt4n", - "fid": 248912, - "pfpUrl": "https://i.imgur.com/UwdZCDB.jpg", - "followers": 146, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd08854bb1f22b95f38c09afc450bd7f3bf110ce9", - "etherscanUrl": "https://etherscan.io/address/0xd08854bb1f22b95f38c09afc450bd7f3bf110ce9", - "xHandle": "_drmoonhatt4n_", - "xUrl": "https://twitter.com/_drmoonhatt4n_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_388930", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "shibavox.eth", - "displayName": "Shibavox", - "fid": 388930, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9d34c137-a15f-4edf-312b-898ef4263f00/original", - "followers": 145, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc68675813492569ccc81e805b92d0ba3fb2e023b", - "etherscanUrl": "https://etherscan.io/address/0xc68675813492569ccc81e805b92d0ba3fb2e023b", - "xHandle": "shibavox", - "xUrl": "https://twitter.com/shibavox", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_647883", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "zepx", - "displayName": "Zephyr Whisper", - "fid": 647883, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/368cd7f2-6fa6-475b-eeec-ccb307d3b500/rectcrop3", - "followers": 144, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfd92304f129c3bbe54f42f1bd12ede327bb5fabb", - "etherscanUrl": "https://etherscan.io/address/0xfd92304f129c3bbe54f42f1bd12ede327bb5fabb", - "xHandle": "claudia1553818", - "xUrl": "https://twitter.com/claudia1553818", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1069047", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "moneyout", - "displayName": "Merukurou.Eth", - "fid": 1069047, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/53a53365-8d53-40ca-672e-54ce5648a300/original", - "followers": 141, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x19716a8df497b8e962041410ceae2ac07196c27d", - "etherscanUrl": "https://etherscan.io/address/0x19716a8df497b8e962041410ceae2ac07196c27d", - "xHandle": "merukurou", - "xUrl": "https://twitter.com/merukurou", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_214139", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "paramount", - "displayName": "Snowflake", - "fid": 214139, - "pfpUrl": "https://i.imgur.com/URZsIbn.jpg", - "followers": 141, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8f30e6f0c1974c3dcd87f05097bf32495d97561a", - "etherscanUrl": "https://etherscan.io/address/0x8f30e6f0c1974c3dcd87f05097bf32495d97561a", - "xHandle": "shiv_ay3", - "xUrl": "https://twitter.com/shiv_ay3", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_343390", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "spread-eagle069", - "displayName": "SlutTamer", - "fid": 343390, - "pfpUrl": "https://res.cloudinary.com/base-app/image/upload/f_auto/v1762765297/43416bdb-8d3c-46f9-94b0-b2eff379702e.jpg", - "followers": 141, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x96f54fa97c865f86c0f75c2be4f7abd1a3c15374", - "etherscanUrl": "https://etherscan.io/address/0x96f54fa97c865f86c0f75c2be4f7abd1a3c15374", - "xHandle": "realreconreal1s", - "xUrl": "https://twitter.com/realreconreal1s", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_643207", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "starligh", - "displayName": "Aria Starlight", - "fid": 643207, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a076df4f-3162-4439-3bec-8f99890b2f00/rectcrop3", - "followers": 139, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb6f3052379f2c487ced60f5309af761e0fe5541c", - "etherscanUrl": "https://etherscan.io/address/0xb6f3052379f2c487ced60f5309af761e0fe5541c", - "xHandle": "aleksej221140", - "xUrl": "https://twitter.com/aleksej221140", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_218274", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "trananh", - "displayName": "Trần Anh", - "fid": 218274, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/84266fe1-a1b2-4f40-147f-ccdd67e08d00/original", - "followers": 138, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2e53857999d649237d56502079243265205fb4b8", - "etherscanUrl": "https://etherscan.io/address/0x2e53857999d649237d56502079243265205fb4b8", - "xHandle": "0xanhtran", - "xUrl": "https://twitter.com/0xanhtran", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_477624", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "elonx1", - "displayName": "M Yasir Ali", - "fid": 477624, - "pfpUrl": "https://beb-public.s3.us-west-1.amazonaws.com/pink.jpg", - "followers": 138, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9b181020fa94fde42a7e923e4066f2c2f0125f9d", - "etherscanUrl": "https://etherscan.io/address/0x9b181020fa94fde42a7e923e4066f2c2f0125f9d", - "xHandle": "alishanoor0425", - "xUrl": "https://twitter.com/alishanoor0425", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_546602", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "diepcrypto", - "displayName": "Dani", - "fid": 546602, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4ffc787b-4444-413d-9bdf-0d759a0e5b00/original", - "followers": 131, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdd2f6052039d9c78a91eade8fd1ac3f2e578f5f3", - "etherscanUrl": "https://etherscan.io/address/0xdd2f6052039d9c78a91eade8fd1ac3f2e578f5f3", - "xHandle": "dani_resear", - "xUrl": "https://twitter.com/dani_resear", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1059930", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "smagajibk", - "displayName": "smagajibk", - "fid": 1059930, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/95aa956f-b6a3-45af-9d26-aff4fba2f100/original", - "followers": 131, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc03a58b0eb18cb0833deb1b086c12b66d509fdf1", - "etherscanUrl": "https://etherscan.io/address/0xc03a58b0eb18cb0833deb1b086c12b66d509fdf1", - "xHandle": "x_magaji", - "xUrl": "https://twitter.com/x_magaji", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_889734", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "enyart", - "displayName": "WilliamBelfort", - "fid": 889734, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/13beabf3-b7ea-4d20-0dbc-6fdf569a0300/original", - "followers": 130, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5150c2864bc321fd2507444aa869c564d8d68e36", - "etherscanUrl": "https://etherscan.io/address/0x5150c2864bc321fd2507444aa869c564d8d68e36", - "xHandle": "williambelfort_", - "xUrl": "https://twitter.com/williambelfort_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_641726", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "timekeepe", - "displayName": "Kairos Timekeeper", - "fid": 641726, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f91a7b39-c529-4998-480e-4d6ce4facf00/rectcrop3", - "followers": 129, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf87cee21b63a7babc04ce6f279a8ce12826ce069", - "etherscanUrl": "https://etherscan.io/address/0xf87cee21b63a7babc04ce6f279a8ce12826ce069", - "xHandle": "abigail6575364", - "xUrl": "https://twitter.com/abigail6575364", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_410582", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "saimona", - "displayName": "Saimona", - "fid": 410582, - "pfpUrl": "https://ipfs.decentralized-content.com/ipfs/bafybeid7kymq6ltgqj3eoy5vt3yet2fwmwvztd75akbativufsnvpagj5q", - "followers": 128, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe780b9211c1ae26de048d934990a48d5645b5637", - "etherscanUrl": "https://etherscan.io/address/0xe780b9211c1ae26de048d934990a48d5645b5637", - "xHandle": "sultan100801", - "xUrl": "https://twitter.com/sultan100801", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1112395", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "svabhishek", - "displayName": "Sistla V Abhishek", - "fid": 1112395, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/21c5cb2a-a3ea-4fb0-2600-7afa83f71800/original", - "followers": 128, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa1af7f33a79349f4ed98665189d91a59ad9d08b5", - "etherscanUrl": "https://etherscan.io/address/0xa1af7f33a79349f4ed98665189d91a59ad9d08b5", - "xHandle": "svabhishek", - "xUrl": "https://twitter.com/svabhishek", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_895194", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mkababio", - "displayName": "Yahya Mukhtar", - "fid": 895194, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c60e3234-723d-4a15-77c3-8d21c3ebad00/rectcrop3", - "followers": 128, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9a227e876b085f9c61b5715cf01d249614c8bc2c", - "etherscanUrl": "https://etherscan.io/address/0x9a227e876b085f9c61b5715cf01d249614c8bc2c", - "xHandle": "maryam47418960", - "xUrl": "https://twitter.com/maryam47418960", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_396437", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dominant", - "displayName": "Dominant base.eth", - "fid": 396437, - "pfpUrl": "https://i.imgur.com/xryMS2x.jpg", - "followers": 128, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x797c00dcedf9ad05ca4009cbac6acf69bb818c39", - "etherscanUrl": "https://etherscan.io/address/0x797c00dcedf9ad05ca4009cbac6acf69bb818c39", - "xHandle": "andreishubs", - "xUrl": "https://twitter.com/andreishubs", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_906094", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cloudhost99", - "displayName": "Cloudhost |🔵 \"base.eth\"🔵", - "fid": 906094, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6be7a286-ac3f-4c0e-d79e-1bbb5f37a400/rectcrop3", - "followers": 127, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc67483b416103918f7b129b18610bb3caa3fddc8", - "etherscanUrl": "https://etherscan.io/address/0xc67483b416103918f7b129b18610bb3caa3fddc8", - "xHandle": "andrysy63902288", - "xUrl": "https://twitter.com/andrysy63902288", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_522874", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "yaelg", - "displayName": "YaElG", - "fid": 522874, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/92e09461-2180-4cea-299d-165ba98a4700/rectcrop3", - "followers": 127, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3ab6573c612cec4a12ec383507e04d1b2533c4fd", - "etherscanUrl": "https://etherscan.io/address/0x3ab6573c612cec4a12ec383507e04d1b2533c4fd", - "xHandle": "yana_mil6", - "xUrl": "https://twitter.com/yana_mil6", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_914123", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "forresttindall", - "displayName": "Forrest", - "fid": 914123, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4c1c3e01-90dc-4f0f-99e8-d0236d8e8800/original", - "followers": 127, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2bd3dd742b7068459fe3b107a5164398e7131800", - "etherscanUrl": "https://etherscan.io/address/0x2bd3dd742b7068459fe3b107a5164398e7131800", - "xHandle": "forresttindall", - "xUrl": "https://twitter.com/forresttindall", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_708891", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "defiboy", - "displayName": "defiboy.base.eth", - "fid": 708891, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d60f54be-fb6b-4abc-6c4c-9364823b6100/rectcrop3", - "followers": 127, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x088571f6d104f967d004c811228ac014fb0a3a8b", - "etherscanUrl": "https://etherscan.io/address/0x088571f6d104f967d004c811228ac014fb0a3a8b", - "xHandle": "defiboyszn", - "xUrl": "https://twitter.com/defiboyszn", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_522292", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "moonverse", - "displayName": "Waziri", - "fid": 522292, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3be0a261-724b-45c4-9b31-42547c65d000/original", - "followers": 125, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x01884f2be5f181027d579216a772b140e9f2d57c", - "etherscanUrl": "https://etherscan.io/address/0x01884f2be5f181027d579216a772b140e9f2d57c", - "xHandle": "akuwazir", - "xUrl": "https://twitter.com/akuwazir", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1132497", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "basefunai", - "displayName": "BASEFUN", - "fid": 1132497, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/16a436a5-6cba-4f50-7156-bd95771a6100/original", - "followers": 125, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2219994d3296fdaded354274d5e00ec3cf55cf17", - "etherscanUrl": "https://etherscan.io/address/0x2219994d3296fdaded354274d5e00ec3cf55cf17", - "xHandle": "basefunai", - "xUrl": "https://twitter.com/basefunai", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_643356", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "eiraex", - "displayName": "Eira Whisper", - "fid": 643356, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4cd4833f-a884-4402-865f-b490bfbfa700/rectcrop3", - "followers": 125, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0d347a6628a1ec53288a04f7c8a725b5beb2d5ab", - "etherscanUrl": "https://etherscan.io/address/0x0d347a6628a1ec53288a04f7c8a725b5beb2d5ab", - "xHandle": "eric46447810505", - "xUrl": "https://twitter.com/eric46447810505", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_914890", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "thang888888", - "displayName": "Monad", - "fid": 914890, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/904513a8-d2a5-40ce-ebd6-34705e9c3700/rectcrop3", - "followers": 124, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8edaa7af8ac828ed58fb138c382ab77d1c502a76", - "etherscanUrl": "https://etherscan.io/address/0x8edaa7af8ac828ed58fb138c382ab77d1c502a76", - "xHandle": "thangha19931991", - "xUrl": "https://twitter.com/thangha19931991", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_639114", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "seraphinaz", - "displayName": "Seraphina Grace", - "fid": 639114, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/08ec5d66-04b5-41da-f6b0-115064494800/rectcrop3", - "followers": 124, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x973f5695133463745856f6ca0b55a0f8487fe441", - "etherscanUrl": "https://etherscan.io/address/0x973f5695133463745856f6ca0b55a0f8487fe441", - "xHandle": "chayagutie22024", - "xUrl": "https://twitter.com/chayagutie22024", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_632548", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "whisperk", - "displayName": "Echo Whisper", - "fid": 632548, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ff3f99b2-1332-466a-01b2-c273ec2c6500/rectcrop3", - "followers": 123, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9d3be1e8f30fd4c7aeb8e33b9a944390a7b4e7f2", - "etherscanUrl": "https://etherscan.io/address/0x9d3be1e8f30fd4c7aeb8e33b9a944390a7b4e7f2", - "xHandle": "odissej158984", - "xUrl": "https://twitter.com/odissej158984", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_936169", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "joshjo", - "displayName": "Joshjo", - "fid": 936169, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/83cb6705-05f8-48a1-cb75-5e91f9ecc700/original", - "followers": 121, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2692ec970cd02dac9f573b94fbf0b780524c4941", - "etherscanUrl": "https://etherscan.io/address/0x2692ec970cd02dac9f573b94fbf0b780524c4941", - "xHandle": "josh_johnson7", - "xUrl": "https://twitter.com/josh_johnson7", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_630705", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "novato", - "displayName": "Nova Quasar", - "fid": 630705, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d1e0769c-10bc-4ec8-24cb-1058fc279200/rectcrop3", - "followers": 121, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7af68e2199889d39bc361c616809bbd934f0de94", - "etherscanUrl": "https://etherscan.io/address/0x7af68e2199889d39bc361c616809bbd934f0de94", - "xHandle": "gordej62786", - "xUrl": "https://twitter.com/gordej62786", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1111551", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "voidhollow", - "displayName": "voidhollow", - "fid": 1111551, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5ba297c6-e907-410c-df71-3d38fbd10c00/original", - "followers": 119, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4454cf99d88088354145be82697856483d08860f", - "etherscanUrl": "https://etherscan.io/address/0x4454cf99d88088354145be82697856483d08860f", - "xHandle": "voidhollowa", - "xUrl": "https://twitter.com/voidhollowa", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_637288", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "eosses", - "displayName": "Eos Dawn", - "fid": 637288, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/82ef94de-62e8-4e47-c87c-5724bd1d1000/rectcrop3", - "followers": 118, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1615269cd460e9a39f30a5afd579246e37b00f8e", - "etherscanUrl": "https://etherscan.io/address/0x1615269cd460e9a39f30a5afd579246e37b00f8e", - "xHandle": "yolanda150491", - "xUrl": "https://twitter.com/yolanda150491", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_647662", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "orionquill", - "displayName": "Orion Quill", - "fid": 647662, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/475d87ac-1f30-4108-03e8-b707553f7200/rectcrop3", - "followers": 118, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x050cf7f327738e5887351318ee2f1c5532c24765", - "etherscanUrl": "https://etherscan.io/address/0x050cf7f327738e5887351318ee2f1c5532c24765", - "xHandle": "seth70580139384", - "xUrl": "https://twitter.com/seth70580139384", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_770862", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "okieroad.eth", - "displayName": "Okie Road", - "fid": 770862, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0451a75c-54ca-4cb5-947e-d5e5b27b6d00/original", - "followers": 118, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6573826d843dadb8f0656dd2f99430273ee72096", - "etherscanUrl": "https://etherscan.io/address/0x6573826d843dadb8f0656dd2f99430273ee72096", - "xHandle": "okie_road", - "xUrl": "https://twitter.com/okie_road", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_208189", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "250389.eth", - "displayName": "Jarment32 🫂", - "fid": 208189, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6e5fc454-da0b-44c5-3c4e-65eba9ddaa00/rectcrop3", - "followers": 117, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x039264fded28cc7f7c21b29cd6cdbbbeb8a5cc9e", - "etherscanUrl": "https://etherscan.io/address/0x039264fded28cc7f7c21b29cd6cdbbbeb8a5cc9e", - "xHandle": "jarment32", - "xUrl": "https://twitter.com/jarment32", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1025599", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "rapid17", - "displayName": "kangreceh🐐", - "fid": 1025599, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9fca209c-ac70-4376-94e7-f922920e1300/rectcrop3", - "followers": 117, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x913fe024c8852391519e88935b173dc4ea57763a", - "etherscanUrl": "https://etherscan.io/address/0x913fe024c8852391519e88935b173dc4ea57763a", - "xHandle": "rapidsarif", - "xUrl": "https://twitter.com/rapidsarif", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_436144", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "krxr", - "displayName": "krxr 🎩🔄🎭👾", - "fid": 436144, - "pfpUrl": "https://res.cloudinary.com/base-app/image/upload/f_auto/v1762015788/1000010115.jpg.jpg", - "followers": 115, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x43c2d6e3fede1950cbfff725db333b40690e58fd", - "etherscanUrl": "https://etherscan.io/address/0x43c2d6e3fede1950cbfff725db333b40690e58fd", - "xHandle": "soss099", - "xUrl": "https://twitter.com/soss099", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1343955", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "windylam", - "displayName": "Windylam.base.eth", - "fid": 1343955, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6a0207d2-a84c-4b83-aecb-e2fe586c6100/original", - "followers": 114, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4d790a99426c7be8a7bbebd04d364930a70fda10", - "etherscanUrl": "https://etherscan.io/address/0x4d790a99426c7be8a7bbebd04d364930a70fda10", - "xHandle": "babydogedoo", - "xUrl": "https://twitter.com/babydogedoo", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_307506", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mdarafat", - "displayName": "MD Aragat", - "fid": 307506, - "pfpUrl": "https://i.imgur.com/hdUkoYH.jpg", - "followers": 114, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdb21a7d32f524b586374901c2727ffd2b839d455", - "etherscanUrl": "https://etherscan.io/address/0xdb21a7d32f524b586374901c2727ffd2b839d455", - "xHandle": "arafatmd5203", - "xUrl": "https://twitter.com/arafatmd5203", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1098142", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "justinroberts22", - "displayName": "Justin Roberts", - "fid": 1098142, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/562725be-0505-42f8-10bf-cb4144cc0800/original", - "followers": 113, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc36941912121ecb6143dedfd81a296dfcb9e4e1c", - "etherscanUrl": "https://etherscan.io/address/0xc36941912121ecb6143dedfd81a296dfcb9e4e1c", - "xHandle": "zlatanwilliams7", - "xUrl": "https://twitter.com/zlatanwilliams7", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1039247", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0xzrx", - "displayName": "zero", - "fid": 1039247, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b5aefefa-a0a1-4965-2bad-3f9353a2a300/original", - "followers": 113, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc0cd40be67c255f849127b26ea6ff45bda23c9cd", - "etherscanUrl": "https://etherscan.io/address/0xc0cd40be67c255f849127b26ea6ff45bda23c9cd", - "xHandle": "andx_lx", - "xUrl": "https://twitter.com/andx_lx", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_523200", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kalistory", - "displayName": "Kali Story_", - "fid": 523200, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/cd4f2c3e-825e-41e2-68d8-47fc757fef00/original", - "followers": 112, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb897178d4b796495ea64e6c2f61a8032c5563960", - "etherscanUrl": "https://etherscan.io/address/0xb897178d4b796495ea64e6c2f61a8032c5563960", - "xHandle": "thekalistory_", - "xUrl": "https://twitter.com/thekalistory_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_470223", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "wasyl", - "displayName": "wasyl", - "fid": 470223, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/34feea73-a0ff-423e-484c-a56511cbe400/original", - "followers": 111, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcd60b85537c813a873bf2ca4ed1a14011e4c305c", - "etherscanUrl": "https://etherscan.io/address/0xcd60b85537c813a873bf2ca4ed1a14011e4c305c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_196104", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "lordkeklol", - "displayName": "lordkek", - "fid": 196104, - "pfpUrl": "https://i.imgur.com/vFZVd4a.jpg", - "followers": 110, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xaf3155897cba051b692276b4fc7ce26980370479", - "etherscanUrl": "https://etherscan.io/address/0xaf3155897cba051b692276b4fc7ce26980370479", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1025595", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bitstore01", - "displayName": "Bits", - "fid": 1025595, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/27d46eac-bc54-400a-797e-174e112c7e00/original", - "followers": 110, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xebf2b6d4c54ee46e138621b787111111a9301aeb", - "etherscanUrl": "https://etherscan.io/address/0xebf2b6d4c54ee46e138621b787111111a9301aeb", - "xHandle": "bitstore01", - "xUrl": "https://twitter.com/bitstore01", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_632700", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "harmonys", - "displayName": "Aria Harmony", - "fid": 632700, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a777f284-d3dc-49ee-8e81-260d93f3d800/rectcrop3", - "followers": 110, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0e768e8bba5b15415c9ad568755063da7c263911", - "etherscanUrl": "https://etherscan.io/address/0x0e768e8bba5b15415c9ad568755063da7c263911", - "xHandle": "isabelle1731224", - "xUrl": "https://twitter.com/isabelle1731224", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_344840", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "calypso", - "displayName": "Chris CATALANO", - "fid": 344840, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4830560d-438b-4a24-74ab-d63a9675a300/original", - "followers": 109, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9efeb581c020fc746c1df51d8d2e829c03aa6674", - "etherscanUrl": "https://etherscan.io/address/0x9efeb581c020fc746c1df51d8d2e829c03aa6674", - "xHandle": "upperplaygroun", - "xUrl": "https://twitter.com/upperplaygroun", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_294666", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "delvalley.eth", - "displayName": "delvalley.degen.eth🎩🎭", - "fid": 294666, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/db5febad-434e-4802-d8f6-2d16ae738600/rectcrop3", - "followers": 108, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x36774ac472949eafb10f24b99f0f719eb5d8d054", - "etherscanUrl": "https://etherscan.io/address/0x36774ac472949eafb10f24b99f0f719eb5d8d054", - "xHandle": "soloandrea23", - "xUrl": "https://twitter.com/soloandrea23", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1028184", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "aldipprt", - "displayName": "yess", - "fid": 1028184, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c9ab31e0-becb-4eb0-6d3a-82b70a7a8c00/rectcrop3", - "followers": 108, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7de96115b3ca30239163246f032c8d4d1c8d712a", - "etherscanUrl": "https://etherscan.io/address/0x7de96115b3ca30239163246f032c8d4d1c8d712a", - "xHandle": "laknat222", - "xUrl": "https://twitter.com/laknat222", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_921676", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "khodox69", - "displayName": "froggy", - "fid": 921676, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d4297a1c-3718-4461-6c27-ecee228e8100/original", - "followers": 107, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe95cfcf2ecdae4dd6348d805043ee78e51a1303e", - "etherscanUrl": "https://etherscan.io/address/0xe95cfcf2ecdae4dd6348d805043ee78e51a1303e", - "xHandle": "khodox6969", - "xUrl": "https://twitter.com/khodox6969", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_257981", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "arthurdy", - "displayName": "Arthur", - "fid": 257981, - "pfpUrl": "https://i.imgur.com/3FrcCBv.jpg", - "followers": 106, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x79575154bcac15bcc6cee1a0b88ca7b7dc6d8ce1", - "etherscanUrl": "https://etherscan.io/address/0x79575154bcac15bcc6cee1a0b88ca7b7dc6d8ce1", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_218063", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "daddyhydro", - "displayName": "daddyhydro | SubCanis ", - "fid": 218063, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b38bf0bb-3609-47cb-9256-b860699d2800/rectcrop3", - "followers": 106, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1958229dcd03de4b9f36d6861aa07171fa790812", - "etherscanUrl": "https://etherscan.io/address/0x1958229dcd03de4b9f36d6861aa07171fa790812", - "xHandle": "scg1001", - "xUrl": "https://twitter.com/scg1001", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_4997", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "otw", - "displayName": "wizardofongz", - "fid": 4997, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b3e72f46-4466-4bd9-dab7-0d1b6b928900/original", - "followers": 105, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd20e8d5e3ddd96c42ded7053842d054bf9e64c29", - "etherscanUrl": "https://etherscan.io/address/0xd20e8d5e3ddd96c42ded7053842d054bf9e64c29", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_891519", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "elbandito77", - "displayName": "elbandito77😺", - "fid": 891519, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ceaa63cc-4431-4bd9-20ba-9ccd38ecca00/rectcrop3", - "followers": 104, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5979fc1894de026978d3feaf4b8cd69d66970cce", - "etherscanUrl": "https://etherscan.io/address/0x5979fc1894de026978d3feaf4b8cd69d66970cce", - "xHandle": "theoldlady_o", - "xUrl": "https://twitter.com/theoldlady_o", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_631229", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "zephyrs", - "displayName": "Zephyr Breeze", - "fid": 631229, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/cb168541-b18a-4534-2a44-cfc2b2eae700/rectcrop3", - "followers": 104, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8917c9b50c89f2e8ea969ba6bf36c267e99ba570", - "etherscanUrl": "https://etherscan.io/address/0x8917c9b50c89f2e8ea969ba6bf36c267e99ba570", - "xHandle": "daniel760917713", - "xUrl": "https://twitter.com/daniel760917713", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_942311", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sadikali", - "displayName": "sadik ali", - "fid": 942311, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/17cfce9e-4847-4f59-ba90-20e6ecd83700/rectcrop3", - "followers": 104, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x60fed58f177587a179559243a257ce7934ad2cdc", - "etherscanUrl": "https://etherscan.io/address/0x60fed58f177587a179559243a257ce7934ad2cdc", - "xHandle": "sadikali18374", - "xUrl": "https://twitter.com/sadikali18374", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1117013", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "xkrut", - "displayName": "0Xkrut _base.eth", - "fid": 1117013, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/097ee01d-ce4c-421e-f294-94c244bb0d00/original", - "followers": 102, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbed0006cc4a257c8651beb1a7df42205cf73276c", - "etherscanUrl": "https://etherscan.io/address/0xbed0006cc4a257c8651beb1a7df42205cf73276c", - "xHandle": "xkrut_", - "xUrl": "https://twitter.com/xkrut_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1060933", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dwiprase", - "displayName": "DenPras", - "fid": 1060933, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/88b985ee-d49a-47fc-37b9-4be163755400/original", - "followers": 102, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2c681e78287900839a9c73b5ae736f2cedb04012", - "etherscanUrl": "https://etherscan.io/address/0x2c681e78287900839a9c73b5ae736f2cedb04012", - "xHandle": "tanakainunft", - "xUrl": "https://twitter.com/tanakainunft", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1090387", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "davspick", - "displayName": "Davspick", - "fid": 1090387, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d8a33e93-c850-4c2f-0159-4165ffd8c000/rectcrop3", - "followers": 102, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x047fdff3d2b46d2a8d5c3a26348d9aef7f9f96ed", - "etherscanUrl": "https://etherscan.io/address/0x047fdff3d2b46d2a8d5c3a26348d9aef7f9f96ed", - "xHandle": "davspick", - "xUrl": "https://twitter.com/davspick", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_994263", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "princetonzart", - "displayName": "princetonzart", - "fid": 994263, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/25dcf022-f773-4626-4846-d270f4a6fa00/rectcrop3", - "followers": 102, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe212f058bbe2dba2f6d87876887025dc78556210", - "etherscanUrl": "https://etherscan.io/address/0xe212f058bbe2dba2f6d87876887025dc78556210", - "xHandle": "princetonzart", - "xUrl": "https://twitter.com/princetonzart", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_630215", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "seraphinaa", - "displayName": "Seraphina", - "fid": 630215, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/44d2d834-1d34-46e2-e89d-725f92be3800/rectcrop3", - "followers": 102, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9829e8f964004d3e98c6dd3981bdf795ade3e3df", - "etherscanUrl": "https://etherscan.io/address/0x9829e8f964004d3e98c6dd3981bdf795ade3e3df", - "xHandle": "elucas91373", - "xUrl": "https://twitter.com/elucas91373", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1315093", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "alexsmartg21771", - "displayName": "artbull", - "fid": 1315093, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/017c9caa-758a-4a04-3a9b-808b30284400/original", - "followers": 101, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x02d12af23804d744775aa4ec83e1dcd3887379b8", - "etherscanUrl": "https://etherscan.io/address/0x02d12af23804d744775aa4ec83e1dcd3887379b8", - "xHandle": "alexsmartg21771", - "xUrl": "https://twitter.com/alexsmartg21771", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_250032", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kelasheh1", - "displayName": "Anim451", - "fid": 250032, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/8476c4eb-bf38-4095-68ab-3bae5b041b00/original", - "followers": 101, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x59b141dc540f7b13e9231792c23c1f5c091a15cb", - "etherscanUrl": "https://etherscan.io/address/0x59b141dc540f7b13e9231792c23c1f5c091a15cb", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_421059", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0xheru", - "displayName": "Her.eth", - "fid": 421059, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b1a3c954-fc02-4db5-e59b-11a6088ff100/original", - "followers": 101, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe3b1db0ed642a40c58d1b6561d60368452bff1fe", - "etherscanUrl": "https://etherscan.io/address/0xe3b1db0ed642a40c58d1b6561d60368452bff1fe", - "xHandle": "zkrodn", - "xUrl": "https://twitter.com/zkrodn", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1045513", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "juissynfts", - "displayName": "Kayadu Lohar", - "fid": 1045513, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/be159811-1ed7-4106-099f-aeef9d263400/original", - "followers": 101, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x769327caff8c08b692ce6edf9a2bf4d5abcd3548", - "etherscanUrl": "https://etherscan.io/address/0x769327caff8c08b692ce6edf9a2bf4d5abcd3548", - "xHandle": "vinnycrypto01", - "xUrl": "https://twitter.com/vinnycrypto01", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1062481", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "topp-airdrop", - "displayName": "Wish me luck 🧬", - "fid": 1062481, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/af9873c3-bfe9-499c-e890-f339346a7100/original", - "followers": 100, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3c70620311367e8d8ef0f0ffb4b4336c525ed7c3", - "etherscanUrl": "https://etherscan.io/address/0x3c70620311367e8d8ef0f0ffb4b4336c525ed7c3", - "xHandle": "topp_airdrop", - "xUrl": "https://twitter.com/topp_airdrop", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_824027", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "9229", - "displayName": "Cool 🎩", - "fid": 824027, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/bd592c4f-9423-4fb2-a01f-3cff7266b600/rectcrop3", - "followers": 100, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x25db866df2fd0e467943e8b21cf21f70cb2a29ad", - "etherscanUrl": "https://etherscan.io/address/0x25db866df2fd0e467943e8b21cf21f70cb2a29ad", - "xHandle": "chanjie112233", - "xUrl": "https://twitter.com/chanjie112233", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_507436", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "esta", - "displayName": "LogicCrafterDz", - "fid": 507436, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/80eabf2a-c5a5-4535-6c2f-2d12471a2300/original", - "followers": 99, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x14f4696369e60fc31477202b7a53c6e552a489d5", - "etherscanUrl": "https://etherscan.io/address/0x14f4696369e60fc31477202b7a53c6e552a489d5", - "xHandle": "arana_lib", - "xUrl": "https://twitter.com/arana_lib", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1237696", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "skuycrot.base.eth", - "displayName": "Wick", - "fid": 1237696, - "pfpUrl": "https://res.cloudinary.com/base-app/image/upload/f_auto/v1763335315/744e044a-3fc8-4e3e-ba2c-cbee73486a5e.png", - "followers": 99, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5d0fc312d1a3cb6c0caa1dae1d774bc0ae9a71f9", - "etherscanUrl": "https://etherscan.io/address/0x5d0fc312d1a3cb6c0caa1dae1d774bc0ae9a71f9", - "xHandle": "wicky_wey", - "xUrl": "https://twitter.com/wicky_wey", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_621625", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mohitbmurkute", - "displayName": "Mohit Murkute", - "fid": 621625, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/89581ea5-5336-4539-c9c3-7c15e2c60300/rectcrop3", - "followers": 98, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x70c6c7c8d763b2ac2e32e4c0c57f678d4b8189d3", - "etherscanUrl": "https://etherscan.io/address/0x70c6c7c8d763b2ac2e32e4c0c57f678d4b8189d3", - "xHandle": "mohitmurku38422", - "xUrl": "https://twitter.com/mohitmurku38422", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_462116", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "taochen", - "displayName": "taochen", - "fid": 462116, - "pfpUrl": "https://i.imgur.com/kV4cUe7.jpg", - "followers": 97, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2df7fb924bfa2817c35e4c527e25b3a4d6b4a6ba", - "etherscanUrl": "https://etherscan.io/address/0x2df7fb924bfa2817c35e4c527e25b3a4d6b4a6ba", - "xHandle": "tes53596747", - "xUrl": "https://twitter.com/tes53596747", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_21694", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "pysger", - "displayName": "pysger", - "fid": 21694, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b9c2f097-9d65-4c77-6085-56c9270df500/original", - "followers": 97, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x00fbbc2362303396c22016da616f5b9afa644af8", - "etherscanUrl": "https://etherscan.io/address/0x00fbbc2362303396c22016da616f5b9afa644af8", - "xHandle": "pysger", - "xUrl": "https://twitter.com/pysger", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1007489", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "zakareia312", - "displayName": "Zakareia312", - "fid": 1007489, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b0de5acd-0062-4f0e-30c8-31fa73cae200/rectcrop3", - "followers": 95, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1daad92dad69e71d688f065e8c7ff8a9ab858add", - "etherscanUrl": "https://etherscan.io/address/0x1daad92dad69e71d688f065e8c7ff8a9ab858add", - "xHandle": "zakareia312", - "xUrl": "https://twitter.com/zakareia312", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_940301", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "tony-tt", - "displayName": "Tony TT", - "fid": 940301, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1b28db4e-29df-4dd8-b55d-2d6e43a6d500/original", - "followers": 94, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa40b94ba0bf1388977c4430e3b87be77814665bb", - "etherscanUrl": "https://etherscan.io/address/0xa40b94ba0bf1388977c4430e3b87be77814665bb", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_235567", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "avlethtl", - "displayName": "Avl", - "fid": 235567, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6926523b-b9f8-4e40-c57f-dd23deb24900/original", - "followers": 94, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2855919965de20d24218c9fcc52765a4a3805d15", - "etherscanUrl": "https://etherscan.io/address/0x2855919965de20d24218c9fcc52765a4a3805d15", - "xHandle": "avlethtl", - "xUrl": "https://twitter.com/avlethtl", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_576235", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "rafaona1", - "displayName": "🐱Raf", - "fid": 576235, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/dafb942a-7a18-4515-e796-05546b7dbf00/rectcrop3", - "followers": 93, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x35f3fc7e0958ba0b264c2c586616a4fd192c919f", - "etherscanUrl": "https://etherscan.io/address/0x35f3fc7e0958ba0b264c2c586616a4fd192c919f", - "xHandle": "rafsanjaniazan", - "xUrl": "https://twitter.com/rafsanjaniazan", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_386722", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "koykoy", - "displayName": "Senditia", - "fid": 386722, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/91d1eefe-d6c0-4d2f-7f89-914b65e53900/original", - "followers": 92, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2b5c163cee1ee1f2bfbc4c2a2fa28fc2a48922f2", - "etherscanUrl": "https://etherscan.io/address/0x2b5c163cee1ee1f2bfbc4c2a2fa28fc2a48922f2", - "xHandle": "kangbaso24", - "xUrl": "https://twitter.com/kangbaso24", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1074508", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "shisangmoebie", - "displayName": "0xsamb", - "fid": 1074508, - "pfpUrl": "https://res.cloudinary.com/base-app/image/upload/f_auto/v1762444547/65eac5cb-1b5a-4b7f-a17e-ce56817ff8d3.jpg.jpg", - "followers": 90, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb99a3817e1ff81536aaf21d0741fc540fc9f36a6", - "etherscanUrl": "https://etherscan.io/address/0xb99a3817e1ff81536aaf21d0741fc540fc9f36a6", - "xHandle": "shisangmoebie", - "xUrl": "https://twitter.com/shisangmoebie", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1081904", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "rahim22", - "displayName": "Rahinss", - "fid": 1081904, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3177db4c-fe46-4ffc-7b3b-8e813bf3f700/rectcrop3", - "followers": 89, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x23f3c82e11d7f83d9b3792d930e91724f9b67cd9", - "etherscanUrl": "https://etherscan.io/address/0x23f3c82e11d7f83d9b3792d930e91724f9b67cd9", - "xHandle": "borsa35804221", - "xUrl": "https://twitter.com/borsa35804221", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_898170", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "romands", - "displayName": "romands.base.eth", - "fid": 898170, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ebde434e-8356-409f-17f0-4d4f87323d00/original", - "followers": 87, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x969464f044b54432b0aa8ba74934cd19d92f43f9", - "etherscanUrl": "https://etherscan.io/address/0x969464f044b54432b0aa8ba74934cd19d92f43f9", - "xHandle": "nstwn29163", - "xUrl": "https://twitter.com/nstwn29163", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1129707", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "obytex39", - "displayName": "Oby", - "fid": 1129707, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2f506022-14ea-451b-f580-463be9e2c400/original", - "followers": 87, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x50c87092834f671ddaa2422b6486ef74f447b07d", - "etherscanUrl": "https://etherscan.io/address/0x50c87092834f671ddaa2422b6486ef74f447b07d", - "xHandle": "obytex44", - "xUrl": "https://twitter.com/obytex44", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1035896", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "elbarra", - "displayName": "Elbarra", - "fid": 1035896, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ab8e28a2-6f8b-4fb2-93dd-d7c809d2aa00/original", - "followers": 87, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7f7cd39f36914568c81ff5686408b7cd1db673c4", - "etherscanUrl": "https://etherscan.io/address/0x7f7cd39f36914568c81ff5686408b7cd1db673c4", - "xHandle": "hendry_arch", - "xUrl": "https://twitter.com/hendry_arch", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1091567", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "c35ar27", - "displayName": "DR0N3", - "fid": 1091567, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b40f3845-6415-4434-975d-df31e613c200/original", - "followers": 86, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0247ffabcc8aa8883eb10873b58630569c06d494", - "etherscanUrl": "https://etherscan.io/address/0x0247ffabcc8aa8883eb10873b58630569c06d494", - "xHandle": "c35ar_27", - "xUrl": "https://twitter.com/c35ar_27", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_904124", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "crypthor", - "displayName": "Crypthor ⚡", - "fid": 904124, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0dba42fe-09b8-40a0-18ab-055180597300/rectcrop3", - "followers": 86, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd7c9ee89236605ef33995aa0e6cabcf099ea7d4c", - "etherscanUrl": "https://etherscan.io/address/0xd7c9ee89236605ef33995aa0e6cabcf099ea7d4c", - "xHandle": "khaelezekiel", - "xUrl": "https://twitter.com/khaelezekiel", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_632723", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sorens", - "displayName": "Soren Raven", - "fid": 632723, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0df074d6-a2f3-4ead-da8d-69eba2894e00/rectcrop3", - "followers": 86, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5fcf92dbb5f836c1fe4a0b4fdf86950f4eb0b739", - "etherscanUrl": "https://etherscan.io/address/0x5fcf92dbb5f836c1fe4a0b4fdf86950f4eb0b739", - "xHandle": "jacques327989", - "xUrl": "https://twitter.com/jacques327989", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_313957", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "messager", - "displayName": "Messager", - "fid": 313957, - "pfpUrl": "https://i.imgur.com/wgVn0oM.jpg", - "followers": 85, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x89a710dcbfb3a6d9e986bf316648b8505e2c2dfe", - "etherscanUrl": "https://etherscan.io/address/0x89a710dcbfb3a6d9e986bf316648b8505e2c2dfe", - "xHandle": "shadowm09811421", - "xUrl": "https://twitter.com/shadowm09811421", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1088084", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "medob", - "displayName": "Crypto Brooo 🆓 🐐", - "fid": 1088084, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4c2b816a-0700-48f4-2b16-d64c7fddfc00/original", - "followers": 84, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2497d0ef68d531c93fef5fd8f6fde826926dadc0", - "etherscanUrl": "https://etherscan.io/address/0x2497d0ef68d531c93fef5fd8f6fde826926dadc0", - "xHandle": "medobee23", - "xUrl": "https://twitter.com/medobee23", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_258883", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bharath", - "displayName": "Bharath", - "fid": 258883, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/35424bae-d1d1-48d3-8e0e-4e660d0daa00/rectcrop3", - "followers": 84, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc661cff9e5c667cc8914356c7d33599bb98400b7", - "etherscanUrl": "https://etherscan.io/address/0xc661cff9e5c667cc8914356c7d33599bb98400b7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_393325", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mdsohel1233", - "displayName": "MD SOHEL MOLLA", - "fid": 393325, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9df0072e-5b90-4c4c-6165-a13d2d392600/rectcrop3", - "followers": 83, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe114490329f4f2baa7c10261a77372362841df6b", - "etherscanUrl": "https://etherscan.io/address/0xe114490329f4f2baa7c10261a77372362841df6b", - "xHandle": "mdsohel1244", - "xUrl": "https://twitter.com/mdsohel1244", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_801257", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kenny73", - "displayName": "Superkenn", - "fid": 801257, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/45fc0a36-4cb0-4344-0635-62c84d2fd700/original", - "followers": 82, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3a68381e116cebebb4a36c2a09e04aac0aff8b30", - "etherscanUrl": "https://etherscan.io/address/0x3a68381e116cebebb4a36c2a09e04aac0aff8b30", - "xHandle": "prettyliciouszs", - "xUrl": "https://twitter.com/prettyliciouszs", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1120686", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "vansh90s", - "displayName": "NineSeconds.eth 🧬", - "fid": 1120686, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/07cd8de6-a3f5-4b84-30e7-a197e3e83000/original", - "followers": 81, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2eefd3d0f201a10b21c62cfd388363aebe58de93", - "etherscanUrl": "https://etherscan.io/address/0x2eefd3d0f201a10b21c62cfd388363aebe58de93", - "xHandle": "9s_eth", - "xUrl": "https://twitter.com/9s_eth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1123160", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "primejoseph001", - "displayName": "PRIME1", - "fid": 1123160, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3c5bc832-97e4-4ba2-c7b7-d5b7523bc400/original", - "followers": 81, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x805ec4d667d1d99c179f1085fd8c098aec45ceda", - "etherscanUrl": "https://etherscan.io/address/0x805ec4d667d1d99c179f1085fd8c098aec45ceda", - "xHandle": "j_joepete", - "xUrl": "https://twitter.com/j_joepete", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1082427", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nfafretty", - "displayName": "Ftytrades", - "fid": 1082427, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/be8deecf-57c0-45e4-0124-f4f136e1a700/original", - "followers": 80, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe953f23f21f781ca736e629f9efd86a4712ae924", - "etherscanUrl": "https://etherscan.io/address/0xe953f23f21f781ca736e629f9efd86a4712ae924", - "xHandle": "nfa_fretty", - "xUrl": "https://twitter.com/nfa_fretty", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1105011", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "scania", - "displayName": "scanïa", - "fid": 1105011, - "pfpUrl": "https://res.cloudinary.com/base-app/image/upload/f_auto/v1762409853/817522c8-89fc-418d-b8fa-3d245a501708.jpg.jpg", - "followers": 80, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x12c2ad47d9b19f1733c313d048de1a442856f729", - "etherscanUrl": "https://etherscan.io/address/0x12c2ad47d9b19f1733c313d048de1a442856f729", - "xHandle": "scania1369", - "xUrl": "https://twitter.com/scania1369", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1058870", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "superus", - "displayName": "Ruslan", - "fid": 1058870, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/14c798ff-dc85-4d9a-56cf-7d79e10b5700/original", - "followers": 80, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xff0065c6407fda63c92aed96b5d2765f2b0e161b", - "etherscanUrl": "https://etherscan.io/address/0xff0065c6407fda63c92aed96b5d2765f2b0e161b", - "xHandle": "content_ar30978", - "xUrl": "https://twitter.com/content_ar30978", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1043976", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "defijay1", - "displayName": "Defi Jay🫶✨️", - "fid": 1043976, - "pfpUrl": "https://res.cloudinary.com/base-app/image/upload/f_auto/v1761776984/IMG-20250722-WA0005.jpg.jpg", - "followers": 80, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xebe78d7ece0a9b7e28dc44c489c6dc33b0252fd6", - "etherscanUrl": "https://etherscan.io/address/0xebe78d7ece0a9b7e28dc44c489c6dc33b0252fd6", - "xHandle": "jayacouture", - "xUrl": "https://twitter.com/jayacouture", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_330364", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "receps", - "displayName": "spyedge", - "fid": 330364, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/23ea074b-3f20-4af0-d132-787d8ce8db00/original", - "followers": 79, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa553b8d12df6c1d3ae3e2121f1a62df27c5580df", - "etherscanUrl": "https://etherscan.io/address/0xa553b8d12df6c1d3ae3e2121f1a62df27c5580df", - "xHandle": "cryptoxds", - "xUrl": "https://twitter.com/cryptoxds", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_929643", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sapubersih", - "displayName": "sapubersih.base.eth", - "fid": 929643, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e3e4074d-c3ca-4c30-2997-1e28cd440d00/original", - "followers": 79, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x60f01fbf5b06cba4290f990e0dafec24b63b45d0", - "etherscanUrl": "https://etherscan.io/address/0x60f01fbf5b06cba4290f990e0dafec24b63b45d0", - "xHandle": "rahmafrida2", - "xUrl": "https://twitter.com/rahmafrida2", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1401826", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "4brownbears", - "displayName": "4brownbears", - "fid": 1401826, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/da7abbfc-b836-468b-0190-b89821314800/original", - "followers": 78, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcb69cdea85f40c4c31f30f30103e1c3bbadd82e0", - "etherscanUrl": "https://etherscan.io/address/0xcb69cdea85f40c4c31f30f30103e1c3bbadd82e0", - "xHandle": "4_brownbears", - "xUrl": "https://twitter.com/4_brownbears", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_201834", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "weisochen.eth", - "displayName": "Weiso", - "fid": 201834, - "pfpUrl": "https://i.imgur.com/qnaC0WY.jpg", - "followers": 77, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfa83e274367d45b48ac3b39c1fa4f3664574d23b", - "etherscanUrl": "https://etherscan.io/address/0xfa83e274367d45b48ac3b39c1fa4f3664574d23b", - "xHandle": "acrwc1", - "xUrl": "https://twitter.com/acrwc1", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_578753", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "vitaliy92", - "displayName": "Vitaliy", - "fid": 578753, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/89d367dd-74fe-4aa3-4e9c-ea30a1717500/rectcrop3", - "followers": 76, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xed6f66c4238afc7c35c4934710f7dd6d7d387b1d", - "etherscanUrl": "https://etherscan.io/address/0xed6f66c4238afc7c35c4934710f7dd6d7d387b1d", - "xHandle": "vtalj176029", - "xUrl": "https://twitter.com/vtalj176029", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1024718", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ayse3466", - "displayName": "Ayse", - "fid": 1024718, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0bf323ea-9688-4517-a639-f536cf6b1000/rectcrop3", - "followers": 76, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0361ce42c37dea0da71324f599f68269e39097c9", - "etherscanUrl": "https://etherscan.io/address/0x0361ce42c37dea0da71324f599f68269e39097c9", - "xHandle": "abaskal66", - "xUrl": "https://twitter.com/abaskal66", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_570483", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kishour92", - "displayName": ".base.eth", - "fid": 570483, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/12ff23b2-bc07-4ee4-28bb-ffce3f818e00/rectcrop3", - "followers": 75, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9264517687027498cfbeb5749853433310244d08", - "etherscanUrl": "https://etherscan.io/address/0x9264517687027498cfbeb5749853433310244d08", - "xHandle": "kishour92", - "xUrl": "https://twitter.com/kishour92", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_302714", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cuan110", - "displayName": "Cuan110", - "fid": 302714, - "pfpUrl": "https://i.imgur.com/6tFy2i6.jpg", - "followers": 75, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1574dcf018762b9f1b3cd39d5ae3bf2913347973", - "etherscanUrl": "https://etherscan.io/address/0x1574dcf018762b9f1b3cd39d5ae3bf2913347973", - "xHandle": "jeoungsot", - "xUrl": "https://twitter.com/jeoungsot", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1106257", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bombiee", - "displayName": "Voxx", - "fid": 1106257, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d0f6ec5a-ca07-4e76-f610-c1499b0dd700/original", - "followers": 74, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x76c81973dedfcc4b803f664d73af77ce79e739eb", - "etherscanUrl": "https://etherscan.io/address/0x76c81973dedfcc4b803f664d73af77ce79e739eb", - "xHandle": "baitclick984", - "xUrl": "https://twitter.com/baitclick984", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_252460", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "duts", - "displayName": "Law.base.eth", - "fid": 252460, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9eb1275c-5ec0-4790-224e-b67a2b6b7a00/rectcrop3", - "followers": 74, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x90f4fe74630e3a25a3e1d7e8585eff53773614c5", - "etherscanUrl": "https://etherscan.io/address/0x90f4fe74630e3a25a3e1d7e8585eff53773614c5", - "xHandle": "0xlawliettee", - "xUrl": "https://twitter.com/0xlawliettee", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_14431", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "druid", - "displayName": "Druid", - "fid": 14431, - "pfpUrl": "https://i.imgur.com/Fe6d6fj.jpg", - "followers": 73, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcabf6af1f694c2a3b0aa6135c2aab0e6f3cfe480", - "etherscanUrl": "https://etherscan.io/address/0xcabf6af1f694c2a3b0aa6135c2aab0e6f3cfe480", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_17742", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "oji3", - "displayName": "oji3", - "fid": 17742, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b6f8ac41-aceb-4abe-58a2-98e5fb17f600/original", - "followers": 73, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x85e2e5048fd575ae42f250a7a64c136269ae0ad8", - "etherscanUrl": "https://etherscan.io/address/0x85e2e5048fd575ae42f250a7a64c136269ae0ad8", - "xHandle": "open3x", - "xUrl": "https://twitter.com/open3x", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1043908", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "oxxelon", - "displayName": "ZOSE", - "fid": 1043908, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2e16af16-e183-41a9-c769-cfab19c38500/original", - "followers": 73, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc5c8e6c56e8ff0786b54f246069d52ebf2e6b32f", - "etherscanUrl": "https://etherscan.io/address/0xc5c8e6c56e8ff0786b54f246069d52ebf2e6b32f", - "xHandle": "galax254", - "xUrl": "https://twitter.com/galax254", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_16036", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "zkmattwyatt", - "displayName": "Matt", - "fid": 16036, - "pfpUrl": "https://i.imgur.com/CwBvxOc.jpg", - "followers": 72, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7b9314a878a222375000ebbe06d475408b3e52fc", - "etherscanUrl": "https://etherscan.io/address/0x7b9314a878a222375000ebbe06d475408b3e52fc", - "xHandle": "zkmattwyatt", - "xUrl": "https://twitter.com/zkmattwyatt", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_337706", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cryptopalangs", - "displayName": "cryptopalangs", - "fid": 337706, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/22baf2a7-ad8b-414c-3331-13f58fdaff00/original", - "followers": 72, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x093890d6db745d7504f511f6ce9d1912c681ce0c", - "etherscanUrl": "https://etherscan.io/address/0x093890d6db745d7504f511f6ce9d1912c681ce0c", - "xHandle": "palangscrypto", - "xUrl": "https://twitter.com/palangscrypto", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_592312", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "habiscus", - "displayName": "Mariam Ajah", - "fid": 592312, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a806cee2-f20c-4809-4667-b0fcefbc2400/rectcrop3", - "followers": 72, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x17f0989b19695cd36c2e637754a320c7644dbcc6", - "etherscanUrl": "https://etherscan.io/address/0x17f0989b19695cd36c2e637754a320c7644dbcc6", - "xHandle": "habiscus6", - "xUrl": "https://twitter.com/habiscus6", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1041324", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "gwess", - "displayName": "gugu.base.eth", - "fid": 1041324, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6b831ddf-70c8-436b-558d-b28c971a6600/original", - "followers": 72, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd4f18397473a708f3e9213d9e3a68e1a29238c66", - "etherscanUrl": "https://etherscan.io/address/0xd4f18397473a708f3e9213d9e3a68e1a29238c66", - "xHandle": "gwess", - "xUrl": "https://twitter.com/gwess", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1310782", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "brandon8danny", - "displayName": "Brandon Danny", - "fid": 1310782, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0c5a54ad-556d-4ab1-e165-917516b27000/original", - "followers": 71, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4925e248ee38fdf29b8dd8a27dbf31100f62334b", - "etherscanUrl": "https://etherscan.io/address/0x4925e248ee38fdf29b8dd8a27dbf31100f62334b", - "xHandle": "brandon8danny", - "xUrl": "https://twitter.com/brandon8danny", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1121094", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "crocky", - "displayName": "Crocky", - "fid": 1121094, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5dccabf9-a8d4-451d-050a-7985a4cb2900/rectcrop3", - "followers": 71, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa9680f1448a0748089acc0df4c4874777476ac4c", - "etherscanUrl": "https://etherscan.io/address/0xa9680f1448a0748089acc0df4c4874777476ac4c", - "xHandle": "offical_crocky", - "xUrl": "https://twitter.com/offical_crocky", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1096779", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "joeyvee", - "displayName": "Ali. Cid", - "fid": 1096779, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4dc864b5-483e-4230-3d37-08f4c2642900/original", - "followers": 69, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3670788ed9ac17f3b40cdd585def527eed1021a6", - "etherscanUrl": "https://etherscan.io/address/0x3670788ed9ac17f3b40cdd585def527eed1021a6", - "xHandle": "joevgamerwi", - "xUrl": "https://twitter.com/joevgamerwi", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_865511", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "megathrust", - "displayName": "Megathrust", - "fid": 865511, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a05fb8c1-f63e-497c-b615-c7585ce84b00/original", - "followers": 69, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfa158cfb6a35a5dde00b12254af9fa7f69c4b963", - "etherscanUrl": "https://etherscan.io/address/0xfa158cfb6a35a5dde00b12254af9fa7f69c4b963", - "xHandle": "aryshfahmi", - "xUrl": "https://twitter.com/aryshfahmi", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1390353", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sergiocryp", - "displayName": "Sergio", - "fid": 1390353, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/176db847-df78-4233-120b-3af790865f00/original", - "followers": 67, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa13d7ab5b0b597accb646e0f6bb488bfad42a3c9", - "etherscanUrl": "https://etherscan.io/address/0xa13d7ab5b0b597accb646e0f6bb488bfad42a3c9", - "xHandle": "sergio_cryp", - "xUrl": "https://twitter.com/sergio_cryp", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_735766", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "abcryptozone", - "displayName": "MD JOYNAL ABEDIN", - "fid": 735766, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d6405873-e3a7-4c3b-c8c0-2380c2114e00/rectcrop3", - "followers": 67, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x415609b9f669ef0938fa6c45cb8bf06ae61115e9", - "etherscanUrl": "https://etherscan.io/address/0x415609b9f669ef0938fa6c45cb8bf06ae61115e9", - "xHandle": "abcryptozone", - "xUrl": "https://twitter.com/abcryptozone", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_871799", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nafanros", - "displayName": "Korea", - "fid": 871799, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0969a566-cd1c-46eb-d2a6-823eae746c00/rectcrop3", - "followers": 66, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe4f6a9fc8fff635d6c22d6a526656fa0b065b809", - "etherscanUrl": "https://etherscan.io/address/0xe4f6a9fc8fff635d6c22d6a526656fa0b065b809", - "xHandle": "ukmmandiri", - "xUrl": "https://twitter.com/ukmmandiri", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_9995", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "fidenza", - "displayName": "Neo", - "fid": 9995, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d966cfba-ffb5-49de-5c01-0f961bb1c200/original", - "followers": 66, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x87c74daa68c50f9b056d03c072016a7add41a96f", - "etherscanUrl": "https://etherscan.io/address/0x87c74daa68c50f9b056d03c072016a7add41a96f", - "xHandle": "drawingcontext", - "xUrl": "https://twitter.com/drawingcontext", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1023720", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "abusayiiid", - "displayName": "Abu Sayed", - "fid": 1023720, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/96586ba3-8202-4749-e85f-abf6f5288c00/rectcrop3", - "followers": 65, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x071d417a794236a8b8b2a2fee75fcd10c4192d34", - "etherscanUrl": "https://etherscan.io/address/0x071d417a794236a8b8b2a2fee75fcd10c4192d34", - "xHandle": "abusayiiid", - "xUrl": "https://twitter.com/abusayiiid", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_267821", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "s0crypto", - "displayName": "s0crypto.eth", - "fid": 267821, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/47dbca6c-987f-420c-ef4b-d192e5d64800/original", - "followers": 64, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x70cf07f16ec6420417eef30178628f72eec68d71", - "etherscanUrl": "https://etherscan.io/address/0x70cf07f16ec6420417eef30178628f72eec68d71", - "xHandle": "s0crypto", - "xUrl": "https://twitter.com/s0crypto", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1066170", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ouln00", - "displayName": "Lannabi Ou", - "fid": 1066170, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/06638f67-2363-4bc3-e852-20dc29fb7a00/rectcrop3", - "followers": 64, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc841f1cfa3a2253642fd543a734cbeabca88f28d", - "etherscanUrl": "https://etherscan.io/address/0xc841f1cfa3a2253642fd543a734cbeabca88f28d", - "xHandle": "stars_telegram", - "xUrl": "https://twitter.com/stars_telegram", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_883412", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "andryrock", - "displayName": "Jhodry", - "fid": 883412, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9a5fd234-acc8-4752-b99a-0e6ab7753300/rectcrop3", - "followers": 64, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf5e141893c3398d9707b4b76404428b4c5a64010", - "etherscanUrl": "https://etherscan.io/address/0xf5e141893c3398d9707b4b76404428b4c5a64010", - "xHandle": "andryyy131211", - "xUrl": "https://twitter.com/andryyy131211", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1069262", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cryptodexx", - "displayName": "Rajib “(✸,✸)", - "fid": 1069262, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f1e6760d-3079-4e4e-8c8c-bb1efae65000/original", - "followers": 64, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xadaa9920a7b9b897dfdb45a8a244754530c31a04", - "etherscanUrl": "https://etherscan.io/address/0xadaa9920a7b9b897dfdb45a8a244754530c31a04", - "xHandle": "133eth", - "xUrl": "https://twitter.com/133eth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_631599", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "embers", - "displayName": "Ember Flame", - "fid": 631599, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a8b3022d-187e-4ed1-2488-bdf41032cf00/rectcrop3", - "followers": 64, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf9a9dac8f44b426b22b2855bac954c63cc69ead5", - "etherscanUrl": "https://etherscan.io/address/0xf9a9dac8f44b426b22b2855bac954c63cc69ead5", - "xHandle": "valeri193274", - "xUrl": "https://twitter.com/valeri193274", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1079083", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0x45", - "displayName": "0x.prosperity", - "fid": 1079083, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e6d335f0-6916-4274-3573-c08a74cd0100/original", - "followers": 63, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x96d821b36768a4e8dbc1b66b721221fd91f7f936", - "etherscanUrl": "https://etherscan.io/address/0x96d821b36768a4e8dbc1b66b721221fd91f7f936", - "xHandle": "0x45__", - "xUrl": "https://twitter.com/0x45__", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_879975", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "khyzzer", - "displayName": "Crypto Guapaldo base.eth", - "fid": 879975, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/67306938-117c-48e2-817c-8808ccadd200/rectcrop3", - "followers": 63, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7ee35e81571f575689b2e1a2076253905b3f1c30", - "etherscanUrl": "https://etherscan.io/address/0x7ee35e81571f575689b2e1a2076253905b3f1c30", - "xHandle": "schzeick", - "xUrl": "https://twitter.com/schzeick", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1228088", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "chiwelife", - "displayName": "Chinwendu Michael", - "fid": 1228088, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/acdfbb9c-8aee-417e-a046-5355a6252700/original", - "followers": 61, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2a6b94045a260c73625696a1b25518a69d17723b", - "etherscanUrl": "https://etherscan.io/address/0x2a6b94045a260c73625696a1b25518a69d17723b", - "xHandle": "chiwelifem", - "xUrl": "https://twitter.com/chiwelifem", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1346247", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kaiwxn.base.eth", - "displayName": "Kai | kaiwxn.base.eth 🐈", - "fid": 1346247, - "pfpUrl": "https://res.cloudinary.com/base-app/image/upload/f_auto/v1761433825/IMG_0472.jpg.jpg", - "followers": 60, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1409e238e5024f917aea3566e22dd28d4763f347", - "etherscanUrl": "https://etherscan.io/address/0x1409e238e5024f917aea3566e22dd28d4763f347", - "xHandle": "gokanrize", - "xUrl": "https://twitter.com/gokanrize", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1340791", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "abdullahwriters", - "displayName": "Abdullah Writers 🧬", - "fid": 1340791, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2d86ecfe-9959-4317-a571-95da07a71300/original", - "followers": 60, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x76074e8a48bf7d819cbf8d26addb591e001f3e3a", - "etherscanUrl": "https://etherscan.io/address/0x76074e8a48bf7d819cbf8d26addb591e001f3e3a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1109964", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "tlenxg", - "displayName": "A. Gonzalez", - "fid": 1109964, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/cefde64b-41a1-413a-d09a-df0da7588a00/original", - "followers": 60, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xff6d13cec8f9b1a9d9cf3f18c51c7eb99baef30c", - "etherscanUrl": "https://etherscan.io/address/0xff6d13cec8f9b1a9d9cf3f18c51c7eb99baef30c", - "xHandle": "tlen_g_", - "xUrl": "https://twitter.com/tlen_g_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1057218", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "famunaz", - "displayName": "0xFamunaz", - "fid": 1057218, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5ef7e4b1-6265-4928-ba01-0a5a1ac39b00/original", - "followers": 60, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe334cdd1ff8c18b6805833f126bef193b2427b0c", - "etherscanUrl": "https://etherscan.io/address/0xe334cdd1ff8c18b6805833f126bef193b2427b0c", - "xHandle": "famunaz", - "xUrl": "https://twitter.com/famunaz", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_882446", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kianu", - "displayName": "Azaticus", - "fid": 882446, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9a4e5975-66e0-402b-e27f-e124f9d15900/original", - "followers": 60, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb1724e84c97fbabb5ea68b7bddce02643c4c2ff5", - "etherscanUrl": "https://etherscan.io/address/0xb1724e84c97fbabb5ea68b7bddce02643c4c2ff5", - "xHandle": "azaticus", - "xUrl": "https://twitter.com/azaticus", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1427360", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "lazycap", - "displayName": "lazycap", - "fid": 1427360, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5a2717bd-8a5e-4596-12ba-67e920d4f600/original", - "followers": 59, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4d56c73fab6953f91a6af71dfd5f28d7dc903715", - "etherscanUrl": "https://etherscan.io/address/0x4d56c73fab6953f91a6af71dfd5f28d7dc903715", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_366595", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "deficoncierge", - "displayName": "R Smith", - "fid": 366595, - "pfpUrl": "https://i.imgur.com/pIbmMyx.jpg", - "followers": 59, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5c8e7e6719781946a78d2588c49890301c5a5718", - "etherscanUrl": "https://etherscan.io/address/0x5c8e7e6719781946a78d2588c49890301c5a5718", - "xHandle": "ooa09", - "xUrl": "https://twitter.com/ooa09", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_375869", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "justlikeag6", - "displayName": "justlikeag6", - "fid": 375869, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a9e53c36-34b1-40b5-589c-7b1c07ae2a00/original", - "followers": 59, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd77d75e50312b3d99e5a046de4173df861bedbd8", - "etherscanUrl": "https://etherscan.io/address/0xd77d75e50312b3d99e5a046de4173df861bedbd8", - "xHandle": "after_ape", - "xUrl": "https://twitter.com/after_ape", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_866092", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "twifyyt", - "displayName": "azizahhh", - "fid": 866092, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/729c9abb-71e2-4cce-29f3-1a473b81a200/rectcrop3", - "followers": 58, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9fdb4d59ffae405a65a167778703ab2305add72b", - "etherscanUrl": "https://etherscan.io/address/0x9fdb4d59ffae405a65a167778703ab2305add72b", - "xHandle": "jessicamoo39701", - "xUrl": "https://twitter.com/jessicamoo39701", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_215299", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "matthu.eth", - "displayName": "matt pereira", - "fid": 215299, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c8c9c3b4-33e4-427e-da47-a32bd9369000/original", - "followers": 57, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xed8d585fb288c96fe553c2c1ee93f63aa4e9b4da", - "etherscanUrl": "https://etherscan.io/address/0xed8d585fb288c96fe553c2c1ee93f63aa4e9b4da", - "xHandle": "crypt0xmatt", - "xUrl": "https://twitter.com/crypt0xmatt", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1145032", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ahmadmukafi", - "displayName": "Ahmadmukafi", - "fid": 1145032, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7ad6ba84-15c5-45e4-1e67-075c53c99800/original", - "followers": 57, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa6b8ec820a54cb909150ca270156096767ded5a7", - "etherscanUrl": "https://etherscan.io/address/0xa6b8ec820a54cb909150ca270156096767ded5a7", - "xHandle": "yzzzz54", - "xUrl": "https://twitter.com/yzzzz54", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1069241", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "rendidenska", - "displayName": "Rendi Denska", - "fid": 1069241, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/dce4d68d-cc03-43e7-825f-4f4f972c0900/original", - "followers": 56, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4c0b88e3dd0c53b54332b9aa78087926580fba14", - "etherscanUrl": "https://etherscan.io/address/0x4c0b88e3dd0c53b54332b9aa78087926580fba14", - "xHandle": "chingu_ngunyah", - "xUrl": "https://twitter.com/chingu_ngunyah", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1082046", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "fedorchenko98063", - "displayName": "Vitalii Fedorchenko", - "fid": 1082046, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/47489923-f2b0-4cd7-a8db-1a619769fb00/rectcrop3", - "followers": 56, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc6ae72aeecd3e326ab5bcfe02dce0ff3a95d2e73", - "etherscanUrl": "https://etherscan.io/address/0xc6ae72aeecd3e326ab5bcfe02dce0ff3a95d2e73", - "xHandle": "fedorcenko98063", - "xUrl": "https://twitter.com/fedorcenko98063", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_635761", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "elysias", - "displayName": "Elysia Songbird", - "fid": 635761, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/80aae318-c865-4d6f-b2bf-260a00888200/rectcrop3", - "followers": 56, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xaabce21bf5ca4b5489d9433d24ad14200db0fa11", - "etherscanUrl": "https://etherscan.io/address/0xaabce21bf5ca4b5489d9433d24ad14200db0fa11", - "xHandle": "raquel819846132", - "xUrl": "https://twitter.com/raquel819846132", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_861122", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "oussbaat", - "displayName": "Mou455", - "fid": 861122, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e697831a-a27e-45bb-1fae-c9e4fcf7bf00/rectcrop3", - "followers": 56, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x662337218b8345b529e404705b3bc77dd006072e", - "etherscanUrl": "https://etherscan.io/address/0x662337218b8345b529e404705b3bc77dd006072e", - "xHandle": "baguimouaad", - "xUrl": "https://twitter.com/baguimouaad", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1218219", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "rebelyouth", - "displayName": "ball", - "fid": 1218219, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/85bbc219-abaf-4ad8-56ff-6bd6b07eb700/original", - "followers": 54, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0df91fdd816d15431d863b469de5b67698c8c4a8", - "etherscanUrl": "https://etherscan.io/address/0x0df91fdd816d15431d863b469de5b67698c8c4a8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_257195", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nung09", - "displayName": "Bear🍯🎩", - "fid": 257195, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b1ae35b3-8f7a-44a5-7ced-9a77f00efa00/rectcrop3", - "followers": 53, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x46dd7a8cd247e1813094df0af9841a479ed8dfc4", - "etherscanUrl": "https://etherscan.io/address/0x46dd7a8cd247e1813094df0af9841a479ed8dfc4", - "xHandle": "nurhasan2000", - "xUrl": "https://twitter.com/nurhasan2000", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_695482", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "5n", - "displayName": "5N", - "fid": 695482, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/95450233-7e03-4211-78d6-18db9670f400/rectcrop3", - "followers": 53, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4fc88b308723e5ff08003d696aed714768b359fa", - "etherscanUrl": "https://etherscan.io/address/0x4fc88b308723e5ff08003d696aed714768b359fa", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1381785", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "marlowe4", - "displayName": "Marlowe", - "fid": 1381785, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/04b4a45b-bd05-4695-e3a8-61e671b6e400/original", - "followers": 51, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc634ac7b2c1261e90633a7f57cdfc6ae5933c4ad", - "etherscanUrl": "https://etherscan.io/address/0xc634ac7b2c1261e90633a7f57cdfc6ae5933c4ad", - "xHandle": "marlowe444", - "xUrl": "https://twitter.com/marlowe444", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1376903", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "panndine", - "displayName": "panndine", - "fid": 1376903, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/fd50b1cf-cfc3-444b-bffb-85b413d58000/original", - "followers": 50, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe2b56fd8f758156a512f6ab7e2c9d04e739d1062", - "etherscanUrl": "https://etherscan.io/address/0xe2b56fd8f758156a512f6ab7e2c9d04e739d1062", - "xHandle": "cryptopudd", - "xUrl": "https://twitter.com/cryptopudd", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1278620", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "pemankord19", - "displayName": "PL", - "fid": 1278620, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e9538137-b9f6-47d1-ff31-45ba092ef000/original", - "followers": 50, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x36a495148467ea239484e475a94759dc82c8b147", - "etherscanUrl": "https://etherscan.io/address/0x36a495148467ea239484e475a94759dc82c8b147", - "xHandle": "lorstanipeman", - "xUrl": "https://twitter.com/lorstanipeman", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_384360", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "oksha", - "displayName": "Oksha🔵", - "fid": 384360, - "pfpUrl": "https://i.imgur.com/qJs0acQ.jpg", - "followers": 50, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6bbc62ba227285d06b4c24835c2c61f6bbf73c18", - "etherscanUrl": "https://etherscan.io/address/0x6bbc62ba227285d06b4c24835c2c61f6bbf73c18", - "xHandle": "okshabae", - "xUrl": "https://twitter.com/okshabae", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1048398", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ojgg", - "displayName": "ojgg.eth", - "fid": 1048398, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d557f948-4b73-4bc4-5b24-261075263e00/rectcrop3", - "followers": 50, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1b0f3a175167792da8eb6275d4711df7cbb68e9e", - "etherscanUrl": "https://etherscan.io/address/0x1b0f3a175167792da8eb6275d4711df7cbb68e9e", - "xHandle": "ajiarrofie", - "xUrl": "https://twitter.com/ajiarrofie", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1058497", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "draven95x", - "displayName": "DRaVeN", - "fid": 1058497, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/bb32f3d9-6cde-43de-3485-8df52adf0700/rectcrop3", - "followers": 50, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7f581ed2965d15523f4ba0ba526161c2b9c10b47", - "etherscanUrl": "https://etherscan.io/address/0x7f581ed2965d15523f4ba0ba526161c2b9c10b47", - "xHandle": "draven95x", - "xUrl": "https://twitter.com/draven95x", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1116007", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "aproxbt", - "displayName": "Aproxbt", - "fid": 1116007, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/444b2d24-eb7c-4c92-b3ac-556649859000/original", - "followers": 50, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1e32c168480f7e5500fe368406bf358bf14d62a6", - "etherscanUrl": "https://etherscan.io/address/0x1e32c168480f7e5500fe368406bf358bf14d62a6", - "xHandle": "aproxbt", - "xUrl": "https://twitter.com/aproxbt", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1097465", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "meimeikyuttt", - "displayName": "M", - "fid": 1097465, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/cebf9c4e-4e9b-4de1-24e0-6f1d86904700/original", - "followers": 50, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x95552f93f618295bfe6cfd7b8ea6cfb40ab10a2d", - "etherscanUrl": "https://etherscan.io/address/0x95552f93f618295bfe6cfd7b8ea6cfb40ab10a2d", - "xHandle": "sixaap", - "xUrl": "https://twitter.com/sixaap", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1281350", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "juvnbaseeth", - "displayName": "Juvn.base.eth", - "fid": 1281350, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/dabebc0e-147b-4a0b-9b68-50bf68700000/original", - "followers": 49, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x56a6327b2cf1f968d760430b702aa76d64ba6f9c", - "etherscanUrl": "https://etherscan.io/address/0x56a6327b2cf1f968d760430b702aa76d64ba6f9c", - "xHandle": "juvnbaseeth", - "xUrl": "https://twitter.com/juvnbaseeth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_471337", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "parvezkhandakar", - "displayName": "Parvezkhandakar", - "fid": 471337, - "pfpUrl": "https://virus.folia.app/img/base/641799429", - "followers": 49, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x13171ca39402682538523f2862dfa8882563b5af", - "etherscanUrl": "https://etherscan.io/address/0x13171ca39402682538523f2862dfa8882563b5af", - "xHandle": "khonkat", - "xUrl": "https://twitter.com/khonkat", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1077049", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "wingjelly", - "displayName": "MrShobuj", - "fid": 1077049, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7d6e6de1-b76d-4ee3-8423-e3f60c692500/rectcrop3", - "followers": 49, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6b057730e77e07191acfd4e0eab8e1f5bd475503", - "etherscanUrl": "https://etherscan.io/address/0x6b057730e77e07191acfd4e0eab8e1f5bd475503", - "xHandle": "mrbeast917", - "xUrl": "https://twitter.com/mrbeast917", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1020832", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dallmayr", - "displayName": "Timur", - "fid": 1020832, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/94cead6b-d1de-42ac-b9bc-13e5b312e400/rectcrop3", - "followers": 49, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbac660d1c7208739c9b5876a8df885f853b84d5c", - "etherscanUrl": "https://etherscan.io/address/0xbac660d1c7208739c9b5876a8df885f853b84d5c", - "xHandle": "lisenkovtimur", - "xUrl": "https://twitter.com/lisenkovtimur", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1159333", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jefrysol", - "displayName": "Jefry.base.eth", - "fid": 1159333, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/77128635-a0a4-4318-e285-3187d619df00/original", - "followers": 48, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x560c2445d147d524294ae827a78745d8d3681133", - "etherscanUrl": "https://etherscan.io/address/0x560c2445d147d524294ae827a78745d8d3681133", - "xHandle": "thesolguy7", - "xUrl": "https://twitter.com/thesolguy7", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_204471", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "aksubiq", - "displayName": "aqsel", - "fid": 204471, - "pfpUrl": "https://i.imgur.com/FF5Powg.jpg", - "followers": 48, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4ed98b49c83e18ff0de08b2382612555205f4679", - "etherscanUrl": "https://etherscan.io/address/0x4ed98b49c83e18ff0de08b2382612555205f4679", - "xHandle": "ubiqq00", - "xUrl": "https://twitter.com/ubiqq00", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1061101", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cznaing", - "displayName": "CZ🔶Naing (Ø,G) nexyai.io", - "fid": 1061101, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/bb281169-f87f-4b24-7ecf-ad407f102600/original", - "followers": 48, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x98f604b01b01d9d7beacc3271d17205ce70077a7", - "etherscanUrl": "https://etherscan.io/address/0x98f604b01b01d9d7beacc3271d17205ce70077a7", - "xHandle": "cz_naing", - "xUrl": "https://twitter.com/cz_naing", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1065937", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "hengky420", - "displayName": "Almah 🧬", - "fid": 1065937, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b2942861-0278-4681-852c-7a0a68c10000/original", - "followers": 47, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1008668f0dca010f60f59ec6518d70a2ccc7b70c", - "etherscanUrl": "https://etherscan.io/address/0x1008668f0dca010f60f59ec6518d70a2ccc7b70c", - "xHandle": "almahmudi420", - "xUrl": "https://twitter.com/almahmudi420", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1060853", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0x74drian", - "displayName": "AW", - "fid": 1060853, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a5e7086c-6022-4629-8f15-fcbacf7b1600/original", - "followers": 47, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x14627d79e6beeab27f8aea820e2c8d1243b3b321", - "etherscanUrl": "https://etherscan.io/address/0x14627d79e6beeab27f8aea820e2c8d1243b3b321", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1065388", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bayuazhari", - "displayName": "Azr_Crypto Finder (Ø,G)", - "fid": 1065388, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6f57167f-c6f7-460f-0de9-6329c649b600/original", - "followers": 47, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfbd0ce377e706c8adc324fdbf62d8c4d5c94db06", - "etherscanUrl": "https://etherscan.io/address/0xfbd0ce377e706c8adc324fdbf62d8c4d5c94db06", - "xHandle": "azrcryptofinder", - "xUrl": "https://twitter.com/azrcryptofinder", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1108556", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "fogtrickster", - "displayName": "fogtrickster", - "fid": 1108556, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f6e91dae-f7b7-49f1-4c52-41eea8fd1f00/original", - "followers": 47, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1af9508e72b4fa8e5cef38e569ba0754793c6461", - "etherscanUrl": "https://etherscan.io/address/0x1af9508e72b4fa8e5cef38e569ba0754793c6461", - "xHandle": "fogtrickster", - "xUrl": "https://twitter.com/fogtrickster", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_440608", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bjpraiz", - "displayName": "Praise", - "fid": 440608, - "pfpUrl": "https://i.imgur.com/efkFyk0.jpg", - "followers": 47, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x48445a6e9e57e9521aa6aaaa833551538454dc91", - "etherscanUrl": "https://etherscan.io/address/0x48445a6e9e57e9521aa6aaaa833551538454dc91", - "xHandle": "praiseabolaji", - "xUrl": "https://twitter.com/praiseabolaji", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_863448", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "hustlebank", - "displayName": "Hustle Bank", - "fid": 863448, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1e8ae20b-d0db-41cb-b520-9af880880300/original", - "followers": 46, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2cdf7c0f8ed4d7fe235822e9f05a228993190c5c", - "etherscanUrl": "https://etherscan.io/address/0x2cdf7c0f8ed4d7fe235822e9f05a228993190c5c", - "xHandle": "atriskcapital", - "xUrl": "https://twitter.com/atriskcapital", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1432065", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "hyunnie", - "displayName": "hyunnie", - "fid": 1432065, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/05dd18f9-8bbc-45e1-8ca0-4e2259501a00/original", - "followers": 45, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xba964797d21a187b403565eaf9656fb67d9b922a", - "etherscanUrl": "https://etherscan.io/address/0xba964797d21a187b403565eaf9656fb67d9b922a", - "xHandle": "lynnxch157", - "xUrl": "https://twitter.com/lynnxch157", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1027755", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "faezeh1372", - "displayName": "Pinocchio", - "fid": 1027755, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0c3def7d-d7a0-46ef-12be-82697ceda900/rectcrop3", - "followers": 45, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9d87fa3245fe127d3b787b1c9958790408afc2e3", - "etherscanUrl": "https://etherscan.io/address/0x9d87fa3245fe127d3b787b1c9958790408afc2e3", - "xHandle": "kiaeifafa", - "xUrl": "https://twitter.com/kiaeifafa", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1030233", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mmdmax", - "displayName": "Mohammad Sadegh Esmaeili Nia", - "fid": 1030233, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0ac59f4b-2695-4b6d-e0ab-816b2d33a600/rectcrop3", - "followers": 44, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd46ec4458659e89b8dbfad5cdfd61608ed34c2ad", - "etherscanUrl": "https://etherscan.io/address/0xd46ec4458659e89b8dbfad5cdfd61608ed34c2ad", - "xHandle": "mamacoin2025", - "xUrl": "https://twitter.com/mamacoin2025", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1083744", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "joynal2789", - "displayName": "JOYNAL ABEDIN", - "fid": 1083744, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2dc49c6e-c444-4324-f1e8-3f5580adfb00/original", - "followers": 44, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4d189caf7714418052ac8178d42be304256f1ddb", - "etherscanUrl": "https://etherscan.io/address/0x4d189caf7714418052ac8178d42be304256f1ddb", - "xHandle": "joynal2789", - "xUrl": "https://twitter.com/joynal2789", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1006290", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "trevbagholder", - "displayName": "Trev", - "fid": 1006290, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/94997742-7ff9-4d95-6cef-c2ff6a935d00/rectcrop3", - "followers": 44, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc12c4dde7cf30d52e9e88bb352ac3a139acce31c", - "etherscanUrl": "https://etherscan.io/address/0xc12c4dde7cf30d52e9e88bb352ac3a139acce31c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1089995", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mkubikoff", - "displayName": "Matthieu Kubikoff", - "fid": 1089995, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a0a179dd-82e4-4847-ed01-a401452c1000/original", - "followers": 43, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe3a9f844dcf349930564622a24e110f149159768", - "etherscanUrl": "https://etherscan.io/address/0xe3a9f844dcf349930564622a24e110f149159768", - "xHandle": "mkubikoff", - "xUrl": "https://twitter.com/mkubikoff", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1109707", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "alphanotus", - "displayName": "Vitamin", - "fid": 1109707, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a8ae40bf-bf66-496b-acc9-e9518910fa00/original", - "followers": 40, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x61337ff4af38dcb930cd48487697a10420f29364", - "etherscanUrl": "https://etherscan.io/address/0x61337ff4af38dcb930cd48487697a10420f29364", - "xHandle": "0tmd_", - "xUrl": "https://twitter.com/0tmd_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1323798", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0xshiba", - "displayName": "🦊", - "fid": 1323798, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/303b9801-e580-46b4-f948-342489d5b400/original", - "followers": 40, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xce6c49beafe6edc0c227519dc824c4b9dbbfd2f6", - "etherscanUrl": "https://etherscan.io/address/0xce6c49beafe6edc0c227519dc824c4b9dbbfd2f6", - "xHandle": "0xwoodchuck", - "xUrl": "https://twitter.com/0xwoodchuck", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1109409", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "vitvic", - "displayName": "Drummer", - "fid": 1109409, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7fa8147a-8d4d-4c7b-6b62-4b317ce02c00/rectcrop3", - "followers": 40, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xeec3b2ade40188b480df71633738ed69cf82c1a8", - "etherscanUrl": "https://etherscan.io/address/0xeec3b2ade40188b480df71633738ed69cf82c1a8", - "xHandle": "richmvs", - "xUrl": "https://twitter.com/richmvs", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_898343", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "indoteam", - "displayName": "Lilis Nuramini", - "fid": 898343, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3637c265-41c4-4641-a5eb-b4e47ca7c000/rectcrop3", - "followers": 40, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xea6a0876da7351c7da14b8c0c7d74caaae554328", - "etherscanUrl": "https://etherscan.io/address/0xea6a0876da7351c7da14b8c0c7d74caaae554328", - "xHandle": "lilis_nuramini", - "xUrl": "https://twitter.com/lilis_nuramini", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1045175", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "redsmile.eth", - "displayName": "redsmile", - "fid": 1045175, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b435160a-1894-4b6e-7de9-c24775d50b00/rectcrop3", - "followers": 39, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3030685952b31b14dc1ce25e3e76fc6c7446a2b0", - "etherscanUrl": "https://etherscan.io/address/0x3030685952b31b14dc1ce25e3e76fc6c7446a2b0", - "xHandle": "tdavlyatchin", - "xUrl": "https://twitter.com/tdavlyatchin", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1045851", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "m-o-salami", - "displayName": "M.O Salami", - "fid": 1045851, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/174c08d0-b124-4916-c1ac-75091b5a7100/rectcrop3", - "followers": 38, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcf8d044f51c04cf4ee280425b69227c440bc0983", - "etherscanUrl": "https://etherscan.io/address/0xcf8d044f51c04cf4ee280425b69227c440bc0983", - "xHandle": "oyinn_kann_sola", - "xUrl": "https://twitter.com/oyinn_kann_sola", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_723808", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "tokyovibes", - "displayName": "sophieraiin", - "fid": 723808, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/74f5f3d4-432b-406c-abfa-fbd718fc2400/rectcrop3", - "followers": 37, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1c62f6557f5a1249fee2a72d7c29e875e70e8202", - "etherscanUrl": "https://etherscan.io/address/0x1c62f6557f5a1249fee2a72d7c29e875e70e8202", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_329481", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mrxpoint", - "displayName": "0xmrxp", - "fid": 329481, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e0c6a28c-bf49-4c60-0d63-07e669d9aa00/original", - "followers": 37, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd97318d9a6f83881457a0e9aabfaf9c3bb125376", - "etherscanUrl": "https://etherscan.io/address/0xd97318d9a6f83881457a0e9aabfaf9c3bb125376", - "xHandle": "0xmrxp", - "xUrl": "https://twitter.com/0xmrxp", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1108555", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cipherwhale", - "displayName": "cipherwhale", - "fid": 1108555, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/14104706-697c-4698-e072-93e973bde100/original", - "followers": 37, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1f19955724eb747030fd5e5e17c95ea8655fe3a5", - "etherscanUrl": "https://etherscan.io/address/0x1f19955724eb747030fd5e5e17c95ea8655fe3a5", - "xHandle": "cipherwhalep", - "xUrl": "https://twitter.com/cipherwhalep", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_415021", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "heyyovz", - "displayName": "heyyovz🔵", - "fid": 415021, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/631e385f-5239-40f0-a392-0503e35ccf00/original", - "followers": 37, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3ae98cfc9e83550b112c27d701c436011a8b75bb", - "etherscanUrl": "https://etherscan.io/address/0x3ae98cfc9e83550b112c27d701c436011a8b75bb", - "xHandle": "heyyovz", - "xUrl": "https://twitter.com/heyyovz", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1409024", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "zeikonft", - "displayName": "zeikonft.base.eth", - "fid": 1409024, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/bac89549-9379-4be0-0928-302cf7e16200/original", - "followers": 36, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe42d34c3662f4b12161cc0fe7ac036e31ab08a22", - "etherscanUrl": "https://etherscan.io/address/0xe42d34c3662f4b12161cc0fe7ac036e31ab08a22", - "xHandle": "zeiko_nft", - "xUrl": "https://twitter.com/zeiko_nft", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_297202", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "babycorn0907", - "displayName": "Naveen 👾", - "fid": 297202, - "pfpUrl": "https://beb-public.s3.us-west-1.amazonaws.com/black.jpg", - "followers": 36, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x02b2338cfc2d1a8d615040eaffed146617a6893e", - "etherscanUrl": "https://etherscan.io/address/0x02b2338cfc2d1a8d615040eaffed146617a6893e", - "xHandle": "babycon95", - "xUrl": "https://twitter.com/babycon95", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1201858", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "tradeos", - "displayName": "Trading Operating System", - "fid": 1201858, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7e5378c1-992d-4c02-2e0a-c95f53883600/rectcrop3", - "followers": 36, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5b80f783ff016afff40891d45bc8832f0c12d2f5", - "etherscanUrl": "https://etherscan.io/address/0x5b80f783ff016afff40891d45bc8832f0c12d2f5", - "xHandle": "yl8805178047861", - "xUrl": "https://twitter.com/yl8805178047861", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_460344", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "fuhao", - "displayName": "Fuhao", - "fid": 460344, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/8ab38642-07e7-4f0f-23f0-d4695ab91400/original", - "followers": 36, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4a6db355c59325783f1dd8c5bf047b88863d0489", - "etherscanUrl": "https://etherscan.io/address/0x4a6db355c59325783f1dd8c5bf047b88863d0489", - "xHandle": "fffuhaoo", - "xUrl": "https://twitter.com/fffuhaoo", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_317904", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "gdegen.eth", - "displayName": "gustav ", - "fid": 317904, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/27a25c48-9474-466d-dfda-318563087b00/original", - "followers": 36, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x22bdac1c8dd5396362cc1101b04d7559025b631e", - "etherscanUrl": "https://etherscan.io/address/0x22bdac1c8dd5396362cc1101b04d7559025b631e", - "xHandle": "liblik", - "xUrl": "https://twitter.com/liblik", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1395983", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mindracer", - "displayName": "mindracer", - "fid": 1395983, - "pfpUrl": "https://res.cloudinary.com/base-app/image/upload/f_auto/v1762240977/IMG_0377.jpg.jpg", - "followers": 35, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xad1a8e33ee6a833091179fa87bb1fddf1fee1a9f", - "etherscanUrl": "https://etherscan.io/address/0xad1a8e33ee6a833091179fa87bb1fddf1fee1a9f", - "xHandle": "muzzledmind101", - "xUrl": "https://twitter.com/muzzledmind101", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_423876", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "calskard", - "displayName": "CalskaRd", - "fid": 423876, - "pfpUrl": "https://i.imgur.com/mP2uotg.jpg", - "followers": 35, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x285ead776abf917b10362131752f45ddc94784e7", - "etherscanUrl": "https://etherscan.io/address/0x285ead776abf917b10362131752f45ddc94784e7", - "xHandle": "emre10530357", - "xUrl": "https://twitter.com/emre10530357", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_210448", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nguyennsh", - "displayName": "Nguyennsh", - "fid": 210448, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e67ead77-623f-40e0-64e1-10d24f9bba00/original", - "followers": 34, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa6e5eb68a230b7d38cb9a601255da4718b1dd7f0", - "etherscanUrl": "https://etherscan.io/address/0xa6e5eb68a230b7d38cb9a601255da4718b1dd7f0", - "xHandle": "hongngu43790371", - "xUrl": "https://twitter.com/hongngu43790371", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_298453", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "morocco", - "displayName": "Cat", - "fid": 298453, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/63fb35f9-7943-49f3-0691-0dfe8c42ba00/rectcrop3", - "followers": 34, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb48b43a933fd6d82cb759593151c9c56eabdcf50", - "etherscanUrl": "https://etherscan.io/address/0xb48b43a933fd6d82cb759593151c9c56eabdcf50", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_452977", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "xxgxrxbxx", - "displayName": "Jack Russell", - "fid": 452977, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f9087000-2743-4d6d-1b7b-bb62a4f31f00/original", - "followers": 34, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5f3606b56cd2c61eccdc6e3f669f2872c32ba391", - "etherscanUrl": "https://etherscan.io/address/0x5f3606b56cd2c61eccdc6e3f669f2872c32ba391", - "xHandle": "jackrussell369", - "xUrl": "https://twitter.com/jackrussell369", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1065377", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0xcuan", - "displayName": "ngar", - "fid": 1065377, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e1e8824c-6e73-4784-7b1a-8bcaf7ecef00/original", - "followers": 33, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x234088b2096b853cc1cf8e5855d612a1565f7c92", - "etherscanUrl": "https://etherscan.io/address/0x234088b2096b853cc1cf8e5855d612a1565f7c92", - "xHandle": "nggar1609", - "xUrl": "https://twitter.com/nggar1609", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1081286", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "atbanklan", - "displayName": "atbanklan.base.eth", - "fid": 1081286, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ab1d88e8-34ee-436c-1a35-075da7a00f00/rectcrop3", - "followers": 33, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb510185ec90020d1acb03e1d2729baaa82b02c57", - "etherscanUrl": "https://etherscan.io/address/0xb510185ec90020d1acb03e1d2729baaa82b02c57", - "xHandle": "receparslan59", - "xUrl": "https://twitter.com/receparslan59", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1064401", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "siregarp", - "displayName": "siregarp", - "fid": 1064401, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d298b0ca-60b2-4ffe-8acc-ac336737b900/original", - "followers": 32, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8d559b31ef0c13b168c6c69f82d465ac442c9e41", - "etherscanUrl": "https://etherscan.io/address/0x8d559b31ef0c13b168c6c69f82d465ac442c9e41", - "xHandle": "wdaw79737", - "xUrl": "https://twitter.com/wdaw79737", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1396394", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "raiwoo", - "displayName": "raiwoo", - "fid": 1396394, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ffd86832-d40b-414e-8428-aa48f21dc300/original", - "followers": 32, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9593dedf93158900f5c9880fafc4046f4f3cda47", - "etherscanUrl": "https://etherscan.io/address/0x9593dedf93158900f5c9880fafc4046f4f3cda47", - "xHandle": "rywcommunity", - "xUrl": "https://twitter.com/rywcommunity", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1163132", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "juggboyballin", - "displayName": "The Wzrd", - "fid": 1163132, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/53bbf9aa-e758-4cf5-9ce3-b581933a5300/original", - "followers": 32, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfe221dc4c4ec741474e9c099088eb349db1b88ae", - "etherscanUrl": "https://etherscan.io/address/0xfe221dc4c4ec741474e9c099088eb349db1b88ae", - "xHandle": "juggboyballin", - "xUrl": "https://twitter.com/juggboyballin", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_489494", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "virtualbacon", - "displayName": "VirtualBacon", - "fid": 489494, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9995f452-1d3d-410d-0bcd-7584a576e000/original", - "followers": 32, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf9e1aa3c7a9e8aff6855cd85f99c80938581c030", - "etherscanUrl": "https://etherscan.io/address/0xf9e1aa3c7a9e8aff6855cd85f99c80938581c030", - "xHandle": "virtualbacon0x", - "xUrl": "https://twitter.com/virtualbacon0x", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1094078", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0xgemmahl", - "displayName": "Gemmahl", - "fid": 1094078, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/51a1588c-9c3b-44fa-65ed-dcbfe0fea800/original", - "followers": 32, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf291b59dbc877801cf720baf5c4afda8347d9303", - "etherscanUrl": "https://etherscan.io/address/0xf291b59dbc877801cf720baf5c4afda8347d9303", - "xHandle": "gemmahl_li", - "xUrl": "https://twitter.com/gemmahl_li", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1404992", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "xcv456", - "displayName": "xcv456", - "fid": 1404992, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3c476bd5-6177-4cd2-db6b-431d568e1500/original", - "followers": 31, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa447e2aebd36cb45e295b0063caf55e365234375", - "etherscanUrl": "https://etherscan.io/address/0xa447e2aebd36cb45e295b0063caf55e365234375", - "xHandle": "qyh559t", - "xUrl": "https://twitter.com/qyh559t", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_902788", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "qvuvp", - "displayName": "blue", - "fid": 902788, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d638549e-89ef-455f-b4ec-32fc4bed0c00/rectcrop3", - "followers": 31, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf3da564c22fd85ce7cff29f1392088f6a8c65c19", - "etherscanUrl": "https://etherscan.io/address/0xf3da564c22fd85ce7cff29f1392088f6a8c65c19", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1456523", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "baseland", - "displayName": "baseland", - "fid": 1456523, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/462f1b94-4399-4230-987f-a588ca6f1d00/original", - "followers": 29, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x11bc30d8cbc54db2a8b8f24033c818102b9f5bd2", - "etherscanUrl": "https://etherscan.io/address/0x11bc30d8cbc54db2a8b8f24033c818102b9f5bd2", - "xHandle": "baselandhq", - "xUrl": "https://twitter.com/baselandhq", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1117277", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "acolytai", - "displayName": "Acolyt", - "fid": 1117277, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/36bceb84-01ca-4b2c-1007-813f34608900/original", - "followers": 29, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x32f24d9de0fbdbbc3b23d55ed70b921a44c7a92d", - "etherscanUrl": "https://etherscan.io/address/0x32f24d9de0fbdbbc3b23d55ed70b921a44c7a92d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1102750", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "djyackz", - "displayName": "🧪ÐOGE ÐJ 👾", - "fid": 1102750, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d38a6491-a3c9-47ef-5976-cf1f996a9f00/rectcrop3", - "followers": 29, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x070f8e48af9c0a1cd21a910bd98e8946b3ec2a48", - "etherscanUrl": "https://etherscan.io/address/0x070f8e48af9c0a1cd21a910bd98e8946b3ec2a48", - "xHandle": "djyackz", - "xUrl": "https://twitter.com/djyackz", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_469529", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ant33k", - "displayName": "ant33k.base.eth", - "fid": 469529, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9bd66603-c355-4d6c-e7c0-4fc710869c00/original", - "followers": 29, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x380cd6c49002b8b5c3da51296d1b48393c1e2f2f", - "etherscanUrl": "https://etherscan.io/address/0x380cd6c49002b8b5c3da51296d1b48393c1e2f2f", - "xHandle": "kof3ina", - "xUrl": "https://twitter.com/kof3ina", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1130996", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dennyagustiana", - "displayName": "Denny Agustiana", - "fid": 1130996, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/df5e5e93-833e-4189-45b5-0d889216e600/original", - "followers": 28, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5548e0c12cde6a1d969e23e0bf13ccc4b51fe023", - "etherscanUrl": "https://etherscan.io/address/0x5548e0c12cde6a1d969e23e0bf13ccc4b51fe023", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1088929", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jdish", - "displayName": "jdish test", - "fid": 1088929, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/be8deecf-57c0-45e4-0124-f4f136e1a700/original", - "followers": 28, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8812268b5d81d9bfeeb9dd17978052489de21d6f", - "etherscanUrl": "https://etherscan.io/address/0x8812268b5d81d9bfeeb9dd17978052489de21d6f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_385986", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "davidoomodee", - "displayName": "Davidoo🐹", - "fid": 385986, - "pfpUrl": "https://i.imgur.com/bOLiBLn.jpg", - "followers": 28, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdf4c556312186d822a72d4308e0b186e6be6e452", - "etherscanUrl": "https://etherscan.io/address/0xdf4c556312186d822a72d4308e0b186e6be6e452", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1023785", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "asep75", - "displayName": "Asep Pujianto", - "fid": 1023785, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3f31d40f-d8f3-41d6-7792-ce13c4ee4f00/rectcrop3", - "followers": 28, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe8611de634088121c8b5dd0ce8c5a89d633ecd68", - "etherscanUrl": "https://etherscan.io/address/0xe8611de634088121c8b5dd0ce8c5a89d633ecd68", - "xHandle": "kiano798", - "xUrl": "https://twitter.com/kiano798", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1368238", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "theonlyvalen.eth", - "displayName": "theonlyvalen", - "fid": 1368238, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9f974791-3b63-4a3a-cf52-cbe483e60e00/original", - "followers": 27, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3382a499ed43763a40676604f4d6a76005ae8580", - "etherscanUrl": "https://etherscan.io/address/0x3382a499ed43763a40676604f4d6a76005ae8580", - "xHandle": "theonlyvalen", - "xUrl": "https://twitter.com/theonlyvalen", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1456811", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "fayc", - "displayName": "Farcaster Ape Yacht Club", - "fid": 1456811, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5ec0d979-c490-44ad-72e6-5bedbc7f0b00/original", - "followers": 27, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x08964310b0b9b97b0dba1ad94591fb4631cc28fb", - "etherscanUrl": "https://etherscan.io/address/0x08964310b0b9b97b0dba1ad94591fb4631cc28fb", - "xHandle": "farcaster_ayc", - "xUrl": "https://twitter.com/farcaster_ayc", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1281853", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "izimaki", - "displayName": "Izimaki", - "fid": 1281853, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/27253808-b357-43c3-5672-01bc3b3be800/original", - "followers": 27, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x06947908661a1f1db4da53f3c02cbc1077beef1e", - "etherscanUrl": "https://etherscan.io/address/0x06947908661a1f1db4da53f3c02cbc1077beef1e", - "xHandle": "izimaki26", - "xUrl": "https://twitter.com/izimaki26", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_875831", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "baethernet", - "displayName": "baethernet", - "fid": 875831, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a14c7a29-5fcb-4651-c8b1-70dda191dc00/rectcrop3", - "followers": 26, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x68ef1404454113c970067d53f40f60b4bb36d216", - "etherscanUrl": "https://etherscan.io/address/0x68ef1404454113c970067d53f40f60b4bb36d216", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1217517", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nakuro", - "displayName": "Nakuro", - "fid": 1217517, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/8d795765-6c52-4153-e1ae-d72ac787a000/original", - "followers": 24, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x117d307543cad1fc0af79cf149ed530911ea7a99", - "etherscanUrl": "https://etherscan.io/address/0x117d307543cad1fc0af79cf149ed530911ea7a99", - "xHandle": "nakuro_0", - "xUrl": "https://twitter.com/nakuro_0", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1070127", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "rukroy", - "displayName": "Crypto Airdrop Radar 🚨", - "fid": 1070127, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/46e6db06-e7c1-47e9-078c-fc8c321c5600/original", - "followers": 24, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7a8cfa1034a28c6d175d43ebb509c118eb9989c8", - "etherscanUrl": "https://etherscan.io/address/0x7a8cfa1034a28c6d175d43ebb509c118eb9989c8", - "xHandle": "airdropalerts9", - "xUrl": "https://twitter.com/airdropalerts9", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1064711", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "rarasaprillia", - "displayName": "Raras Aprillia", - "fid": 1064711, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/77e0551d-2f4d-491c-eee5-59ebfc121300/original", - "followers": 24, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdd331dd5acf41f29b36296edfd41e5989758b540", - "etherscanUrl": "https://etherscan.io/address/0xdd331dd5acf41f29b36296edfd41e5989758b540", - "xHandle": "rarasaprillia", - "xUrl": "https://twitter.com/rarasaprillia", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_330800", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "adywal", - "displayName": "Adywal", - "fid": 330800, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/cf22d5fe-b696-4be7-77bc-8c45a645f700/rectcrop3", - "followers": 24, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x547174b6876d80095d8cca5c139dd3be2ffb8aae", - "etherscanUrl": "https://etherscan.io/address/0x547174b6876d80095d8cca5c139dd3be2ffb8aae", - "xHandle": "adywal07", - "xUrl": "https://twitter.com/adywal07", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_646724", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "gtimaster101", - "displayName": "GTI_Sonu_mehta", - "fid": 646724, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5381bad1-d94f-42e4-89a7-2971ae242200/original", - "followers": 23, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1f1b3e62cb506821d21b835fa834cd74e0114f87", - "etherscanUrl": "https://etherscan.io/address/0x1f1b3e62cb506821d21b835fa834cd74e0114f87", - "xHandle": "berachain1o1", - "xUrl": "https://twitter.com/berachain1o1", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_997370", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nurulp", - "displayName": "Nurul Putri Azizah", - "fid": 997370, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f02a4628-39d3-4c06-7a8d-c9222be5f200/rectcrop3", - "followers": 23, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x02c05c04a1434ffce9a98af09e5ea858b010c250", - "etherscanUrl": "https://etherscan.io/address/0x02c05c04a1434ffce9a98af09e5ea858b010c250", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_892577", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nadillaazhary", - "displayName": "Dillaa.base.eth", - "fid": 892577, - "pfpUrl": "https://warpcast.com/avatar.png?t=1746442698761", - "followers": 23, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc0a4addb835ee725e85f7f6be2bc6250594b67b8", - "etherscanUrl": "https://etherscan.io/address/0xc0a4addb835ee725e85f7f6be2bc6250594b67b8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1371953", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ireomotoyosi", - "displayName": "ireomotoyosi", - "fid": 1371953, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5567fc3e-c6a7-4b6d-b410-a5c46554ab00/original", - "followers": 22, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbb8ca3341b31f927622fa73fc7447851635cd24a", - "etherscanUrl": "https://etherscan.io/address/0xbb8ca3341b31f927622fa73fc7447851635cd24a", - "xHandle": "oluwafisayomi78", - "xUrl": "https://twitter.com/oluwafisayomi78", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1440099", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "crystalgravy", - "displayName": "crystalgravy", - "fid": 1440099, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/568e2139-0de4-4482-50bf-a1e2d4f4a800/original", - "followers": 22, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4ea509807482193144cb16eee1d8d9fa23d90c56", - "etherscanUrl": "https://etherscan.io/address/0x4ea509807482193144cb16eee1d8d9fa23d90c56", - "xHandle": "crystalgravy2", - "xUrl": "https://twitter.com/crystalgravy2", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_921513", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "adonistelfair", - "displayName": "Adonis Telfair", - "fid": 921513, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/cba6627c-33e5-4f70-c77a-149df2db7400/rectcrop3", - "followers": 22, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd9cb4c017a07e8e45bf01cb6704221281f46cca1", - "etherscanUrl": "https://etherscan.io/address/0xd9cb4c017a07e8e45bf01cb6704221281f46cca1", - "xHandle": "anton_igna46066", - "xUrl": "https://twitter.com/anton_igna46066", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1449993", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "baper21", - "displayName": "baper21", - "fid": 1449993, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/85c00938-f9b3-437f-f7f8-123d19440100/original", - "followers": 21, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0698776945644386cb35e5dd0dddda69a647a7d6", - "etherscanUrl": "https://etherscan.io/address/0x0698776945644386cb35e5dd0dddda69a647a7d6", - "xHandle": "butut_mobi41596", - "xUrl": "https://twitter.com/butut_mobi41596", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1103319", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "alycan", - "displayName": "Ali Can", - "fid": 1103319, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/23949490-c943-4f17-8441-e9dd1295a000/rectcrop3", - "followers": 21, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd10eb8905f23d44f84b91d5620e14540a349f257", - "etherscanUrl": "https://etherscan.io/address/0xd10eb8905f23d44f84b91d5620e14540a349f257", - "xHandle": "ebubekir195077", - "xUrl": "https://twitter.com/ebubekir195077", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_537150", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "millionx", - "displayName": "Sam", - "fid": 537150, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/585cf7a1-5935-4a57-4e19-15abd12d3900/rectcrop3", - "followers": 21, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x34a4aa35ce7545dc8597a13606ae619e69cf1650", - "etherscanUrl": "https://etherscan.io/address/0x34a4aa35ce7545dc8597a13606ae619e69cf1650", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1004526", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "smoll-viking", - "displayName": "Basedmidget.base.eth", - "fid": 1004526, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/93b7bacb-61ed-4f7e-1a0b-1440d88ebc00/original", - "followers": 21, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9cccd0151b288fe46142682d24ba8757baa46313", - "etherscanUrl": "https://etherscan.io/address/0x9cccd0151b288fe46142682d24ba8757baa46313", - "xHandle": "motenforcement", - "xUrl": "https://twitter.com/motenforcement", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1420061", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "xszee", - "displayName": "xszee", - "fid": 1420061, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/95e044eb-c3e1-47ca-ae1a-6cfce9f2ce00/original", - "followers": 20, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfca0c12dcc1d5397cf725e3b14ec37e7ab1fbc65", - "etherscanUrl": "https://etherscan.io/address/0xfca0c12dcc1d5397cf725e3b14ec37e7ab1fbc65", - "xHandle": "setang91907", - "xUrl": "https://twitter.com/setang91907", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_473549", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dreamsbender", - "displayName": "Dreamsbender", - "fid": 473549, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c1ad69b0-14d5-4b84-7d3a-7af7b4e5a100/rectcrop3", - "followers": 20, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0da489b734ff82b8489d70bc65df3cbbf8f7b985", - "etherscanUrl": "https://etherscan.io/address/0x0da489b734ff82b8489d70bc65df3cbbf8f7b985", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_902604", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "zoradeployers", - "displayName": "raindeployz🌈", - "fid": 902604, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e7df4a7a-20ee-46d3-f749-05c21c62c600/original", - "followers": 20, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8fbf1a6a613d59580144d1b79030b1e9305c6161", - "etherscanUrl": "https://etherscan.io/address/0x8fbf1a6a613d59580144d1b79030b1e9305c6161", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_923169", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cruxito", - "displayName": "cruxito.base.eth", - "fid": 923169, - "pfpUrl": "https://imagedelivery.net/g4iQ0bIzMZrjFMgjAnSGfw/702f7ef8-02ff-4f71-04fa-17d9af169600/public", - "followers": 20, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x525170a4b878cfa83d43f439b49d8b6766d4961e", - "etherscanUrl": "https://etherscan.io/address/0x525170a4b878cfa83d43f439b49d8b6766d4961e", - "xHandle": "dl681645", - "xUrl": "https://twitter.com/dl681645", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1049507", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "knoxfort", - "displayName": "Denis", - "fid": 1049507, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/368cfc5e-f64b-4446-adaf-635493308b00/rectcrop3", - "followers": 20, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x573e02402851d1cdcd1ab8d3f37dc523676a1c61", - "etherscanUrl": "https://etherscan.io/address/0x573e02402851d1cdcd1ab8d3f37dc523676a1c61", - "xHandle": "denisgacic", - "xUrl": "https://twitter.com/denisgacic", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_350549", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "lianne", - "displayName": "Lianne Li", - "fid": 350549, - "pfpUrl": "https://i.imgur.com/1yVdolQ.jpg", - "followers": 20, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd33f3095f2aa6ccba996a70507976ed6ed3b1b54", - "etherscanUrl": "https://etherscan.io/address/0xd33f3095f2aa6ccba996a70507976ed6ed3b1b54", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1395669", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "gmach", - "displayName": "gmach", - "fid": 1395669, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2491c17a-f877-4832-8bbd-184ef3027100/original", - "followers": 19, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcbbcbcbc048d88f86cc3006d10f65653b49eba87", - "etherscanUrl": "https://etherscan.io/address/0xcbbcbcbc048d88f86cc3006d10f65653b49eba87", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1315983", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "deshicryptoguy", - "displayName": "Creator Economics", - "fid": 1315983, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/40055371-15a8-48e4-fe45-4c9511913800/original", - "followers": 19, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xab855a955c872a8f61813df077ed61eb03fe639c", - "etherscanUrl": "https://etherscan.io/address/0xab855a955c872a8f61813df077ed61eb03fe639c", - "xHandle": "bhandarysu99076", - "xUrl": "https://twitter.com/bhandarysu99076", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_890711", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ivan86", - "displayName": "Jesse.base_eth", - "fid": 890711, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/8337bcd2-b94f-4be3-4041-6e526ec54200/original", - "followers": 19, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb948cd1361fe7d01e973ff7e260d126dda13368b", - "etherscanUrl": "https://etherscan.io/address/0xb948cd1361fe7d01e973ff7e260d126dda13368b", - "xHandle": "ivan86__clone", - "xUrl": "https://twitter.com/ivan86__clone", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1073907", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jackto", - "displayName": "Jackto.Eth", - "fid": 1073907, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e3f291f4-f256-45f5-21cd-9a37c6cd0200/original", - "followers": 19, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x409677bf21ae777b89d7ecad36e42f247c5258e5", - "etherscanUrl": "https://etherscan.io/address/0x409677bf21ae777b89d7ecad36e42f247c5258e5", - "xHandle": "rokoksurya1688", - "xUrl": "https://twitter.com/rokoksurya1688", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_562482", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "zlyusia", - "displayName": "Ivan.base.eth", - "fid": 562482, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/75f064da-838d-401e-41df-a1b7bb1dcb00/rectcrop3", - "followers": 19, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x01c1432ae9433e3cc055286a64692c05fa924caf", - "etherscanUrl": "https://etherscan.io/address/0x01c1432ae9433e3cc055286a64692c05fa924caf", - "xHandle": "zlyusia", - "xUrl": "https://twitter.com/zlyusia", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1018605", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "powerchain", - "displayName": "Powerchain🎩", - "fid": 1018605, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/36e148e8-5535-4b51-4170-57161d01d100/rectcrop3", - "followers": 19, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4c1cb5a57c3de27ee2ad8cd0c1b921cf280cb2eb", - "etherscanUrl": "https://etherscan.io/address/0x4c1cb5a57c3de27ee2ad8cd0c1b921cf280cb2eb", - "xHandle": "doranjori33948", - "xUrl": "https://twitter.com/doranjori33948", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_274857", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kmsami-eth", - "displayName": "kmsami.base.eth", - "fid": 274857, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d533d526-5ed4-4441-e062-6ea977b0b100/original", - "followers": 18, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5f5af1c426f5f24296e9bbb76d2d15531aa660e6", - "etherscanUrl": "https://etherscan.io/address/0x5f5af1c426f5f24296e9bbb76d2d15531aa660e6", - "xHandle": "kmsami_eth", - "xUrl": "https://twitter.com/kmsami_eth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1176700", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "aizeus", - "displayName": "AIzeus", - "fid": 1176700, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4a07896c-30bf-4bc3-dc96-c0816183a000/rectcrop3", - "followers": 18, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe1822681f0de0e43fb4b7597f34ab28b5cfab465", - "etherscanUrl": "https://etherscan.io/address/0xe1822681f0de0e43fb4b7597f34ab28b5cfab465", - "xHandle": "artgedit", - "xUrl": "https://twitter.com/artgedit", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1149714", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "soapboxchats", - "displayName": "SoapBox", - "fid": 1149714, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/032c65c0-16a6-4931-465c-9f219b635a00/original", - "followers": 18, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xca719d896e57547faa7b802f9fea95004a971a2f", - "etherscanUrl": "https://etherscan.io/address/0xca719d896e57547faa7b802f9fea95004a971a2f", - "xHandle": "soapboxchats", - "xUrl": "https://twitter.com/soapboxchats", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1039626", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "avurdbkkh", - "displayName": "avur", - "fid": 1039626, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2a3bff1c-8ca1-4947-7dfd-f08dcfbc6b00/original", - "followers": 18, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x29f423e3264847a2c73f44cf7e73ebb8a85b1499", - "etherscanUrl": "https://etherscan.io/address/0x29f423e3264847a2c73f44cf7e73ebb8a85b1499", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_971817", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mansamu", - "displayName": "Musa Salihu Yusuf", - "fid": 971817, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ced9a509-ac55-449d-e9e7-20ac78ac2300/rectcrop3", - "followers": 18, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4bc32fb0931bde3ee992fa48e24b8b49c2b2c32a", - "etherscanUrl": "https://etherscan.io/address/0x4bc32fb0931bde3ee992fa48e24b8b49c2b2c32a", - "xHandle": "musay89732", - "xUrl": "https://twitter.com/musay89732", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1433167", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "emoneth", - "displayName": "emon.eth 🥦", - "fid": 1433167, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/84a4fd72-ef1f-4071-882c-a1db8437d600/original", - "followers": 17, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0ac3263dd17cba808a2f06895fd5fd32b3bce4d1", - "etherscanUrl": "https://etherscan.io/address/0x0ac3263dd17cba808a2f06895fd5fd32b3bce4d1", - "xHandle": "ripotor", - "xUrl": "https://twitter.com/ripotor", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1166227", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ntrenched", - "displayName": "Entrenched", - "fid": 1166227, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/86e49edc-dc93-4864-5de7-e7d42fe11800/original", - "followers": 17, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf32af4aa6445158f01351250fe422beed142a350", - "etherscanUrl": "https://etherscan.io/address/0xf32af4aa6445158f01351250fe422beed142a350", - "xHandle": "entrenched_", - "xUrl": "https://twitter.com/entrenched_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_361263", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "killermanilla", - "displayName": "N3CRIMINY", - "fid": 361263, - "pfpUrl": "https://i.imgur.com/89UGhN0.jpg", - "followers": 17, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc049f4173606ddc39c5b345ee4cbebb033989a46", - "etherscanUrl": "https://etherscan.io/address/0xc049f4173606ddc39c5b345ee4cbebb033989a46", - "xHandle": "andrewb28834084", - "xUrl": "https://twitter.com/andrewb28834084", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1050161", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "wadidaw", - "displayName": "Wadidaw", - "fid": 1050161, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/86375ee3-ae58-46cb-44a9-09a8b5501c00/rectcrop3", - "followers": 17, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd1e34572134489714b9c5e0722229830c27bba10", - "etherscanUrl": "https://etherscan.io/address/0xd1e34572134489714b9c5e0722229830c27bba10", - "xHandle": "growcrit", - "xUrl": "https://twitter.com/growcrit", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_779840", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "payalm", - "displayName": "PixelPips", - "fid": 779840, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f8f4372d-088a-4bfd-d70b-172a03b67200/original", - "followers": 17, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0e0f6b9b8b452bc06dcb12fcb8864ac9b7df620d", - "etherscanUrl": "https://etherscan.io/address/0x0e0f6b9b8b452bc06dcb12fcb8864ac9b7df620d", - "xHandle": "pmpixelpips", - "xUrl": "https://twitter.com/pmpixelpips", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_236695", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jayjay24", - "displayName": "Jayjay", - "fid": 236695, - "pfpUrl": "https://i.imgur.com/3AfO6G1.jpg", - "followers": 17, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x857a8982b991bd4ee796030c4491a61a18132f4f", - "etherscanUrl": "https://etherscan.io/address/0x857a8982b991bd4ee796030c4491a61a18132f4f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1139047", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jasonween", - "displayName": "Cinemadose", - "fid": 1139047, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e0376439-8ef3-44ad-47d9-513323079c00/original", - "followers": 16, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa30f6f091abbed787129ba4c1bd0371d6acdcdc5", - "etherscanUrl": "https://etherscan.io/address/0xa30f6f091abbed787129ba4c1bd0371d6acdcdc5", - "xHandle": "thanvanhuy3", - "xUrl": "https://twitter.com/thanvanhuy3", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_232535", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "faustboar", - "displayName": "faust", - "fid": 232535, - "pfpUrl": "https://i.imgur.com/1bHaI6X.jpg", - "followers": 16, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe2dc9b7ebc8934370cb5185ba7343b713fd5a390", - "etherscanUrl": "https://etherscan.io/address/0xe2dc9b7ebc8934370cb5185ba7343b713fd5a390", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_873906", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ibrahimkevon", - "displayName": "ibrahimkevon", - "fid": 873906, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/27652bd1-50d8-4268-4d65-ec535459d200/rectcrop3", - "followers": 16, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcebe160a03a4b6dc8b7080c25954eb07f5c58df2", - "etherscanUrl": "https://etherscan.io/address/0xcebe160a03a4b6dc8b7080c25954eb07f5c58df2", - "xHandle": "sergey785286", - "xUrl": "https://twitter.com/sergey785286", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1045102", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "theworldisyours", - "displayName": "lazy", - "fid": 1045102, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/52bb42ad-bf80-4e6b-de1b-4dee79648d00/original", - "followers": 16, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x73959dbee46f8373ff6155f9cde8e00673dac70d", - "etherscanUrl": "https://etherscan.io/address/0x73959dbee46f8373ff6155f9cde8e00673dac70d", - "xHandle": "poehislisu17065", - "xUrl": "https://twitter.com/poehislisu17065", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1014628", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sreemonojit", - "displayName": "SreeMonojit", - "fid": 1014628, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/54fd9eb4-1505-4a58-54cd-98cafe1e2500/rectcrop3", - "followers": 16, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x046162736e2b722fe4518e1150e63c85f54d9102", - "etherscanUrl": "https://etherscan.io/address/0x046162736e2b722fe4518e1150e63c85f54d9102", - "xHandle": "smonojit52617", - "xUrl": "https://twitter.com/smonojit52617", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1337722", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "midwestmind", - "displayName": "Living_and_Loving_Art", - "fid": 1337722, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f8adbf86-a6be-4110-aa64-dc6c3be24b00/original", - "followers": 15, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4e4927eacfe47cb1c0aec6c55e6f31bdb9cf71af", - "etherscanUrl": "https://etherscan.io/address/0x4e4927eacfe47cb1c0aec6c55e6f31bdb9cf71af", - "xHandle": "midwestartlover", - "xUrl": "https://twitter.com/midwestartlover", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1318077", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nazila1991", - "displayName": "Nazila", - "fid": 1318077, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f616057f-8504-4d5c-ad2f-ad170c029d00/original", - "followers": 15, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xaceb1d8deb198ba079d0922c8d01dd094e6299a3", - "etherscanUrl": "https://etherscan.io/address/0xaceb1d8deb198ba079d0922c8d01dd094e6299a3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_956923", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "infatuatedsimp", - "displayName": "TheSimp", - "fid": 956923, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f7918dbe-11fb-4041-9fce-aa088b4ca900/rectcrop3", - "followers": 15, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc63274b565ba479902cb9db2901decabd6e433df", - "etherscanUrl": "https://etherscan.io/address/0xc63274b565ba479902cb9db2901decabd6e433df", - "xHandle": "infatuated_simp", - "xUrl": "https://twitter.com/infatuated_simp", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1190450", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jokky11", - "displayName": "Joke Bello", - "fid": 1190450, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5a2717bd-8a5e-4596-12ba-67e920d4f600/original", - "followers": 14, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7ae5c7c802a0fe88cde6a4127e2ee39c51165bcf", - "etherscanUrl": "https://etherscan.io/address/0x7ae5c7c802a0fe88cde6a4127e2ee39c51165bcf", - "xHandle": "jokky11", - "xUrl": "https://twitter.com/jokky11", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1044609", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "staiwo", - "displayName": "yadnusot", - "fid": 1044609, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b3c39d72-59cb-4092-b9c7-575eab951900/rectcrop3", - "followers": 14, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x44d1265d0dbabad2113b730a66fe691964f75a7e", - "etherscanUrl": "https://etherscan.io/address/0x44d1265d0dbabad2113b730a66fe691964f75a7e", - "xHandle": "therealyadnuso", - "xUrl": "https://twitter.com/therealyadnuso", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1172362", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ledon", - "displayName": "B.U.S.Y", - "fid": 1172362, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/bd731d7b-fdc5-4adf-7590-78224a5d5500/original", - "followers": 14, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9a3ff25f0d52b034590a85c8fe48417cfb8d8be4", - "etherscanUrl": "https://etherscan.io/address/0x9a3ff25f0d52b034590a85c8fe48417cfb8d8be4", - "xHandle": "777_busy", - "xUrl": "https://twitter.com/777_busy", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1238229", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "duckphart-greg", - "displayName": "DuckPhart (Greg)", - "fid": 1238229, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/52611e26-4472-4146-a3ba-24164bf06b00/original", - "followers": 14, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x36894e4c92681c642d59c801f2625bf635ee7363", - "etherscanUrl": "https://etherscan.io/address/0x36894e4c92681c642d59c801f2625bf635ee7363", - "xHandle": "roughyield96866", - "xUrl": "https://twitter.com/roughyield96866", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1114372", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sendarc", - "displayName": "Send Arcade", - "fid": 1114372, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/32119ac7-d01c-4ccd-45e6-5fd9a9851900/rectcrop3", - "followers": 14, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xee47b36cdab18ddece299020a07dc23909e78d83", - "etherscanUrl": "https://etherscan.io/address/0xee47b36cdab18ddece299020a07dc23909e78d83", - "xHandle": "sendarcade_", - "xUrl": "https://twitter.com/sendarcade_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1046735", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "alexetrafilm", - "displayName": "AlexEtraFilm", - "fid": 1046735, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/16b3ec47-ea61-4e60-f9b6-ea889f116900/rectcrop3", - "followers": 14, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x13cc2cea6b5a1bb76c0198127c1c7f2e471d21f6", - "etherscanUrl": "https://etherscan.io/address/0x13cc2cea6b5a1bb76c0198127c1c7f2e471d21f6", - "xHandle": "etrafilm", - "xUrl": "https://twitter.com/etrafilm", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_930688", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ssshamim2003", - "displayName": "SS Shamim", - "fid": 930688, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/09e41db4-621e-442d-2cbb-c998ea11bc00/rectcrop3", - "followers": 14, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x396c113345d572bd5fbeec2e3f4ccbe3f84fedb4", - "etherscanUrl": "https://etherscan.io/address/0x396c113345d572bd5fbeec2e3f4ccbe3f84fedb4", - "xHandle": "litegam36172214", - "xUrl": "https://twitter.com/litegam36172214", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_992014", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "elpablo102002", - "displayName": "el base.eth", - "fid": 992014, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/522ea2a4-f435-453d-6498-4bc5bcf0e600/original", - "followers": 13, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x45673f46bf151c08e9c5dd20828c310a01a06ea8", - "etherscanUrl": "https://etherscan.io/address/0x45673f46bf151c08e9c5dd20828c310a01a06ea8", - "xHandle": "pabloc59890", - "xUrl": "https://twitter.com/pabloc59890", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1141066", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "thamrin30", - "displayName": "Usni", - "fid": 1141066, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/be8deecf-57c0-45e4-0124-f4f136e1a700/original", - "followers": 13, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2591536b3da1e1c052f30e764d5ebab905f0e1f4", - "etherscanUrl": "https://etherscan.io/address/0x2591536b3da1e1c052f30e764d5ebab905f0e1f4", - "xHandle": "thamrin30", - "xUrl": "https://twitter.com/thamrin30", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1060709", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "suirewardsme", - "displayName": "j", - "fid": 1060709, - "pfpUrl": "https://warpcast.com/avatar.png?t=1746173124470", - "followers": 13, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf7778822cb066ff5a912700e201ada0615c64289", - "etherscanUrl": "https://etherscan.io/address/0xf7778822cb066ff5a912700e201ada0615c64289", - "xHandle": "gabrielgubl", - "xUrl": "https://twitter.com/gabrielgubl", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1097181", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "pinmaa", - "displayName": "Pinma", - "fid": 1097181, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/be8deecf-57c0-45e4-0124-f4f136e1a700/original", - "followers": 12, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd1a194b91cb19465a7297dee2908ee245c5eb5f6", - "etherscanUrl": "https://etherscan.io/address/0xd1a194b91cb19465a7297dee2908ee245c5eb5f6", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1076090", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "whalebux", - "displayName": "WhaleBux.Shinzo.base", - "fid": 1076090, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/79694762-43fa-49b8-3309-e4d25fb2d600/original", - "followers": 12, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa904912ffe8a94070bd09f724bfddfa3b5ffab2b", - "etherscanUrl": "https://etherscan.io/address/0xa904912ffe8a94070bd09f724bfddfa3b5ffab2b", - "xHandle": "ishinzo369", - "xUrl": "https://twitter.com/ishinzo369", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1020834", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jalantze", - "displayName": "Jalantze", - "fid": 1020834, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e3260d68-ecce-4b18-3d45-e9f121c63600/rectcrop3", - "followers": 12, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe234d042249aee329859f280fed1a3f6e38b2b86", - "etherscanUrl": "https://etherscan.io/address/0xe234d042249aee329859f280fed1a3f6e38b2b86", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1072385", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "milom655", - "displayName": "Milon CLONE", - "fid": 1072385, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/407e3149-fbf5-4002-5abf-b23127c6ac00/original", - "followers": 12, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6b64b116c85f40c7d239cf3365537daa2a2542a8", - "etherscanUrl": "https://etherscan.io/address/0x6b64b116c85f40c7d239cf3365537daa2a2542a8", - "xHandle": "mdmilon91772240", - "xUrl": "https://twitter.com/mdmilon91772240", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1068329", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mdziyad09", - "displayName": "Lee", - "fid": 1068329, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3969a40a-59bf-4bb9-a505-c6dcc765bc00/original", - "followers": 12, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x135ec645150354ac1e31c21085bc13a3c27a0369", - "etherscanUrl": "https://etherscan.io/address/0x135ec645150354ac1e31c21085bc13a3c27a0369", - "xHandle": "mdziyad00", - "xUrl": "https://twitter.com/mdziyad00", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_892611", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "martin2up", - "displayName": "MartinUp", - "fid": 892611, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1dee55fa-e203-478b-3479-5825d956bc00/rectcrop3", - "followers": 12, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2485f4d78a264558620e5c0fc68e11f5a37dc82c", - "etherscanUrl": "https://etherscan.io/address/0x2485f4d78a264558620e5c0fc68e11f5a37dc82c", - "xHandle": "martin_up18383", - "xUrl": "https://twitter.com/martin_up18383", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1042006", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "joycst12", - "displayName": "Joy", - "fid": 1042006, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/cfb0c619-da6c-4b42-4ff5-0eeb899bf500/rectcrop3", - "followers": 12, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe9a27ffa10a3d0ca1419ebd8d0c7a2016dc86a47", - "etherscanUrl": "https://etherscan.io/address/0xe9a27ffa10a3d0ca1419ebd8d0c7a2016dc86a47", - "xHandle": "joy21535857", - "xUrl": "https://twitter.com/joy21535857", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_871631", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "predragshields", - "displayName": "predragshields", - "fid": 871631, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/52642312-3e6e-44c8-ac77-416702c42500/rectcrop3", - "followers": 11, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0fde43989e6d409da0ace5b71bab98c25fa70a09", - "etherscanUrl": "https://etherscan.io/address/0x0fde43989e6d409da0ace5b71bab98c25fa70a09", - "xHandle": "varnava497346", - "xUrl": "https://twitter.com/varnava497346", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1143542", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "anyanwu43", - "displayName": "promiseanyanwu \"Meshchain.Ai\"", - "fid": 1143542, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d6ee6b69-c59e-479e-9a68-c863553e4e00/original", - "followers": 11, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbdf44ba97b590c627ed2cf30d025fa0ea6f72209", - "etherscanUrl": "https://etherscan.io/address/0xbdf44ba97b590c627ed2cf30d025fa0ea6f72209", - "xHandle": "promise19576748", - "xUrl": "https://twitter.com/promise19576748", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1105642", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sergiy7777777", - "displayName": "sergiy7.base.eth", - "fid": 1105642, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/16a70821-6d6d-4859-7afe-e2a8f9563700/rectcrop3", - "followers": 11, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdd213059c43db698f739821a3375946860047cd0", - "etherscanUrl": "https://etherscan.io/address/0xdd213059c43db698f739821a3375946860047cd0", - "xHandle": "jekpotj", - "xUrl": "https://twitter.com/jekpotj", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_397526", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "trader06", - "displayName": "Jon Kamper", - "fid": 397526, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c89e1a4c-ea84-4f6b-4797-1a04faff6e00/rectcrop3", - "followers": 10, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x73bc0ec716e95ccd8d5c9a12f526948375a9d8b9", - "etherscanUrl": "https://etherscan.io/address/0x73bc0ec716e95ccd8d5c9a12f526948375a9d8b9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1139879", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "musrifah01", - "displayName": "Musrifah", - "fid": 1139879, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e86d8665-76d7-434c-fe8e-c9b14a3d4f00/original", - "followers": 10, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xba426e90e9daa0c4c91614471e78a416c97ae93e", - "etherscanUrl": "https://etherscan.io/address/0xba426e90e9daa0c4c91614471e78a416c97ae93e", - "xHandle": "musrifahiffah", - "xUrl": "https://twitter.com/musrifahiffah", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_939912", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "samratbera", - "displayName": "Samrat", - "fid": 939912, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/859d6464-6221-47b4-935e-38806c987100/rectcrop3", - "followers": 10, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa8fec6936f36e5a36a81d01b6b937ca186f7f668", - "etherscanUrl": "https://etherscan.io/address/0xa8fec6936f36e5a36a81d01b6b937ca186f7f668", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1080433", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jayzbase", - "displayName": "Stephen Jayz", - "fid": 1080433, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/be8deecf-57c0-45e4-0124-f4f136e1a700/original", - "followers": 10, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3e8ec51fb7b8f077374ed9e89df432a93d03f94c", - "etherscanUrl": "https://etherscan.io/address/0x3e8ec51fb7b8f077374ed9e89df432a93d03f94c", - "xHandle": "i_feadigo", - "xUrl": "https://twitter.com/i_feadigo", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_385945", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mmokkil", - "displayName": "Morema🐹", - "fid": 385945, - "pfpUrl": "https://i.imgur.com/t8VVFDH.jpg", - "followers": 10, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x18a996bfc78a753e38277b01744061544e986365", - "etherscanUrl": "https://etherscan.io/address/0x18a996bfc78a753e38277b01744061544e986365", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1417104", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "blakebluu", - "displayName": "BlakeBluu", - "fid": 1417104, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/037bc61f-d478-425f-cc0a-1e2bbb227700/original", - "followers": 9, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa6002a7442f29097fd457ba61005456b476c509e", - "etherscanUrl": "https://etherscan.io/address/0xa6002a7442f29097fd457ba61005456b476c509e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1424781", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "iamon", - "displayName": "iamon", - "fid": 1424781, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/95e044eb-c3e1-47ca-ae1a-6cfce9f2ce00/original", - "followers": 9, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x59da9a6adc43413d6f2d8d8166b7d4113326f0a0", - "etherscanUrl": "https://etherscan.io/address/0x59da9a6adc43413d6f2d8d8166b7d4113326f0a0", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1026537", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mehedihasan2mh", - "displayName": "Mehedi hasan", - "fid": 1026537, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5c98c715-32d1-4af3-8e32-7fcdb837a200/rectcrop3", - "followers": 9, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5b5a856edeed5ed06aacfdf8c2c4c6ac66aaad50", - "etherscanUrl": "https://etherscan.io/address/0x5b5a856edeed5ed06aacfdf8c2c4c6ac66aaad50", - "xHandle": "mehedihasan9rt", - "xUrl": "https://twitter.com/mehedihasan9rt", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1131524", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "legend47", - "displayName": "Idris Ishaq", - "fid": 1131524, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/aba16f57-1805-444b-9069-ae47d3009600/original", - "followers": 9, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2d2f7302692eb2443effbc1e806e8195d57143c8", - "etherscanUrl": "https://etherscan.io/address/0x2d2f7302692eb2443effbc1e806e8195d57143c8", - "xHandle": "idrisishaq47", - "xUrl": "https://twitter.com/idrisishaq47", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1089092", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "subhajit321", - "displayName": "I", - "fid": 1089092, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/95e044eb-c3e1-47ca-ae1a-6cfce9f2ce00/original", - "followers": 9, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfc93050f38352b12b487c82ed56d0300ad8c3304", - "etherscanUrl": "https://etherscan.io/address/0xfc93050f38352b12b487c82ed56d0300ad8c3304", - "xHandle": "subhajitma18999", - "xUrl": "https://twitter.com/subhajitma18999", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1043007", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "renzoskye", - "displayName": "Renzo Skye", - "fid": 1043007, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/06754546-461a-42d4-bbf7-885eb0197200/rectcrop3", - "followers": 9, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x401cdee44fc91cc98e3bbca4365f714e46457c9c", - "etherscanUrl": "https://etherscan.io/address/0x401cdee44fc91cc98e3bbca4365f714e46457c9c", - "xHandle": "renzo_skye", - "xUrl": "https://twitter.com/renzo_skye", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1120226", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cwyxhsw", - "displayName": "发财小狗屁1.0", - "fid": 1120226, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ae7f00d3-65dc-4f2e-6212-48b6ab87f800/original", - "followers": 8, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xab880948d63028a159bed4e11c26a6a2141a5f40", - "etherscanUrl": "https://etherscan.io/address/0xab880948d63028a159bed4e11c26a6a2141a5f40", - "xHandle": "cwyxhqz", - "xUrl": "https://twitter.com/cwyxhqz", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1052654", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "tikulma", - "displayName": "-4°", - "fid": 1052654, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5a4db9aa-a028-49a0-e2cf-d0117e844500/original", - "followers": 8, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd05c705891464d52347c9becfd61c6245985aa4e", - "etherscanUrl": "https://etherscan.io/address/0xd05c705891464d52347c9becfd61c6245985aa4e", - "xHandle": "tikulmax", - "xUrl": "https://twitter.com/tikulmax", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_892561", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "duts67", - "displayName": "Decu0x", - "fid": 892561, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3ae5cd78-1623-4451-fabb-e3fa48ee6100/rectcrop3", - "followers": 8, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2d335378ce0d4f8ef89dc2b4b5efa3b3613db3a2", - "etherscanUrl": "https://etherscan.io/address/0x2d335378ce0d4f8ef89dc2b4b5efa3b3613db3a2", - "xHandle": "guaremperorr", - "xUrl": "https://twitter.com/guaremperorr", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1028484", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "oxaave", - "displayName": "0xaave", - "fid": 1028484, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4c3360d9-c75f-4825-f93a-d3c227eb9700/rectcrop3", - "followers": 8, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7b0c0adae0ce0b91d45b2d419265f45e5f88f8df", - "etherscanUrl": "https://etherscan.io/address/0x7b0c0adae0ce0b91d45b2d419265f45e5f88f8df", - "xHandle": "_0xrey", - "xUrl": "https://twitter.com/_0xrey", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_897481", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "garyma", - "displayName": "GaryMa", - "fid": 897481, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/29db9d9b-bd33-49b2-271c-4a27e8948200/rectcrop3", - "followers": 7, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb924f68aae1a544366f93ea453f9da56f239c88b", - "etherscanUrl": "https://etherscan.io/address/0xb924f68aae1a544366f93ea453f9da56f239c88b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1398640", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mr-hunt-g", - "displayName": "mr-hunt-g", - "fid": 1398640, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5a2717bd-8a5e-4596-12ba-67e920d4f600/original", - "followers": 7, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd4ed83f523433432619044ed5fc37abafebd287a", - "etherscanUrl": "https://etherscan.io/address/0xd4ed83f523433432619044ed5fc37abafebd287a", - "xHandle": "mr_hunt_g", - "xUrl": "https://twitter.com/mr_hunt_g", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1388907", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jake-clover", - "displayName": "jake-clover", - "fid": 1388907, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4f0e22e9-a2bb-48a7-ebff-1ba77c0aae00/original", - "followers": 7, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd53764ccd3f79d66e0fae632cf8204ad83b3661b", - "etherscanUrl": "https://etherscan.io/address/0xd53764ccd3f79d66e0fae632cf8204ad83b3661b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1141363", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "stonimals", - "displayName": "Stonimals", - "fid": 1141363, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d886e695-6e38-48e5-6a8f-7d0f9ded8300/original", - "followers": 7, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb365ae5a015f1dfcd693307aabc4bf52f4a5c500", - "etherscanUrl": "https://etherscan.io/address/0xb365ae5a015f1dfcd693307aabc4bf52f4a5c500", - "xHandle": "stonimals", - "xUrl": "https://twitter.com/stonimals", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1106287", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "scannooor", - "displayName": "Scannooor", - "fid": 1106287, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ef26192b-05c5-4e9d-64c3-9852dc43f000/original", - "followers": 7, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3d94af1db57d58da44ce9e57a418cacf9770ff64", - "etherscanUrl": "https://etherscan.io/address/0x3d94af1db57d58da44ce9e57a418cacf9770ff64", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1106404", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "superautism.eth", - "displayName": "superautism", - "fid": 1106404, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/fa9eb92d-f9b2-48cd-c619-780d93f35500/original", - "followers": 7, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4c67abab31de6d82c1e308614476c55b0b228d08", - "etherscanUrl": "https://etherscan.io/address/0x4c67abab31de6d82c1e308614476c55b0b228d08", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1099466", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "clankr", - "displayName": "ClankR.xyz", - "fid": 1099466, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e907abda-a816-4281-7fb5-1218db96f100/original", - "followers": 7, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x966a7467fa31cf9316ba04c6cd819916e0fc7827", - "etherscanUrl": "https://etherscan.io/address/0x966a7467fa31cf9316ba04c6cd819916e0fc7827", - "xHandle": "clankr_xyz", - "xUrl": "https://twitter.com/clankr_xyz", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1427359", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "0xshakuna", - "displayName": "0xshakuna", - "fid": 1427359, - "pfpUrl": "https://res.cloudinary.com/base-app/image/upload/f_auto/v1762143088/1000276976.jpg.jpg", - "followers": 6, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7ce56394b654895a4c723397ae8afbba64ed1164", - "etherscanUrl": "https://etherscan.io/address/0x7ce56394b654895a4c723397ae8afbba64ed1164", - "xHandle": "waltershaku", - "xUrl": "https://twitter.com/waltershaku", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1373135", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ethmascot", - "displayName": "ethmascot", - "fid": 1373135, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e3a07671-537a-4bdb-194f-cbcea73d6300/original", - "followers": 6, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb36fc534916665e1c8c5e53041fa29be599f4eb1", - "etherscanUrl": "https://etherscan.io/address/0xb36fc534916665e1c8c5e53041fa29be599f4eb1", - "xHandle": "0x_nizsa", - "xUrl": "https://twitter.com/0x_nizsa", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1138356", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "samtechrana", - "displayName": "RANA", - "fid": 1138356, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/551efa6d-f86b-4fb6-5afc-21c706e26800/original", - "followers": 6, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x269f13e3ad42ea7863040866db8a580589bc00a5", - "etherscanUrl": "https://etherscan.io/address/0x269f13e3ad42ea7863040866db8a580589bc00a5", - "xHandle": "diya667366", - "xUrl": "https://twitter.com/diya667366", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1178811", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "degentruffles", - "displayName": "Degen Truffles", - "fid": 1178811, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/af757db7-5e62-438c-9b16-c3c7ce4cf700/original", - "followers": 6, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe347655221dd751da3a42652f190b5324c08ec28", - "etherscanUrl": "https://etherscan.io/address/0xe347655221dd751da3a42652f190b5324c08ec28", - "xHandle": "degentruffles", - "xUrl": "https://twitter.com/degentruffles", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1117768", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "heyanonai", - "displayName": "Hey Anon", - "fid": 1117768, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/df546a28-9ef4-443a-7e0b-df7714614e00/original", - "followers": 6, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x197a1e37de8ca195645df8c1e55419a971291365", - "etherscanUrl": "https://etherscan.io/address/0x197a1e37de8ca195645df8c1e55419a971291365", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1093602", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "apeofct", - "displayName": "Monke", - "fid": 1093602, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/cfdb912e-ea8f-42b2-dbc6-742fcb780200/original", - "followers": 6, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1e275f0739c09751580beeea3ea7a91154d53a44", - "etherscanUrl": "https://etherscan.io/address/0x1e275f0739c09751580beeea3ea7a91154d53a44", - "xHandle": "apeofct", - "xUrl": "https://twitter.com/apeofct", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_901234", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "xione", - "displayName": "xione", - "fid": 901234, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b7b3b0b9-3b45-4d18-4dc9-3e6edbeed500/rectcrop3", - "followers": 6, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9037d62bc9e03faebc3cc14f9a38366c141b9455", - "etherscanUrl": "https://etherscan.io/address/0x9037d62bc9e03faebc3cc14f9a38366c141b9455", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1070591", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "timir548", - "displayName": "Timir Roy", - "fid": 1070591, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/64f87b81-c673-4097-429a-a217339be800/original", - "followers": 6, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5a5b2bcbb3e862e97b93769f631ceb10f16ef94e", - "etherscanUrl": "https://etherscan.io/address/0x5a5b2bcbb3e862e97b93769f631ceb10f16ef94e", - "xHandle": "timirroy366710", - "xUrl": "https://twitter.com/timirroy366710", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1059167", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "universal-truth", - "displayName": "Universal Truth Engine", - "fid": 1059167, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a1e51592-d749-4b12-70ca-9f87de93da00/original", - "followers": 6, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe9dfeb4e7163f4e2f0ace20eae2b2cc4177a9fb0", - "etherscanUrl": "https://etherscan.io/address/0xe9dfeb4e7163f4e2f0ace20eae2b2cc4177a9fb0", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_968126", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "basevine", - "displayName": "Base Vine", - "fid": 968126, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f63b8377-9032-4e3f-83a0-7e71c3463c00/original", - "followers": 6, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x51ac51967a4be9879f7cce67681e0a65525cfa80", - "etherscanUrl": "https://etherscan.io/address/0x51ac51967a4be9879f7cce67681e0a65525cfa80", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_538182", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "betatron", - "displayName": "betatron", - "fid": 538182, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6b459c47-bc80-4af2-98ce-b52824444900/rectcrop3", - "followers": 6, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x886c01d23a50c144b6c80f92799a895f54cc74be", - "etherscanUrl": "https://etherscan.io/address/0x886c01d23a50c144b6c80f92799a895f54cc74be", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1427295", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "punkofficial", - "displayName": "punkofficial", - "fid": 1427295, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e9095c68-b34f-4a46-3638-2ef96110fa00/original", - "followers": 5, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x17d6ccfb5b53984de3af3e52e9a9a228ac36075f", - "etherscanUrl": "https://etherscan.io/address/0x17d6ccfb5b53984de3af3e52e9a9a228ac36075f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_432731", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "jeff-pesos", - "displayName": "Jeff.Pesos.btc ", - "fid": 432731, - "pfpUrl": "https://i.imgur.com/kFQXtOl.jpg", - "followers": 5, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0939b6f997a950e6e3b6c120344b2ab021a8e4bb", - "etherscanUrl": "https://etherscan.io/address/0x0939b6f997a950e6e3b6c120344b2ab021a8e4bb", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1068988", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dim20230610", - "displayName": "diman20230610", - "fid": 1068988, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b199acc7-478d-4927-1f05-07ca55282300/rectcrop3", - "followers": 5, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x89b81ae1e84679cc825888426403ba7b95e2ed11", - "etherscanUrl": "https://etherscan.io/address/0x89b81ae1e84679cc825888426403ba7b95e2ed11", - "xHandle": "dim20230610", - "xUrl": "https://twitter.com/dim20230610", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1087928", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "umeduk", - "displayName": "Umi", - "fid": 1087928, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/13cd6c7b-8fd2-4768-48ca-e32ac3620100/original", - "followers": 5, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd6993fd8788e0f912c2b8570720eb8cf709c3ef0", - "etherscanUrl": "https://etherscan.io/address/0xd6993fd8788e0f912c2b8570720eb8cf709c3ef0", - "xHandle": "kotadiyaum65472", - "xUrl": "https://twitter.com/kotadiyaum65472", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1085719", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ahmad98319", - "displayName": "Ahmad Usman", - "fid": 1085719, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0d7c18e8-53f7-4fc4-2736-d1ebf5195100/original", - "followers": 5, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4a5a6fd9c1f38454a8b8551f6aac2e1821f3ffec", - "etherscanUrl": "https://etherscan.io/address/0x4a5a6fd9c1f38454a8b8551f6aac2e1821f3ffec", - "xHandle": "ahmad98319", - "xUrl": "https://twitter.com/ahmad98319", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_385964", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dmakroni29313", - "displayName": "Davidee🐹", - "fid": 385964, - "pfpUrl": "https://i.imgur.com/mVNKHxV.jpg", - "followers": 5, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6c13ec2cfc126b58f756b46259df1be64bf32cfb", - "etherscanUrl": "https://etherscan.io/address/0x6c13ec2cfc126b58f756b46259df1be64bf32cfb", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1046992", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "trendr", - "displayName": "Condor", - "fid": 1046992, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f6911201-a7a5-45cf-c3ad-a33e869fd300/original", - "followers": 5, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3a34fc26bae35aa68d1f88fc8459ad58f3ecd149", - "etherscanUrl": "https://etherscan.io/address/0x3a34fc26bae35aa68d1f88fc8459ad58f3ecd149", - "xHandle": "im_a_richb", - "xUrl": "https://twitter.com/im_a_richb", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1043098", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "simonm", - "displayName": "simonm", - "fid": 1043098, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e89cca88-0aa2-4079-092f-c7b02d1ea900/rectcrop3", - "followers": 5, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x775267a8012b0d164fc134ecb06aa6d92c68c268", - "etherscanUrl": "https://etherscan.io/address/0x775267a8012b0d164fc134ecb06aa6d92c68c268", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1028922", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "vombatusfans", - "displayName": "vombatusfans", - "fid": 1028922, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/aa1f14a7-62ba-4ff0-18c4-c7e2abfe7900/rectcrop3", - "followers": 5, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe797cde5272e1c9b43fe0cb1bb86a0635733034e", - "etherscanUrl": "https://etherscan.io/address/0xe797cde5272e1c9b43fe0cb1bb86a0635733034e", - "xHandle": "makerdao2023", - "xUrl": "https://twitter.com/makerdao2023", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1022416", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mulahcrypto", - "displayName": "Mulah Crypto", - "fid": 1022416, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9a06025c-d050-4bd1-b041-a1541c53d200/rectcrop3", - "followers": 4, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x31a65ee41c036a89fd40adb11838c186ecf534f7", - "etherscanUrl": "https://etherscan.io/address/0x31a65ee41c036a89fd40adb11838c186ecf534f7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1179382", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "catbusonbase", - "displayName": "catbus", - "fid": 1179382, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/32501781-02a1-4ff1-7370-8fff3c819100/original", - "followers": 4, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd52bdf7731e150cfb8ccf65f7c898411b11a8502", - "etherscanUrl": "https://etherscan.io/address/0xd52bdf7731e150cfb8ccf65f7c898411b11a8502", - "xHandle": "catbus_", - "xUrl": "https://twitter.com/catbus_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1142444", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "disenchakma", - "displayName": "Notun Priyo", - "fid": 1142444, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b98214a4-9189-474e-fc1c-d06b7be7f300/rectcrop3", - "followers": 4, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3d234d042c4fb0ce3041034bba7fab4601dc4143", - "etherscanUrl": "https://etherscan.io/address/0x3d234d042c4fb0ce3041034bba7fab4601dc4143", - "xHandle": "disenchakma", - "xUrl": "https://twitter.com/disenchakma", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1097084", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "shanmugaprabhu", - "displayName": "shanmugaprabhu 75T5BPGG", - "fid": 1097084, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/8b677e4d-f487-49b0-4a7a-d4cacff8fa00/original", - "followers": 4, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x200f6be6851a20350c7828434ade016aa3966df8", - "etherscanUrl": "https://etherscan.io/address/0x200f6be6851a20350c7828434ade016aa3966df8", - "xHandle": "shanmugaprabhu0", - "xUrl": "https://twitter.com/shanmugaprabhu0", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1086122", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mekas", - "displayName": "Mekas", - "fid": 1086122, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4b22765e-9c8c-48fb-df41-759f48494b00/original", - "followers": 4, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc4c968a93c2ddbc12ba1b4b5790afdf175d0ce61", - "etherscanUrl": "https://etherscan.io/address/0xc4c968a93c2ddbc12ba1b4b5790afdf175d0ce61", - "xHandle": "0xmekas", - "xUrl": "https://twitter.com/0xmekas", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1072250", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sim202308145602", - "displayName": "SimSon20230816 (Ø,G) $ODY", - "fid": 1072250, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/be46a903-be25-44b7-2bbd-ac1171e8ce00/original", - "followers": 4, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x93850071912ababb40ac57acaec340d9e0f3161e", - "etherscanUrl": "https://etherscan.io/address/0x93850071912ababb40ac57acaec340d9e0f3161e", - "xHandle": "sim202308145602", - "xUrl": "https://twitter.com/sim202308145602", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1071552", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dim10p", - "displayName": "Glory (Ø,G) $ODY", - "fid": 1071552, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/66f933c6-6809-48f5-7734-78ad44b17300/original", - "followers": 4, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb20d873ceabd15c81b67e5b6b802abf7877a6952", - "etherscanUrl": "https://etherscan.io/address/0xb20d873ceabd15c81b67e5b6b802abf7877a6952", - "xHandle": "dim10p", - "xUrl": "https://twitter.com/dim10p", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1039610", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "reynaangelica", - "displayName": "reyna", - "fid": 1039610, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9c51dec8-9008-4f23-fa47-10692d95e800/rectcrop3", - "followers": 4, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2f410b33e13155e9309f3e6f361f56dceea40a74", - "etherscanUrl": "https://etherscan.io/address/0x2f410b33e13155e9309f3e6f361f56dceea40a74", - "xHandle": "domikajojo31310", - "xUrl": "https://twitter.com/domikajojo31310", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1022718", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mastepe", - "displayName": "KING", - "fid": 1022718, - "pfpUrl": "https://beb-public.s3.us-west-1.amazonaws.com/black.jpg", - "followers": 4, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf2668ecc2b55487dbdde569130679ae8fa00a208", - "etherscanUrl": "https://etherscan.io/address/0xf2668ecc2b55487dbdde569130679ae8fa00a208", - "xHandle": "omtepe_", - "xUrl": "https://twitter.com/omtepe_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1033911", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "minokun024", - "displayName": "minokun024", - "fid": 1033911, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7380c5d4-4992-46d8-6172-bc3e51f19100/rectcrop3", - "followers": 4, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x090224a68d83240cc64169bb7e99048d3f04710e", - "etherscanUrl": "https://etherscan.io/address/0x090224a68d83240cc64169bb7e99048d3f04710e", - "xHandle": "makerdao2023", - "xUrl": "https://twitter.com/makerdao2023", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1030524", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "minokun664", - "displayName": "minokun664", - "fid": 1030524, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/021c1177-1683-4ae5-78a6-bf8deb659100/rectcrop3", - "followers": 4, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe7699b53ce206b389523b8b88c1bd38baf25a6bc", - "etherscanUrl": "https://etherscan.io/address/0xe7699b53ce206b389523b8b88c1bd38baf25a6bc", - "xHandle": "makerdao2023", - "xUrl": "https://twitter.com/makerdao2023", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1021340", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "sayftyhazard", - "displayName": "Sayf", - "fid": 1021340, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a9c09acc-9efe-4e00-0c79-23877318af00/rectcrop3", - "followers": 4, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1310811a6b53401bfba70cef1118090ae147252b", - "etherscanUrl": "https://etherscan.io/address/0x1310811a6b53401bfba70cef1118090ae147252b", - "xHandle": "sayftyhazard", - "xUrl": "https://twitter.com/sayftyhazard", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1029329", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "lisagail", - "displayName": "lisa gail", - "fid": 1029329, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/96551903-ca0c-4087-dd28-9081d87cdd00/rectcrop3", - "followers": 4, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4a3c0fccd3ef7256d72d29aaed585eb01ed53b31", - "etherscanUrl": "https://etherscan.io/address/0x4a3c0fccd3ef7256d72d29aaed585eb01ed53b31", - "xHandle": "makerdao2023", - "xUrl": "https://twitter.com/makerdao2023", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1457260", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bardetti34", - "displayName": "CTokenized", - "fid": 1457260, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2dfd7a34-eced-4376-c5d0-6343dbdfa000/original", - "followers": 3, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfe8385a206c18c08c86421d4b7bbd8e2ce7a40a7", - "etherscanUrl": "https://etherscan.io/address/0xfe8385a206c18c08c86421d4b7bbd8e2ce7a40a7", - "xHandle": "ctokenized", - "xUrl": "https://twitter.com/ctokenized", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1377353", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cfgijosh", - "displayName": "Joshua", - "fid": 1377353, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3708f2b1-79b9-4f8a-ec3e-797f9ca45100/original", - "followers": 3, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x708d2714e33e99a3dd9b0439dbabb95d5e8acf19", - "etherscanUrl": "https://etherscan.io/address/0x708d2714e33e99a3dd9b0439dbabb95d5e8acf19", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1368041", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "basetv", - "displayName": "basetv", - "fid": 1368041, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/8b05f951-4c9b-47f0-c5ec-810073ddf600/original", - "followers": 3, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6d030dadcfe95800caef7bc50a006f2d5f3ae6f6", - "etherscanUrl": "https://etherscan.io/address/0x6d030dadcfe95800caef7bc50a006f2d5f3ae6f6", - "xHandle": "basetv328642", - "xUrl": "https://twitter.com/basetv328642", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1063818", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "est10088", - "displayName": "Fcvk", - "fid": 1063818, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f28902ee-9cb5-4f1f-0014-169092dfe000/original", - "followers": 3, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf2f39775b85a2ff90e048deac9dc4d9148b0d67d", - "etherscanUrl": "https://etherscan.io/address/0xf2f39775b85a2ff90e048deac9dc4d9148b0d67d", - "xHandle": "est10088", - "xUrl": "https://twitter.com/est10088", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_941794", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "tinaconstance", - "displayName": "TinaConstance", - "fid": 941794, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9e685f34-4a1b-46b9-5e5b-cfe09a55ca00/original", - "followers": 3, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5dd2fe68d82fabefa7a9cde290c2744ac4e3f1a6", - "etherscanUrl": "https://etherscan.io/address/0x5dd2fe68d82fabefa7a9cde290c2744ac4e3f1a6", - "xHandle": "immekeygarcia", - "xUrl": "https://twitter.com/immekeygarcia", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1065525", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "iangarry", - "displayName": "Intann 🕊️", - "fid": 1065525, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b65842e8-a305-4116-423a-5b19e7715400/rectcrop3", - "followers": 3, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa6200b3bc8000838c259b129630414198dbec66d", - "etherscanUrl": "https://etherscan.io/address/0xa6200b3bc8000838c259b129630414198dbec66d", - "xHandle": "intannsui", - "xUrl": "https://twitter.com/intannsui", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1043613", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "incentiv", - "displayName": "Incentiv", - "fid": 1043613, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/dc24cd22-f777-4edf-5ee7-1817d1449200/rectcrop3", - "followers": 3, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2177ad2185076d3e97a1faff0addc0efa5fc68d1", - "etherscanUrl": "https://etherscan.io/address/0x2177ad2185076d3e97a1faff0addc0efa5fc68d1", - "xHandle": "incentivnet", - "xUrl": "https://twitter.com/incentivnet", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1034318", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "minokun406", - "displayName": "minokun406", - "fid": 1034318, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1c378da7-b30c-4d33-2887-67b75f368b00/rectcrop3", - "followers": 3, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6825e875bdd5ab1778f91cdf7aa3b3ec991af8a6", - "etherscanUrl": "https://etherscan.io/address/0x6825e875bdd5ab1778f91cdf7aa3b3ec991af8a6", - "xHandle": "makerdao2023", - "xUrl": "https://twitter.com/makerdao2023", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1130854", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "oleaden", - "displayName": "Oleaden", - "fid": 1130854, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/fff668dc-e930-4b40-f9bd-71f97ff7da00/original", - "followers": 2, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9c6e44f2d545137ddff8a0391f60695b0e3b8d4c", - "etherscanUrl": "https://etherscan.io/address/0x9c6e44f2d545137ddff8a0391f60695b0e3b8d4c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1447069", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cowboytommy", - "displayName": "cowboytommy", - "fid": 1447069, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/8ac3d53f-8c75-4249-99f8-dca5a4857000/original", - "followers": 2, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5d412b23ce29bf7aea99c617a3df2351608c1689", - "etherscanUrl": "https://etherscan.io/address/0x5d412b23ce29bf7aea99c617a3df2351608c1689", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1411799", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dubclub", - "displayName": "Dub Club", - "fid": 1411799, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/27fa97e2-b79a-4822-dfe8-6799966fc900/original", - "followers": 2, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf44797a6a6da88185981758afdea9e54104bc10a", - "etherscanUrl": "https://etherscan.io/address/0xf44797a6a6da88185981758afdea9e54104bc10a", - "xHandle": "dubclubtools", - "xUrl": "https://twitter.com/dubclubtools", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1074159", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mansueto", - "displayName": "Singay", - "fid": 1074159, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0866c9a8-00fb-4767-e8fa-5233fc663300/original", - "followers": 2, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa575632df1acad8d9d42149584746daba45f8b70", - "etherscanUrl": "https://etherscan.io/address/0xa575632df1acad8d9d42149584746daba45f8b70", - "xHandle": "singay1990", - "xUrl": "https://twitter.com/singay1990", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1181860", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "demydov", - "displayName": "MykhailoAwsm", - "fid": 1181860, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/53cfb839-1ec5-4557-96be-ba5df5160700/rectcrop3", - "followers": 2, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3e665597ca2387d82e168c614d5cb4c8fb0483e9", - "etherscanUrl": "https://etherscan.io/address/0x3e665597ca2387d82e168c614d5cb4c8fb0483e9", - "xHandle": "demidovmsh", - "xUrl": "https://twitter.com/demidovmsh", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1319826", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "boehner", - "displayName": "Boehner", - "fid": 1319826, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/fdee4fb5-8236-47f0-c513-fa48f05fd000/original", - "followers": 2, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x00f4ed4f2d02f0da30a646ce2ec09703efd77554", - "etherscanUrl": "https://etherscan.io/address/0x00f4ed4f2d02f0da30a646ce2ec09703efd77554", - "xHandle": "andrewboehner", - "xUrl": "https://twitter.com/andrewboehner", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1062174", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "ruelxynd", - "displayName": "Xbrox", - "fid": 1062174, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b23dac94-b9aa-4d9a-1f57-0e1979a2bc00/original", - "followers": 2, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xace3d54d12f502d99e171a28a0453779c4b9a863", - "etherscanUrl": "https://etherscan.io/address/0xace3d54d12f502d99e171a28a0453779c4b9a863", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1094938", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "bobjones", - "displayName": "Bob Jones", - "fid": 1094938, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/be8deecf-57c0-45e4-0124-f4f136e1a700/original", - "followers": 2, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7659dac1d9fdd653de41e7788194a3557e30353b", - "etherscanUrl": "https://etherscan.io/address/0x7659dac1d9fdd653de41e7788194a3557e30353b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_947464", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "xanthemelville", - "displayName": "XantheMelville", - "fid": 947464, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/59371d8b-5397-401c-caec-178cbcb9a900/original", - "followers": 2, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xeb7b1fcf889f3deb2102ca64699bb298a02f1a3e", - "etherscanUrl": "https://etherscan.io/address/0xeb7b1fcf889f3deb2102ca64699bb298a02f1a3e", - "xHandle": "ostralopitek88", - "xUrl": "https://twitter.com/ostralopitek88", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1030296", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "minokun662", - "displayName": "minokun662", - "fid": 1030296, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f3bb63f4-0079-49ca-c2ed-91bcd779c300/rectcrop3", - "followers": 2, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x73fe169af391f32e8dd16ad8bd6e58e4ddc9ee71", - "etherscanUrl": "https://etherscan.io/address/0x73fe169af391f32e8dd16ad8bd6e58e4ddc9ee71", - "xHandle": "makerdao2023", - "xUrl": "https://twitter.com/makerdao2023", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1428588", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "growlz", - "displayName": "growlz", - "fid": 1428588, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7c1d684d-f705-4cba-cbe1-303980c15500/original", - "followers": 1, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa2c603cef72ae0674b65586792bf650ae528d9a3", - "etherscanUrl": "https://etherscan.io/address/0xa2c603cef72ae0674b65586792bf650ae528d9a3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1368048", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "basedjpegapp", - "displayName": "basedjpegapp", - "fid": 1368048, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/608266c3-20ab-4a00-d1bc-135babd55100/original", - "followers": 1, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x161810de482e91d4cb11a21d22109200163a3ea2", - "etherscanUrl": "https://etherscan.io/address/0x161810de482e91d4cb11a21d22109200163a3ea2", - "xHandle": "based_jpegapp", - "xUrl": "https://twitter.com/based_jpegapp", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1122746", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "dikihs", - "displayName": "Diki Agung Setiawan", - "fid": 1122746, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5a2717bd-8a5e-4596-12ba-67e920d4f600/original", - "followers": 1, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf4e3d88a1f67522541cc5e7039900a2f9ae10271", - "etherscanUrl": "https://etherscan.io/address/0xf4e3d88a1f67522541cc5e7039900a2f9ae10271", - "xHandle": "ridwangoden", - "xUrl": "https://twitter.com/ridwangoden", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1153406", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "totocrypto00", - "displayName": "Toto头头crypto", - "fid": 1153406, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/da4b2e0c-691a-4156-342c-612fe6008d00/original", - "followers": 1, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa2d0552b7b7e299e376f08338d6f1c9298eb3e23", - "etherscanUrl": "https://etherscan.io/address/0xa2d0552b7b7e299e376f08338d6f1c9298eb3e23", - "xHandle": "toto_crypto00", - "xUrl": "https://twitter.com/toto_crypto00", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1082329", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "etheria", - "displayName": "Etheria | Monaverse", - "fid": 1082329, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/e94bb192-8034-425c-a1e4-fbfb0a044600/original", - "followers": 1, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa4a8ea48224d9c72921047c78bfc99e27efa9dd4", - "etherscanUrl": "https://etherscan.io/address/0xa4a8ea48224d9c72921047c78bfc99e27efa9dd4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1074624", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "clanking", - "displayName": "KING", - "fid": 1074624, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a5116556-0ef1-472b-8dcf-724c6a038400/original", - "followers": 1, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x94bc3069238dc6eb83fd0a0918cb345c78d7ca8e", - "etherscanUrl": "https://etherscan.io/address/0x94bc3069238dc6eb83fd0a0918cb345c78d7ca8e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1046197", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "eliano", - "displayName": "eliano", - "fid": 1046197, - "pfpUrl": "https://gateway.pinata.cloud/ipfs/bafkreihnek7pmxf2gzbsuyxxftphik5dx45famufoxxsvblwsm5mkanzpa", - "followers": 1, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x71eb1b04c922e5ec477aadc102ef76e0b49895d1", - "etherscanUrl": "https://etherscan.io/address/0x71eb1b04c922e5ec477aadc102ef76e0b49895d1", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1034211", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "minokun030", - "displayName": "minokun030", - "fid": 1034211, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/fb5bd213-dce4-4aae-829e-bd22d0e0eb00/rectcrop3", - "followers": 1, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe7972151fa20e97cc1704069bd844fe770ec1ed9", - "etherscanUrl": "https://etherscan.io/address/0xe7972151fa20e97cc1704069bd844fe770ec1ed9", - "xHandle": "makerdao2023", - "xUrl": "https://twitter.com/makerdao2023", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_213289", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nericomio", - "displayName": "nericomio", - "fid": 213289, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1859d689-88f8-47ed-957e-3781789e7900/rectcrop3", - "followers": 0, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb6db973ba138e0b670ede3ea3e9ac5c4f642c4f0", - "etherscanUrl": "https://etherscan.io/address/0xb6db973ba138e0b670ede3ea3e9ac5c4f642c4f0", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1341953", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "lumaprotocol", - "displayName": "Luma Protocol", - "fid": 1341953, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3cc80af3-40c4-4ae9-4429-a6dae1131f00/original", - "followers": 0, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa36b1f8642f9cb6c32ddfe09add76bebf3c35fb9", - "etherscanUrl": "https://etherscan.io/address/0xa36b1f8642f9cb6c32ddfe09add76bebf3c35fb9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1006492", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "nguyencryp", - "displayName": "Nguyen", - "fid": 1006492, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/21b88345-f1e7-4f19-dc09-684bcc2e3400/rectcrop3", - "followers": 0, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa207581eaad68be8d7e6500d75f631227f664072", - "etherscanUrl": "https://etherscan.io/address/0xa207581eaad68be8d7e6500d75f631227f664072", - "xHandle": "nguyencryptoo", - "xUrl": "https://twitter.com/nguyencryptoo", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1111084", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "kong000", - "displayName": "wukong000", - "fid": 1111084, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5567fc3e-c6a7-4b6d-b410-a5c46554ab00/original", - "followers": 0, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xeff9e5b403aa90b8def0373bd3b11d361f2c8b8d", - "etherscanUrl": "https://etherscan.io/address/0xeff9e5b403aa90b8def0373bd3b11d361f2c8b8d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1102675", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "donnshim", - "displayName": "Signal Crypto", - "fid": 1102675, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/961a142c-1c48-4538-abe2-bb49bd450700/original", - "followers": 0, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5db0e3b839279fc1ea1c85d587ffbf2908aa6cb7", - "etherscanUrl": "https://etherscan.io/address/0x5db0e3b839279fc1ea1c85d587ffbf2908aa6cb7", - "xHandle": "donnshim", - "xUrl": "https://twitter.com/donnshim", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1086953", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "miquel1000", - "displayName": "MickeyS111", - "fid": 1086953, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6b2231be-cf3a-4f9a-a9b9-d23a0fbc2500/original", - "followers": 0, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9bee5ab67a6a52d6e9b3b375553a3cb0003259c6", - "etherscanUrl": "https://etherscan.io/address/0x9bee5ab67a6a52d6e9b3b375553a3cb0003259c6", - "xHandle": "mickeys111", - "xUrl": "https://twitter.com/mickeys111", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1089478", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "cryptonewbie22", - "displayName": "Steven", - "fid": 1089478, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/95e044eb-c3e1-47ca-ae1a-6cfce9f2ce00/original", - "followers": 0, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x61243c0bcf71ec21037fefbf447d2386bf5816c1", - "etherscanUrl": "https://etherscan.io/address/0x61243c0bcf71ec21037fefbf447d2386bf5816c1", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1065412", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "tedysdr09", - "displayName": "Tedy", - "fid": 1065412, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/320a8fc0-fa4c-452b-6cad-21f28a0f6000/original", - "followers": 0, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0096303dd04dee3d58e77d428e85d28c0ecf50ae", - "etherscanUrl": "https://etherscan.io/address/0x0096303dd04dee3d58e77d428e85d28c0ecf50ae", - "xHandle": "tedy_sdr", - "xUrl": "https://twitter.com/tedy_sdr", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1051240", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "emmanuelrich", - "displayName": "udo emmanuel", - "fid": 1051240, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1046693b-755c-4da7-4cae-a28150854b00/original", - "followers": 0, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x91a43a731a6a57221777315c845d7939d8f8af71", - "etherscanUrl": "https://etherscan.io/address/0x91a43a731a6a57221777315c845d7939d8f8af71", - "xHandle": "udoemmanuel2017", - "xUrl": "https://twitter.com/udoemmanuel2017", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1062525", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "mougakuin", - "displayName": "Mou", - "fid": 1062525, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3af7234e-d82c-4000-d8c5-1d448e6a7400/original", - "followers": 0, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2b087787ae4d0df2bb4c52e8a138768ca94f6de3", - "etherscanUrl": "https://etherscan.io/address/0x2b087787ae4d0df2bb4c52e8a138768ca94f6de3", - "xHandle": "xmeta_domains", - "xUrl": "https://twitter.com/xmeta_domains", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1045210", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "vitaeth", - "displayName": "Warp", - "fid": 1045210, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3e09aff5-7950-4151-e453-b79e057bd000/rectcrop3", - "followers": 0, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x816e3c83806c33ea981e91579c93c93ea1975c1c", - "etherscanUrl": "https://etherscan.io/address/0x816e3c83806c33ea981e91579c93c93ea1975c1c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1022195", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "iceclank", - "displayName": "Ice Clank", - "fid": 1022195, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0f03aafe-c011-4417-e70a-97144b6cd500/rectcrop3", - "followers": 0, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x58d953827e1c4167c0c989058e56867c53ec30f3", - "etherscanUrl": "https://etherscan.io/address/0x58d953827e1c4167c0c989058e56867c53ec30f3", - "xHandle": "clankice", - "xUrl": "https://twitter.com/clankice", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1044018", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "frankovacs", - "displayName": "fran", - "fid": 1044018, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d3619150-e86a-47ff-38fe-5b5837c15900/rectcrop3", - "followers": 0, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc30ffac4b356e5ef020422d1bf822bd77f29cbde", - "etherscanUrl": "https://etherscan.io/address/0xc30ffac4b356e5ef020422d1bf822bd77f29cbde", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1030968", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "minokun667", - "displayName": "minokun667", - "fid": 1030968, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d10a06bd-df5b-4779-266d-af8684801400/rectcrop3", - "followers": 0, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9922bf1cd975c4835a4aaa43101b3ae634d396f0", - "etherscanUrl": "https://etherscan.io/address/0x9922bf1cd975c4835a4aaa43101b3ae634d396f0", - "xHandle": "makerdao2023", - "xUrl": "https://twitter.com/makerdao2023", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1043217", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "horseinme", - "displayName": "BasedTitcoin", - "fid": 1043217, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/132396f2-8046-4684-691f-f7eb4449ae00/rectcrop3", - "followers": 0, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9ebe852bc2c7b2fe6968d034656cb50dbbc6a038", - "etherscanUrl": "https://etherscan.io/address/0x9ebe852bc2c7b2fe6968d034656cb50dbbc6a038", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1034494", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "yuyulll1", - "displayName": "yuyulll1", - "fid": 1034494, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c15b4533-592f-42a4-a850-141f6643e300/rectcrop3", - "followers": 0, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x592365e90c50ca9f52c7fd8a4c4b48f592d79e2e", - "etherscanUrl": "https://etherscan.io/address/0x592365e90c50ca9f52c7fd8a4c4b48f592d79e2e", - "xHandle": "makerdao2023", - "xUrl": "https://twitter.com/makerdao2023", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1010713", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "brubino.eth", - "displayName": "Beppe Rubino", - "fid": 1010713, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5562f47d-1c9f-48fb-edf8-834f38a93700/rectcrop3", - "followers": 0, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xafd8d4a1c9fb949b6b486571a52b16d45ece3517", - "etherscanUrl": "https://etherscan.io/address/0xafd8d4a1c9fb949b6b486571a52b16d45ece3517", - "xHandle": "brubino73", - "xUrl": "https://twitter.com/brubino73", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1033828", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "minokun019", - "displayName": "minokun019", - "fid": 1033828, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3fadf7e0-2b2b-40de-330b-0c67b6c7f200/rectcrop3", - "followers": 0, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf255e53b3fb96a8d1572673c4a791d701bb9113f", - "etherscanUrl": "https://etherscan.io/address/0xf255e53b3fb96a8d1572673c4a791d701bb9113f", - "xHandle": "makerdao2023", - "xUrl": "https://twitter.com/makerdao2023", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1030698", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "minokun665", - "displayName": "minokun665", - "fid": 1030698, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/06ea30b4-b77a-407e-e277-f063a4caec00/rectcrop3", - "followers": 0, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1891a16e591697eaf37f465052674620cc275053", - "etherscanUrl": "https://etherscan.io/address/0x1891a16e591697eaf37f465052674620cc275053", - "xHandle": "makerdao2023", - "xUrl": "https://twitter.com/makerdao2023", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1032091", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "minokun669", - "displayName": "minokun669", - "fid": 1032091, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/32a0072d-3189-4f0c-5a87-685e02e16800/rectcrop3", - "followers": 0, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x40883fe7813e9cb500ff18d25d72b93b94cc404d", - "etherscanUrl": "https://etherscan.io/address/0x40883fe7813e9cb500ff18d25d72b93b94cc404d", - "xHandle": "makerdao2023", - "xUrl": "https://twitter.com/makerdao2023", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1032668", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "minokun404", - "displayName": "minokun404", - "fid": 1032668, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/4c4ea000-ae00-483c-38eb-0a80cd8deb00/rectcrop3", - "followers": 0, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc2a412d3ef21007c96172b81b584f3ac567aef46", - "etherscanUrl": "https://etherscan.io/address/0xc2a412d3ef21007c96172b81b584f3ac567aef46", - "xHandle": "makerdao2023", - "xUrl": "https://twitter.com/makerdao2023", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1032369", - "balanceRaw": "0", - "balanceFormatted": "", - "username": "minokun402", - "displayName": "minokun402", - "fid": 1032369, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b70f5d0f-fb03-4127-4894-bc792c781600/rectcrop3", - "followers": 0, - "lastTransferAt": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf605f69625b04dbe25686683299dc283ccd29832", - "etherscanUrl": "https://etherscan.io/address/0xf605f69625b04dbe25686683299dc283ccd29832", - "xHandle": "makerdao2023", - "xUrl": "https://twitter.com/makerdao2023", - "githubHandle": null, - "githubUrl": null - } - ], - "tokenSymbol": null, - "tokenDecimals": null, - "lastUpdatedTimestamp": "2025-11-17T21:47:14.470Z", - "lastFetchSettings": { - "source": "farcasterChannel", - "channelName": "clanker", - "channelFilter": "all" - } -} diff --git a/src/config/clanker/initialSpaces/exploreTabs/clanker.json b/src/config/clanker/initialSpaces/exploreTabs/clanker.json deleted file mode 100644 index ab0937188..000000000 --- a/src/config/clanker/initialSpaces/exploreTabs/clanker.json +++ /dev/null @@ -1,18918 +0,0 @@ -{ - "members": [ - { - "address": "0xc1a6fbedae68e1472dbb91fe29b51f7a0bd44f97", - "balanceRaw": "56317028663018893878469", - "balanceFormatted": "56317.028663018893878469", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc1a6fbedae68e1472dbb91fe29b51f7a0bd44f97", - "etherscanUrl": "https://etherscan.io/address/0xc1a6fbedae68e1472dbb91fe29b51f7a0bd44f97", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc983bf6bc9d7062a879b47b5dcf12caa76761324", - "balanceRaw": "45465504128586018863760", - "balanceFormatted": "45465.50412858601886376", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc983bf6bc9d7062a879b47b5dcf12caa76761324", - "etherscanUrl": "https://etherscan.io/address/0xc983bf6bc9d7062a879b47b5dcf12caa76761324", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x60787d9820984dab3bfbf5ad0ed5152444a56f48", - "balanceRaw": "33301751307740739128373", - "balanceFormatted": "33301.751307740739128373", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x60787d9820984dab3bfbf5ad0ed5152444a56f48", - "etherscanUrl": "https://etherscan.io/address/0x60787d9820984dab3bfbf5ad0ed5152444a56f48", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xffa8db7b38579e6a2d14f9b347a9ace4d044cd54", - "balanceRaw": "27973505644780000000000", - "balanceFormatted": "27973.50564478", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xffa8db7b38579e6a2d14f9b347a9ace4d044cd54", - "etherscanUrl": "https://etherscan.io/address/0xffa8db7b38579e6a2d14f9b347a9ace4d044cd54", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x301f742573c76ee107cb8e263f2d5c009ecbfb28", - "balanceRaw": "25111107960689975853613", - "balanceFormatted": "25111.107960689975853613", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x301f742573c76ee107cb8e263f2d5c009ecbfb28", - "etherscanUrl": "https://etherscan.io/address/0x301f742573c76ee107cb8e263f2d5c009ecbfb28", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3c716d252b53891753de883894ff29a9ebee0f54", - "balanceRaw": "19965418590000000000000", - "balanceFormatted": "19965.41859", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3c716d252b53891753de883894ff29a9ebee0f54", - "etherscanUrl": "https://etherscan.io/address/0x3c716d252b53891753de883894ff29a9ebee0f54", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x351a910d4a853870e23bfd7761ee720a43ddb889", - "balanceRaw": "18499420695598908776867", - "balanceFormatted": "18499.420695598908776867", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x351a910d4a853870e23bfd7761ee720a43ddb889", - "etherscanUrl": "https://etherscan.io/address/0x351a910d4a853870e23bfd7761ee720a43ddb889", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x73d8bd54f7cf5fab43fe4ef40a62d390644946db", - "balanceRaw": "17632494960921718683375", - "balanceFormatted": "17632.494960921718683375", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x73d8bd54f7cf5fab43fe4ef40a62d390644946db", - "etherscanUrl": "https://etherscan.io/address/0x73d8bd54f7cf5fab43fe4ef40a62d390644946db", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8d4ab2a3e89eadfdc729204adf863a0bfc7746f6", - "balanceRaw": "15519819463824622498812", - "balanceFormatted": "15519.819463824622498812", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8d4ab2a3e89eadfdc729204adf863a0bfc7746f6", - "etherscanUrl": "https://etherscan.io/address/0x8d4ab2a3e89eadfdc729204adf863a0bfc7746f6", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xada74cabf4716d83e1771a3ca427fecc9be8901e", - "balanceRaw": "15040991805940295170152", - "balanceFormatted": "15040.991805940295170152", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xada74cabf4716d83e1771a3ca427fecc9be8901e", - "etherscanUrl": "https://etherscan.io/address/0xada74cabf4716d83e1771a3ca427fecc9be8901e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x4e3ae00e8323558fa5cac04b152238924aa31b60", - "balanceRaw": "13563381171433224107039", - "balanceFormatted": "13563.381171433224107039", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4e3ae00e8323558fa5cac04b152238924aa31b60", - "etherscanUrl": "https://etherscan.io/address/0x4e3ae00e8323558fa5cac04b152238924aa31b60", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc024d81e80b010b7f941aca53e9d2af7f588d5fc", - "balanceRaw": "13093683024644407894498", - "balanceFormatted": "13093.683024644407894498", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc024d81e80b010b7f941aca53e9d2af7f588d5fc", - "etherscanUrl": "https://etherscan.io/address/0xc024d81e80b010b7f941aca53e9d2af7f588d5fc", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x2cde9919e81b20b4b33dd562a48a84b54c48f00c", - "balanceRaw": "13000000000000000000000", - "balanceFormatted": "13000", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2cde9919e81b20b4b33dd562a48a84b54c48f00c", - "etherscanUrl": "https://etherscan.io/address/0x2cde9919e81b20b4b33dd562a48a84b54c48f00c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0d0707963952f2fba59dd06f2b425ace40b492fe", - "balanceRaw": "10391176962313534157568", - "balanceFormatted": "10391.176962313534157568", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0d0707963952f2fba59dd06f2b425ace40b492fe", - "etherscanUrl": "https://etherscan.io/address/0x0d0707963952f2fba59dd06f2b425ace40b492fe", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x58918db69302e21e0478c97f20de9153b9ee92d6", - "balanceRaw": "10039028152250400714088", - "balanceFormatted": "10039.028152250400714088", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x58918db69302e21e0478c97f20de9153b9ee92d6", - "etherscanUrl": "https://etherscan.io/address/0x58918db69302e21e0478c97f20de9153b9ee92d6", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x916be95459f406ed8a8a4b4b957c6c0d349d523f", - "balanceRaw": "10000824480988407844846", - "balanceFormatted": "10000.824480988407844846", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x916be95459f406ed8a8a4b4b957c6c0d349d523f", - "etherscanUrl": "https://etherscan.io/address/0x916be95459f406ed8a8a4b4b957c6c0d349d523f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd2f2c63961f63b16082e275e9ba49a474633e672", - "balanceRaw": "9301147025274116379515", - "balanceFormatted": "9301.147025274116379515", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd2f2c63961f63b16082e275e9ba49a474633e672", - "etherscanUrl": "https://etherscan.io/address/0xd2f2c63961f63b16082e275e9ba49a474633e672", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6f92fa1f814cadad5d6510af617b420d6be7af05", - "balanceRaw": "9244725609162634700419", - "balanceFormatted": "9244.725609162634700419", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6f92fa1f814cadad5d6510af617b420d6be7af05", - "etherscanUrl": "https://etherscan.io/address/0x6f92fa1f814cadad5d6510af617b420d6be7af05", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x28b7c3553461a726b70d73dc46b2db715e36583a", - "balanceRaw": "8610407980341901303029", - "balanceFormatted": "8610.407980341901303029", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x28b7c3553461a726b70d73dc46b2db715e36583a", - "etherscanUrl": "https://etherscan.io/address/0x28b7c3553461a726b70d73dc46b2db715e36583a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf855aa266f7106a1765c1349c434fbca2fb71af3", - "balanceRaw": "8341373477136586680813", - "balanceFormatted": "8341.373477136586680813", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf855aa266f7106a1765c1349c434fbca2fb71af3", - "etherscanUrl": "https://etherscan.io/address/0xf855aa266f7106a1765c1349c434fbca2fb71af3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd23fe2db317e1a96454a2d1c7e8fc0dbf19bb000", - "balanceRaw": "8053371746816659513061", - "balanceFormatted": "8053.371746816659513061", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd23fe2db317e1a96454a2d1c7e8fc0dbf19bb000", - "etherscanUrl": "https://etherscan.io/address/0xd23fe2db317e1a96454a2d1c7e8fc0dbf19bb000", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5deb0821cfa27fc71708b8807d572ac9c7816a19", - "balanceRaw": "7640706311840565959900", - "balanceFormatted": "7640.7063118405659599", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5deb0821cfa27fc71708b8807d572ac9c7816a19", - "etherscanUrl": "https://etherscan.io/address/0x5deb0821cfa27fc71708b8807d572ac9c7816a19", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x1c887f42cfdecf8a250d96ead42d2c105851f80f", - "balanceRaw": "7575683300282082491293", - "balanceFormatted": "7575.683300282082491293", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1c887f42cfdecf8a250d96ead42d2c105851f80f", - "etherscanUrl": "https://etherscan.io/address/0x1c887f42cfdecf8a250d96ead42d2c105851f80f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc0dd7ac8113fdad11a68a2544e57ef30ccdcbbcb", - "balanceRaw": "7149391062156327430000", - "balanceFormatted": "7149.39106215632743", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc0dd7ac8113fdad11a68a2544e57ef30ccdcbbcb", - "etherscanUrl": "https://etherscan.io/address/0xc0dd7ac8113fdad11a68a2544e57ef30ccdcbbcb", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc314d77cab5f7c62845c36d5e28ddeea0e94aa03", - "balanceRaw": "7149391062156327430000", - "balanceFormatted": "7149.39106215632743", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc314d77cab5f7c62845c36d5e28ddeea0e94aa03", - "etherscanUrl": "https://etherscan.io/address/0xc314d77cab5f7c62845c36d5e28ddeea0e94aa03", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd544799e6514d9568ee7ee7c7362307bcb102f64", - "balanceRaw": "7149391062156327430000", - "balanceFormatted": "7149.39106215632743", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd544799e6514d9568ee7ee7c7362307bcb102f64", - "etherscanUrl": "https://etherscan.io/address/0xd544799e6514d9568ee7ee7c7362307bcb102f64", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf136f91c66ad1161fe03b3325dcc30eeb5edb02e", - "balanceRaw": "7149391062156327430000", - "balanceFormatted": "7149.39106215632743", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf136f91c66ad1161fe03b3325dcc30eeb5edb02e", - "etherscanUrl": "https://etherscan.io/address/0xf136f91c66ad1161fe03b3325dcc30eeb5edb02e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3fda366788bffd025774f23300fd64c5a5e70d25", - "balanceRaw": "6939614327160116839200", - "balanceFormatted": "6939.6143271601168392", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3fda366788bffd025774f23300fd64c5a5e70d25", - "etherscanUrl": "https://etherscan.io/address/0x3fda366788bffd025774f23300fd64c5a5e70d25", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5e40b238289bb0bcf59a71639f3e21729e297964", - "balanceRaw": "6654468437111516198740", - "balanceFormatted": "6654.46843711151619874", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5e40b238289bb0bcf59a71639f3e21729e297964", - "etherscanUrl": "https://etherscan.io/address/0x5e40b238289bb0bcf59a71639f3e21729e297964", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x498581ff718922c3f8e6a244956af099b2652b2b", - "balanceRaw": "6257045038829559072380", - "balanceFormatted": "6257.04503882955907238", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x498581ff718922c3f8e6a244956af099b2652b2b", - "etherscanUrl": "https://etherscan.io/address/0x498581ff718922c3f8e6a244956af099b2652b2b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0e86aaf378c4082c1ede2448b0f30c24b1e0e5fc", - "balanceRaw": "5840073138193857183753", - "balanceFormatted": "5840.073138193857183753", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0e86aaf378c4082c1ede2448b0f30c24b1e0e5fc", - "etherscanUrl": "https://etherscan.io/address/0x0e86aaf378c4082c1ede2448b0f30c24b1e0e5fc", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf2043109af60d2e1e9a0321fd8d6ec1f28baa727", - "balanceRaw": "5651624636362457748258", - "balanceFormatted": "5651.624636362457748258", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf2043109af60d2e1e9a0321fd8d6ec1f28baa727", - "etherscanUrl": "https://etherscan.io/address/0xf2043109af60d2e1e9a0321fd8d6ec1f28baa727", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x892557865fead1a207b46bb8d7c8dc6b6d149faa", - "balanceRaw": "5079724202461255704173", - "balanceFormatted": "5079.724202461255704173", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x892557865fead1a207b46bb8d7c8dc6b6d149faa", - "etherscanUrl": "https://etherscan.io/address/0x892557865fead1a207b46bb8d7c8dc6b6d149faa", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x009913a28a9ac6c881f05c992b697f76ef526bbc", - "balanceRaw": "5009749224510100234356", - "balanceFormatted": "5009.749224510100234356", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x009913a28a9ac6c881f05c992b697f76ef526bbc", - "etherscanUrl": "https://etherscan.io/address/0x009913a28a9ac6c881f05c992b697f76ef526bbc", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xaba8a38902c9837f89e287131c5e0ee8951ea73a", - "balanceRaw": "5000000000000000000000", - "balanceFormatted": "5000", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xaba8a38902c9837f89e287131c5e0ee8951ea73a", - "etherscanUrl": "https://etherscan.io/address/0xaba8a38902c9837f89e287131c5e0ee8951ea73a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7dafba1d69f6c01ae7567ffd7b046ca03b706f83", - "balanceRaw": "4505048845000000000000", - "balanceFormatted": "4505.048845", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7dafba1d69f6c01ae7567ffd7b046ca03b706f83", - "etherscanUrl": "https://etherscan.io/address/0x7dafba1d69f6c01ae7567ffd7b046ca03b706f83", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x20fe51a9229eef2cf8ad9e89d91cab9312cf3b7a", - "balanceRaw": "4473454965645866920794", - "balanceFormatted": "4473.454965645866920794", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x20fe51a9229eef2cf8ad9e89d91cab9312cf3b7a", - "etherscanUrl": "https://etherscan.io/address/0x20fe51a9229eef2cf8ad9e89d91cab9312cf3b7a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x2933782b5a8d72f2754103d1489614f29bfa4625", - "balanceRaw": "4309229874917509205981", - "balanceFormatted": "4309.229874917509205981", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2933782b5a8d72f2754103d1489614f29bfa4625", - "etherscanUrl": "https://etherscan.io/address/0x2933782b5a8d72f2754103d1489614f29bfa4625", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_3036", - "balanceRaw": "4196630273020396143351", - "balanceFormatted": "4196.630273020396143351", - "lastTransferAt": null, - "username": "dwayne", - "displayName": "Dwayne 'The Jock' Ronson", - "fid": 3036, - "followers": 5825, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/322bc667-a666-4670-d36c-cce86689ef00/rectcrop3", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8f75005973b6395f82e2026af37e9d6864cd0798", - "etherscanUrl": "https://etherscan.io/address/0x8f75005973b6395f82e2026af37e9d6864cd0798", - "xHandle": "dwaynetjr", - "xUrl": "https://twitter.com/dwaynetjr", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf60a605eda07952f691a1d86171a06539c931988", - "balanceRaw": "4106565890000000000000", - "balanceFormatted": "4106.56589", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf60a605eda07952f691a1d86171a06539c931988", - "etherscanUrl": "https://etherscan.io/address/0xf60a605eda07952f691a1d86171a06539c931988", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xdc163e65275368b3145a1a386737d0cd6127c1e2", - "balanceRaw": "4073646871070280951559", - "balanceFormatted": "4073.646871070280951559", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdc163e65275368b3145a1a386737d0cd6127c1e2", - "etherscanUrl": "https://etherscan.io/address/0xdc163e65275368b3145a1a386737d0cd6127c1e2", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf8191d98ae98d2f7abdfb63a9b0b812b93c873aa", - "balanceRaw": "4032087925000000000000", - "balanceFormatted": "4032.087925", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf8191d98ae98d2f7abdfb63a9b0b812b93c873aa", - "etherscanUrl": "https://etherscan.io/address/0xf8191d98ae98d2f7abdfb63a9b0b812b93c873aa", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xfcd65cf309493762135d370fdbb1ca10b1ec74a2", - "balanceRaw": "4023338931765799317974", - "balanceFormatted": "4023.338931765799317974", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfcd65cf309493762135d370fdbb1ca10b1ec74a2", - "etherscanUrl": "https://etherscan.io/address/0xfcd65cf309493762135d370fdbb1ca10b1ec74a2", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb46ddd09146cf8c59c275f4cf277e8de6868f838", - "balanceRaw": "4000000000000000000000", - "balanceFormatted": "4000", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb46ddd09146cf8c59c275f4cf277e8de6868f838", - "etherscanUrl": "https://etherscan.io/address/0xb46ddd09146cf8c59c275f4cf277e8de6868f838", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf131022f41c0d95378a550397f5f1af47cd5900e", - "balanceRaw": "4000000000000000000000", - "balanceFormatted": "4000", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf131022f41c0d95378a550397f5f1af47cd5900e", - "etherscanUrl": "https://etherscan.io/address/0xf131022f41c0d95378a550397f5f1af47cd5900e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5661f2740e55c325e5a219cd537bc1fb954e1c81", - "balanceRaw": "3944377692008478371801", - "balanceFormatted": "3944.377692008478371801", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5661f2740e55c325e5a219cd537bc1fb954e1c81", - "etherscanUrl": "https://etherscan.io/address/0x5661f2740e55c325e5a219cd537bc1fb954e1c81", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xaa8fc25fda37281f07ebb013030ad5070597e54d", - "balanceRaw": "3877917124413381356337", - "balanceFormatted": "3877.917124413381356337", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xaa8fc25fda37281f07ebb013030ad5070597e54d", - "etherscanUrl": "https://etherscan.io/address/0xaa8fc25fda37281f07ebb013030ad5070597e54d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0aa3f4f5d7b5769c1f1d6de2dcab56e431bc51d3", - "balanceRaw": "3826416178479899791681", - "balanceFormatted": "3826.416178479899791681", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0aa3f4f5d7b5769c1f1d6de2dcab56e431bc51d3", - "etherscanUrl": "https://etherscan.io/address/0x0aa3f4f5d7b5769c1f1d6de2dcab56e431bc51d3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe1673847ce40765cf3687fa7eccab0f8ac878e47", - "balanceRaw": "3792117990868523330774", - "balanceFormatted": "3792.117990868523330774", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe1673847ce40765cf3687fa7eccab0f8ac878e47", - "etherscanUrl": "https://etherscan.io/address/0xe1673847ce40765cf3687fa7eccab0f8ac878e47", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5d2bb9e5a29657935010fdf2a911e957d51b6c51", - "balanceRaw": "3777042648039074490577", - "balanceFormatted": "3777.042648039074490577", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5d2bb9e5a29657935010fdf2a911e957d51b6c51", - "etherscanUrl": "https://etherscan.io/address/0x5d2bb9e5a29657935010fdf2a911e957d51b6c51", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xed0526c047edecbcbd9cd7531c2ea86cf9566f5d", - "balanceRaw": "3734709247967221641564", - "balanceFormatted": "3734.709247967221641564", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xed0526c047edecbcbd9cd7531c2ea86cf9566f5d", - "etherscanUrl": "https://etherscan.io/address/0xed0526c047edecbcbd9cd7531c2ea86cf9566f5d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd3e0341b361134014e0c89378b3e36bc5020cd97", - "balanceRaw": "3676387158399862425919", - "balanceFormatted": "3676.387158399862425919", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd3e0341b361134014e0c89378b3e36bc5020cd97", - "etherscanUrl": "https://etherscan.io/address/0xd3e0341b361134014e0c89378b3e36bc5020cd97", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x056c0ace599ff4b6a5f3e252794c541c05812be6", - "balanceRaw": "3636858116964210804166", - "balanceFormatted": "3636.858116964210804166", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x056c0ace599ff4b6a5f3e252794c541c05812be6", - "etherscanUrl": "https://etherscan.io/address/0x056c0ace599ff4b6a5f3e252794c541c05812be6", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa591d0b5fd0fc0fd8a0976c68298f6b28208e871", - "balanceRaw": "3454305283181306945899", - "balanceFormatted": "3454.305283181306945899", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa591d0b5fd0fc0fd8a0976c68298f6b28208e871", - "etherscanUrl": "https://etherscan.io/address/0xa591d0b5fd0fc0fd8a0976c68298f6b28208e871", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6455327f820edd69c4cd665b995e0fec679d7f9e", - "balanceRaw": "3383000000000000000000", - "balanceFormatted": "3383", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6455327f820edd69c4cd665b995e0fec679d7f9e", - "etherscanUrl": "https://etherscan.io/address/0x6455327f820edd69c4cd665b995e0fec679d7f9e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0646f27c9c7931e24260cfb6cc0beb3c71a136e8", - "balanceRaw": "3333365027364753326010", - "balanceFormatted": "3333.36502736475332601", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0646f27c9c7931e24260cfb6cc0beb3c71a136e8", - "etherscanUrl": "https://etherscan.io/address/0x0646f27c9c7931e24260cfb6cc0beb3c71a136e8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x18b0f4547a89fe4c5fe84f258bea3601fa281e9f", - "balanceRaw": "3191540716508088017571", - "balanceFormatted": "3191.540716508088017571", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x18b0f4547a89fe4c5fe84f258bea3601fa281e9f", - "etherscanUrl": "https://etherscan.io/address/0x18b0f4547a89fe4c5fe84f258bea3601fa281e9f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xec67ad9721f3856ec8c474032fd722122f91fdb8", - "balanceRaw": "3111318145735955095361", - "balanceFormatted": "3111.318145735955095361", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xec67ad9721f3856ec8c474032fd722122f91fdb8", - "etherscanUrl": "https://etherscan.io/address/0xec67ad9721f3856ec8c474032fd722122f91fdb8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x4767d1b638b0c5898f2ae9382ee8c7fd4883f857", - "balanceRaw": "3080137628014511140600", - "balanceFormatted": "3080.1376280145111406", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4767d1b638b0c5898f2ae9382ee8c7fd4883f857", - "etherscanUrl": "https://etherscan.io/address/0x4767d1b638b0c5898f2ae9382ee8c7fd4883f857", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbee3717cce30e95cc3f4e1a8b52e47f3b5f69d27", - "balanceRaw": "3000284314353323141168", - "balanceFormatted": "3000.284314353323141168", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbee3717cce30e95cc3f4e1a8b52e47f3b5f69d27", - "etherscanUrl": "https://etherscan.io/address/0xbee3717cce30e95cc3f4e1a8b52e47f3b5f69d27", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8a5adc20b7e17d6535e34ff31bc9549ac2771286", - "balanceRaw": "2987737809687290003447", - "balanceFormatted": "2987.737809687290003447", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8a5adc20b7e17d6535e34ff31bc9549ac2771286", - "etherscanUrl": "https://etherscan.io/address/0x8a5adc20b7e17d6535e34ff31bc9549ac2771286", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd0bb0c1972a1e6585525e1c4267d583b5fca7de4", - "balanceRaw": "2955009627482435464497", - "balanceFormatted": "2955.009627482435464497", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd0bb0c1972a1e6585525e1c4267d583b5fca7de4", - "etherscanUrl": "https://etherscan.io/address/0xd0bb0c1972a1e6585525e1c4267d583b5fca7de4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x50d5d7ae0d61d1de5ebbe33e9f7e5efbb5d091a0", - "balanceRaw": "2945029656217627699406", - "balanceFormatted": "2945.029656217627699406", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x50d5d7ae0d61d1de5ebbe33e9f7e5efbb5d091a0", - "etherscanUrl": "https://etherscan.io/address/0x50d5d7ae0d61d1de5ebbe33e9f7e5efbb5d091a0", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1015735", - "balanceRaw": "2903638033979413440318", - "balanceFormatted": "2903.638033979413440318", - "lastTransferAt": null, - "username": "theneetguy.eth", - "displayName": "The Neet Guy", - "fid": 1015735, - "followers": 547, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/820c1a61-94b0-4ad0-b74f-eb2066a82c00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9db704bae6a8f4582cd79b3a6eb88211aad936b3", - "etherscanUrl": "https://etherscan.io/address/0x9db704bae6a8f4582cd79b3a6eb88211aad936b3", - "xHandle": "theneetguy", - "xUrl": "https://twitter.com/theneetguy", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6ca37e08760ed7acd4d1d4d88918d3aa1b8e5618", - "balanceRaw": "2779576093804788696557", - "balanceFormatted": "2779.576093804788696557", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6ca37e08760ed7acd4d1d4d88918d3aa1b8e5618", - "etherscanUrl": "https://etherscan.io/address/0x6ca37e08760ed7acd4d1d4d88918d3aa1b8e5618", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xcb52f68714c5a09a4c36e9a73647270c504499f7", - "balanceRaw": "2750000000000000000000", - "balanceFormatted": "2750", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcb52f68714c5a09a4c36e9a73647270c504499f7", - "etherscanUrl": "https://etherscan.io/address/0xcb52f68714c5a09a4c36e9a73647270c504499f7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbc90bd26b6f69b7b9b7698ba3c4b2c7f76d710af", - "balanceRaw": "2727099154263089917818", - "balanceFormatted": "2727.099154263089917818", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbc90bd26b6f69b7b9b7698ba3c4b2c7f76d710af", - "etherscanUrl": "https://etherscan.io/address/0xbc90bd26b6f69b7b9b7698ba3c4b2c7f76d710af", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_6208", - "balanceRaw": "2714745904252595926241", - "balanceFormatted": "2714.745904252595926241", - "lastTransferAt": null, - "username": "wwarren", - "displayName": "Will Warren", - "fid": 6208, - "followers": 4532, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3ac8eae9-6273-46c2-1579-87092e894000/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3c0e301fca17ffe6f5f97fd4719a616004b6050a", - "etherscanUrl": "https://etherscan.io/address/0x3c0e301fca17ffe6f5f97fd4719a616004b6050a", - "xHandle": "willwarren", - "xUrl": "https://twitter.com/willwarren", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_16512", - "balanceRaw": "2708194202977082875727", - "balanceFormatted": "2708.194202977082875727", - "lastTransferAt": null, - "username": "0xbamboostrong", - "displayName": "bamboostrong", - "fid": 16512, - "followers": 1722, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7dff3ec1-191f-4c8e-a84d-d562c2e69d00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x133bd0684e97d0a46b9adffb4af5de36c92a9256", - "etherscanUrl": "https://etherscan.io/address/0x133bd0684e97d0a46b9adffb4af5de36c92a9256", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0ae0b5594da29f0d3e08bbcdf007579d3bb5fd52", - "balanceRaw": "2624539898035786916303", - "balanceFormatted": "2624.539898035786916303", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0ae0b5594da29f0d3e08bbcdf007579d3bb5fd52", - "etherscanUrl": "https://etherscan.io/address/0x0ae0b5594da29f0d3e08bbcdf007579d3bb5fd52", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0112ecdc00582fd622b0ae8b2c55001727f97946", - "balanceRaw": "2572636798312073373085", - "balanceFormatted": "2572.636798312073373085", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0112ecdc00582fd622b0ae8b2c55001727f97946", - "etherscanUrl": "https://etherscan.io/address/0x0112ecdc00582fd622b0ae8b2c55001727f97946", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x97b9d2102a9a65a26e1ee82d59e42d1b73b68689", - "balanceRaw": "2535354456969673000000", - "balanceFormatted": "2535.354456969673", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x97b9d2102a9a65a26e1ee82d59e42d1b73b68689", - "etherscanUrl": "https://etherscan.io/address/0x97b9d2102a9a65a26e1ee82d59e42d1b73b68689", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd17e7602be8f78aeb62d769286784d6dd9a56742", - "balanceRaw": "2530656722100919418073", - "balanceFormatted": "2530.656722100919418073", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd17e7602be8f78aeb62d769286784d6dd9a56742", - "etherscanUrl": "https://etherscan.io/address/0xd17e7602be8f78aeb62d769286784d6dd9a56742", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb11f0625dcfb71d8095f019aecf1815ca5746776", - "balanceRaw": "2381032432271135067606", - "balanceFormatted": "2381.032432271135067606", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb11f0625dcfb71d8095f019aecf1815ca5746776", - "etherscanUrl": "https://etherscan.io/address/0xb11f0625dcfb71d8095f019aecf1815ca5746776", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe907b2f99e995926db9d3e0d307454c0a94ea220", - "balanceRaw": "2373598016647386713345", - "balanceFormatted": "2373.598016647386713345", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe907b2f99e995926db9d3e0d307454c0a94ea220", - "etherscanUrl": "https://etherscan.io/address/0xe907b2f99e995926db9d3e0d307454c0a94ea220", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_930130", - "balanceRaw": "2371674303209009140903", - "balanceFormatted": "2371.674303209009140903", - "lastTransferAt": null, - "username": "bananabob", - "displayName": "Geth", - "fid": 930130, - "followers": 458, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/42383698-4bf4-4e4d-3b8c-5053ebe24800/rectcrop3", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x938ce09a1f21123d3d8af975525238313d0220a7", - "etherscanUrl": "https://etherscan.io/address/0x938ce09a1f21123d3d8af975525238313d0220a7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x46162f7fadd5f3245300a1986ab4610666c57f65", - "balanceRaw": "2332933859093269838133", - "balanceFormatted": "2332.933859093269838133", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x46162f7fadd5f3245300a1986ab4610666c57f65", - "etherscanUrl": "https://etherscan.io/address/0x46162f7fadd5f3245300a1986ab4610666c57f65", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd13da05b9288ba4961973110594bd0fe3428791f", - "balanceRaw": "2303842119855392014535", - "balanceFormatted": "2303.842119855392014535", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd13da05b9288ba4961973110594bd0fe3428791f", - "etherscanUrl": "https://etherscan.io/address/0xd13da05b9288ba4961973110594bd0fe3428791f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x30308bf960bea8167146c86696fa98f70f944e2c", - "balanceRaw": "2298009522903461053295", - "balanceFormatted": "2298.009522903461053295", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x30308bf960bea8167146c86696fa98f70f944e2c", - "etherscanUrl": "https://etherscan.io/address/0x30308bf960bea8167146c86696fa98f70f944e2c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x14a0d8a5b419be59304bb5463339aacd37a5e635", - "balanceRaw": "2250766372659690354078", - "balanceFormatted": "2250.766372659690354078", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x14a0d8a5b419be59304bb5463339aacd37a5e635", - "etherscanUrl": "https://etherscan.io/address/0x14a0d8a5b419be59304bb5463339aacd37a5e635", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xcb332a38a9770a99e0f986eb821f37190ad8cff3", - "balanceRaw": "2243269324726458031174", - "balanceFormatted": "2243.269324726458031174", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcb332a38a9770a99e0f986eb821f37190ad8cff3", - "etherscanUrl": "https://etherscan.io/address/0xcb332a38a9770a99e0f986eb821f37190ad8cff3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7e90590542e36df645696f161abdc526bfb6d247", - "balanceRaw": "2161157034526076518260", - "balanceFormatted": "2161.15703452607651826", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7e90590542e36df645696f161abdc526bfb6d247", - "etherscanUrl": "https://etherscan.io/address/0x7e90590542e36df645696f161abdc526bfb6d247", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xeac18ab911a688772ff832d0d8b06be381ef1bfd", - "balanceRaw": "2160000000000000000000", - "balanceFormatted": "2160", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xeac18ab911a688772ff832d0d8b06be381ef1bfd", - "etherscanUrl": "https://etherscan.io/address/0xeac18ab911a688772ff832d0d8b06be381ef1bfd", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x75fb62aa7d072a6a96692b207278a760e5df42cc", - "balanceRaw": "2113533707128123166740", - "balanceFormatted": "2113.53370712812316674", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x75fb62aa7d072a6a96692b207278a760e5df42cc", - "etherscanUrl": "https://etherscan.io/address/0x75fb62aa7d072a6a96692b207278a760e5df42cc", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x58da1b5811ee7b1a2797eca47a5cd52ec131228f", - "balanceRaw": "2044147611336621281898", - "balanceFormatted": "2044.147611336621281898", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x58da1b5811ee7b1a2797eca47a5cd52ec131228f", - "etherscanUrl": "https://etherscan.io/address/0x58da1b5811ee7b1a2797eca47a5cd52ec131228f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_9933", - "balanceRaw": "2035273564067190950420", - "balanceFormatted": "2035.27356406719095042", - "lastTransferAt": null, - "username": "m00npapi.eth", - "displayName": "m00npapi.eth", - "fid": 9933, - "followers": 22628, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1e8e6cb7-8fea-40f9-1dc7-47810f634700/original", - "ensName": "m00npapi.eth", - "ensAvatarUrl": null, - "primaryAddress": "0xbf4977f1295454cb46d95fe7d8e1d99e32d8aed1", - "etherscanUrl": "https://etherscan.io/address/0xbf4977f1295454cb46d95fe7d8e1d99e32d8aed1", - "xHandle": "m00npapi", - "xUrl": "https://twitter.com/m00npapi", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe6c1a7c3b84f1871c30d14aeb443b667ff71c410", - "balanceRaw": "2009051250748479691600", - "balanceFormatted": "2009.0512507484796916", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe6c1a7c3b84f1871c30d14aeb443b667ff71c410", - "etherscanUrl": "https://etherscan.io/address/0xe6c1a7c3b84f1871c30d14aeb443b667ff71c410", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x4f835c3bbe19b569fbfceabc42c95631f354b532", - "balanceRaw": "2000000000775671234149", - "balanceFormatted": "2000.000000775671234149", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4f835c3bbe19b569fbfceabc42c95631f354b532", - "etherscanUrl": "https://etherscan.io/address/0x4f835c3bbe19b569fbfceabc42c95631f354b532", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9824697f7c12cabada9b57842060931c48dea969", - "balanceRaw": "1999997245643576986856", - "balanceFormatted": "1999.997245643576986856", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9824697f7c12cabada9b57842060931c48dea969", - "etherscanUrl": "https://etherscan.io/address/0x9824697f7c12cabada9b57842060931c48dea969", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x2da9580235d1dc096919f5c231500e506351c55d", - "balanceRaw": "1992727177184776552674", - "balanceFormatted": "1992.727177184776552674", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2da9580235d1dc096919f5c231500e506351c55d", - "etherscanUrl": "https://etherscan.io/address/0x2da9580235d1dc096919f5c231500e506351c55d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb7333d779c6ecdfc4507a53706b0e173bd086a18", - "balanceRaw": "1923309287668906454528", - "balanceFormatted": "1923.309287668906454528", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb7333d779c6ecdfc4507a53706b0e173bd086a18", - "etherscanUrl": "https://etherscan.io/address/0xb7333d779c6ecdfc4507a53706b0e173bd086a18", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_987012", - "balanceRaw": "1814104724423308838077", - "balanceFormatted": "1814.104724423308838077", - "lastTransferAt": null, - "username": "rafflemaster", - "displayName": "Raffle Bot", - "fid": 987012, - "followers": 17, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/bcbd8e7e-ab5a-4a8f-0efa-4cf2705f8400/rectcrop3", - "ensName": "rafflemaster.eth", - "ensAvatarUrl": null, - "primaryAddress": "0xdf948a0ab481b952ade31cae4e2e348509a66c56", - "etherscanUrl": "https://etherscan.io/address/0xdf948a0ab481b952ade31cae4e2e348509a66c56", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1338", - "balanceRaw": "1799999760340202812579", - "balanceFormatted": "1799.999760340202812579", - "lastTransferAt": null, - "username": "bli.eth", - "displayName": "Brian Li 🍊👾", - "fid": 1338, - "followers": 5594, - "pfpUrl": "https://i.imgur.com/84EUSLS.jpg", - "ensName": "bli.eth", - "ensAvatarUrl": null, - "primaryAddress": "0x033707e9847c41515e337d16e01addf18cd9adc8", - "etherscanUrl": "https://etherscan.io/address/0x033707e9847c41515e337d16e01addf18cd9adc8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xdd3a68eb8324c3ff54f39fc4bca933d11812ca41", - "balanceRaw": "1782872192308190882077", - "balanceFormatted": "1782.872192308190882077", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdd3a68eb8324c3ff54f39fc4bca933d11812ca41", - "etherscanUrl": "https://etherscan.io/address/0xdd3a68eb8324c3ff54f39fc4bca933d11812ca41", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xef73c5e0cba2d03a7dd66012ca4375502ef6f7aa", - "balanceRaw": "1779864792618147056595", - "balanceFormatted": "1779.864792618147056595", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xef73c5e0cba2d03a7dd66012ca4375502ef6f7aa", - "etherscanUrl": "https://etherscan.io/address/0xef73c5e0cba2d03a7dd66012ca4375502ef6f7aa", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x44df085447dbebcf69c6675c3b8a795c7fdeb3f4", - "balanceRaw": "1776041150663203626767", - "balanceFormatted": "1776.041150663203626767", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x44df085447dbebcf69c6675c3b8a795c7fdeb3f4", - "etherscanUrl": "https://etherscan.io/address/0x44df085447dbebcf69c6675c3b8a795c7fdeb3f4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0019ce38fad6767c15f124591395f23ffff08412", - "balanceRaw": "1752585611759236007937", - "balanceFormatted": "1752.585611759236007937", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0019ce38fad6767c15f124591395f23ffff08412", - "etherscanUrl": "https://etherscan.io/address/0x0019ce38fad6767c15f124591395f23ffff08412", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_884391", - "balanceRaw": "1725213850118366982895", - "balanceFormatted": "1725.213850118366982895", - "lastTransferAt": null, - "username": "bluezingboing", - "displayName": "Alexandre Gagnon", - "fid": 884391, - "followers": 4, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/25769ed1-24ed-45a5-fec4-7d755e583d00/rectcrop3", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5688cbfd16d49c16d89f24f8787d264731ee1273", - "etherscanUrl": "https://etherscan.io/address/0x5688cbfd16d49c16d89f24f8787d264731ee1273", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd56de11dcc755b880633cd7ea73c9a8c215eb8b5", - "balanceRaw": "1724970931129266138215", - "balanceFormatted": "1724.970931129266138215", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd56de11dcc755b880633cd7ea73c9a8c215eb8b5", - "etherscanUrl": "https://etherscan.io/address/0xd56de11dcc755b880633cd7ea73c9a8c215eb8b5", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xdb3176bb2ba5d0004ed21787adaf563e7bd812d5", - "balanceRaw": "1702661065434925056411", - "balanceFormatted": "1702.661065434925056411", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdb3176bb2ba5d0004ed21787adaf563e7bd812d5", - "etherscanUrl": "https://etherscan.io/address/0xdb3176bb2ba5d0004ed21787adaf563e7bd812d5", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x95c54255e8e0c5b95d6ea16b97843ce00fce6153", - "balanceRaw": "1626311833554227154900", - "balanceFormatted": "1626.3118335542271549", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x95c54255e8e0c5b95d6ea16b97843ce00fce6153", - "etherscanUrl": "https://etherscan.io/address/0x95c54255e8e0c5b95d6ea16b97843ce00fce6153", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd929fce80152e986088af291b582890a6c6bc619", - "balanceRaw": "1620519994062597530604", - "balanceFormatted": "1620.519994062597530604", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd929fce80152e986088af291b582890a6c6bc619", - "etherscanUrl": "https://etherscan.io/address/0xd929fce80152e986088af291b582890a6c6bc619", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x53ea79b7339ccd0897f0882dc3ab8d4520c2c305", - "balanceRaw": "1608392964138100391471", - "balanceFormatted": "1608.392964138100391471", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x53ea79b7339ccd0897f0882dc3ab8d4520c2c305", - "etherscanUrl": "https://etherscan.io/address/0x53ea79b7339ccd0897f0882dc3ab8d4520c2c305", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x44551ca46fa5592bb572e20043f7c3d54c85cad7", - "balanceRaw": "1588270918788552981858", - "balanceFormatted": "1588.270918788552981858", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x44551ca46fa5592bb572e20043f7c3d54c85cad7", - "etherscanUrl": "https://etherscan.io/address/0x44551ca46fa5592bb572e20043f7c3d54c85cad7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9b1de5b0d8f033d171a1057333960afd66249d65", - "balanceRaw": "1570151296503123323944", - "balanceFormatted": "1570.151296503123323944", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9b1de5b0d8f033d171a1057333960afd66249d65", - "etherscanUrl": "https://etherscan.io/address/0x9b1de5b0d8f033d171a1057333960afd66249d65", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_444067", - "balanceRaw": "1568744376717373218039", - "balanceFormatted": "1568.744376717373218039", - "lastTransferAt": null, - "username": "macster", - "displayName": "Yonii🌸", - "fid": 444067, - "followers": 8361, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/77ea3c84-a38a-4091-528a-02e4afaafc00/rectcrop3", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8676cf3a5ca6e068eb586c948ca06c119eb38e03", - "etherscanUrl": "https://etherscan.io/address/0x8676cf3a5ca6e068eb586c948ca06c119eb38e03", - "xHandle": "goyagl90", - "xUrl": "https://twitter.com/goyagl90", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9266f125fb2ecb730d9953b46de9c32e2fa83e4a", - "balanceRaw": "1554045694932061424979", - "balanceFormatted": "1554.045694932061424979", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9266f125fb2ecb730d9953b46de9c32e2fa83e4a", - "etherscanUrl": "https://etherscan.io/address/0x9266f125fb2ecb730d9953b46de9c32e2fa83e4a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0637dbecb11094e1ed251fc612512c6ce1391711", - "balanceRaw": "1547025959792423248575", - "balanceFormatted": "1547.025959792423248575", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0637dbecb11094e1ed251fc612512c6ce1391711", - "etherscanUrl": "https://etherscan.io/address/0x0637dbecb11094e1ed251fc612512c6ce1391711", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x526c9e89f01bd507b07ea0bbdddce2e1b9a0c687", - "balanceRaw": "1525709588676633666293", - "balanceFormatted": "1525.709588676633666293", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x526c9e89f01bd507b07ea0bbdddce2e1b9a0c687", - "etherscanUrl": "https://etherscan.io/address/0x526c9e89f01bd507b07ea0bbdddce2e1b9a0c687", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x05427963cdfc15414adb87726f308dd84dc909d8", - "balanceRaw": "1522780570000000000000", - "balanceFormatted": "1522.78057", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x05427963cdfc15414adb87726f308dd84dc909d8", - "etherscanUrl": "https://etherscan.io/address/0x05427963cdfc15414adb87726f308dd84dc909d8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe674c81bdf132cf6e46be106c82b3ab7a4afd483", - "balanceRaw": "1504338660741786683734", - "balanceFormatted": "1504.338660741786683734", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe674c81bdf132cf6e46be106c82b3ab7a4afd483", - "etherscanUrl": "https://etherscan.io/address/0xe674c81bdf132cf6e46be106c82b3ab7a4afd483", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x264765a02e7596a75eabf098e5183cc426559ce3", - "balanceRaw": "1500000000000000000000", - "balanceFormatted": "1500", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x264765a02e7596a75eabf098e5183cc426559ce3", - "etherscanUrl": "https://etherscan.io/address/0x264765a02e7596a75eabf098e5183cc426559ce3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5b9d1af871a3fe9037ee425771cbec466f9c5454", - "balanceRaw": "1465674834491140325702", - "balanceFormatted": "1465.674834491140325702", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5b9d1af871a3fe9037ee425771cbec466f9c5454", - "etherscanUrl": "https://etherscan.io/address/0x5b9d1af871a3fe9037ee425771cbec466f9c5454", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xdfd27839961f9787cef4bdc31e45fe7aba858d26", - "balanceRaw": "1458182662760459762313", - "balanceFormatted": "1458.182662760459762313", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdfd27839961f9787cef4bdc31e45fe7aba858d26", - "etherscanUrl": "https://etherscan.io/address/0xdfd27839961f9787cef4bdc31e45fe7aba858d26", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc06156b98f04727ed5137b51a728509128d3bc03", - "balanceRaw": "1444926308477241408731", - "balanceFormatted": "1444.926308477241408731", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc06156b98f04727ed5137b51a728509128d3bc03", - "etherscanUrl": "https://etherscan.io/address/0xc06156b98f04727ed5137b51a728509128d3bc03", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x1985ea6e9c68e1c272d8209f3b478ac2fdb25c87", - "balanceRaw": "1430416001169130374397", - "balanceFormatted": "1430.416001169130374397", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1985ea6e9c68e1c272d8209f3b478ac2fdb25c87", - "etherscanUrl": "https://etherscan.io/address/0x1985ea6e9c68e1c272d8209f3b478ac2fdb25c87", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x10f16811b18aac6a9a85476297226ad8aa079af1", - "balanceRaw": "1425526067769582803628", - "balanceFormatted": "1425.526067769582803628", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x10f16811b18aac6a9a85476297226ad8aa079af1", - "etherscanUrl": "https://etherscan.io/address/0x10f16811b18aac6a9a85476297226ad8aa079af1", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc7d308be9097033204009b72d4079bbde1b1a019", - "balanceRaw": "1391523744115611970090", - "balanceFormatted": "1391.52374411561197009", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc7d308be9097033204009b72d4079bbde1b1a019", - "etherscanUrl": "https://etherscan.io/address/0xc7d308be9097033204009b72d4079bbde1b1a019", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_193909", - "balanceRaw": "1390245604853722514234", - "balanceFormatted": "1390.245604853722514234", - "lastTransferAt": null, - "username": "degen-agent", - "displayName": "degenGPT", - "fid": 193909, - "followers": 329, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5188fa0e-780e-440d-0d21-709a75f32200/rectcrop3", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe6d2525c3885eefc47308843dfa20de83650eb56", - "etherscanUrl": "https://etherscan.io/address/0xe6d2525c3885eefc47308843dfa20de83650eb56", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_2904", - "balanceRaw": "1378066718852397693760", - "balanceFormatted": "1378.06671885239769376", - "lastTransferAt": null, - "username": "wake", - "displayName": "wake", - "fid": 2904, - "followers": 97989, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5f4119a0-530c-404c-9203-5b0c5d15be00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x26a8593ddfa481e8ed4121cde60248e411e18572", - "etherscanUrl": "https://etherscan.io/address/0x26a8593ddfa481e8ed4121cde60248e411e18572", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb7ff4e7c1af2eeaeafa9d28baff92188554589c6", - "balanceRaw": "1375158524543949087434", - "balanceFormatted": "1375.158524543949087434", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb7ff4e7c1af2eeaeafa9d28baff92188554589c6", - "etherscanUrl": "https://etherscan.io/address/0xb7ff4e7c1af2eeaeafa9d28baff92188554589c6", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9990eddf051ead0a90987b269f33540a2b58ddff", - "balanceRaw": "1349570828484599262015", - "balanceFormatted": "1349.570828484599262015", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9990eddf051ead0a90987b269f33540a2b58ddff", - "etherscanUrl": "https://etherscan.io/address/0x9990eddf051ead0a90987b269f33540a2b58ddff", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x40ebc1ac8d4fedd2e144b75fe9c0420be82750c6", - "balanceRaw": "1339507168186132528275", - "balanceFormatted": "1339.507168186132528275", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x40ebc1ac8d4fedd2e144b75fe9c0420be82750c6", - "etherscanUrl": "https://etherscan.io/address/0x40ebc1ac8d4fedd2e144b75fe9c0420be82750c6", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x61afa5e5d7e1a70a537dc5b8252d64c8a1d92c6d", - "balanceRaw": "1327087386000354165772", - "balanceFormatted": "1327.087386000354165772", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x61afa5e5d7e1a70a537dc5b8252d64c8a1d92c6d", - "etherscanUrl": "https://etherscan.io/address/0x61afa5e5d7e1a70a537dc5b8252d64c8a1d92c6d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_18183", - "balanceRaw": "1306372192974170321988", - "balanceFormatted": "1306.372192974170321988", - "lastTransferAt": null, - "username": "windump", - "displayName": "Winder", - "fid": 18183, - "followers": 1364, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/34f672e3-17c7-4093-537c-24a9f7405600/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc89a0e99daefefb17a58231e2588c1118766e290", - "etherscanUrl": "https://etherscan.io/address/0xc89a0e99daefefb17a58231e2588c1118766e290", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x64171c5ae2848cec2cd92dde571f12a363f0b916", - "balanceRaw": "1297734218473562042890", - "balanceFormatted": "1297.73421847356204289", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x64171c5ae2848cec2cd92dde571f12a363f0b916", - "etherscanUrl": "https://etherscan.io/address/0x64171c5ae2848cec2cd92dde571f12a363f0b916", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x24e38acd773616cdf54a1c12ecadcd5d047ce600", - "balanceRaw": "1275241845887011885851", - "balanceFormatted": "1275.241845887011885851", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x24e38acd773616cdf54a1c12ecadcd5d047ce600", - "etherscanUrl": "https://etherscan.io/address/0x24e38acd773616cdf54a1c12ecadcd5d047ce600", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbcb9afcfedaf5d374fd81f2eb97fda84cf852ca6", - "balanceRaw": "1270727562126435398350", - "balanceFormatted": "1270.72756212643539835", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbcb9afcfedaf5d374fd81f2eb97fda84cf852ca6", - "etherscanUrl": "https://etherscan.io/address/0xbcb9afcfedaf5d374fd81f2eb97fda84cf852ca6", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf0de651e39cd04c0b5eb4b3f8f721bb86b1a227c", - "balanceRaw": "1236721783936390963767", - "balanceFormatted": "1236.721783936390963767", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf0de651e39cd04c0b5eb4b3f8f721bb86b1a227c", - "etherscanUrl": "https://etherscan.io/address/0xf0de651e39cd04c0b5eb4b3f8f721bb86b1a227c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7a5c1a532a89b50c84f9ffd7f915093f5c637081", - "balanceRaw": "1233473635767542751066", - "balanceFormatted": "1233.473635767542751066", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7a5c1a532a89b50c84f9ffd7f915093f5c637081", - "etherscanUrl": "https://etherscan.io/address/0x7a5c1a532a89b50c84f9ffd7f915093f5c637081", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6be61fd375e4ac4df6334413f4761caac46d1840", - "balanceRaw": "1232450411026081971888", - "balanceFormatted": "1232.450411026081971888", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6be61fd375e4ac4df6334413f4761caac46d1840", - "etherscanUrl": "https://etherscan.io/address/0x6be61fd375e4ac4df6334413f4761caac46d1840", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x420115a39de334dcb7c2cacf4d785c6cbc1ebaf8", - "balanceRaw": "1226985538959093415936", - "balanceFormatted": "1226.985538959093415936", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x420115a39de334dcb7c2cacf4d785c6cbc1ebaf8", - "etherscanUrl": "https://etherscan.io/address/0x420115a39de334dcb7c2cacf4d785c6cbc1ebaf8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbb1ee4b42518d75be69256af2ff1e9fcac55d417", - "balanceRaw": "1220273722368580610655", - "balanceFormatted": "1220.273722368580610655", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbb1ee4b42518d75be69256af2ff1e9fcac55d417", - "etherscanUrl": "https://etherscan.io/address/0xbb1ee4b42518d75be69256af2ff1e9fcac55d417", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x80d576fe3f14715be00fd227f334584823058c40", - "balanceRaw": "1215664307450505106068", - "balanceFormatted": "1215.664307450505106068", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x80d576fe3f14715be00fd227f334584823058c40", - "etherscanUrl": "https://etherscan.io/address/0x80d576fe3f14715be00fd227f334584823058c40", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x801ee733215e29b5870f2942810a907a5fe8ae84", - "balanceRaw": "1183166984371791283348", - "balanceFormatted": "1183.166984371791283348", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x801ee733215e29b5870f2942810a907a5fe8ae84", - "etherscanUrl": "https://etherscan.io/address/0x801ee733215e29b5870f2942810a907a5fe8ae84", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3dba4145a6af95a4e6b2624ca9e97529f0cbafef", - "balanceRaw": "1157020680566170896730", - "balanceFormatted": "1157.02068056617089673", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3dba4145a6af95a4e6b2624ca9e97529f0cbafef", - "etherscanUrl": "https://etherscan.io/address/0x3dba4145a6af95a4e6b2624ca9e97529f0cbafef", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x774789cede651b264d26088dba3d5b2b6e8b0d7c", - "balanceRaw": "1156443438776889114637", - "balanceFormatted": "1156.443438776889114637", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x774789cede651b264d26088dba3d5b2b6e8b0d7c", - "etherscanUrl": "https://etherscan.io/address/0x774789cede651b264d26088dba3d5b2b6e8b0d7c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xedfcd5ac886adf49f87e439aae51105c525cbb2b", - "balanceRaw": "1146605769914721455326", - "balanceFormatted": "1146.605769914721455326", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xedfcd5ac886adf49f87e439aae51105c525cbb2b", - "etherscanUrl": "https://etherscan.io/address/0xedfcd5ac886adf49f87e439aae51105c525cbb2b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x87013298d78729687dc686ae4fe712511c34dd8e", - "balanceRaw": "1142814694539418574702", - "balanceFormatted": "1142.814694539418574702", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x87013298d78729687dc686ae4fe712511c34dd8e", - "etherscanUrl": "https://etherscan.io/address/0x87013298d78729687dc686ae4fe712511c34dd8e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x854c94adaea43e0e4a6f47185b14b84e59326d7f", - "balanceRaw": "1134758825000639699517", - "balanceFormatted": "1134.758825000639699517", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x854c94adaea43e0e4a6f47185b14b84e59326d7f", - "etherscanUrl": "https://etherscan.io/address/0x854c94adaea43e0e4a6f47185b14b84e59326d7f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd411810f89390f6bd77766196681e6441abb4cbe", - "balanceRaw": "1118364407437996970186", - "balanceFormatted": "1118.364407437996970186", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd411810f89390f6bd77766196681e6441abb4cbe", - "etherscanUrl": "https://etherscan.io/address/0xd411810f89390f6bd77766196681e6441abb4cbe", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe33ffabf893d7832bd186404408dcb9484e50bfe", - "balanceRaw": "1111890590999147004912", - "balanceFormatted": "1111.890590999147004912", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe33ffabf893d7832bd186404408dcb9484e50bfe", - "etherscanUrl": "https://etherscan.io/address/0xe33ffabf893d7832bd186404408dcb9484e50bfe", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb3af88f40dcb2934a8ad026ee6084aea4628910a", - "balanceRaw": "1102628725669617481406", - "balanceFormatted": "1102.628725669617481406", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb3af88f40dcb2934a8ad026ee6084aea4628910a", - "etherscanUrl": "https://etherscan.io/address/0xb3af88f40dcb2934a8ad026ee6084aea4628910a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x1cd902496d684b94990a7be7cb6827cbe6e3c6b9", - "balanceRaw": "1088073073196373212698", - "balanceFormatted": "1088.073073196373212698", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1cd902496d684b94990a7be7cb6827cbe6e3c6b9", - "etherscanUrl": "https://etherscan.io/address/0x1cd902496d684b94990a7be7cb6827cbe6e3c6b9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x17a30350771d02409046a683b18fe1c13ccfc4a8", - "balanceRaw": "1081920000000000000000", - "balanceFormatted": "1081.92", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x17a30350771d02409046a683b18fe1c13ccfc4a8", - "etherscanUrl": "https://etherscan.io/address/0x17a30350771d02409046a683b18fe1c13ccfc4a8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9c7d6415547090f177c36f2b1af58a6b3152d344", - "balanceRaw": "1080064662863317342257", - "balanceFormatted": "1080.064662863317342257", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9c7d6415547090f177c36f2b1af58a6b3152d344", - "etherscanUrl": "https://etherscan.io/address/0x9c7d6415547090f177c36f2b1af58a6b3152d344", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5e04fb9a06ee4913f910f452be1e7e369d5ba2fb", - "balanceRaw": "1079864951843173643978", - "balanceFormatted": "1079.864951843173643978", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5e04fb9a06ee4913f910f452be1e7e369d5ba2fb", - "etherscanUrl": "https://etherscan.io/address/0x5e04fb9a06ee4913f910f452be1e7e369d5ba2fb", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x50bf4102d424b1f59803cd442c77c17a539e607d", - "balanceRaw": "1079845108112862074025", - "balanceFormatted": "1079.845108112862074025", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x50bf4102d424b1f59803cd442c77c17a539e607d", - "etherscanUrl": "https://etherscan.io/address/0x50bf4102d424b1f59803cd442c77c17a539e607d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbde5e699120504f1b0c326c8b43dc0fe68a4a259", - "balanceRaw": "1039220015864144000000", - "balanceFormatted": "1039.220015864144", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbde5e699120504f1b0c326c8b43dc0fe68a4a259", - "etherscanUrl": "https://etherscan.io/address/0xbde5e699120504f1b0c326c8b43dc0fe68a4a259", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x39a1b49bc0fb633ba5078af981b83f7f0b4940c1", - "balanceRaw": "1037939760820802965415", - "balanceFormatted": "1037.939760820802965415", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x39a1b49bc0fb633ba5078af981b83f7f0b4940c1", - "etherscanUrl": "https://etherscan.io/address/0x39a1b49bc0fb633ba5078af981b83f7f0b4940c1", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1430", - "balanceRaw": "1037114178047261315673", - "balanceFormatted": "1037.114178047261315673", - "lastTransferAt": null, - "username": "wijuwiju.eth", - "displayName": "wijuwiju.eth", - "fid": 1430, - "followers": 56060, - "pfpUrl": "https://i.imgur.com/oeyZjsk.jpg", - "ensName": "wijuwiju.eth", - "ensAvatarUrl": null, - "primaryAddress": "0xf4844a06d4f995c4c03195afcb5aa59dcbb5b4fc", - "etherscanUrl": "https://etherscan.io/address/0xf4844a06d4f995c4c03195afcb5aa59dcbb5b4fc", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6cf5cd17ef9f1129da38d90c0517a2fc00b378e6", - "balanceRaw": "1015097147938038960343", - "balanceFormatted": "1015.097147938038960343", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6cf5cd17ef9f1129da38d90c0517a2fc00b378e6", - "etherscanUrl": "https://etherscan.io/address/0x6cf5cd17ef9f1129da38d90c0517a2fc00b378e6", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0e6ac57fa9352f0ebfe697cd6eb4b41612050317", - "balanceRaw": "1009582042623131939158", - "balanceFormatted": "1009.582042623131939158", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0e6ac57fa9352f0ebfe697cd6eb4b41612050317", - "etherscanUrl": "https://etherscan.io/address/0x0e6ac57fa9352f0ebfe697cd6eb4b41612050317", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc3980171a52ffc5d625a6ce75f82fd37e6a6e393", - "balanceRaw": "1007147259161384985074", - "balanceFormatted": "1007.147259161384985074", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc3980171a52ffc5d625a6ce75f82fd37e6a6e393", - "etherscanUrl": "https://etherscan.io/address/0xc3980171a52ffc5d625a6ce75f82fd37e6a6e393", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x99c3281562bdcdc8987d07dabdddb2e964015b53", - "balanceRaw": "1000905208635101634152", - "balanceFormatted": "1000.905208635101634152", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x99c3281562bdcdc8987d07dabdddb2e964015b53", - "etherscanUrl": "https://etherscan.io/address/0x99c3281562bdcdc8987d07dabdddb2e964015b53", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x327220c33dad6f5a201c179bc398b3f0a24a05a7", - "balanceRaw": "1000705729776160331718", - "balanceFormatted": "1000.705729776160331718", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x327220c33dad6f5a201c179bc398b3f0a24a05a7", - "etherscanUrl": "https://etherscan.io/address/0x327220c33dad6f5a201c179bc398b3f0a24a05a7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd8ba783baad43169f307f560b5274f473d5171fd", - "balanceRaw": "1000267252344556855217", - "balanceFormatted": "1000.267252344556855217", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd8ba783baad43169f307f560b5274f473d5171fd", - "etherscanUrl": "https://etherscan.io/address/0xd8ba783baad43169f307f560b5274f473d5171fd", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x37ce10b37a199fc7a771765ba378b4a26bb2432f", - "balanceRaw": "1000000001683318355539", - "balanceFormatted": "1000.000001683318355539", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x37ce10b37a199fc7a771765ba378b4a26bb2432f", - "etherscanUrl": "https://etherscan.io/address/0x37ce10b37a199fc7a771765ba378b4a26bb2432f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xfe7928154946637d9904b2e2ca942246a488faa5", - "balanceRaw": "1000000000000000000000", - "balanceFormatted": "1000", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfe7928154946637d9904b2e2ca942246a488faa5", - "etherscanUrl": "https://etherscan.io/address/0xfe7928154946637d9904b2e2ca942246a488faa5", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8037292105d46ae56d4fb06a2df6188930fa01de", - "balanceRaw": "1000000000000000000000", - "balanceFormatted": "1000", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8037292105d46ae56d4fb06a2df6188930fa01de", - "etherscanUrl": "https://etherscan.io/address/0x8037292105d46ae56d4fb06a2df6188930fa01de", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3bf02c2002331ff13e4cdeeba1227d84ce7251e4", - "balanceRaw": "1000000000000000000000", - "balanceFormatted": "1000", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3bf02c2002331ff13e4cdeeba1227d84ce7251e4", - "etherscanUrl": "https://etherscan.io/address/0x3bf02c2002331ff13e4cdeeba1227d84ce7251e4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd77ab9f802242073672fd704046beb8acebba6b6", - "balanceRaw": "1000000000000000000000", - "balanceFormatted": "1000", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd77ab9f802242073672fd704046beb8acebba6b6", - "etherscanUrl": "https://etherscan.io/address/0xd77ab9f802242073672fd704046beb8acebba6b6", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xff02b67f7258de66400205a8c80c0c00d86d771f", - "balanceRaw": "1000000000000000000000", - "balanceFormatted": "1000", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xff02b67f7258de66400205a8c80c0c00d86d771f", - "etherscanUrl": "https://etherscan.io/address/0xff02b67f7258de66400205a8c80c0c00d86d771f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x1157a2076b9bb22a85cc2c162f20fab3898f4101", - "balanceRaw": "999954000000000000000", - "balanceFormatted": "999.954", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1157a2076b9bb22a85cc2c162f20fab3898f4101", - "etherscanUrl": "https://etherscan.io/address/0x1157a2076b9bb22a85cc2c162f20fab3898f4101", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x947ba15d0c9ec51057b1061297da112ecfd6af25", - "balanceRaw": "988513571955452475490", - "balanceFormatted": "988.51357195545247549", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x947ba15d0c9ec51057b1061297da112ecfd6af25", - "etherscanUrl": "https://etherscan.io/address/0x947ba15d0c9ec51057b1061297da112ecfd6af25", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe2174b0dcbb74ab8e0648a784839efc64c99af53", - "balanceRaw": "979118228549969487559", - "balanceFormatted": "979.118228549969487559", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe2174b0dcbb74ab8e0648a784839efc64c99af53", - "etherscanUrl": "https://etherscan.io/address/0xe2174b0dcbb74ab8e0648a784839efc64c99af53", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x29806ed7bc5904e81e7d6255f8718991c7db07de", - "balanceRaw": "978126141036094740695", - "balanceFormatted": "978.126141036094740695", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x29806ed7bc5904e81e7d6255f8718991c7db07de", - "etherscanUrl": "https://etherscan.io/address/0x29806ed7bc5904e81e7d6255f8718991c7db07de", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa584464d42c805e6d08cb3b688e5b4a8fad92309", - "balanceRaw": "975954481034675657245", - "balanceFormatted": "975.954481034675657245", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa584464d42c805e6d08cb3b688e5b4a8fad92309", - "etherscanUrl": "https://etherscan.io/address/0xa584464d42c805e6d08cb3b688e5b4a8fad92309", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x98a3da6293e56674d250fa9ada4c684dc34e4e0b", - "balanceRaw": "971477079692848427168", - "balanceFormatted": "971.477079692848427168", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x98a3da6293e56674d250fa9ada4c684dc34e4e0b", - "etherscanUrl": "https://etherscan.io/address/0x98a3da6293e56674d250fa9ada4c684dc34e4e0b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0c323730b96732d9e2a354863af9bbfce539b10d", - "balanceRaw": "959713709008444708737", - "balanceFormatted": "959.713709008444708737", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0c323730b96732d9e2a354863af9bbfce539b10d", - "etherscanUrl": "https://etherscan.io/address/0x0c323730b96732d9e2a354863af9bbfce539b10d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe87b933e326195d399725a8d4f7abde743bcfb1a", - "balanceRaw": "952948670627422306652", - "balanceFormatted": "952.948670627422306652", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe87b933e326195d399725a8d4f7abde743bcfb1a", - "etherscanUrl": "https://etherscan.io/address/0xe87b933e326195d399725a8d4f7abde743bcfb1a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_393392", - "balanceRaw": "949115498797784110287", - "balanceFormatted": "949.115498797784110287", - "lastTransferAt": null, - "username": "kien", - "displayName": "Kien Nguyen", - "fid": 393392, - "followers": 1242, - "pfpUrl": "https://i.imgur.com/fuTuQOW.jpg", - "ensName": "kennik28.eth", - "ensAvatarUrl": null, - "primaryAddress": "0x900d69d45a6bbd7562e2b4e175a36d395f681c06", - "etherscanUrl": "https://etherscan.io/address/0x900d69d45a6bbd7562e2b4e175a36d395f681c06", - "xHandle": "kiennguyen_nft", - "xUrl": "https://twitter.com/kiennguyen_nft", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbada235e37f8ceba768cb167518c869a3765992c", - "balanceRaw": "947444533152411635070", - "balanceFormatted": "947.44453315241163507", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbada235e37f8ceba768cb167518c869a3765992c", - "etherscanUrl": "https://etherscan.io/address/0xbada235e37f8ceba768cb167518c869a3765992c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x17497b8c50a7c641fa265fb3a784fbd1c14264c2", - "balanceRaw": "934961394658532844832", - "balanceFormatted": "934.961394658532844832", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x17497b8c50a7c641fa265fb3a784fbd1c14264c2", - "etherscanUrl": "https://etherscan.io/address/0x17497b8c50a7c641fa265fb3a784fbd1c14264c2", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x96245e3bca44106e4b005dba0470e7a89315dc8f", - "balanceRaw": "933020293449363493551", - "balanceFormatted": "933.020293449363493551", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x96245e3bca44106e4b005dba0470e7a89315dc8f", - "etherscanUrl": "https://etherscan.io/address/0x96245e3bca44106e4b005dba0470e7a89315dc8f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x51c72848c68a965f66fa7a88855f9f7784502a7f", - "balanceRaw": "929098293107022418163", - "balanceFormatted": "929.098293107022418163", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x51c72848c68a965f66fa7a88855f9f7784502a7f", - "etherscanUrl": "https://etherscan.io/address/0x51c72848c68a965f66fa7a88855f9f7784502a7f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa25628822ede2195d88b153bfe88a2375176d498", - "balanceRaw": "926900871780725725084", - "balanceFormatted": "926.900871780725725084", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa25628822ede2195d88b153bfe88a2375176d498", - "etherscanUrl": "https://etherscan.io/address/0xa25628822ede2195d88b153bfe88a2375176d498", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xba2f57bb46e8c9eaa0a584ee077bcf10bc4a272e", - "balanceRaw": "925000000000000000000", - "balanceFormatted": "925", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xba2f57bb46e8c9eaa0a584ee077bcf10bc4a272e", - "etherscanUrl": "https://etherscan.io/address/0xba2f57bb46e8c9eaa0a584ee077bcf10bc4a272e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x4baec390073e768f2334b2d7e3c0c3c6e19f03df", - "balanceRaw": "900244430393518351854", - "balanceFormatted": "900.244430393518351854", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4baec390073e768f2334b2d7e3c0c3c6e19f03df", - "etherscanUrl": "https://etherscan.io/address/0x4baec390073e768f2334b2d7e3c0c3c6e19f03df", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6105c1ad5fa00106e78d53e6d0b7f5f01379c33b", - "balanceRaw": "900056330557044381876", - "balanceFormatted": "900.056330557044381876", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6105c1ad5fa00106e78d53e6d0b7f5f01379c33b", - "etherscanUrl": "https://etherscan.io/address/0x6105c1ad5fa00106e78d53e6d0b7f5f01379c33b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc3d9ca74d559709743c0d0611b958a7c98509bc1", - "balanceRaw": "893581717385517910785", - "balanceFormatted": "893.581717385517910785", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc3d9ca74d559709743c0d0611b958a7c98509bc1", - "etherscanUrl": "https://etherscan.io/address/0xc3d9ca74d559709743c0d0611b958a7c98509bc1", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb8e6d31e7b212b2b7250ee9c26c56cebbfbe6b23", - "balanceRaw": "889520000000000000000", - "balanceFormatted": "889.52", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb8e6d31e7b212b2b7250ee9c26c56cebbfbe6b23", - "etherscanUrl": "https://etherscan.io/address/0xb8e6d31e7b212b2b7250ee9c26c56cebbfbe6b23", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9a97080304b6ed457c9cf9697bfa851edd022674", - "balanceRaw": "884616979172091809152", - "balanceFormatted": "884.616979172091809152", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9a97080304b6ed457c9cf9697bfa851edd022674", - "etherscanUrl": "https://etherscan.io/address/0x9a97080304b6ed457c9cf9697bfa851edd022674", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc319d034dfda52c56cc4a13ccdc782c6627d52e3", - "balanceRaw": "882468637698523256800", - "balanceFormatted": "882.4686376985232568", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc319d034dfda52c56cc4a13ccdc782c6627d52e3", - "etherscanUrl": "https://etherscan.io/address/0xc319d034dfda52c56cc4a13ccdc782c6627d52e3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x105d8f9b1935b52952ef3f25b70836cb985c20f4", - "balanceRaw": "876373200830000000000", - "balanceFormatted": "876.37320083", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x105d8f9b1935b52952ef3f25b70836cb985c20f4", - "etherscanUrl": "https://etherscan.io/address/0x105d8f9b1935b52952ef3f25b70836cb985c20f4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb7caf868835eac09f4fcb0ab85b942d8722c26de", - "balanceRaw": "876327941721784249876", - "balanceFormatted": "876.327941721784249876", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb7caf868835eac09f4fcb0ab85b942d8722c26de", - "etherscanUrl": "https://etherscan.io/address/0xb7caf868835eac09f4fcb0ab85b942d8722c26de", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x588808dbe54c20986e98e17f7b5b89dd2dee316d", - "balanceRaw": "873998488580842272594", - "balanceFormatted": "873.998488580842272594", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x588808dbe54c20986e98e17f7b5b89dd2dee316d", - "etherscanUrl": "https://etherscan.io/address/0x588808dbe54c20986e98e17f7b5b89dd2dee316d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xef49eb38d571c35196926e973df197733c892525", - "balanceRaw": "863336979370078577333", - "balanceFormatted": "863.336979370078577333", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xef49eb38d571c35196926e973df197733c892525", - "etherscanUrl": "https://etherscan.io/address/0xef49eb38d571c35196926e973df197733c892525", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbbea53912ae66107c0c54ad3debc349986db69fa", - "balanceRaw": "858946900299941373754", - "balanceFormatted": "858.946900299941373754", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbbea53912ae66107c0c54ad3debc349986db69fa", - "etherscanUrl": "https://etherscan.io/address/0xbbea53912ae66107c0c54ad3debc349986db69fa", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x32631223a3b8f7bb28dc3c067d5f880902730dfa", - "balanceRaw": "852000000000000000000", - "balanceFormatted": "852", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x32631223a3b8f7bb28dc3c067d5f880902730dfa", - "etherscanUrl": "https://etherscan.io/address/0x32631223a3b8f7bb28dc3c067d5f880902730dfa", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6a10910049c7c95e78251b0431986b0fb5be4af3", - "balanceRaw": "850192657734960549476", - "balanceFormatted": "850.192657734960549476", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6a10910049c7c95e78251b0431986b0fb5be4af3", - "etherscanUrl": "https://etherscan.io/address/0x6a10910049c7c95e78251b0431986b0fb5be4af3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8a62d9c31626cd4e99870f7d6b63d5379afa1493", - "balanceRaw": "849007814057556095202", - "balanceFormatted": "849.007814057556095202", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8a62d9c31626cd4e99870f7d6b63d5379afa1493", - "etherscanUrl": "https://etherscan.io/address/0x8a62d9c31626cd4e99870f7d6b63d5379afa1493", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_876109", - "balanceRaw": "821133914777359930893", - "balanceFormatted": "821.133914777359930893", - "lastTransferAt": null, - "username": "seeker-block", - "displayName": "seeker-block", - "fid": 876109, - "followers": 1344, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/db081652-222a-45c2-3b2f-b5e9f3705400/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0cbb693e0199cb7ad4f3c846f43045cb95247fc5", - "etherscanUrl": "https://etherscan.io/address/0x0cbb693e0199cb7ad4f3c846f43045cb95247fc5", - "xHandle": "aliideez", - "xUrl": "https://twitter.com/aliideez", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd2dd7b597fd2435b6db61ddf48544fd931e6869f", - "balanceRaw": "818974291600000000000", - "balanceFormatted": "818.9742916", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd2dd7b597fd2435b6db61ddf48544fd931e6869f", - "etherscanUrl": "https://etherscan.io/address/0xd2dd7b597fd2435b6db61ddf48544fd931e6869f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb7013a0e54f74bd054d968ee0c6a8d24acc0476f", - "balanceRaw": "814348630478394464747", - "balanceFormatted": "814.348630478394464747", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb7013a0e54f74bd054d968ee0c6a8d24acc0476f", - "etherscanUrl": "https://etherscan.io/address/0xb7013a0e54f74bd054d968ee0c6a8d24acc0476f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x01b27b60afe52330a2f464aa4625431365dc3e07", - "balanceRaw": "806353079720749712213", - "balanceFormatted": "806.353079720749712213", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x01b27b60afe52330a2f464aa4625431365dc3e07", - "etherscanUrl": "https://etherscan.io/address/0x01b27b60afe52330a2f464aa4625431365dc3e07", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x18a2c31f15e4492fe6098efd32704f83a988512d", - "balanceRaw": "805236455691929094808", - "balanceFormatted": "805.236455691929094808", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x18a2c31f15e4492fe6098efd32704f83a988512d", - "etherscanUrl": "https://etherscan.io/address/0x18a2c31f15e4492fe6098efd32704f83a988512d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xdacadddf821ce45523d0ca0ef14623894044647f", - "balanceRaw": "802717452996612488204", - "balanceFormatted": "802.717452996612488204", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdacadddf821ce45523d0ca0ef14623894044647f", - "etherscanUrl": "https://etherscan.io/address/0xdacadddf821ce45523d0ca0ef14623894044647f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xda933dd24632990b3293f1823d2d1734d3f94446", - "balanceRaw": "802575913104676315222", - "balanceFormatted": "802.575913104676315222", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xda933dd24632990b3293f1823d2d1734d3f94446", - "etherscanUrl": "https://etherscan.io/address/0xda933dd24632990b3293f1823d2d1734d3f94446", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc45550188138e74daa2fe6fdd12070a2dc13fa5c", - "balanceRaw": "800026284253056988218", - "balanceFormatted": "800.026284253056988218", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc45550188138e74daa2fe6fdd12070a2dc13fa5c", - "etherscanUrl": "https://etherscan.io/address/0xc45550188138e74daa2fe6fdd12070a2dc13fa5c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x2f4ba8a9d9daf9a9c82b003b641298469c5aad48", - "balanceRaw": "800000000000000000000", - "balanceFormatted": "800", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2f4ba8a9d9daf9a9c82b003b641298469c5aad48", - "etherscanUrl": "https://etherscan.io/address/0x2f4ba8a9d9daf9a9c82b003b641298469c5aad48", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x85e2acd5826a07740b3f36785a439b414385febe", - "balanceRaw": "799065407438522672585", - "balanceFormatted": "799.065407438522672585", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x85e2acd5826a07740b3f36785a439b414385febe", - "etherscanUrl": "https://etherscan.io/address/0x85e2acd5826a07740b3f36785a439b414385febe", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_886945", - "balanceRaw": "797637915010625647727", - "balanceFormatted": "797.637915010625647727", - "lastTransferAt": null, - "username": "zuoyouya", - "displayName": "我爱pe", - "fid": 886945, - "followers": 29, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b5dc5968-3a75-468b-234a-8a481611ae00/original", - "ensName": "cwyxhqz.eth", - "ensAvatarUrl": null, - "primaryAddress": "0x8e07ab8fc9e5f2613b17a5e5069673d522d0207a", - "etherscanUrl": "https://etherscan.io/address/0x8e07ab8fc9e5f2613b17a5e5069673d522d0207a", - "xHandle": "misk86429903", - "xUrl": "https://twitter.com/misk86429903", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa73072adc6c34859426fcc29bc6ca2cac07c93c3", - "balanceRaw": "774799999999999992320", - "balanceFormatted": "774.79999999999999232", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa73072adc6c34859426fcc29bc6ca2cac07c93c3", - "etherscanUrl": "https://etherscan.io/address/0xa73072adc6c34859426fcc29bc6ca2cac07c93c3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf89b8563420116ac20b68930e8da28cdc9024393", - "balanceRaw": "769508648462595937240", - "balanceFormatted": "769.50864846259593724", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf89b8563420116ac20b68930e8da28cdc9024393", - "etherscanUrl": "https://etherscan.io/address/0xf89b8563420116ac20b68930e8da28cdc9024393", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9063d9ca9c606bc72a7a76021a8417397230a6c3", - "balanceRaw": "761646963372824341567", - "balanceFormatted": "761.646963372824341567", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9063d9ca9c606bc72a7a76021a8417397230a6c3", - "etherscanUrl": "https://etherscan.io/address/0x9063d9ca9c606bc72a7a76021a8417397230a6c3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x66d47066ed2db09e06a21f1e4e03c8384c5af245", - "balanceRaw": "758692336464924883996", - "balanceFormatted": "758.692336464924883996", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x66d47066ed2db09e06a21f1e4e03c8384c5af245", - "etherscanUrl": "https://etherscan.io/address/0x66d47066ed2db09e06a21f1e4e03c8384c5af245", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x997bfc3f4d58c8bedf6ed743978ce5a6749da2d0", - "balanceRaw": "758616600000000000000", - "balanceFormatted": "758.6166", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x997bfc3f4d58c8bedf6ed743978ce5a6749da2d0", - "etherscanUrl": "https://etherscan.io/address/0x997bfc3f4d58c8bedf6ed743978ce5a6749da2d0", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9aa9da33965cf28a1c895850cc30e2515a53e4b8", - "balanceRaw": "750252339444097301129", - "balanceFormatted": "750.252339444097301129", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9aa9da33965cf28a1c895850cc30e2515a53e4b8", - "etherscanUrl": "https://etherscan.io/address/0x9aa9da33965cf28a1c895850cc30e2515a53e4b8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_227522", - "balanceRaw": "740000000000000000000", - "balanceFormatted": "740", - "lastTransferAt": null, - "username": "mzemlu", - "displayName": "mzemlu 🎩 👒💙🦈", - "fid": 227522, - "followers": 2605, - "pfpUrl": "https://i.imgur.com/OzLHWuN.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x180d4a7c54badb851aac45cdd612a13e666a9ff6", - "etherscanUrl": "https://etherscan.io/address/0x180d4a7c54badb851aac45cdd612a13e666a9ff6", - "xHandle": "mzemlu777", - "xUrl": "https://twitter.com/mzemlu777", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x87422d6a1b319cfe044a61e6ca90f9f043674eba", - "balanceRaw": "733552897532638482646", - "balanceFormatted": "733.552897532638482646", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x87422d6a1b319cfe044a61e6ca90f9f043674eba", - "etherscanUrl": "https://etherscan.io/address/0x87422d6a1b319cfe044a61e6ca90f9f043674eba", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_8046", - "balanceRaw": "733120376155950917407", - "balanceFormatted": "733.120376155950917407", - "lastTransferAt": null, - "username": "rsa.eth", - "displayName": "Ryan Sean Adams (rsa.eth)", - "fid": 8046, - "followers": 98825, - "pfpUrl": "https://i.imgur.com/Z66YsVZ.jpg", - "ensName": "rsa.eth", - "ensAvatarUrl": null, - "primaryAddress": "0x779083edc9fa2b11e4f813c22dc2e6400292dc8b", - "etherscanUrl": "https://etherscan.io/address/0x779083edc9fa2b11e4f813c22dc2e6400292dc8b", - "xHandle": "ryansadams", - "xUrl": "https://twitter.com/ryansadams", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa6e2dc44418f5ad213c86db4dbb203a59d13c49f", - "balanceRaw": "722782064289764916987", - "balanceFormatted": "722.782064289764916987", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa6e2dc44418f5ad213c86db4dbb203a59d13c49f", - "etherscanUrl": "https://etherscan.io/address/0xa6e2dc44418f5ad213c86db4dbb203a59d13c49f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb5523e3a226de6ef310b90050ee41683e41d4252", - "balanceRaw": "714134778542084375288", - "balanceFormatted": "714.134778542084375288", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb5523e3a226de6ef310b90050ee41683e41d4252", - "etherscanUrl": "https://etherscan.io/address/0xb5523e3a226de6ef310b90050ee41683e41d4252", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_15732", - "balanceRaw": "705007848349542444768", - "balanceFormatted": "705.007848349542444768", - "lastTransferAt": null, - "username": "kompreni", - "displayName": "kompreni 🚂", - "fid": 15732, - "followers": 10887, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2b7cdbbc-92fe-4d37-450a-fdbb88e7e100/original", - "ensName": "chejazi.eth", - "ensAvatarUrl": null, - "primaryAddress": "0x0aadd2e08f1bf4be8b82ca6e402b7b76b3db76e9", - "etherscanUrl": "https://etherscan.io/address/0x0aadd2e08f1bf4be8b82ca6e402b7b76b3db76e9", - "xHandle": "kompreni", - "xUrl": "https://twitter.com/kompreni", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_272109", - "balanceRaw": "700000000000000000000", - "balanceFormatted": "700", - "lastTransferAt": null, - "username": "heesh", - "displayName": "Heeshillio", - "fid": 272109, - "followers": 414, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/1e66ec60-269c-486c-7707-d42a916ca400/rectcrop3", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x49db5a1c9fef0e2f995b46b0b71bebbd912bf0e3", - "etherscanUrl": "https://etherscan.io/address/0x49db5a1c9fef0e2f995b46b0b71bebbd912bf0e3", - "xHandle": "heeshillio", - "xUrl": "https://twitter.com/heeshillio", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x242e2d70d3adc00a9ef23ced6e88811fcefca788", - "balanceRaw": "693491727305128612178", - "balanceFormatted": "693.491727305128612178", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x242e2d70d3adc00a9ef23ced6e88811fcefca788", - "etherscanUrl": "https://etherscan.io/address/0x242e2d70d3adc00a9ef23ced6e88811fcefca788", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x77bac8a59ac25911710f829cdfbb11b4d319b398", - "balanceRaw": "693046283754962050852", - "balanceFormatted": "693.046283754962050852", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x77bac8a59ac25911710f829cdfbb11b4d319b398", - "etherscanUrl": "https://etherscan.io/address/0x77bac8a59ac25911710f829cdfbb11b4d319b398", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xfbef7475ba925a85cfcd32dc8650b8f9204fb493", - "balanceRaw": "692247883910000000000", - "balanceFormatted": "692.24788391", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfbef7475ba925a85cfcd32dc8650b8f9204fb493", - "etherscanUrl": "https://etherscan.io/address/0xfbef7475ba925a85cfcd32dc8650b8f9204fb493", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_247143", - "balanceRaw": "683998970946426426318", - "balanceFormatted": "683.998970946426426318", - "lastTransferAt": null, - "username": "kylepatrick.eth", - "displayName": "Kyle Patrick", - "fid": 247143, - "followers": 48066, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/15b35784-d490-4144-15c4-62edc2a58c00/rectcrop3", - "ensName": "kylepatrick.eth", - "ensAvatarUrl": null, - "primaryAddress": "0x33be45c761b50f44e966d901fa6d0528a74865e5", - "etherscanUrl": "https://etherscan.io/address/0x33be45c761b50f44e966d901fa6d0528a74865e5", - "xHandle": "kylepatrick_eth", - "xUrl": "https://twitter.com/kylepatrick_eth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_3734", - "balanceRaw": "677883456965832652239", - "balanceFormatted": "677.883456965832652239", - "lastTransferAt": null, - "username": "sasquatch", - "displayName": "Sasquatch テ", - "fid": 3734, - "followers": 5761, - "pfpUrl": "https://i.imgur.com/ZfjkWSo.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x76607b6f78191eef8934eb45ecb030def67d8b33", - "etherscanUrl": "https://etherscan.io/address/0x76607b6f78191eef8934eb45ecb030def67d8b33", - "xHandle": "partsasquatch", - "xUrl": "https://twitter.com/partsasquatch", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x07427c476501e03b1802541625cefd1df24484d2", - "balanceRaw": "673891746544337633886", - "balanceFormatted": "673.891746544337633886", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x07427c476501e03b1802541625cefd1df24484d2", - "etherscanUrl": "https://etherscan.io/address/0x07427c476501e03b1802541625cefd1df24484d2", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb187803a4ac9c5a498e470aab82de203f5870ab8", - "balanceRaw": "667149157624692200869", - "balanceFormatted": "667.149157624692200869", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb187803a4ac9c5a498e470aab82de203f5870ab8", - "etherscanUrl": "https://etherscan.io/address/0xb187803a4ac9c5a498e470aab82de203f5870ab8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x114d90ce0891727113eea7ba9307add0d015ba4b", - "balanceRaw": "659668907534714988269", - "balanceFormatted": "659.668907534714988269", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x114d90ce0891727113eea7ba9307add0d015ba4b", - "etherscanUrl": "https://etherscan.io/address/0x114d90ce0891727113eea7ba9307add0d015ba4b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x4be82a293c31c853ed806284f132bc8ceb314d5e", - "balanceRaw": "649342038263738638095", - "balanceFormatted": "649.342038263738638095", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4be82a293c31c853ed806284f132bc8ceb314d5e", - "etherscanUrl": "https://etherscan.io/address/0x4be82a293c31c853ed806284f132bc8ceb314d5e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x921c973c4992d1c0011a8154b6862726759688bc", - "balanceRaw": "639992093636779401050", - "balanceFormatted": "639.99209363677940105", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x921c973c4992d1c0011a8154b6862726759688bc", - "etherscanUrl": "https://etherscan.io/address/0x921c973c4992d1c0011a8154b6862726759688bc", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6c7d98079023f05c2b57dfc933fa0903a2c95411", - "balanceRaw": "638555348244022466048", - "balanceFormatted": "638.555348244022466048", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6c7d98079023f05c2b57dfc933fa0903a2c95411", - "etherscanUrl": "https://etherscan.io/address/0x6c7d98079023f05c2b57dfc933fa0903a2c95411", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8dd66d34a93bb2e49c52ca6fdc0ac0c05c9e2172", - "balanceRaw": "634991019447417902469", - "balanceFormatted": "634.991019447417902469", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8dd66d34a93bb2e49c52ca6fdc0ac0c05c9e2172", - "etherscanUrl": "https://etherscan.io/address/0x8dd66d34a93bb2e49c52ca6fdc0ac0c05c9e2172", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9105b76e8560c7c23f8435e3f180a0600f9b0ad5", - "balanceRaw": "634414581474141065849", - "balanceFormatted": "634.414581474141065849", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9105b76e8560c7c23f8435e3f180a0600f9b0ad5", - "etherscanUrl": "https://etherscan.io/address/0x9105b76e8560c7c23f8435e3f180a0600f9b0ad5", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x302d86e24d7474f0408a53e26a1111c041a94093", - "balanceRaw": "624870032663091333069", - "balanceFormatted": "624.870032663091333069", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x302d86e24d7474f0408a53e26a1111c041a94093", - "etherscanUrl": "https://etherscan.io/address/0x302d86e24d7474f0408a53e26a1111c041a94093", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd01fe03aba9eb73ff92e2b64e22daf99458e4f54", - "balanceRaw": "624688206136715372625", - "balanceFormatted": "624.688206136715372625", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd01fe03aba9eb73ff92e2b64e22daf99458e4f54", - "etherscanUrl": "https://etherscan.io/address/0xd01fe03aba9eb73ff92e2b64e22daf99458e4f54", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x66f93a7d92231ce9b296ffb5d4045a01950b476d", - "balanceRaw": "624559887828300404265", - "balanceFormatted": "624.559887828300404265", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x66f93a7d92231ce9b296ffb5d4045a01950b476d", - "etherscanUrl": "https://etherscan.io/address/0x66f93a7d92231ce9b296ffb5d4045a01950b476d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xaf64f47f6b27c8d69ae2950c71bb4899387e0139", - "balanceRaw": "623608281280000000000", - "balanceFormatted": "623.60828128", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xaf64f47f6b27c8d69ae2950c71bb4899387e0139", - "etherscanUrl": "https://etherscan.io/address/0xaf64f47f6b27c8d69ae2950c71bb4899387e0139", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_485497", - "balanceRaw": "620000000000000000000", - "balanceFormatted": "620", - "lastTransferAt": null, - "username": "ficsik", - "displayName": "Vorobey 🎩", - "fid": 485497, - "followers": 475, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/aedc735e-09e4-4599-afb5-96c0b8fca100/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc2d2078ee867e40c9f76e2eb8abbf47634341686", - "etherscanUrl": "https://etherscan.io/address/0xc2d2078ee867e40c9f76e2eb8abbf47634341686", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x58d7849378e2f7b082e204a22a7a082e4db85f18", - "balanceRaw": "617280611000000000000", - "balanceFormatted": "617.280611", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x58d7849378e2f7b082e204a22a7a082e4db85f18", - "etherscanUrl": "https://etherscan.io/address/0x58d7849378e2f7b082e204a22a7a082e4db85f18", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x2dc37949d73cc648ca45c6e22253654a1f1865d8", - "balanceRaw": "602050810619682558256", - "balanceFormatted": "602.050810619682558256", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2dc37949d73cc648ca45c6e22253654a1f1865d8", - "etherscanUrl": "https://etherscan.io/address/0x2dc37949d73cc648ca45c6e22253654a1f1865d8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6b65eff428a3c224e7cacc547795d3c1f15fce79", - "balanceRaw": "598543117567732183378", - "balanceFormatted": "598.543117567732183378", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6b65eff428a3c224e7cacc547795d3c1f15fce79", - "etherscanUrl": "https://etherscan.io/address/0x6b65eff428a3c224e7cacc547795d3c1f15fce79", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa890edc0aef41f163577a5c11ca703077ecdbe13", - "balanceRaw": "597577126346134566079", - "balanceFormatted": "597.577126346134566079", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa890edc0aef41f163577a5c11ca703077ecdbe13", - "etherscanUrl": "https://etherscan.io/address/0xa890edc0aef41f163577a5c11ca703077ecdbe13", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x2dcce04416ec8fad376b733fac16cfb03cfb189e", - "balanceRaw": "591173782242933296261", - "balanceFormatted": "591.173782242933296261", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2dcce04416ec8fad376b733fac16cfb03cfb189e", - "etherscanUrl": "https://etherscan.io/address/0x2dcce04416ec8fad376b733fac16cfb03cfb189e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7bacf95e95b9032c1a37830d48efaf66a9bd2c30", - "balanceRaw": "589291029917563166848", - "balanceFormatted": "589.291029917563166848", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7bacf95e95b9032c1a37830d48efaf66a9bd2c30", - "etherscanUrl": "https://etherscan.io/address/0x7bacf95e95b9032c1a37830d48efaf66a9bd2c30", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf8c1a2ca1496f318067594a76c0276b29deca6fb", - "balanceRaw": "577100835208824753280", - "balanceFormatted": "577.10083520882475328", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf8c1a2ca1496f318067594a76c0276b29deca6fb", - "etherscanUrl": "https://etherscan.io/address/0xf8c1a2ca1496f318067594a76c0276b29deca6fb", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x024045e87ebdc93ee190a2e60c7dc6c501a5893c", - "balanceRaw": "573161000000000000000", - "balanceFormatted": "573.161", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x024045e87ebdc93ee190a2e60c7dc6c501a5893c", - "etherscanUrl": "https://etherscan.io/address/0x024045e87ebdc93ee190a2e60c7dc6c501a5893c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xacdb45dcf58a8da1706468b5828dec484f37c5e3", - "balanceRaw": "560256896297505749083", - "balanceFormatted": "560.256896297505749083", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xacdb45dcf58a8da1706468b5828dec484f37c5e3", - "etherscanUrl": "https://etherscan.io/address/0xacdb45dcf58a8da1706468b5828dec484f37c5e3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x15deaacce662bcbc9fa55ab76feb9710b16be260", - "balanceRaw": "556113756235936502690", - "balanceFormatted": "556.11375623593650269", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x15deaacce662bcbc9fa55ab76feb9710b16be260", - "etherscanUrl": "https://etherscan.io/address/0x15deaacce662bcbc9fa55ab76feb9710b16be260", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_268977", - "balanceRaw": "554437661727836809292", - "balanceFormatted": "554.437661727836809292", - "lastTransferAt": null, - "username": "einik", - "displayName": "einik", - "fid": 268977, - "followers": 2102, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/837d8dfd-39e1-4c72-5e82-bcdce4f3c700/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfdc041664250f4137f3f0ce8167646f45e2ee753", - "etherscanUrl": "https://etherscan.io/address/0xfdc041664250f4137f3f0ce8167646f45e2ee753", - "xHandle": "feliperatfi", - "xUrl": "https://twitter.com/feliperatfi", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x56aea0e4510e6f79b3e13581a5863a15a9f40d75", - "balanceRaw": "551383434380000000000", - "balanceFormatted": "551.38343438", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x56aea0e4510e6f79b3e13581a5863a15a9f40d75", - "etherscanUrl": "https://etherscan.io/address/0x56aea0e4510e6f79b3e13581a5863a15a9f40d75", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x2499bdf18e33eb830bcf0d1c1e47b7e0cca07071", - "balanceRaw": "549999968901538014356", - "balanceFormatted": "549.999968901538014356", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2499bdf18e33eb830bcf0d1c1e47b7e0cca07071", - "etherscanUrl": "https://etherscan.io/address/0x2499bdf18e33eb830bcf0d1c1e47b7e0cca07071", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x65961a1b0744662b695346b77de3c31fae9ca0fe", - "balanceRaw": "548037568048671617474", - "balanceFormatted": "548.037568048671617474", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x65961a1b0744662b695346b77de3c31fae9ca0fe", - "etherscanUrl": "https://etherscan.io/address/0x65961a1b0744662b695346b77de3c31fae9ca0fe", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5f569f81a0696bfccdc8b3809366d3c67b352ee3", - "balanceRaw": "544247943031028583676", - "balanceFormatted": "544.247943031028583676", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5f569f81a0696bfccdc8b3809366d3c67b352ee3", - "etherscanUrl": "https://etherscan.io/address/0x5f569f81a0696bfccdc8b3809366d3c67b352ee3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1668", - "balanceRaw": "542203719396852243857", - "balanceFormatted": "542.203719396852243857", - "lastTransferAt": null, - "username": "tomu", - "displayName": "tomu", - "fid": 1668, - "followers": 21678, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b470eabb-b1a5-42c4-58ad-04a3c9be2e00/original", - "ensName": "tomu.eth", - "ensAvatarUrl": null, - "primaryAddress": "0x71738f8ba6853cf8c3ec2b98ef42d2306480c6ff", - "etherscanUrl": "https://etherscan.io/address/0x71738f8ba6853cf8c3ec2b98ef42d2306480c6ff", - "xHandle": "david_tomu", - "xUrl": "https://twitter.com/david_tomu", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x48641f49e33da2d73a9b6d986d1545c35a57b2c7", - "balanceRaw": "541380720000000000000", - "balanceFormatted": "541.38072", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x48641f49e33da2d73a9b6d986d1545c35a57b2c7", - "etherscanUrl": "https://etherscan.io/address/0x48641f49e33da2d73a9b6d986d1545c35a57b2c7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x73c4af31d6df48a730fdb82015e232205d6cf041", - "balanceRaw": "540330807656341582345", - "balanceFormatted": "540.330807656341582345", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x73c4af31d6df48a730fdb82015e232205d6cf041", - "etherscanUrl": "https://etherscan.io/address/0x73c4af31d6df48a730fdb82015e232205d6cf041", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0589a28de396df27cd710d81103d8541e6371fa5", - "balanceRaw": "537893460638198671095", - "balanceFormatted": "537.893460638198671095", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0589a28de396df27cd710d81103d8541e6371fa5", - "etherscanUrl": "https://etherscan.io/address/0x0589a28de396df27cd710d81103d8541e6371fa5", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xed841526a8c7c90e30369afb363c3b6153311004", - "balanceRaw": "536338655199278471300", - "balanceFormatted": "536.3386551992784713", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xed841526a8c7c90e30369afb363c3b6153311004", - "etherscanUrl": "https://etherscan.io/address/0xed841526a8c7c90e30369afb363c3b6153311004", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7b7825c1f25cba1b5dbb119c54f39475e175a969", - "balanceRaw": "533726006625692969023", - "balanceFormatted": "533.726006625692969023", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7b7825c1f25cba1b5dbb119c54f39475e175a969", - "etherscanUrl": "https://etherscan.io/address/0x7b7825c1f25cba1b5dbb119c54f39475e175a969", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x705f95654ab855abebb5aca678887c3333fb99c4", - "balanceRaw": "531220506920399590664", - "balanceFormatted": "531.220506920399590664", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x705f95654ab855abebb5aca678887c3333fb99c4", - "etherscanUrl": "https://etherscan.io/address/0x705f95654ab855abebb5aca678887c3333fb99c4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8308d51ab1beb4cdfb48ca5d1b15d8c0e96badd8", - "balanceRaw": "527102899190478296156", - "balanceFormatted": "527.102899190478296156", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8308d51ab1beb4cdfb48ca5d1b15d8c0e96badd8", - "etherscanUrl": "https://etherscan.io/address/0x8308d51ab1beb4cdfb48ca5d1b15d8c0e96badd8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x05d314204172fcc504522288a9e57449f16486f9", - "balanceRaw": "520895294578143218618", - "balanceFormatted": "520.895294578143218618", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x05d314204172fcc504522288a9e57449f16486f9", - "etherscanUrl": "https://etherscan.io/address/0x05d314204172fcc504522288a9e57449f16486f9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x349f7c57f5e6f0549a6f3d98c9d3d0f41de72c58", - "balanceRaw": "518374770510764294018", - "balanceFormatted": "518.374770510764294018", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x349f7c57f5e6f0549a6f3d98c9d3d0f41de72c58", - "etherscanUrl": "https://etherscan.io/address/0x349f7c57f5e6f0549a6f3d98c9d3d0f41de72c58", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xafecbbcca163d63f8c4b3b170adb12892a772f33", - "balanceRaw": "516981032664207317147", - "balanceFormatted": "516.981032664207317147", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xafecbbcca163d63f8c4b3b170adb12892a772f33", - "etherscanUrl": "https://etherscan.io/address/0xafecbbcca163d63f8c4b3b170adb12892a772f33", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6a3e98f964e93f183a9e5bb607dd9df75110d2ac", - "balanceRaw": "513334236991072114266", - "balanceFormatted": "513.334236991072114266", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6a3e98f964e93f183a9e5bb607dd9df75110d2ac", - "etherscanUrl": "https://etherscan.io/address/0x6a3e98f964e93f183a9e5bb607dd9df75110d2ac", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_21829", - "balanceRaw": "510066468165369729079", - "balanceFormatted": "510.066468165369729079", - "lastTransferAt": null, - "username": "way", - "displayName": "guozu🎩⛓️🔵 ⚪️🐹", - "fid": 21829, - "followers": 942, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/534442f2-5356-4a22-8dff-a2bf9e620f00/original", - "ensName": "guozu.eth", - "ensAvatarUrl": null, - "primaryAddress": "0x7a566e3cb6060e596961fde5fe36c764a0b7813b", - "etherscanUrl": "https://etherscan.io/address/0x7a566e3cb6060e596961fde5fe36c764a0b7813b", - "xHandle": "lost12306", - "xUrl": "https://twitter.com/lost12306", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8d4328de104bf34b03a070e5da9e2def9784b8f3", - "balanceRaw": "500485528439158934524", - "balanceFormatted": "500.485528439158934524", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8d4328de104bf34b03a070e5da9e2def9784b8f3", - "etherscanUrl": "https://etherscan.io/address/0x8d4328de104bf34b03a070e5da9e2def9784b8f3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_226372", - "balanceRaw": "500046208077319120698", - "balanceFormatted": "500.046208077319120698", - "lastTransferAt": null, - "username": "billbandito", - "displayName": "billbandito 🎩", - "fid": 226372, - "followers": 1310, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ee08238e-8853-4681-bc71-30e3fe211500/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x19c605e89a64b231f1551f72de310e38e9bba5c4", - "etherscanUrl": "https://etherscan.io/address/0x19c605e89a64b231f1551f72de310e38e9bba5c4", - "xHandle": "billbandito", - "xUrl": "https://twitter.com/billbandito", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x82420279fcd0ad0be74096b28b92cc459581d9c2", - "balanceRaw": "500000000000000000000", - "balanceFormatted": "500", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x82420279fcd0ad0be74096b28b92cc459581d9c2", - "etherscanUrl": "https://etherscan.io/address/0x82420279fcd0ad0be74096b28b92cc459581d9c2", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3cfac80fa7402b7a200ac514ef27b8c4b608cddc", - "balanceRaw": "500000000000000000000", - "balanceFormatted": "500", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3cfac80fa7402b7a200ac514ef27b8c4b608cddc", - "etherscanUrl": "https://etherscan.io/address/0x3cfac80fa7402b7a200ac514ef27b8c4b608cddc", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x494dbe5a26948fc9c67a1b22447812d7ac9e9393", - "balanceRaw": "493306343047987957860", - "balanceFormatted": "493.30634304798795786", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x494dbe5a26948fc9c67a1b22447812d7ac9e9393", - "etherscanUrl": "https://etherscan.io/address/0x494dbe5a26948fc9c67a1b22447812d7ac9e9393", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb77d1c34c99a3db22314f512acc3058e550b9bb9", - "balanceRaw": "489612157183034537976", - "balanceFormatted": "489.612157183034537976", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb77d1c34c99a3db22314f512acc3058e550b9bb9", - "etherscanUrl": "https://etherscan.io/address/0xb77d1c34c99a3db22314f512acc3058e550b9bb9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8437abb337b0675c76cefeaef33e10ce2792865b", - "balanceRaw": "488565511948275615402", - "balanceFormatted": "488.565511948275615402", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8437abb337b0675c76cefeaef33e10ce2792865b", - "etherscanUrl": "https://etherscan.io/address/0x8437abb337b0675c76cefeaef33e10ce2792865b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x504ce9e51e508c85a161058c12e970a903d482fc", - "balanceRaw": "479491566800000000000", - "balanceFormatted": "479.4915668", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x504ce9e51e508c85a161058c12e970a903d482fc", - "etherscanUrl": "https://etherscan.io/address/0x504ce9e51e508c85a161058c12e970a903d482fc", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xef6de17ec1a07eb4531b595cf0d8608177b94f9c", - "balanceRaw": "478607790240923064853", - "balanceFormatted": "478.607790240923064853", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xef6de17ec1a07eb4531b595cf0d8608177b94f9c", - "etherscanUrl": "https://etherscan.io/address/0xef6de17ec1a07eb4531b595cf0d8608177b94f9c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5a68a6ea21d542a1182bb1e380978c903e120751", - "balanceRaw": "477104448342325128024", - "balanceFormatted": "477.104448342325128024", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5a68a6ea21d542a1182bb1e380978c903e120751", - "etherscanUrl": "https://etherscan.io/address/0x5a68a6ea21d542a1182bb1e380978c903e120751", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xad69b56ccf3ba166ce81d13c49cfa72ea27339db", - "balanceRaw": "476245935542070300850", - "balanceFormatted": "476.24593554207030085", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xad69b56ccf3ba166ce81d13c49cfa72ea27339db", - "etherscanUrl": "https://etherscan.io/address/0xad69b56ccf3ba166ce81d13c49cfa72ea27339db", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x10638c0da58ad38b0d8cef534bd6ded271700843", - "balanceRaw": "475976858474569905616", - "balanceFormatted": "475.976858474569905616", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x10638c0da58ad38b0d8cef534bd6ded271700843", - "etherscanUrl": "https://etherscan.io/address/0x10638c0da58ad38b0d8cef534bd6ded271700843", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x90632858ac76ac03713e74d1c56d67437d647cee", - "balanceRaw": "473646066711055587983", - "balanceFormatted": "473.646066711055587983", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x90632858ac76ac03713e74d1c56d67437d647cee", - "etherscanUrl": "https://etherscan.io/address/0x90632858ac76ac03713e74d1c56d67437d647cee", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9224cf7956b8787f1e015349ba2937cef29215d8", - "balanceRaw": "460950266046138695105", - "balanceFormatted": "460.950266046138695105", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9224cf7956b8787f1e015349ba2937cef29215d8", - "etherscanUrl": "https://etherscan.io/address/0x9224cf7956b8787f1e015349ba2937cef29215d8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xde7ed18083adeeec6c6c1650d2e9c183d8a33274", - "balanceRaw": "460070036107967565021", - "balanceFormatted": "460.070036107967565021", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xde7ed18083adeeec6c6c1650d2e9c183d8a33274", - "etherscanUrl": "https://etherscan.io/address/0xde7ed18083adeeec6c6c1650d2e9c183d8a33274", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe8d661f2b87f75dddbeca33548d0397270551049", - "balanceRaw": "459457532792815444436", - "balanceFormatted": "459.457532792815444436", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe8d661f2b87f75dddbeca33548d0397270551049", - "etherscanUrl": "https://etherscan.io/address/0xe8d661f2b87f75dddbeca33548d0397270551049", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6652587803f3b8ff1ba810b05effd872390fc217", - "balanceRaw": "459452296085755933601", - "balanceFormatted": "459.452296085755933601", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6652587803f3b8ff1ba810b05effd872390fc217", - "etherscanUrl": "https://etherscan.io/address/0x6652587803f3b8ff1ba810b05effd872390fc217", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd7fcef68dd72b3c205e4e410023dceab1db9b281", - "balanceRaw": "459095684867705394971", - "balanceFormatted": "459.095684867705394971", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd7fcef68dd72b3c205e4e410023dceab1db9b281", - "etherscanUrl": "https://etherscan.io/address/0xd7fcef68dd72b3c205e4e410023dceab1db9b281", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3ad8842eeded3a063d621eeae09216ef5e4f3e95", - "balanceRaw": "458677518061940196982", - "balanceFormatted": "458.677518061940196982", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3ad8842eeded3a063d621eeae09216ef5e4f3e95", - "etherscanUrl": "https://etherscan.io/address/0x3ad8842eeded3a063d621eeae09216ef5e4f3e95", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0c9b201ce29a7bc3f0a37bb1f17899bf5ec83b29", - "balanceRaw": "448486770788543973519", - "balanceFormatted": "448.486770788543973519", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0c9b201ce29a7bc3f0a37bb1f17899bf5ec83b29", - "etherscanUrl": "https://etherscan.io/address/0x0c9b201ce29a7bc3f0a37bb1f17899bf5ec83b29", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x50e6d538dd6bc2d8db28c4840ecdc7bad69c541e", - "balanceRaw": "447979583675361164109", - "balanceFormatted": "447.979583675361164109", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x50e6d538dd6bc2d8db28c4840ecdc7bad69c541e", - "etherscanUrl": "https://etherscan.io/address/0x50e6d538dd6bc2d8db28c4840ecdc7bad69c541e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x99d3c8a836bd24dbbf12e5fe6dfc0519278ed07c", - "balanceRaw": "438674615501312040291", - "balanceFormatted": "438.674615501312040291", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x99d3c8a836bd24dbbf12e5fe6dfc0519278ed07c", - "etherscanUrl": "https://etherscan.io/address/0x99d3c8a836bd24dbbf12e5fe6dfc0519278ed07c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x23029bd661dd7fac3ed8c325ccb73919ab35ed84", - "balanceRaw": "435324101930421872412", - "balanceFormatted": "435.324101930421872412", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x23029bd661dd7fac3ed8c325ccb73919ab35ed84", - "etherscanUrl": "https://etherscan.io/address/0x23029bd661dd7fac3ed8c325ccb73919ab35ed84", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_301292", - "balanceRaw": "431344473808898903542", - "balanceFormatted": "431.344473808898903542", - "lastTransferAt": null, - "username": "multichainmaxi", - "displayName": "Multi Chain Maxi", - "fid": 301292, - "followers": 8, - "pfpUrl": "https://i.imgur.com/fO0bdej.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x06197e5302f24f02117af37aa07fc1b6449b7f94", - "etherscanUrl": "https://etherscan.io/address/0x06197e5302f24f02117af37aa07fc1b6449b7f94", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9cbaa68affcbadd644a5bef878ba3a28258f401c", - "balanceRaw": "430275363432126185323", - "balanceFormatted": "430.275363432126185323", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9cbaa68affcbadd644a5bef878ba3a28258f401c", - "etherscanUrl": "https://etherscan.io/address/0x9cbaa68affcbadd644a5bef878ba3a28258f401c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0cf1611ec22f0e78c31eb1f08fc04b2d619a2326", - "balanceRaw": "424950379625138489938", - "balanceFormatted": "424.950379625138489938", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0cf1611ec22f0e78c31eb1f08fc04b2d619a2326", - "etherscanUrl": "https://etherscan.io/address/0x0cf1611ec22f0e78c31eb1f08fc04b2d619a2326", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5fd0ff1925fd29c13fb4fa45869ace06e56a602a", - "balanceRaw": "421563384775098642066", - "balanceFormatted": "421.563384775098642066", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5fd0ff1925fd29c13fb4fa45869ace06e56a602a", - "etherscanUrl": "https://etherscan.io/address/0x5fd0ff1925fd29c13fb4fa45869ace06e56a602a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_429", - "balanceRaw": "420000000000000000000", - "balanceFormatted": "420", - "lastTransferAt": null, - "username": "czar", - "displayName": "czar ", - "fid": 429, - "followers": 8780, - "pfpUrl": "https://lh3.googleusercontent.com/X0af6snDGRShmvCYTza1s0WXDssHuvXV5lsQOv-8E_dWTlo89CbZ1gd-Ty5yaLNEZ6klrswNjHU_lPybSAep336eDxY9Ik7L0uC8aw", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xec5fe052c043b497dafed0519ce61139996d7d69", - "etherscanUrl": "https://etherscan.io/address/0xec5fe052c043b497dafed0519ce61139996d7d69", - "xHandle": "_bloodbones", - "xUrl": "https://twitter.com/_bloodbones", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5c559f4183b70792ea255f68add1a1c95e98713d", - "balanceRaw": "419747472834937116099", - "balanceFormatted": "419.747472834937116099", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5c559f4183b70792ea255f68add1a1c95e98713d", - "etherscanUrl": "https://etherscan.io/address/0x5c559f4183b70792ea255f68add1a1c95e98713d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb46acf18bbb4e6f745a672ec361c98d6881b6a5b", - "balanceRaw": "419250802749063964612", - "balanceFormatted": "419.250802749063964612", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb46acf18bbb4e6f745a672ec361c98d6881b6a5b", - "etherscanUrl": "https://etherscan.io/address/0xb46acf18bbb4e6f745a672ec361c98d6881b6a5b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x58214b8d884159ecb44bcb2935f0db69b6364bbe", - "balanceRaw": "419240522845936919359", - "balanceFormatted": "419.240522845936919359", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x58214b8d884159ecb44bcb2935f0db69b6364bbe", - "etherscanUrl": "https://etherscan.io/address/0x58214b8d884159ecb44bcb2935f0db69b6364bbe", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3fe643095a759ebfa6186617e341b698c40c5233", - "balanceRaw": "416855702742182796601", - "balanceFormatted": "416.855702742182796601", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3fe643095a759ebfa6186617e341b698c40c5233", - "etherscanUrl": "https://etherscan.io/address/0x3fe643095a759ebfa6186617e341b698c40c5233", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8c9cad8a30e208331b8dcb29641a054b42528524", - "balanceRaw": "416616217497547498606", - "balanceFormatted": "416.616217497547498606", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8c9cad8a30e208331b8dcb29641a054b42528524", - "etherscanUrl": "https://etherscan.io/address/0x8c9cad8a30e208331b8dcb29641a054b42528524", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_347930", - "balanceRaw": "412999263548042992406", - "balanceFormatted": "412.999263548042992406", - "lastTransferAt": null, - "username": "taylorwebb.eth", - "displayName": "TaylorWebb.eth", - "fid": 347930, - "followers": 2619, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/8e3f259c-8536-425d-feee-b84e1f126300/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6dc8f1d4841ebe9b85da130db9918f3cf07c1a9a", - "etherscanUrl": "https://etherscan.io/address/0x6dc8f1d4841ebe9b85da130db9918f3cf07c1a9a", - "xHandle": "taylorwebb_eth", - "xUrl": "https://twitter.com/taylorwebb_eth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x97076d58dcd034c8a53b7d98baa36e1ab5abcdb8", - "balanceRaw": "410834064620244831893", - "balanceFormatted": "410.834064620244831893", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x97076d58dcd034c8a53b7d98baa36e1ab5abcdb8", - "etherscanUrl": "https://etherscan.io/address/0x97076d58dcd034c8a53b7d98baa36e1ab5abcdb8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf7c6104a11429803fd643f9e86c111c11a557926", - "balanceRaw": "410508953071003008398", - "balanceFormatted": "410.508953071003008398", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf7c6104a11429803fd643f9e86c111c11a557926", - "etherscanUrl": "https://etherscan.io/address/0xf7c6104a11429803fd643f9e86c111c11a557926", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9e62f38f948b0ab5d277faa6c379307d98953ada", - "balanceRaw": "409423517790003313910", - "balanceFormatted": "409.42351779000331391", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9e62f38f948b0ab5d277faa6c379307d98953ada", - "etherscanUrl": "https://etherscan.io/address/0x9e62f38f948b0ab5d277faa6c379307d98953ada", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf027f3918887f364a01ef6288780475146cbaec3", - "balanceRaw": "405206149475507714553", - "balanceFormatted": "405.206149475507714553", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf027f3918887f364a01ef6288780475146cbaec3", - "etherscanUrl": "https://etherscan.io/address/0xf027f3918887f364a01ef6288780475146cbaec3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x95dec0ba8ed45d3e91b5e57a7e7d480eb8fb77ef", - "balanceRaw": "403362676113425875045", - "balanceFormatted": "403.362676113425875045", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x95dec0ba8ed45d3e91b5e57a7e7d480eb8fb77ef", - "etherscanUrl": "https://etherscan.io/address/0x95dec0ba8ed45d3e91b5e57a7e7d480eb8fb77ef", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x60f5e75938419667305da249ff9b2ea2985eb082", - "balanceRaw": "401057433532689178119", - "balanceFormatted": "401.057433532689178119", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x60f5e75938419667305da249ff9b2ea2985eb082", - "etherscanUrl": "https://etherscan.io/address/0x60f5e75938419667305da249ff9b2ea2985eb082", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x445f71d5c7b67e24bd54c6b21b5ce66292effbb8", - "balanceRaw": "400850354668297778574", - "balanceFormatted": "400.850354668297778574", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x445f71d5c7b67e24bd54c6b21b5ce66292effbb8", - "etherscanUrl": "https://etherscan.io/address/0x445f71d5c7b67e24bd54c6b21b5ce66292effbb8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x60c84a74b4d44f600e2667d567080c431c60018d", - "balanceRaw": "400000000000000000000", - "balanceFormatted": "400", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x60c84a74b4d44f600e2667d567080c431c60018d", - "etherscanUrl": "https://etherscan.io/address/0x60c84a74b4d44f600e2667d567080c431c60018d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x4123ffc81579e38dfff74b75d8bdf21c41e62237", - "balanceRaw": "400000000000000000000", - "balanceFormatted": "400", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4123ffc81579e38dfff74b75d8bdf21c41e62237", - "etherscanUrl": "https://etherscan.io/address/0x4123ffc81579e38dfff74b75d8bdf21c41e62237", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x891653c071a19d89b737fb440ca7c6d00e55a942", - "balanceRaw": "397422311409048754762", - "balanceFormatted": "397.422311409048754762", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x891653c071a19d89b737fb440ca7c6d00e55a942", - "etherscanUrl": "https://etherscan.io/address/0x891653c071a19d89b737fb440ca7c6d00e55a942", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x43a64c64d773b20b7f817cdd984f71be4da5dc1e", - "balanceRaw": "397091205480959489631", - "balanceFormatted": "397.091205480959489631", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x43a64c64d773b20b7f817cdd984f71be4da5dc1e", - "etherscanUrl": "https://etherscan.io/address/0x43a64c64d773b20b7f817cdd984f71be4da5dc1e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x85b5902236d4b22af643319dca41d6ed7343bb32", - "balanceRaw": "394759102657244267666", - "balanceFormatted": "394.759102657244267666", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x85b5902236d4b22af643319dca41d6ed7343bb32", - "etherscanUrl": "https://etherscan.io/address/0x85b5902236d4b22af643319dca41d6ed7343bb32", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7d839c938b3a16821e01b8e20d3d0d92a6f49f89", - "balanceRaw": "392565424225677947773", - "balanceFormatted": "392.565424225677947773", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7d839c938b3a16821e01b8e20d3d0d92a6f49f89", - "etherscanUrl": "https://etherscan.io/address/0x7d839c938b3a16821e01b8e20d3d0d92a6f49f89", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x325e4c05bc02320636c307c862a96393dd53f34f", - "balanceRaw": "391100000012000000000", - "balanceFormatted": "391.100000012", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x325e4c05bc02320636c307c862a96393dd53f34f", - "etherscanUrl": "https://etherscan.io/address/0x325e4c05bc02320636c307c862a96393dd53f34f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe5379d2df1a854019482b33c77e5c4e4c5d406f8", - "balanceRaw": "390074548518650899201", - "balanceFormatted": "390.074548518650899201", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe5379d2df1a854019482b33c77e5c4e4c5d406f8", - "etherscanUrl": "https://etherscan.io/address/0xe5379d2df1a854019482b33c77e5c4e4c5d406f8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xabe8fe965caafa63a4537b30eaebe4b97af52e43", - "balanceRaw": "388337500000000000000", - "balanceFormatted": "388.3375", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xabe8fe965caafa63a4537b30eaebe4b97af52e43", - "etherscanUrl": "https://etherscan.io/address/0xabe8fe965caafa63a4537b30eaebe4b97af52e43", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x00000b7a7a859f1b7ab55579fd4e7b0b22064f3d", - "balanceRaw": "383768641746989299149", - "balanceFormatted": "383.768641746989299149", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x00000b7a7a859f1b7ab55579fd4e7b0b22064f3d", - "etherscanUrl": "https://etherscan.io/address/0x00000b7a7a859f1b7ab55579fd4e7b0b22064f3d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9ebc7bae8f55d09e28a907c27babdc1451e6f60f", - "balanceRaw": "382952946420123874722", - "balanceFormatted": "382.952946420123874722", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9ebc7bae8f55d09e28a907c27babdc1451e6f60f", - "etherscanUrl": "https://etherscan.io/address/0x9ebc7bae8f55d09e28a907c27babdc1451e6f60f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x4be879f966b863144f1ea478a8cfa3ee58add381", - "balanceRaw": "381049493353829005735", - "balanceFormatted": "381.049493353829005735", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4be879f966b863144f1ea478a8cfa3ee58add381", - "etherscanUrl": "https://etherscan.io/address/0x4be879f966b863144f1ea478a8cfa3ee58add381", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9fe37742b5a4dd3e0d777f9a22acca9d16a2c098", - "balanceRaw": "376136354715678801763", - "balanceFormatted": "376.136354715678801763", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9fe37742b5a4dd3e0d777f9a22acca9d16a2c098", - "etherscanUrl": "https://etherscan.io/address/0x9fe37742b5a4dd3e0d777f9a22acca9d16a2c098", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6834338666bb5dbe61a4f1f64d1fc3241654eace", - "balanceRaw": "375432652572346741039", - "balanceFormatted": "375.432652572346741039", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6834338666bb5dbe61a4f1f64d1fc3241654eace", - "etherscanUrl": "https://etherscan.io/address/0x6834338666bb5dbe61a4f1f64d1fc3241654eace", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe97392dfa4c71c9b0dceb19c6a476835fda56397", - "balanceRaw": "373191986093927214990", - "balanceFormatted": "373.19198609392721499", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe97392dfa4c71c9b0dceb19c6a476835fda56397", - "etherscanUrl": "https://etherscan.io/address/0xe97392dfa4c71c9b0dceb19c6a476835fda56397", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa1d5e7408b6744fa45a488a3c2a179c2e2688685", - "balanceRaw": "366888100441506328398", - "balanceFormatted": "366.888100441506328398", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa1d5e7408b6744fa45a488a3c2a179c2e2688685", - "etherscanUrl": "https://etherscan.io/address/0xa1d5e7408b6744fa45a488a3c2a179c2e2688685", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_274042", - "balanceRaw": "366815729889544047207", - "balanceFormatted": "366.815729889544047207", - "lastTransferAt": null, - "username": "leftyyyy", - "displayName": "leftyyyy", - "fid": 274042, - "followers": 186, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c512c409-a99d-4242-2d70-e5a0d6962700/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8c0b446e33355f5b0325d2aa38e35b760ff6f78c", - "etherscanUrl": "https://etherscan.io/address/0x8c0b446e33355f5b0325d2aa38e35b760ff6f78c", - "xHandle": "leftyyyy_eth", - "xUrl": "https://twitter.com/leftyyyy_eth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_742756", - "balanceRaw": "365000064758755758765", - "balanceFormatted": "365.000064758755758765", - "lastTransferAt": null, - "username": "metamediadao.eth", - "displayName": "MetaMedia", - "fid": 742756, - "followers": 29, - "pfpUrl": "https://supercast.mypinata.cloud/ipfs/QmXigm81EeuV7YRMY3WKYW77DE79jWks28ZknSAsVHdQkU?filename=2025-01-28-18.56.46.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0078ae1617b89bbf219ebee143cad69461805026", - "etherscanUrl": "https://etherscan.io/address/0x0078ae1617b89bbf219ebee143cad69461805026", - "xHandle": "metamediadao", - "xUrl": "https://twitter.com/metamediadao", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x80ed220084d5fc6776afe2ed1e4d46fc85a3a034", - "balanceRaw": "364600000000000119378", - "balanceFormatted": "364.600000000000119378", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x80ed220084d5fc6776afe2ed1e4d46fc85a3a034", - "etherscanUrl": "https://etherscan.io/address/0x80ed220084d5fc6776afe2ed1e4d46fc85a3a034", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xeb7cd9b035f6ef4f05a3b0c818fa1f40eca4518d", - "balanceRaw": "364460854499563013170", - "balanceFormatted": "364.46085449956301317", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xeb7cd9b035f6ef4f05a3b0c818fa1f40eca4518d", - "etherscanUrl": "https://etherscan.io/address/0xeb7cd9b035f6ef4f05a3b0c818fa1f40eca4518d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc964a73bde38e2758fa9638bbf9a54431f31339d", - "balanceRaw": "363561300000000000000", - "balanceFormatted": "363.5613", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc964a73bde38e2758fa9638bbf9a54431f31339d", - "etherscanUrl": "https://etherscan.io/address/0xc964a73bde38e2758fa9638bbf9a54431f31339d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xba404463f73518fea84a3d61ee4c0c6ed897e6ba", - "balanceRaw": "360088847774127307471", - "balanceFormatted": "360.088847774127307471", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xba404463f73518fea84a3d61ee4c0c6ed897e6ba", - "etherscanUrl": "https://etherscan.io/address/0xba404463f73518fea84a3d61ee4c0c6ed897e6ba", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x30db01087febd7b67dc486812b322b4867cb8ca7", - "balanceRaw": "354773877600450615256", - "balanceFormatted": "354.773877600450615256", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x30db01087febd7b67dc486812b322b4867cb8ca7", - "etherscanUrl": "https://etherscan.io/address/0x30db01087febd7b67dc486812b322b4867cb8ca7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_498910", - "balanceRaw": "354742419363510491776", - "balanceFormatted": "354.742419363510491776", - "lastTransferAt": null, - "username": "yobo", - "displayName": "yobo", - "fid": 498910, - "followers": 170, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/edcef297-dcbd-442d-4607-e257ad606200/rectcrop3", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5d17aa6d59ed660b52e459b137ac9834fd18fb58", - "etherscanUrl": "https://etherscan.io/address/0x5d17aa6d59ed660b52e459b137ac9834fd18fb58", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3c84b3b05307d21c4579c13b134efdd4755d2e01", - "balanceRaw": "353225184562365993877", - "balanceFormatted": "353.225184562365993877", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3c84b3b05307d21c4579c13b134efdd4755d2e01", - "etherscanUrl": "https://etherscan.io/address/0x3c84b3b05307d21c4579c13b134efdd4755d2e01", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe1121ccfaa8d454cb77beb38001451b4ebe52445", - "balanceRaw": "351518898752550496966", - "balanceFormatted": "351.518898752550496966", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe1121ccfaa8d454cb77beb38001451b4ebe52445", - "etherscanUrl": "https://etherscan.io/address/0xe1121ccfaa8d454cb77beb38001451b4ebe52445", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xff510f0198686fa4140535ffedd10fa4e7996ac0", - "balanceRaw": "351096156863143361522", - "balanceFormatted": "351.096156863143361522", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xff510f0198686fa4140535ffedd10fa4e7996ac0", - "etherscanUrl": "https://etherscan.io/address/0xff510f0198686fa4140535ffedd10fa4e7996ac0", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x519ebc5eaa5b6e9c05cb238bd81b40d2534b13ab", - "balanceRaw": "350639139053339139219", - "balanceFormatted": "350.639139053339139219", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x519ebc5eaa5b6e9c05cb238bd81b40d2534b13ab", - "etherscanUrl": "https://etherscan.io/address/0x519ebc5eaa5b6e9c05cb238bd81b40d2534b13ab", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x85adc60471886ba677ee561136b9e2c6e577992d", - "balanceRaw": "350507052713221461111", - "balanceFormatted": "350.507052713221461111", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x85adc60471886ba677ee561136b9e2c6e577992d", - "etherscanUrl": "https://etherscan.io/address/0x85adc60471886ba677ee561136b9e2c6e577992d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xadafeae7b38d54b60bb0d4caa6351a128d9a1f0c", - "balanceRaw": "350040507650454149421", - "balanceFormatted": "350.040507650454149421", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xadafeae7b38d54b60bb0d4caa6351a128d9a1f0c", - "etherscanUrl": "https://etherscan.io/address/0xadafeae7b38d54b60bb0d4caa6351a128d9a1f0c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe8f1410bcf8b809c6b26a3fe8330977b0ad7f7ed", - "balanceRaw": "347669143080278718419", - "balanceFormatted": "347.669143080278718419", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe8f1410bcf8b809c6b26a3fe8330977b0ad7f7ed", - "etherscanUrl": "https://etherscan.io/address/0xe8f1410bcf8b809c6b26a3fe8330977b0ad7f7ed", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x1cd308b46ba09f25eb29ad92ad68587b29843db4", - "balanceRaw": "347558801789370153045", - "balanceFormatted": "347.558801789370153045", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1cd308b46ba09f25eb29ad92ad68587b29843db4", - "etherscanUrl": "https://etherscan.io/address/0x1cd308b46ba09f25eb29ad92ad68587b29843db4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x20755f335ed6547dec97fd9193699e771abe9222", - "balanceRaw": "345211873261140244867", - "balanceFormatted": "345.211873261140244867", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x20755f335ed6547dec97fd9193699e771abe9222", - "etherscanUrl": "https://etherscan.io/address/0x20755f335ed6547dec97fd9193699e771abe9222", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x017509667887ae02b114b46dbf81a025e37d1a75", - "balanceRaw": "342443114381422217764", - "balanceFormatted": "342.443114381422217764", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x017509667887ae02b114b46dbf81a025e37d1a75", - "etherscanUrl": "https://etherscan.io/address/0x017509667887ae02b114b46dbf81a025e37d1a75", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3b7eade8739ea9bb9bc3298ba9fb4e7db25adf2a", - "balanceRaw": "333470000000000000000", - "balanceFormatted": "333.47", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3b7eade8739ea9bb9bc3298ba9fb4e7db25adf2a", - "etherscanUrl": "https://etherscan.io/address/0x3b7eade8739ea9bb9bc3298ba9fb4e7db25adf2a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_552113", - "balanceRaw": "332968181099413705597", - "balanceFormatted": "332.968181099413705597", - "lastTransferAt": null, - "username": "everglow", - "displayName": "4u", - "fid": 552113, - "followers": 16, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/46cb9cd3-6935-434b-36d0-1afec2d0ca00/rectcrop3", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x319b587b43f4b28d7bfd526c23b970657b5948c1", - "etherscanUrl": "https://etherscan.io/address/0x319b587b43f4b28d7bfd526c23b970657b5948c1", - "xHandle": "4u4everglow", - "xUrl": "https://twitter.com/4u4everglow", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xeb730c8b66d5136b48c0242f6c2c0f359c752bb2", - "balanceRaw": "330344945728006672636", - "balanceFormatted": "330.344945728006672636", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xeb730c8b66d5136b48c0242f6c2c0f359c752bb2", - "etherscanUrl": "https://etherscan.io/address/0xeb730c8b66d5136b48c0242f6c2c0f359c752bb2", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x411118324f766b4bda7f3b0c03e8c0e37b70feaa", - "balanceRaw": "328916089487422312908", - "balanceFormatted": "328.916089487422312908", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x411118324f766b4bda7f3b0c03e8c0e37b70feaa", - "etherscanUrl": "https://etherscan.io/address/0x411118324f766b4bda7f3b0c03e8c0e37b70feaa", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbf31cafcc018f4bc994b43bcc011f3876d5c862a", - "balanceRaw": "326208625254155353625", - "balanceFormatted": "326.208625254155353625", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbf31cafcc018f4bc994b43bcc011f3876d5c862a", - "etherscanUrl": "https://etherscan.io/address/0xbf31cafcc018f4bc994b43bcc011f3876d5c862a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_415872", - "balanceRaw": "324822883602656179980", - "balanceFormatted": "324.82288360265617998", - "lastTransferAt": null, - "username": "boothtempleton.eth", - "displayName": "Booth Templeton", - "fid": 415872, - "followers": 6802, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/394dba3e-7cbf-4f25-0bfd-bd2d0cf88000/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x460f26b3f84d04521aa8125aeada55046953644d", - "etherscanUrl": "https://etherscan.io/address/0x460f26b3f84d04521aa8125aeada55046953644d", - "xHandle": "boothtempleton", - "xUrl": "https://twitter.com/boothtempleton", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf5c804effad38b4e327fe3f4758668a793828372", - "balanceRaw": "323103356221402678961", - "balanceFormatted": "323.103356221402678961", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf5c804effad38b4e327fe3f4758668a793828372", - "etherscanUrl": "https://etherscan.io/address/0xf5c804effad38b4e327fe3f4758668a793828372", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x186185b4c7290d3e2a58578d497791b2dc7f9563", - "balanceRaw": "321540842081504924748", - "balanceFormatted": "321.540842081504924748", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x186185b4c7290d3e2a58578d497791b2dc7f9563", - "etherscanUrl": "https://etherscan.io/address/0x186185b4c7290d3e2a58578d497791b2dc7f9563", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xaf8ba42f9b239f43c70c7832a72653f853999f73", - "balanceRaw": "314556341728845906855", - "balanceFormatted": "314.556341728845906855", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xaf8ba42f9b239f43c70c7832a72653f853999f73", - "etherscanUrl": "https://etherscan.io/address/0xaf8ba42f9b239f43c70c7832a72653f853999f73", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x78f617fb4636ed0f8139280e0cf02fb7a4e5eb05", - "balanceRaw": "313931448042982315639", - "balanceFormatted": "313.931448042982315639", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x78f617fb4636ed0f8139280e0cf02fb7a4e5eb05", - "etherscanUrl": "https://etherscan.io/address/0x78f617fb4636ed0f8139280e0cf02fb7a4e5eb05", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5546d4f3830836fd0ff1640d789e103284fe680b", - "balanceRaw": "310925369471631888579", - "balanceFormatted": "310.925369471631888579", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5546d4f3830836fd0ff1640d789e103284fe680b", - "etherscanUrl": "https://etherscan.io/address/0x5546d4f3830836fd0ff1640d789e103284fe680b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x544c4ba3cf1620cd56693d6bb4d2c234aea4fda7", - "balanceRaw": "309024012164654768434", - "balanceFormatted": "309.024012164654768434", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x544c4ba3cf1620cd56693d6bb4d2c234aea4fda7", - "etherscanUrl": "https://etherscan.io/address/0x544c4ba3cf1620cd56693d6bb4d2c234aea4fda7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x2c3a895cf6bbfbe88b4a35494d2ff3392971d688", - "balanceRaw": "307284285224241451273", - "balanceFormatted": "307.284285224241451273", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2c3a895cf6bbfbe88b4a35494d2ff3392971d688", - "etherscanUrl": "https://etherscan.io/address/0x2c3a895cf6bbfbe88b4a35494d2ff3392971d688", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_8447", - "balanceRaw": "306160963243797828156", - "balanceFormatted": "306.160963243797828156", - "lastTransferAt": null, - "username": "eriks", - "displayName": "Erik", - "fid": 8447, - "followers": 82073, - "pfpUrl": "https://i.seadn.io/s/raw/files/ad549379a71c7948b405e10d84e5e7b7.png?w=500&auto=format", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x140022b7554d5e5ad88c3148a48a65409c4bcf44", - "etherscanUrl": "https://etherscan.io/address/0x140022b7554d5e5ad88c3148a48a65409c4bcf44", - "xHandle": "erikpsmit", - "xUrl": "https://twitter.com/erikpsmit", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6fe8552fe5f30569f853b3510add745111245529", - "balanceRaw": "304602984240000000000", - "balanceFormatted": "304.60298424", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6fe8552fe5f30569f853b3510add745111245529", - "etherscanUrl": "https://etherscan.io/address/0x6fe8552fe5f30569f853b3510add745111245529", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_13267", - "balanceRaw": "304031285093529440389", - "balanceFormatted": "304.031285093529440389", - "lastTransferAt": null, - "username": "daddysgotit.eth", - "displayName": "cosmic-thinker.eth 🎩", - "fid": 13267, - "followers": 4332, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0f024a9a-4563-4779-afc4-45ac4835bb00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcd1ec7cce2932f5a3d7cf0df558058d343d36e46", - "etherscanUrl": "https://etherscan.io/address/0xcd1ec7cce2932f5a3d7cf0df558058d343d36e46", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8afaae1f3556ae853512b48b53e8d253dd060a20", - "balanceRaw": "303161370563704913845", - "balanceFormatted": "303.161370563704913845", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8afaae1f3556ae853512b48b53e8d253dd060a20", - "etherscanUrl": "https://etherscan.io/address/0x8afaae1f3556ae853512b48b53e8d253dd060a20", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x486e39f9406adb55cff8323626787565cdf4174e", - "balanceRaw": "303052294217971035649", - "balanceFormatted": "303.052294217971035649", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x486e39f9406adb55cff8323626787565cdf4174e", - "etherscanUrl": "https://etherscan.io/address/0x486e39f9406adb55cff8323626787565cdf4174e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_11548", - "balanceRaw": "302357262871500884457", - "balanceFormatted": "302.357262871500884457", - "lastTransferAt": null, - "username": "mcoso.eth", - "displayName": "McOso 🎩🐊", - "fid": 11548, - "followers": 1082, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/642a78f6-8e11-43e2-3bd8-3d14ce87be00/rectcrop3", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0aa9d7cdabe4e035587885338d5aa3615394d22b", - "etherscanUrl": "https://etherscan.io/address/0x0aa9d7cdabe4e035587885338d5aa3615394d22b", - "xHandle": "mcoso_", - "xUrl": "https://twitter.com/mcoso_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x99fc4ab2de4f857cdfc3b9d6f8b7d3cf42c14791", - "balanceRaw": "301450485143980540152", - "balanceFormatted": "301.450485143980540152", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x99fc4ab2de4f857cdfc3b9d6f8b7d3cf42c14791", - "etherscanUrl": "https://etherscan.io/address/0x99fc4ab2de4f857cdfc3b9d6f8b7d3cf42c14791", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf85b95dec02cef1d7a7cb36680bab392e455bddd", - "balanceRaw": "301216441719255784675", - "balanceFormatted": "301.216441719255784675", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf85b95dec02cef1d7a7cb36680bab392e455bddd", - "etherscanUrl": "https://etherscan.io/address/0xf85b95dec02cef1d7a7cb36680bab392e455bddd", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5ebe7a5895f654bcb811db49913a2589d388a104", - "balanceRaw": "301000000000000000000", - "balanceFormatted": "301", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5ebe7a5895f654bcb811db49913a2589d388a104", - "etherscanUrl": "https://etherscan.io/address/0x5ebe7a5895f654bcb811db49913a2589d388a104", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8431702b08b8dced40e137f73c0bfcce4cb96d1f", - "balanceRaw": "300829774799740556968", - "balanceFormatted": "300.829774799740556968", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8431702b08b8dced40e137f73c0bfcce4cb96d1f", - "etherscanUrl": "https://etherscan.io/address/0x8431702b08b8dced40e137f73c0bfcce4cb96d1f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xde0240d607258f07b1d9347996d6fcf03db58643", - "balanceRaw": "300563130000000000000", - "balanceFormatted": "300.56313", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xde0240d607258f07b1d9347996d6fcf03db58643", - "etherscanUrl": "https://etherscan.io/address/0xde0240d607258f07b1d9347996d6fcf03db58643", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x528def993fe84daa25f5f71d0bc1ec07c6bee785", - "balanceRaw": "298688653515760810669", - "balanceFormatted": "298.688653515760810669", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x528def993fe84daa25f5f71d0bc1ec07c6bee785", - "etherscanUrl": "https://etherscan.io/address/0x528def993fe84daa25f5f71d0bc1ec07c6bee785", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_18975", - "balanceRaw": "297572284849799777456", - "balanceFormatted": "297.572284849799777456", - "lastTransferAt": null, - "username": "taliskye", - "displayName": "Drake 🫘", - "fid": 18975, - "followers": 3319, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c0def01a-2297-4a61-f81d-8379a0d54e00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xabdc34f1def5ed2e60f72f34d67567b6d954e7e8", - "etherscanUrl": "https://etherscan.io/address/0xabdc34f1def5ed2e60f72f34d67567b6d954e7e8", - "xHandle": "taliskye_", - "xUrl": "https://twitter.com/taliskye_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1379770", - "balanceRaw": "297432387125312820984", - "balanceFormatted": "297.432387125312820984", - "lastTransferAt": null, - "username": "baseddoctor", - "displayName": "BasedDoctor", - "fid": 1379770, - "followers": 167, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/52fdaf01-0ae0-4185-7d26-8d355c5e9300/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x21234e9620e31cc8e00756012283a708e3e25a75", - "etherscanUrl": "https://etherscan.io/address/0x21234e9620e31cc8e00756012283a708e3e25a75", - "xHandle": "rickgrimesmd", - "xUrl": "https://twitter.com/rickgrimesmd", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9172e60f04381bc62bcce231fa4890c66ac4c792", - "balanceRaw": "295725553140809802704", - "balanceFormatted": "295.725553140809802704", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9172e60f04381bc62bcce231fa4890c66ac4c792", - "etherscanUrl": "https://etherscan.io/address/0x9172e60f04381bc62bcce231fa4890c66ac4c792", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa0efafce355b0d6d1741a9d753d2b49ea1bc73a6", - "balanceRaw": "293756507207948449426", - "balanceFormatted": "293.756507207948449426", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa0efafce355b0d6d1741a9d753d2b49ea1bc73a6", - "etherscanUrl": "https://etherscan.io/address/0xa0efafce355b0d6d1741a9d753d2b49ea1bc73a6", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x71f395d2ebd284238d4cfe9a500d9d2f5e67dbeb", - "balanceRaw": "292958622983866662708", - "balanceFormatted": "292.958622983866662708", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x71f395d2ebd284238d4cfe9a500d9d2f5e67dbeb", - "etherscanUrl": "https://etherscan.io/address/0x71f395d2ebd284238d4cfe9a500d9d2f5e67dbeb", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x1add7d6202989ef4f1e57fedf4090938184a1846", - "balanceRaw": "291212000000000000000", - "balanceFormatted": "291.212", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1add7d6202989ef4f1e57fedf4090938184a1846", - "etherscanUrl": "https://etherscan.io/address/0x1add7d6202989ef4f1e57fedf4090938184a1846", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x2f463642fa60cb1f1fd1c57af25b34b7a9a99586", - "balanceRaw": "290340517584224589167", - "balanceFormatted": "290.340517584224589167", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2f463642fa60cb1f1fd1c57af25b34b7a9a99586", - "etherscanUrl": "https://etherscan.io/address/0x2f463642fa60cb1f1fd1c57af25b34b7a9a99586", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x54a1c84a0d7a07060fd76bb6dd75e73c21e1b616", - "balanceRaw": "289343487751300625732", - "balanceFormatted": "289.343487751300625732", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x54a1c84a0d7a07060fd76bb6dd75e73c21e1b616", - "etherscanUrl": "https://etherscan.io/address/0x54a1c84a0d7a07060fd76bb6dd75e73c21e1b616", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3353b59ce199c667f18fb8c13c640b2ae98273d2", - "balanceRaw": "288813856997943407213", - "balanceFormatted": "288.813856997943407213", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3353b59ce199c667f18fb8c13c640b2ae98273d2", - "etherscanUrl": "https://etherscan.io/address/0x3353b59ce199c667f18fb8c13c640b2ae98273d2", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xabb56620ae17b1a06a11a790d34dc56fc8c3a9d8", - "balanceRaw": "285287366398383649399", - "balanceFormatted": "285.287366398383649399", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xabb56620ae17b1a06a11a790d34dc56fc8c3a9d8", - "etherscanUrl": "https://etherscan.io/address/0xabb56620ae17b1a06a11a790d34dc56fc8c3a9d8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1020", - "balanceRaw": "284701315937205503282", - "balanceFormatted": "284.701315937205503282", - "lastTransferAt": null, - "username": "jake", - "displayName": "JAKE", - "fid": 1020, - "followers": 53043, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/60673c14-95bf-423e-37c4-2a221de62700/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5baa44bb6b7bd79c89628696f1186bcaeb3453aa", - "etherscanUrl": "https://etherscan.io/address/0x5baa44bb6b7bd79c89628696f1186bcaeb3453aa", - "xHandle": "0fjake", - "xUrl": "https://twitter.com/0fjake", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xdd3b529d78492cd6f208e971d55a0ff3d4fd8ee7", - "balanceRaw": "284255411375224459941", - "balanceFormatted": "284.255411375224459941", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdd3b529d78492cd6f208e971d55a0ff3d4fd8ee7", - "etherscanUrl": "https://etherscan.io/address/0xdd3b529d78492cd6f208e971d55a0ff3d4fd8ee7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbe86073a0b68f0d830dbfeded556638b3d6b6bc9", - "balanceRaw": "282000000000000000000", - "balanceFormatted": "282", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbe86073a0b68f0d830dbfeded556638b3d6b6bc9", - "etherscanUrl": "https://etherscan.io/address/0xbe86073a0b68f0d830dbfeded556638b3d6b6bc9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x085d279039128f6dc81857a9eca712799168e361", - "balanceRaw": "277929729751276627432", - "balanceFormatted": "277.929729751276627432", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x085d279039128f6dc81857a9eca712799168e361", - "etherscanUrl": "https://etherscan.io/address/0x085d279039128f6dc81857a9eca712799168e361", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x16882352c7939865a89986e62edb16e7a4455a54", - "balanceRaw": "275907189452043724435", - "balanceFormatted": "275.907189452043724435", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x16882352c7939865a89986e62edb16e7a4455a54", - "etherscanUrl": "https://etherscan.io/address/0x16882352c7939865a89986e62edb16e7a4455a54", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x199b8f1f8e4a71a8b1595aa50daba17970407497", - "balanceRaw": "274336023623895219175", - "balanceFormatted": "274.336023623895219175", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x199b8f1f8e4a71a8b1595aa50daba17970407497", - "etherscanUrl": "https://etherscan.io/address/0x199b8f1f8e4a71a8b1595aa50daba17970407497", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x014973672d3e446e8b4ef14f8ded17dd8d8e4666", - "balanceRaw": "273573953529779462641", - "balanceFormatted": "273.573953529779462641", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x014973672d3e446e8b4ef14f8ded17dd8d8e4666", - "etherscanUrl": "https://etherscan.io/address/0x014973672d3e446e8b4ef14f8ded17dd8d8e4666", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0162b70d56e45a60d8d53257f115481717ea0145", - "balanceRaw": "272790160761608346210", - "balanceFormatted": "272.79016076160834621", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0162b70d56e45a60d8d53257f115481717ea0145", - "etherscanUrl": "https://etherscan.io/address/0x0162b70d56e45a60d8d53257f115481717ea0145", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc5ae2abb47517b65213d4aa9f5f204b758850bbd", - "balanceRaw": "272560230331467542554", - "balanceFormatted": "272.560230331467542554", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc5ae2abb47517b65213d4aa9f5f204b758850bbd", - "etherscanUrl": "https://etherscan.io/address/0xc5ae2abb47517b65213d4aa9f5f204b758850bbd", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x38a27a4dfe08879dee38d1e81a79156653ced953", - "balanceRaw": "272437378379645388856", - "balanceFormatted": "272.437378379645388856", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x38a27a4dfe08879dee38d1e81a79156653ced953", - "etherscanUrl": "https://etherscan.io/address/0x38a27a4dfe08879dee38d1e81a79156653ced953", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5f8db22d78e0217a748d9c4ab7c1f26319e15f86", - "balanceRaw": "271650739252667833204", - "balanceFormatted": "271.650739252667833204", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5f8db22d78e0217a748d9c4ab7c1f26319e15f86", - "etherscanUrl": "https://etherscan.io/address/0x5f8db22d78e0217a748d9c4ab7c1f26319e15f86", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb7f2d25332451bae891250e9e9cce434f9331f32", - "balanceRaw": "271572831048038708496", - "balanceFormatted": "271.572831048038708496", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb7f2d25332451bae891250e9e9cce434f9331f32", - "etherscanUrl": "https://etherscan.io/address/0xb7f2d25332451bae891250e9e9cce434f9331f32", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x27d51aa306e4626ae79b6deb814fc3d0a51eba6b", - "balanceRaw": "271243331086671830952", - "balanceFormatted": "271.243331086671830952", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x27d51aa306e4626ae79b6deb814fc3d0a51eba6b", - "etherscanUrl": "https://etherscan.io/address/0x27d51aa306e4626ae79b6deb814fc3d0a51eba6b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0396b61ea276315c9c3ef23a8a8797e249237168", - "balanceRaw": "270900541742063559143", - "balanceFormatted": "270.900541742063559143", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0396b61ea276315c9c3ef23a8a8797e249237168", - "etherscanUrl": "https://etherscan.io/address/0x0396b61ea276315c9c3ef23a8a8797e249237168", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7b6b670b3a1fc5128ba291f30375f53ce2cdaebb", - "balanceRaw": "269666405130059309362", - "balanceFormatted": "269.666405130059309362", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7b6b670b3a1fc5128ba291f30375f53ce2cdaebb", - "etherscanUrl": "https://etherscan.io/address/0x7b6b670b3a1fc5128ba291f30375f53ce2cdaebb", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_264222", - "balanceRaw": "269530888295152308348", - "balanceFormatted": "269.530888295152308348", - "lastTransferAt": null, - "username": "dialethia.eth", - "displayName": "dialethia", - "fid": 264222, - "followers": 5648, - "pfpUrl": "https://i.imgur.com/2zdKSRq.png", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5955029a2ca306ee068e229622366c44afe54bae", - "etherscanUrl": "https://etherscan.io/address/0x5955029a2ca306ee068e229622366c44afe54bae", - "xHandle": "0xdialethia", - "xUrl": "https://twitter.com/0xdialethia", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0054d246176eddc8df6d439e78507137d7b130ec", - "balanceRaw": "268483821213371990261", - "balanceFormatted": "268.483821213371990261", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0054d246176eddc8df6d439e78507137d7b130ec", - "etherscanUrl": "https://etherscan.io/address/0x0054d246176eddc8df6d439e78507137d7b130ec", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x79bf8fe8f0c6986b447a9394852072542972ce9c", - "balanceRaw": "268292412289764705836", - "balanceFormatted": "268.292412289764705836", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x79bf8fe8f0c6986b447a9394852072542972ce9c", - "etherscanUrl": "https://etherscan.io/address/0x79bf8fe8f0c6986b447a9394852072542972ce9c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x94df44b70179cccbaa182311bbaba63c19d51e16", - "balanceRaw": "266068860230067203057", - "balanceFormatted": "266.068860230067203057", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x94df44b70179cccbaa182311bbaba63c19d51e16", - "etherscanUrl": "https://etherscan.io/address/0x94df44b70179cccbaa182311bbaba63c19d51e16", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0759957bea437af13d81415a1aa471326d0a4d26", - "balanceRaw": "265549935684457582871", - "balanceFormatted": "265.549935684457582871", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0759957bea437af13d81415a1aa471326d0a4d26", - "etherscanUrl": "https://etherscan.io/address/0x0759957bea437af13d81415a1aa471326d0a4d26", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xedd88ca63d7c0b9cf182afdee3852258f31961f9", - "balanceRaw": "264920890595486960362", - "balanceFormatted": "264.920890595486960362", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xedd88ca63d7c0b9cf182afdee3852258f31961f9", - "etherscanUrl": "https://etherscan.io/address/0xedd88ca63d7c0b9cf182afdee3852258f31961f9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe913be52a90a2d5e758aeea9372dce76f41babe9", - "balanceRaw": "264267000000000000000", - "balanceFormatted": "264.267", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe913be52a90a2d5e758aeea9372dce76f41babe9", - "etherscanUrl": "https://etherscan.io/address/0xe913be52a90a2d5e758aeea9372dce76f41babe9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc316d5d47159e2e03f9a53bf0a396341439a6a83", - "balanceRaw": "262386249790439152578", - "balanceFormatted": "262.386249790439152578", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc316d5d47159e2e03f9a53bf0a396341439a6a83", - "etherscanUrl": "https://etherscan.io/address/0xc316d5d47159e2e03f9a53bf0a396341439a6a83", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x760319b01094708792a5125b7526508de11baf86", - "balanceRaw": "258866909618915070712", - "balanceFormatted": "258.866909618915070712", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x760319b01094708792a5125b7526508de11baf86", - "etherscanUrl": "https://etherscan.io/address/0x760319b01094708792a5125b7526508de11baf86", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_18723", - "balanceRaw": "257444963978107264208", - "balanceFormatted": "257.444963978107264208", - "lastTransferAt": null, - "username": "brixbounty", - "displayName": "BrixBountyFarm 🎩", - "fid": 18723, - "followers": 16936, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9f56c07a-c0fd-46aa-f99f-958ab78fee00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc2fef20f7e8d2dd0d27f56389c731bd2943aa43d", - "etherscanUrl": "https://etherscan.io/address/0xc2fef20f7e8d2dd0d27f56389c731bd2943aa43d", - "xHandle": "brix_farm", - "xUrl": "https://twitter.com/brix_farm", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe7f89f11471a735fec2b8a72df3bed4fc82959ec", - "balanceRaw": "257169851934188467482", - "balanceFormatted": "257.169851934188467482", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe7f89f11471a735fec2b8a72df3bed4fc82959ec", - "etherscanUrl": "https://etherscan.io/address/0xe7f89f11471a735fec2b8a72df3bed4fc82959ec", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0b059dbeceb224952dd559738bcea88a4a299d85", - "balanceRaw": "254064035051835468726", - "balanceFormatted": "254.064035051835468726", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0b059dbeceb224952dd559738bcea88a4a299d85", - "etherscanUrl": "https://etherscan.io/address/0x0b059dbeceb224952dd559738bcea88a4a299d85", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x53acecb0a49706ebe52f6c64f0e7ce32a155019a", - "balanceRaw": "252303241645138553598", - "balanceFormatted": "252.303241645138553598", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x53acecb0a49706ebe52f6c64f0e7ce32a155019a", - "etherscanUrl": "https://etherscan.io/address/0x53acecb0a49706ebe52f6c64f0e7ce32a155019a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x700c102d584d6e4d61eaa2282680211e91fb4c6a", - "balanceRaw": "251525801020429344091", - "balanceFormatted": "251.525801020429344091", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x700c102d584d6e4d61eaa2282680211e91fb4c6a", - "etherscanUrl": "https://etherscan.io/address/0x700c102d584d6e4d61eaa2282680211e91fb4c6a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd7e5cf676a0baa36a4ae9fb921deb8c8efc1d873", - "balanceRaw": "250712748020261092118", - "balanceFormatted": "250.712748020261092118", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd7e5cf676a0baa36a4ae9fb921deb8c8efc1d873", - "etherscanUrl": "https://etherscan.io/address/0xd7e5cf676a0baa36a4ae9fb921deb8c8efc1d873", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x040bab597c4eb1d1ba12a3ec0826985879e7a94d", - "balanceRaw": "250674243660744220628", - "balanceFormatted": "250.674243660744220628", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x040bab597c4eb1d1ba12a3ec0826985879e7a94d", - "etherscanUrl": "https://etherscan.io/address/0x040bab597c4eb1d1ba12a3ec0826985879e7a94d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x69371a036f5bf1eb55845351aca64e6d58795978", - "balanceRaw": "250000003887017191170", - "balanceFormatted": "250.00000388701719117", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x69371a036f5bf1eb55845351aca64e6d58795978", - "etherscanUrl": "https://etherscan.io/address/0x69371a036f5bf1eb55845351aca64e6d58795978", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd9c3415bf8600f007a1b4199df967c25a3e00eea", - "balanceRaw": "250000000000000000000", - "balanceFormatted": "250", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd9c3415bf8600f007a1b4199df967c25a3e00eea", - "etherscanUrl": "https://etherscan.io/address/0xd9c3415bf8600f007a1b4199df967c25a3e00eea", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5e6ae23643749280abde5aa6a60ef2e6ddfa7667", - "balanceRaw": "249187116772029829911", - "balanceFormatted": "249.187116772029829911", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5e6ae23643749280abde5aa6a60ef2e6ddfa7667", - "etherscanUrl": "https://etherscan.io/address/0x5e6ae23643749280abde5aa6a60ef2e6ddfa7667", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd24c77a76a3f7240bdd075dd622f104d72491c94", - "balanceRaw": "248272236673200272195", - "balanceFormatted": "248.272236673200272195", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd24c77a76a3f7240bdd075dd622f104d72491c94", - "etherscanUrl": "https://etherscan.io/address/0xd24c77a76a3f7240bdd075dd622f104d72491c94", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x1051a51c5c2016d15228f4fba52935ab44f8a1ee", - "balanceRaw": "248259431666692248465", - "balanceFormatted": "248.259431666692248465", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1051a51c5c2016d15228f4fba52935ab44f8a1ee", - "etherscanUrl": "https://etherscan.io/address/0x1051a51c5c2016d15228f4fba52935ab44f8a1ee", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x24f1d2b31d30b9dc64ca6383dcbf364a917d716b", - "balanceRaw": "247984994351263926843", - "balanceFormatted": "247.984994351263926843", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x24f1d2b31d30b9dc64ca6383dcbf364a917d716b", - "etherscanUrl": "https://etherscan.io/address/0x24f1d2b31d30b9dc64ca6383dcbf364a917d716b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_254680", - "balanceRaw": "246619884747896118729", - "balanceFormatted": "246.619884747896118729", - "lastTransferAt": null, - "username": "blueflame", - "displayName": "BlueFlame", - "fid": 254680, - "followers": 896, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/cff366fe-308e-4a55-89bb-76975c05bc00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x68f84795ca2686abef84197ba9a4f1d5b0c33f9c", - "etherscanUrl": "https://etherscan.io/address/0x68f84795ca2686abef84197ba9a4f1d5b0c33f9c", - "xHandle": "blueflame2021", - "xUrl": "https://twitter.com/blueflame2021", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa0817e753a4dc9431c5ef720212d9ecca3dcc3fb", - "balanceRaw": "246296678493853557881", - "balanceFormatted": "246.296678493853557881", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa0817e753a4dc9431c5ef720212d9ecca3dcc3fb", - "etherscanUrl": "https://etherscan.io/address/0xa0817e753a4dc9431c5ef720212d9ecca3dcc3fb", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8b901516de433f19b7a56e6ac2071460104e7fca", - "balanceRaw": "245905789282567742821", - "balanceFormatted": "245.905789282567742821", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8b901516de433f19b7a56e6ac2071460104e7fca", - "etherscanUrl": "https://etherscan.io/address/0x8b901516de433f19b7a56e6ac2071460104e7fca", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x64f667d79c9a106f26ecfab49e64dd78e7446e6a", - "balanceRaw": "245591000000000000000", - "balanceFormatted": "245.591", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x64f667d79c9a106f26ecfab49e64dd78e7446e6a", - "etherscanUrl": "https://etherscan.io/address/0x64f667d79c9a106f26ecfab49e64dd78e7446e6a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x38770b4df927eb753474790bc10e3e4bbb11b6bf", - "balanceRaw": "245356410817843976122", - "balanceFormatted": "245.356410817843976122", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x38770b4df927eb753474790bc10e3e4bbb11b6bf", - "etherscanUrl": "https://etherscan.io/address/0x38770b4df927eb753474790bc10e3e4bbb11b6bf", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x4920c8652041380075f30fd9e52aac58a3fd61cd", - "balanceRaw": "244156060591898310857", - "balanceFormatted": "244.156060591898310857", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4920c8652041380075f30fd9e52aac58a3fd61cd", - "etherscanUrl": "https://etherscan.io/address/0x4920c8652041380075f30fd9e52aac58a3fd61cd", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x265b3e14ecc4045159d1f360a02b9c860c361631", - "balanceRaw": "243219196789838305019", - "balanceFormatted": "243.219196789838305019", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x265b3e14ecc4045159d1f360a02b9c860c361631", - "etherscanUrl": "https://etherscan.io/address/0x265b3e14ecc4045159d1f360a02b9c860c361631", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x4ff8da11372b0de4427cefdd131009e64878e035", - "balanceRaw": "243196625024879454478", - "balanceFormatted": "243.196625024879454478", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4ff8da11372b0de4427cefdd131009e64878e035", - "etherscanUrl": "https://etherscan.io/address/0x4ff8da11372b0de4427cefdd131009e64878e035", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x73d0ad010cbc5e15bdbc0dbf6b7ca856f93f78a9", - "balanceRaw": "242851246811295596383", - "balanceFormatted": "242.851246811295596383", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x73d0ad010cbc5e15bdbc0dbf6b7ca856f93f78a9", - "etherscanUrl": "https://etherscan.io/address/0x73d0ad010cbc5e15bdbc0dbf6b7ca856f93f78a9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6af6660ed5ecbfa39db3d6bf23e17bd426a183ce", - "balanceRaw": "241722985653162518048", - "balanceFormatted": "241.722985653162518048", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6af6660ed5ecbfa39db3d6bf23e17bd426a183ce", - "etherscanUrl": "https://etherscan.io/address/0x6af6660ed5ecbfa39db3d6bf23e17bd426a183ce", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbee831412ebf5098c20ad64553f4fc7a94daf7fb", - "balanceRaw": "239221262722497618018", - "balanceFormatted": "239.221262722497618018", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbee831412ebf5098c20ad64553f4fc7a94daf7fb", - "etherscanUrl": "https://etherscan.io/address/0xbee831412ebf5098c20ad64553f4fc7a94daf7fb", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa468737f8b5bd46f4a33386d364eabf7e6868689", - "balanceRaw": "238630709707444995444", - "balanceFormatted": "238.630709707444995444", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa468737f8b5bd46f4a33386d364eabf7e6868689", - "etherscanUrl": "https://etherscan.io/address/0xa468737f8b5bd46f4a33386d364eabf7e6868689", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc9050ceade10d17343dd26d2122fbd6b05ff492e", - "balanceRaw": "237500000000000000000", - "balanceFormatted": "237.5", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc9050ceade10d17343dd26d2122fbd6b05ff492e", - "etherscanUrl": "https://etherscan.io/address/0xc9050ceade10d17343dd26d2122fbd6b05ff492e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_4715", - "balanceRaw": "236710200856613595826", - "balanceFormatted": "236.710200856613595826", - "lastTransferAt": null, - "username": "logonaut.eth", - "displayName": "logonaut.eth 🎩🍖↑", - "fid": 4715, - "followers": 7365, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7f0be6da-0fe9-4b50-0207-e0b6d1ce3600/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x70faa7333f352f74452afc24156f171100a0a8ab", - "etherscanUrl": "https://etherscan.io/address/0x70faa7333f352f74452afc24156f171100a0a8ab", - "xHandle": "logonaut", - "xUrl": "https://twitter.com/logonaut", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe22ea5b777f20ffb64e1516661ace5c4e9a1f23d", - "balanceRaw": "236609410504051943994", - "balanceFormatted": "236.609410504051943994", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe22ea5b777f20ffb64e1516661ace5c4e9a1f23d", - "etherscanUrl": "https://etherscan.io/address/0xe22ea5b777f20ffb64e1516661ace5c4e9a1f23d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe73c2cdd538a0e2b709c68c7f99e66d0e30b6e50", - "balanceRaw": "236148720468870921463", - "balanceFormatted": "236.148720468870921463", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe73c2cdd538a0e2b709c68c7f99e66d0e30b6e50", - "etherscanUrl": "https://etherscan.io/address/0xe73c2cdd538a0e2b709c68c7f99e66d0e30b6e50", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x2097121c7d9383d7a3cb71bfee1b7d0dc30b2603", - "balanceRaw": "235736318540366892298", - "balanceFormatted": "235.736318540366892298", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2097121c7d9383d7a3cb71bfee1b7d0dc30b2603", - "etherscanUrl": "https://etherscan.io/address/0x2097121c7d9383d7a3cb71bfee1b7d0dc30b2603", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xfeb9253145cfc1e4266cbbde349adfd3252e4822", - "balanceRaw": "234517675811989955624", - "balanceFormatted": "234.517675811989955624", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfeb9253145cfc1e4266cbbde349adfd3252e4822", - "etherscanUrl": "https://etherscan.io/address/0xfeb9253145cfc1e4266cbbde349adfd3252e4822", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe37fa838dabe3ee31394b620e9ebbc04c7d3494f", - "balanceRaw": "233896641360287618356", - "balanceFormatted": "233.896641360287618356", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe37fa838dabe3ee31394b620e9ebbc04c7d3494f", - "etherscanUrl": "https://etherscan.io/address/0xe37fa838dabe3ee31394b620e9ebbc04c7d3494f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc077b0af5257b1f5bfdf3d33f79a6ada68d01128", - "balanceRaw": "233125238358626785628", - "balanceFormatted": "233.125238358626785628", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc077b0af5257b1f5bfdf3d33f79a6ada68d01128", - "etherscanUrl": "https://etherscan.io/address/0xc077b0af5257b1f5bfdf3d33f79a6ada68d01128", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x2e5a9aaf0eb86bb41cbd0f41fd7020fd4ff663d5", - "balanceRaw": "230731392995052127513", - "balanceFormatted": "230.731392995052127513", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2e5a9aaf0eb86bb41cbd0f41fd7020fd4ff663d5", - "etherscanUrl": "https://etherscan.io/address/0x2e5a9aaf0eb86bb41cbd0f41fd7020fd4ff663d5", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_473136", - "balanceRaw": "229516473511931882271", - "balanceFormatted": "229.516473511931882271", - "lastTransferAt": null, - "username": "thebestpizza.eth", - "displayName": "pizza.base.eth", - "fid": 473136, - "followers": 2747, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d3106f15-94e4-440e-a4d1-83d07a3aaa00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xae9ab8062a3557b88cdd82ef826c448e08adc6cd", - "etherscanUrl": "https://etherscan.io/address/0xae9ab8062a3557b88cdd82ef826c448e08adc6cd", - "xHandle": "defidough", - "xUrl": "https://twitter.com/defidough", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb68646232b416d5a67002c42a30595abbe8667f9", - "balanceRaw": "229369371952589027602", - "balanceFormatted": "229.369371952589027602", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb68646232b416d5a67002c42a30595abbe8667f9", - "etherscanUrl": "https://etherscan.io/address/0xb68646232b416d5a67002c42a30595abbe8667f9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa20d701210f8b883932fb1fa329c5140f1750bc5", - "balanceRaw": "227366622887963130299", - "balanceFormatted": "227.366622887963130299", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa20d701210f8b883932fb1fa329c5140f1750bc5", - "etherscanUrl": "https://etherscan.io/address/0xa20d701210f8b883932fb1fa329c5140f1750bc5", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x398d189847215aad57083c488b867929ab4225cb", - "balanceRaw": "225435278324003469941", - "balanceFormatted": "225.435278324003469941", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x398d189847215aad57083c488b867929ab4225cb", - "etherscanUrl": "https://etherscan.io/address/0x398d189847215aad57083c488b867929ab4225cb", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xafe4043c9ffd31753c5be2b76dfc45aaa70ebd6f", - "balanceRaw": "225000000000000000000", - "balanceFormatted": "225", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xafe4043c9ffd31753c5be2b76dfc45aaa70ebd6f", - "etherscanUrl": "https://etherscan.io/address/0xafe4043c9ffd31753c5be2b76dfc45aaa70ebd6f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9ee7a2ee3a182f9a2fe1632f49fd9d3029faa41c", - "balanceRaw": "224665588586883910281", - "balanceFormatted": "224.665588586883910281", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9ee7a2ee3a182f9a2fe1632f49fd9d3029faa41c", - "etherscanUrl": "https://etherscan.io/address/0x9ee7a2ee3a182f9a2fe1632f49fd9d3029faa41c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6ff21069dce762a9f12b6622c4321731ba19be7b", - "balanceRaw": "223264658376726187835", - "balanceFormatted": "223.264658376726187835", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6ff21069dce762a9f12b6622c4321731ba19be7b", - "etherscanUrl": "https://etherscan.io/address/0x6ff21069dce762a9f12b6622c4321731ba19be7b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5b7a15f1b70f7bacb63779c4cfd543a9e2ba0b0c", - "balanceRaw": "223026255217839868192", - "balanceFormatted": "223.026255217839868192", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5b7a15f1b70f7bacb63779c4cfd543a9e2ba0b0c", - "etherscanUrl": "https://etherscan.io/address/0x5b7a15f1b70f7bacb63779c4cfd543a9e2ba0b0c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf6fb2a7f3f85aaf1bff9205465a051b537a7fd7c", - "balanceRaw": "222126403930674141159", - "balanceFormatted": "222.126403930674141159", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf6fb2a7f3f85aaf1bff9205465a051b537a7fd7c", - "etherscanUrl": "https://etherscan.io/address/0xf6fb2a7f3f85aaf1bff9205465a051b537a7fd7c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_22469", - "balanceRaw": "222043016780913034984", - "balanceFormatted": "222.043016780913034984", - "lastTransferAt": null, - "username": "openai", - "displayName": "closedai", - "fid": 22469, - "followers": 550, - "pfpUrl": "https://i.imgur.com/NgPMSfU.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x931ade3eef687cc2dc7a9705766d1e045c799b69", - "etherscanUrl": "https://etherscan.io/address/0x931ade3eef687cc2dc7a9705766d1e045c799b69", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0c2ba8a4bba6c019a1771c73e304d383da4773ff", - "balanceRaw": "221813966312419892929", - "balanceFormatted": "221.813966312419892929", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0c2ba8a4bba6c019a1771c73e304d383da4773ff", - "etherscanUrl": "https://etherscan.io/address/0x0c2ba8a4bba6c019a1771c73e304d383da4773ff", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x1f238b47ab9fd915e5852601e83c6c1358297870", - "balanceRaw": "221703732270162246524", - "balanceFormatted": "221.703732270162246524", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1f238b47ab9fd915e5852601e83c6c1358297870", - "etherscanUrl": "https://etherscan.io/address/0x1f238b47ab9fd915e5852601e83c6c1358297870", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_14513", - "balanceRaw": "220761725294067369429", - "balanceFormatted": "220.761725294067369429", - "lastTransferAt": null, - "username": "oxdriezhen", - "displayName": "Oxdriezhen", - "fid": 14513, - "followers": 2700, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/fa340ef1-1b62-41d0-32f2-d2e16aac1e00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xae1b8f620f794804f59c09a263ece5d165acdb19", - "etherscanUrl": "https://etherscan.io/address/0xae1b8f620f794804f59c09a263ece5d165acdb19", - "xHandle": "oxdriezhen", - "xUrl": "https://twitter.com/oxdriezhen", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6b6ca48da27304eeeb1d15d355d948f68b852733", - "balanceRaw": "220350205729362352002", - "balanceFormatted": "220.350205729362352002", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6b6ca48da27304eeeb1d15d355d948f68b852733", - "etherscanUrl": "https://etherscan.io/address/0x6b6ca48da27304eeeb1d15d355d948f68b852733", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf6b9751e4a08cdcb893ab64f8ff577de7697a8ed", - "balanceRaw": "220000000000000000000", - "balanceFormatted": "220", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf6b9751e4a08cdcb893ab64f8ff577de7697a8ed", - "etherscanUrl": "https://etherscan.io/address/0xf6b9751e4a08cdcb893ab64f8ff577de7697a8ed", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x427b2cd29f68cd073104552d2bd732d3a8138b0e", - "balanceRaw": "218917222277840158533", - "balanceFormatted": "218.917222277840158533", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x427b2cd29f68cd073104552d2bd732d3a8138b0e", - "etherscanUrl": "https://etherscan.io/address/0x427b2cd29f68cd073104552d2bd732d3a8138b0e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5920482c8a5c532aae791663ac79d1e855ecb496", - "balanceRaw": "218269001522348101591", - "balanceFormatted": "218.269001522348101591", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5920482c8a5c532aae791663ac79d1e855ecb496", - "etherscanUrl": "https://etherscan.io/address/0x5920482c8a5c532aae791663ac79d1e855ecb496", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x47fbbd7af79f53430923bf05220ab3416277b862", - "balanceRaw": "217718616552181407448", - "balanceFormatted": "217.718616552181407448", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x47fbbd7af79f53430923bf05220ab3416277b862", - "etherscanUrl": "https://etherscan.io/address/0x47fbbd7af79f53430923bf05220ab3416277b862", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_3206", - "balanceRaw": "216327457181796910751", - "balanceFormatted": "216.327457181796910751", - "lastTransferAt": null, - "username": "yb", - "displayName": "YB", - "fid": 3206, - "followers": 90640, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7fa32f08-3b7a-41aa-367a-01ca18567e00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xaaac643f572e40e61185d41f67bf8a44cac70df5", - "etherscanUrl": "https://etherscan.io/address/0xaaac643f572e40e61185d41f67bf8a44cac70df5", - "xHandle": "yb_effect", - "xUrl": "https://twitter.com/yb_effect", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x09f60480242fef344e5d8850b372293d8a593c4b", - "balanceRaw": "215000000000000000000", - "balanceFormatted": "215", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x09f60480242fef344e5d8850b372293d8a593c4b", - "etherscanUrl": "https://etherscan.io/address/0x09f60480242fef344e5d8850b372293d8a593c4b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x74cabcfc2f8727f7565e34d9b4199706ca676054", - "balanceRaw": "214938846411135016568", - "balanceFormatted": "214.938846411135016568", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x74cabcfc2f8727f7565e34d9b4199706ca676054", - "etherscanUrl": "https://etherscan.io/address/0x74cabcfc2f8727f7565e34d9b4199706ca676054", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5869458f360d8c1ce49e35fccb3d0a1f25e8d533", - "balanceRaw": "214647527607883060925", - "balanceFormatted": "214.647527607883060925", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5869458f360d8c1ce49e35fccb3d0a1f25e8d533", - "etherscanUrl": "https://etherscan.io/address/0x5869458f360d8c1ce49e35fccb3d0a1f25e8d533", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_532180", - "balanceRaw": "214354178936030999791", - "balanceFormatted": "214.354178936030999791", - "lastTransferAt": null, - "username": "e9fht1p", - "displayName": "eightpap", - "fid": 532180, - "followers": 17, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7f75706d-d9b8-4d4f-5576-aa7a4c53a600/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe8729380d933df3b3859d8e9200510554148bf91", - "etherscanUrl": "https://etherscan.io/address/0xe8729380d933df3b3859d8e9200510554148bf91", - "xHandle": "eightpal1881", - "xUrl": "https://twitter.com/eightpal1881", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0bc2ce997d4a1f35bfd7902f7bcce9c6ae5dc889", - "balanceRaw": "213709958012878874231", - "balanceFormatted": "213.709958012878874231", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0bc2ce997d4a1f35bfd7902f7bcce9c6ae5dc889", - "etherscanUrl": "https://etherscan.io/address/0x0bc2ce997d4a1f35bfd7902f7bcce9c6ae5dc889", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8e0ba5a1b678ecfa08999161d0e4a0383ef7808b", - "balanceRaw": "213370099865258628614", - "balanceFormatted": "213.370099865258628614", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8e0ba5a1b678ecfa08999161d0e4a0383ef7808b", - "etherscanUrl": "https://etherscan.io/address/0x8e0ba5a1b678ecfa08999161d0e4a0383ef7808b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe56c99cb1c72f3605ea65ca3a1fbc093d6bd7f96", - "balanceRaw": "212646777963854742142", - "balanceFormatted": "212.646777963854742142", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe56c99cb1c72f3605ea65ca3a1fbc093d6bd7f96", - "etherscanUrl": "https://etherscan.io/address/0xe56c99cb1c72f3605ea65ca3a1fbc093d6bd7f96", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd38edd95502d866b063321ac0e65dcd692517663", - "balanceRaw": "212365491045807498607", - "balanceFormatted": "212.365491045807498607", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd38edd95502d866b063321ac0e65dcd692517663", - "etherscanUrl": "https://etherscan.io/address/0xd38edd95502d866b063321ac0e65dcd692517663", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa0a43bad9065dff177f9eb4bfb9095cd6e5b427f", - "balanceRaw": "210592062855230308418", - "balanceFormatted": "210.592062855230308418", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa0a43bad9065dff177f9eb4bfb9095cd6e5b427f", - "etherscanUrl": "https://etherscan.io/address/0xa0a43bad9065dff177f9eb4bfb9095cd6e5b427f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x36c04d310e0d3519520d65c1d2b53c8267542b6e", - "balanceRaw": "210410050309575033324", - "balanceFormatted": "210.410050309575033324", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x36c04d310e0d3519520d65c1d2b53c8267542b6e", - "etherscanUrl": "https://etherscan.io/address/0x36c04d310e0d3519520d65c1d2b53c8267542b6e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xae6b64d890a5c0f745c344ffb21f7939da753719", - "balanceRaw": "210400360481966297782", - "balanceFormatted": "210.400360481966297782", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xae6b64d890a5c0f745c344ffb21f7939da753719", - "etherscanUrl": "https://etherscan.io/address/0xae6b64d890a5c0f745c344ffb21f7939da753719", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd97107064b8c7520aae6c6e6ca75bb2aa73b670d", - "balanceRaw": "210081598417061500000", - "balanceFormatted": "210.0815984170615", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd97107064b8c7520aae6c6e6ca75bb2aa73b670d", - "etherscanUrl": "https://etherscan.io/address/0xd97107064b8c7520aae6c6e6ca75bb2aa73b670d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8b877a039aa4a4c039c376bb238a77e1bfeba048", - "balanceRaw": "210011198790000000000", - "balanceFormatted": "210.01119879", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8b877a039aa4a4c039c376bb238a77e1bfeba048", - "etherscanUrl": "https://etherscan.io/address/0x8b877a039aa4a4c039c376bb238a77e1bfeba048", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9bed533bd73286bdedd95b188f878c7bf057c564", - "balanceRaw": "208789059644486346640", - "balanceFormatted": "208.78905964448634664", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9bed533bd73286bdedd95b188f878c7bf057c564", - "etherscanUrl": "https://etherscan.io/address/0x9bed533bd73286bdedd95b188f878c7bf057c564", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xed4c0b24323ebf79a9fa6e7b3bcc57f6c85948be", - "balanceRaw": "208172414282676950201", - "balanceFormatted": "208.172414282676950201", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xed4c0b24323ebf79a9fa6e7b3bcc57f6c85948be", - "etherscanUrl": "https://etherscan.io/address/0xed4c0b24323ebf79a9fa6e7b3bcc57f6c85948be", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7f04235686f24b9abbf968c2afd1c749924585d3", - "balanceRaw": "207481616008941246177", - "balanceFormatted": "207.481616008941246177", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7f04235686f24b9abbf968c2afd1c749924585d3", - "etherscanUrl": "https://etherscan.io/address/0x7f04235686f24b9abbf968c2afd1c749924585d3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_187961", - "balanceRaw": "205489702817123738086", - "balanceFormatted": "205.489702817123738086", - "lastTransferAt": null, - "username": "kripcat.eth", - "displayName": "kripcat.eth", - "fid": 187961, - "followers": 2614, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/76800669-2c78-44d1-c21f-29ef9e90cd00/rectcrop3", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2ac878b9e400f72d16204a99a85aba05f435d83f", - "etherscanUrl": "https://etherscan.io/address/0x2ac878b9e400f72d16204a99a85aba05f435d83f", - "xHandle": "prehensilethumb", - "xUrl": "https://twitter.com/prehensilethumb", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0a2d5d48569a89974022f434fff7b790bb94b6e5", - "balanceRaw": "205078775109861986304", - "balanceFormatted": "205.078775109861986304", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0a2d5d48569a89974022f434fff7b790bb94b6e5", - "etherscanUrl": "https://etherscan.io/address/0x0a2d5d48569a89974022f434fff7b790bb94b6e5", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6856e36e1464af6b94f30bf4b64ccee17453ca94", - "balanceRaw": "204641925529289801728", - "balanceFormatted": "204.641925529289801728", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6856e36e1464af6b94f30bf4b64ccee17453ca94", - "etherscanUrl": "https://etherscan.io/address/0x6856e36e1464af6b94f30bf4b64ccee17453ca94", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_285", - "balanceRaw": "202714981725979739410", - "balanceFormatted": "202.71498172597973941", - "lastTransferAt": null, - "username": "uno", - "displayName": "uno", - "fid": 285, - "followers": 441, - "pfpUrl": "https://i.imgur.com/kEoTsHq.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9edb3c32fa6a1a206df009706cbc369a0835ddf1", - "etherscanUrl": "https://etherscan.io/address/0x9edb3c32fa6a1a206df009706cbc369a0835ddf1", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_241858", - "balanceRaw": "202353811069756997705", - "balanceFormatted": "202.353811069756997705", - "lastTransferAt": null, - "username": "ozzee", - "displayName": "Ozzee", - "fid": 241858, - "followers": 185, - "pfpUrl": "https://ipfs.decentralized-content.com/ipfs/Qmc3zYWXxeTSNwkcMPMVAu1WQemm3tu3fNkm7hfTKrLGh7/5885.png", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xab6ff46c50cccf079bd6c519ccf34c188f80bd72", - "etherscanUrl": "https://etherscan.io/address/0xab6ff46c50cccf079bd6c519ccf34c188f80bd72", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xed8984c3044d59158ce59e87a600b1f1763b469f", - "balanceRaw": "201277068681824231718", - "balanceFormatted": "201.277068681824231718", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xed8984c3044d59158ce59e87a600b1f1763b469f", - "etherscanUrl": "https://etherscan.io/address/0xed8984c3044d59158ce59e87a600b1f1763b469f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6e326d2c636e8b659c864aebc9d46b1c985c9a5c", - "balanceRaw": "201253528040789001693", - "balanceFormatted": "201.253528040789001693", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6e326d2c636e8b659c864aebc9d46b1c985c9a5c", - "etherscanUrl": "https://etherscan.io/address/0x6e326d2c636e8b659c864aebc9d46b1c985c9a5c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd5a64cef6c1ed686f9b648ddb40576e52328f4f7", - "balanceRaw": "201149397668628226054", - "balanceFormatted": "201.149397668628226054", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd5a64cef6c1ed686f9b648ddb40576e52328f4f7", - "etherscanUrl": "https://etherscan.io/address/0xd5a64cef6c1ed686f9b648ddb40576e52328f4f7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_4003", - "balanceRaw": "200959704062570950641", - "balanceFormatted": "200.959704062570950641", - "lastTransferAt": null, - "username": "0xpuneet", - "displayName": "Puneet-Building Clankline", - "fid": 4003, - "followers": 295, - "pfpUrl": "https://i.imgur.com/GOWUa1q.jpg", - "ensName": "puneet.eth", - "ensAvatarUrl": null, - "primaryAddress": "0xe4365a836e842a0fa48d3f260ad3d100695afe32", - "etherscanUrl": "https://etherscan.io/address/0xe4365a836e842a0fa48d3f260ad3d100695afe32", - "xHandle": "pkaura", - "xUrl": "https://twitter.com/pkaura", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xadb060cd145ab4c71cbb317592aaf64df595cc86", - "balanceRaw": "200954366531611604054", - "balanceFormatted": "200.954366531611604054", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xadb060cd145ab4c71cbb317592aaf64df595cc86", - "etherscanUrl": "https://etherscan.io/address/0xadb060cd145ab4c71cbb317592aaf64df595cc86", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x06a7f3c9f03312341c24cd65fe465e369383ed4e", - "balanceRaw": "200693262953900548157", - "balanceFormatted": "200.693262953900548157", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x06a7f3c9f03312341c24cd65fe465e369383ed4e", - "etherscanUrl": "https://etherscan.io/address/0x06a7f3c9f03312341c24cd65fe465e369383ed4e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9e5bc656db2cdef6fc10f9c4332148712613de75", - "balanceRaw": "200606578289334023842", - "balanceFormatted": "200.606578289334023842", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9e5bc656db2cdef6fc10f9c4332148712613de75", - "etherscanUrl": "https://etherscan.io/address/0x9e5bc656db2cdef6fc10f9c4332148712613de75", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1452256", - "balanceRaw": "200566737140364010628", - "balanceFormatted": "200.566737140364010628", - "lastTransferAt": null, - "username": "tommyjohn.base.eth", - "displayName": "Tommy_John", - "fid": 1452256, - "followers": 0, - "pfpUrl": "https://res.cloudinary.com/base-app/image/upload/f_auto/v1762561796/5caf25de-604a-4112-82d5-998e771a340b.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x411a335c0761b4964bf2071f659cec77dd866264", - "etherscanUrl": "https://etherscan.io/address/0x411a335c0761b4964bf2071f659cec77dd866264", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x2ef57bfc83abfb48ee82a323820a3853a0ef34b9", - "balanceRaw": "200180179588272595220", - "balanceFormatted": "200.18017958827259522", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2ef57bfc83abfb48ee82a323820a3853a0ef34b9", - "etherscanUrl": "https://etherscan.io/address/0x2ef57bfc83abfb48ee82a323820a3853a0ef34b9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa2b2d2e259ab4bc7baa2078d597b33ab44d248b8", - "balanceRaw": "200000000739727989024", - "balanceFormatted": "200.000000739727989024", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa2b2d2e259ab4bc7baa2078d597b33ab44d248b8", - "etherscanUrl": "https://etherscan.io/address/0xa2b2d2e259ab4bc7baa2078d597b33ab44d248b8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9dbe078cb4c73069aa155fbbd28ca197755be734", - "balanceRaw": "200000000000000000000", - "balanceFormatted": "200", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9dbe078cb4c73069aa155fbbd28ca197755be734", - "etherscanUrl": "https://etherscan.io/address/0x9dbe078cb4c73069aa155fbbd28ca197755be734", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_400771", - "balanceRaw": "200000000000000000000", - "balanceFormatted": "200", - "lastTransferAt": null, - "username": "ryen", - "displayName": "ryen 🎩", - "fid": 400771, - "followers": 577, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/eb2fc335-a095-4a14-ab67-9c90bf868b00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4d733d31d4723a8357495b56fb5a3852952abf10", - "etherscanUrl": "https://etherscan.io/address/0x4d733d31d4723a8357495b56fb5a3852952abf10", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa3d332bc848afda0121132fb1ba65b25ab9831b5", - "balanceRaw": "199700000000000000000", - "balanceFormatted": "199.7", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa3d332bc848afda0121132fb1ba65b25ab9831b5", - "etherscanUrl": "https://etherscan.io/address/0xa3d332bc848afda0121132fb1ba65b25ab9831b5", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xce6117c74ac2059cf22f86f3559d895d80be2357", - "balanceRaw": "199683033489367030651", - "balanceFormatted": "199.683033489367030651", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xce6117c74ac2059cf22f86f3559d895d80be2357", - "etherscanUrl": "https://etherscan.io/address/0xce6117c74ac2059cf22f86f3559d895d80be2357", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7d998c6abb2f9b80cfe29d2ac7eb2c75c3e1087f", - "balanceRaw": "199559634854937327388", - "balanceFormatted": "199.559634854937327388", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7d998c6abb2f9b80cfe29d2ac7eb2c75c3e1087f", - "etherscanUrl": "https://etherscan.io/address/0x7d998c6abb2f9b80cfe29d2ac7eb2c75c3e1087f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3bcfabbc817962aecd6e86a6ead6726c099372ef", - "balanceRaw": "199397752962420320947", - "balanceFormatted": "199.397752962420320947", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3bcfabbc817962aecd6e86a6ead6726c099372ef", - "etherscanUrl": "https://etherscan.io/address/0x3bcfabbc817962aecd6e86a6ead6726c099372ef", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xfc8e410501fa7213c1efec6fe497262355b1f063", - "balanceRaw": "199001372190080407542", - "balanceFormatted": "199.001372190080407542", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfc8e410501fa7213c1efec6fe497262355b1f063", - "etherscanUrl": "https://etherscan.io/address/0xfc8e410501fa7213c1efec6fe497262355b1f063", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x06b6a61e22a263c61adf15a83b349ba05ff30e48", - "balanceRaw": "198725728346209245742", - "balanceFormatted": "198.725728346209245742", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x06b6a61e22a263c61adf15a83b349ba05ff30e48", - "etherscanUrl": "https://etherscan.io/address/0x06b6a61e22a263c61adf15a83b349ba05ff30e48", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xcdec866689af5eca60da47733c669fc44dc4500c", - "balanceRaw": "198689191049703624858", - "balanceFormatted": "198.689191049703624858", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcdec866689af5eca60da47733c669fc44dc4500c", - "etherscanUrl": "https://etherscan.io/address/0xcdec866689af5eca60da47733c669fc44dc4500c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_10178", - "balanceRaw": "198386635077522450000", - "balanceFormatted": "198.38663507752245", - "lastTransferAt": null, - "username": "owl", - "displayName": "Hoot 🎩", - "fid": 10178, - "followers": 10149, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3b534cba-af6a-425f-145a-54653d0ac500/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe19261c53d9d6c6d63bc248de3c2e5d5938f24d6", - "etherscanUrl": "https://etherscan.io/address/0xe19261c53d9d6c6d63bc248de3c2e5d5938f24d6", - "xHandle": "mediaquery", - "xUrl": "https://twitter.com/mediaquery", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa4252055fd9df90925018c6f783a0c2b6bc04ac2", - "balanceRaw": "197693364543458153436", - "balanceFormatted": "197.693364543458153436", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa4252055fd9df90925018c6f783a0c2b6bc04ac2", - "etherscanUrl": "https://etherscan.io/address/0xa4252055fd9df90925018c6f783a0c2b6bc04ac2", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xfd3dd3b9fd276d4e1fbc0b6133481d0e953bd703", - "balanceRaw": "196591450854701927624", - "balanceFormatted": "196.591450854701927624", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfd3dd3b9fd276d4e1fbc0b6133481d0e953bd703", - "etherscanUrl": "https://etherscan.io/address/0xfd3dd3b9fd276d4e1fbc0b6133481d0e953bd703", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x475db33497d71259813097efc470683865c37a19", - "balanceRaw": "196423858650454460308", - "balanceFormatted": "196.423858650454460308", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x475db33497d71259813097efc470683865c37a19", - "etherscanUrl": "https://etherscan.io/address/0x475db33497d71259813097efc470683865c37a19", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_379794", - "balanceRaw": "195292613394875093009", - "balanceFormatted": "195.292613394875093009", - "lastTransferAt": null, - "username": "c8ake", - "displayName": "c8ake", - "fid": 379794, - "followers": 165, - "pfpUrl": "https://i.imgur.com/KDn1S2m.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x29121a060e1c4720607c10ebba08af3d7485d452", - "etherscanUrl": "https://etherscan.io/address/0x29121a060e1c4720607c10ebba08af3d7485d452", - "xHandle": "c8ake", - "xUrl": "https://twitter.com/c8ake", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xdca05f7ed88abc808941f68132a139d7c856591a", - "balanceRaw": "194875527054260852759", - "balanceFormatted": "194.875527054260852759", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdca05f7ed88abc808941f68132a139d7c856591a", - "etherscanUrl": "https://etherscan.io/address/0xdca05f7ed88abc808941f68132a139d7c856591a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5eacd1bf7a2d7c22160a05ece74a5ff1cf08f8ee", - "balanceRaw": "193000000000000000000", - "balanceFormatted": "193", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5eacd1bf7a2d7c22160a05ece74a5ff1cf08f8ee", - "etherscanUrl": "https://etherscan.io/address/0x5eacd1bf7a2d7c22160a05ece74a5ff1cf08f8ee", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x1674613b9652fa24b236b45980871934d95bea20", - "balanceRaw": "190296455559285669482", - "balanceFormatted": "190.296455559285669482", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1674613b9652fa24b236b45980871934d95bea20", - "etherscanUrl": "https://etherscan.io/address/0x1674613b9652fa24b236b45980871934d95bea20", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5df1454be7446c97e8b0cfcb00c6dc9aea8580d0", - "balanceRaw": "189722514456741809495", - "balanceFormatted": "189.722514456741809495", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5df1454be7446c97e8b0cfcb00c6dc9aea8580d0", - "etherscanUrl": "https://etherscan.io/address/0x5df1454be7446c97e8b0cfcb00c6dc9aea8580d0", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa35d130d23fb53630de490d921737a48d3631427", - "balanceRaw": "189424380327358670660", - "balanceFormatted": "189.42438032735867066", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa35d130d23fb53630de490d921737a48d3631427", - "etherscanUrl": "https://etherscan.io/address/0xa35d130d23fb53630de490d921737a48d3631427", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xaf04317585e439b93987c297178ca62a1e04f6d0", - "balanceRaw": "189369833145754927792", - "balanceFormatted": "189.369833145754927792", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xaf04317585e439b93987c297178ca62a1e04f6d0", - "etherscanUrl": "https://etherscan.io/address/0xaf04317585e439b93987c297178ca62a1e04f6d0", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf3b57de1fa1f82da73c6f34bfdcb303881a099cb", - "balanceRaw": "188595946401948045645", - "balanceFormatted": "188.595946401948045645", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf3b57de1fa1f82da73c6f34bfdcb303881a099cb", - "etherscanUrl": "https://etherscan.io/address/0xf3b57de1fa1f82da73c6f34bfdcb303881a099cb", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x22c64e05aabde039a7c9792d6886d8c2d714b2e9", - "balanceRaw": "188027628064986835162", - "balanceFormatted": "188.027628064986835162", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x22c64e05aabde039a7c9792d6886d8c2d714b2e9", - "etherscanUrl": "https://etherscan.io/address/0x22c64e05aabde039a7c9792d6886d8c2d714b2e9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7098d5ee6c5f1d1641f3a872e9acf0b97cc8aa75", - "balanceRaw": "187765504109214880965", - "balanceFormatted": "187.765504109214880965", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7098d5ee6c5f1d1641f3a872e9acf0b97cc8aa75", - "etherscanUrl": "https://etherscan.io/address/0x7098d5ee6c5f1d1641f3a872e9acf0b97cc8aa75", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb1e910c354ee07e7162e708eba7a89d9b01e19a1", - "balanceRaw": "187200939530803199627", - "balanceFormatted": "187.200939530803199627", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb1e910c354ee07e7162e708eba7a89d9b01e19a1", - "etherscanUrl": "https://etherscan.io/address/0xb1e910c354ee07e7162e708eba7a89d9b01e19a1", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_21941", - "balanceRaw": "185160743466760308282", - "balanceFormatted": "185.160743466760308282", - "lastTransferAt": null, - "username": "hipseynussle.eth", - "displayName": "HipseyNussle.eth ツ 🦉🎩", - "fid": 21941, - "followers": 1183, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/62f284fc-5777-4d26-497f-2038455f2100/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3864689ecf3dcc903c1514b9a48e4c68f6df74fd", - "etherscanUrl": "https://etherscan.io/address/0x3864689ecf3dcc903c1514b9a48e4c68f6df74fd", - "xHandle": "thehipseynussle", - "xUrl": "https://twitter.com/thehipseynussle", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbc98fba393c9a42c0cae2e32eccf5b2fb0a060d1", - "balanceRaw": "185115203074559575939", - "balanceFormatted": "185.115203074559575939", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbc98fba393c9a42c0cae2e32eccf5b2fb0a060d1", - "etherscanUrl": "https://etherscan.io/address/0xbc98fba393c9a42c0cae2e32eccf5b2fb0a060d1", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xad3586798ab36f0763e9ba5b601d8f0f485a47ef", - "balanceRaw": "184749120047453784152", - "balanceFormatted": "184.749120047453784152", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xad3586798ab36f0763e9ba5b601d8f0f485a47ef", - "etherscanUrl": "https://etherscan.io/address/0xad3586798ab36f0763e9ba5b601d8f0f485a47ef", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe798a9404143f1aba9d751e2454cb5c1a573363e", - "balanceRaw": "184361000000000000000", - "balanceFormatted": "184.361", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe798a9404143f1aba9d751e2454cb5c1a573363e", - "etherscanUrl": "https://etherscan.io/address/0xe798a9404143f1aba9d751e2454cb5c1a573363e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xebbdacfc5d313efad720a66d125df338d850dd60", - "balanceRaw": "183032386490564738491", - "balanceFormatted": "183.032386490564738491", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xebbdacfc5d313efad720a66d125df338d850dd60", - "etherscanUrl": "https://etherscan.io/address/0xebbdacfc5d313efad720a66d125df338d850dd60", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xafdda377ba687e615ff9c709f40a4aa166f9fbca", - "balanceRaw": "182253778753693084249", - "balanceFormatted": "182.253778753693084249", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xafdda377ba687e615ff9c709f40a4aa166f9fbca", - "etherscanUrl": "https://etherscan.io/address/0xafdda377ba687e615ff9c709f40a4aa166f9fbca", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe73aaf36edd9bde33dbf9eb42047d326333319e0", - "balanceRaw": "180530702743633378675", - "balanceFormatted": "180.530702743633378675", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe73aaf36edd9bde33dbf9eb42047d326333319e0", - "etherscanUrl": "https://etherscan.io/address/0xe73aaf36edd9bde33dbf9eb42047d326333319e0", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe6c3c2f2027f6a49bae108a73053b313139dcb90", - "balanceRaw": "180438693875480017472", - "balanceFormatted": "180.438693875480017472", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe6c3c2f2027f6a49bae108a73053b313139dcb90", - "etherscanUrl": "https://etherscan.io/address/0xe6c3c2f2027f6a49bae108a73053b313139dcb90", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xab2dcf4a68fc91d099fee8818141fc748da5381d", - "balanceRaw": "179871669685476459050", - "balanceFormatted": "179.87166968547645905", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xab2dcf4a68fc91d099fee8818141fc748da5381d", - "etherscanUrl": "https://etherscan.io/address/0xab2dcf4a68fc91d099fee8818141fc748da5381d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_534695", - "balanceRaw": "179800070351791489024", - "balanceFormatted": "179.800070351791489024", - "lastTransferAt": null, - "username": "dkqid", - "displayName": "djqid", - "fid": 534695, - "followers": 27, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b58e577e-3eaa-412a-9382-1904212df300/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa20e776fcdaf6365045aec42d5bcc20cc350baf3", - "etherscanUrl": "https://etherscan.io/address/0xa20e776fcdaf6365045aec42d5bcc20cc350baf3", - "xHandle": "leejame385", - "xUrl": "https://twitter.com/leejame385", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x853d61d82842f93a15b185d211ce652a7a5203e9", - "balanceRaw": "179629155807567480484", - "balanceFormatted": "179.629155807567480484", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x853d61d82842f93a15b185d211ce652a7a5203e9", - "etherscanUrl": "https://etherscan.io/address/0x853d61d82842f93a15b185d211ce652a7a5203e9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_3065", - "balanceRaw": "178260158382193505758", - "balanceFormatted": "178.260158382193505758", - "lastTransferAt": null, - "username": "roam0x", - "displayName": "roamonchain", - "fid": 3065, - "followers": 211, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f884d900-d963-462a-fd62-8fad6376f700/rectcrop3", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9c7e79a9dba40ead9cc4a6bc5c22ecaca11b8364", - "etherscanUrl": "https://etherscan.io/address/0x9c7e79a9dba40ead9cc4a6bc5c22ecaca11b8364", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7ae686db9af3d6c0a288a13effc56a243bd63499", - "balanceRaw": "175546643047213624186", - "balanceFormatted": "175.546643047213624186", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7ae686db9af3d6c0a288a13effc56a243bd63499", - "etherscanUrl": "https://etherscan.io/address/0x7ae686db9af3d6c0a288a13effc56a243bd63499", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x05e8dd1f80323ce2dc72d59165633063e03fcf67", - "balanceRaw": "175429831961376094315", - "balanceFormatted": "175.429831961376094315", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x05e8dd1f80323ce2dc72d59165633063e03fcf67", - "etherscanUrl": "https://etherscan.io/address/0x05e8dd1f80323ce2dc72d59165633063e03fcf67", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x90558ee553cfa1fa84460914c006f0faacd995af", - "balanceRaw": "175001613886137743158", - "balanceFormatted": "175.001613886137743158", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x90558ee553cfa1fa84460914c006f0faacd995af", - "etherscanUrl": "https://etherscan.io/address/0x90558ee553cfa1fa84460914c006f0faacd995af", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x4b5ec54ee662190451a03bf304cf1bd7bb4c5323", - "balanceRaw": "174324000000000000000", - "balanceFormatted": "174.324", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4b5ec54ee662190451a03bf304cf1bd7bb4c5323", - "etherscanUrl": "https://etherscan.io/address/0x4b5ec54ee662190451a03bf304cf1bd7bb4c5323", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3c81d333a9a8bff97132e75f45ac34dde8bb0524", - "balanceRaw": "172482002767202971061", - "balanceFormatted": "172.482002767202971061", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3c81d333a9a8bff97132e75f45ac34dde8bb0524", - "etherscanUrl": "https://etherscan.io/address/0x3c81d333a9a8bff97132e75f45ac34dde8bb0524", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xee2971eb3e15a3ca069cae540c3c6d8f4e5bb622", - "balanceRaw": "172000000000000000000", - "balanceFormatted": "172", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xee2971eb3e15a3ca069cae540c3c6d8f4e5bb622", - "etherscanUrl": "https://etherscan.io/address/0xee2971eb3e15a3ca069cae540c3c6d8f4e5bb622", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6852d09c25af748b29b9ec3bb1fe24fbd36bfca4", - "balanceRaw": "171721555804416338884", - "balanceFormatted": "171.721555804416338884", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6852d09c25af748b29b9ec3bb1fe24fbd36bfca4", - "etherscanUrl": "https://etherscan.io/address/0x6852d09c25af748b29b9ec3bb1fe24fbd36bfca4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x48522240da553d522d7e7b3a5519fd902742e283", - "balanceRaw": "171589897596763467577", - "balanceFormatted": "171.589897596763467577", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x48522240da553d522d7e7b3a5519fd902742e283", - "etherscanUrl": "https://etherscan.io/address/0x48522240da553d522d7e7b3a5519fd902742e283", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x321a0de4ade71c59ca759b90c14ad4e215c43ae6", - "balanceRaw": "171429097720641043776", - "balanceFormatted": "171.429097720641043776", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x321a0de4ade71c59ca759b90c14ad4e215c43ae6", - "etherscanUrl": "https://etherscan.io/address/0x321a0de4ade71c59ca759b90c14ad4e215c43ae6", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8d073291358c5872497d0ecffacdd4ac3e1b48b0", - "balanceRaw": "170430917605030775907", - "balanceFormatted": "170.430917605030775907", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8d073291358c5872497d0ecffacdd4ac3e1b48b0", - "etherscanUrl": "https://etherscan.io/address/0x8d073291358c5872497d0ecffacdd4ac3e1b48b0", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5fbeb5e603c80433339f699903372e45a525c756", - "balanceRaw": "169130625900000000000", - "balanceFormatted": "169.1306259", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5fbeb5e603c80433339f699903372e45a525c756", - "etherscanUrl": "https://etherscan.io/address/0x5fbeb5e603c80433339f699903372e45a525c756", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9f534754f763eea62e9750641f8604ebe389b8a7", - "balanceRaw": "168789433912755629056", - "balanceFormatted": "168.789433912755629056", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9f534754f763eea62e9750641f8604ebe389b8a7", - "etherscanUrl": "https://etherscan.io/address/0x9f534754f763eea62e9750641f8604ebe389b8a7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa61d0dcc56e42ffab4f27140172067cf6dba7f6b", - "balanceRaw": "168667269366424024679", - "balanceFormatted": "168.667269366424024679", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa61d0dcc56e42ffab4f27140172067cf6dba7f6b", - "etherscanUrl": "https://etherscan.io/address/0xa61d0dcc56e42ffab4f27140172067cf6dba7f6b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc43ca08cefdb3aefb37e3516cb0ada4575b2f9b8", - "balanceRaw": "167678701614870816575", - "balanceFormatted": "167.678701614870816575", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc43ca08cefdb3aefb37e3516cb0ada4575b2f9b8", - "etherscanUrl": "https://etherscan.io/address/0xc43ca08cefdb3aefb37e3516cb0ada4575b2f9b8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x105778eae2172309f5fbe99888a9ab4ed8ecbbaf", - "balanceRaw": "167525499337032654673", - "balanceFormatted": "167.525499337032654673", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x105778eae2172309f5fbe99888a9ab4ed8ecbbaf", - "etherscanUrl": "https://etherscan.io/address/0x105778eae2172309f5fbe99888a9ab4ed8ecbbaf", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa1470f85c7ef3883694ccb8f26b16604216ba215", - "balanceRaw": "167422197913869731474", - "balanceFormatted": "167.422197913869731474", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa1470f85c7ef3883694ccb8f26b16604216ba215", - "etherscanUrl": "https://etherscan.io/address/0xa1470f85c7ef3883694ccb8f26b16604216ba215", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x28eeaa69ee51b816733f360f86e2ab51bbc6b5e4", - "balanceRaw": "166907392147780460844", - "balanceFormatted": "166.907392147780460844", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x28eeaa69ee51b816733f360f86e2ab51bbc6b5e4", - "etherscanUrl": "https://etherscan.io/address/0x28eeaa69ee51b816733f360f86e2ab51bbc6b5e4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x99b98b80d17e07dfb03407ba60de4e9ea3a28b5b", - "balanceRaw": "165725918583334143321", - "balanceFormatted": "165.725918583334143321", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x99b98b80d17e07dfb03407ba60de4e9ea3a28b5b", - "etherscanUrl": "https://etherscan.io/address/0x99b98b80d17e07dfb03407ba60de4e9ea3a28b5b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbed3a6d9276e00af377191e2a0eb6db7d16b2a3d", - "balanceRaw": "165646072239483040137", - "balanceFormatted": "165.646072239483040137", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbed3a6d9276e00af377191e2a0eb6db7d16b2a3d", - "etherscanUrl": "https://etherscan.io/address/0xbed3a6d9276e00af377191e2a0eb6db7d16b2a3d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb9925bf1f0c6cdbbaf7da11ccf1a552efe1b049f", - "balanceRaw": "163635856163928155809", - "balanceFormatted": "163.635856163928155809", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb9925bf1f0c6cdbbaf7da11ccf1a552efe1b049f", - "etherscanUrl": "https://etherscan.io/address/0xb9925bf1f0c6cdbbaf7da11ccf1a552efe1b049f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x058de7c6e46d8b7c505442455e7f089fbd1cbb1c", - "balanceRaw": "162964825236802817824", - "balanceFormatted": "162.964825236802817824", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x058de7c6e46d8b7c505442455e7f089fbd1cbb1c", - "etherscanUrl": "https://etherscan.io/address/0x058de7c6e46d8b7c505442455e7f089fbd1cbb1c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x831769afc80fff6fb4d3e25e7bff2f7d6fb45862", - "balanceRaw": "161358131384915323255", - "balanceFormatted": "161.358131384915323255", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x831769afc80fff6fb4d3e25e7bff2f7d6fb45862", - "etherscanUrl": "https://etherscan.io/address/0x831769afc80fff6fb4d3e25e7bff2f7d6fb45862", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6c008e6565bcbed9b697e751170b17fcbac790fb", - "balanceRaw": "161354868158109701577", - "balanceFormatted": "161.354868158109701577", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6c008e6565bcbed9b697e751170b17fcbac790fb", - "etherscanUrl": "https://etherscan.io/address/0x6c008e6565bcbed9b697e751170b17fcbac790fb", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_366587", - "balanceRaw": "161178748941128780900", - "balanceFormatted": "161.1787489411287809", - "lastTransferAt": null, - "username": "jkm.eth", - "displayName": "James M 🌒🎩", - "fid": 366587, - "followers": 245, - "pfpUrl": "https://i.imgur.com/LcOdmx5.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdcf56f7856b1c40e933d5a93a20ad44c74d46f89", - "etherscanUrl": "https://etherscan.io/address/0xdcf56f7856b1c40e933d5a93a20ad44c74d46f89", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x038b30a95daaf3a4f5a938d268f50a1397e4f378", - "balanceRaw": "160960447371702304964", - "balanceFormatted": "160.960447371702304964", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x038b30a95daaf3a4f5a938d268f50a1397e4f378", - "etherscanUrl": "https://etherscan.io/address/0x038b30a95daaf3a4f5a938d268f50a1397e4f378", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9beae70b01d0aa17d106693e2cd3c6a19cd712b3", - "balanceRaw": "160613259074282511874", - "balanceFormatted": "160.613259074282511874", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9beae70b01d0aa17d106693e2cd3c6a19cd712b3", - "etherscanUrl": "https://etherscan.io/address/0x9beae70b01d0aa17d106693e2cd3c6a19cd712b3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x1286ca25f8d2371059242b6eee2e6c8c0e7e47e5", - "balanceRaw": "160344000000000000000", - "balanceFormatted": "160.344", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1286ca25f8d2371059242b6eee2e6c8c0e7e47e5", - "etherscanUrl": "https://etherscan.io/address/0x1286ca25f8d2371059242b6eee2e6c8c0e7e47e5", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x02b0c14d2c03852ccebd1b2e66ab3c075a57711d", - "balanceRaw": "160031570292440228043", - "balanceFormatted": "160.031570292440228043", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x02b0c14d2c03852ccebd1b2e66ab3c075a57711d", - "etherscanUrl": "https://etherscan.io/address/0x02b0c14d2c03852ccebd1b2e66ab3c075a57711d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_534662", - "balanceRaw": "159452253898590805082", - "balanceFormatted": "159.452253898590805082", - "lastTransferAt": null, - "username": "thr33hm", - "displayName": "hmson", - "fid": 534662, - "followers": 5, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ce9f4d49-2656-444b-a844-85ebe28bae00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc356eeb26eb26103126ac319a2f09b20b5782929", - "etherscanUrl": "https://etherscan.io/address/0xc356eeb26eb26103126ac319a2f09b20b5782929", - "xHandle": "sonhm20", - "xUrl": "https://twitter.com/sonhm20", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb36b97ae57e6579c81703a481421f08536e32d61", - "balanceRaw": "158990086151316785925", - "balanceFormatted": "158.990086151316785925", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb36b97ae57e6579c81703a481421f08536e32d61", - "etherscanUrl": "https://etherscan.io/address/0xb36b97ae57e6579c81703a481421f08536e32d61", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe866d6c6b3bf8dcddb7d358525d00313bc13bf0f", - "balanceRaw": "158814999510641434089", - "balanceFormatted": "158.814999510641434089", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe866d6c6b3bf8dcddb7d358525d00313bc13bf0f", - "etherscanUrl": "https://etherscan.io/address/0xe866d6c6b3bf8dcddb7d358525d00313bc13bf0f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1442741", - "balanceRaw": "158630809676277272050", - "balanceFormatted": "158.63080967627727205", - "lastTransferAt": null, - "username": "!1442741", - "displayName": null, - "fid": 1442741, - "followers": 0, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe99f07e99e8a2b6572cd13c6c935ba15491f6ee9", - "etherscanUrl": "https://etherscan.io/address/0xe99f07e99e8a2b6572cd13c6c935ba15491f6ee9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf1bccae559b3e045546514634141f51dd88caede", - "balanceRaw": "158457574618833988498", - "balanceFormatted": "158.457574618833988498", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf1bccae559b3e045546514634141f51dd88caede", - "etherscanUrl": "https://etherscan.io/address/0xf1bccae559b3e045546514634141f51dd88caede", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xac30bd0389b6c21cb1dd3aa7743654246c2b3f44", - "balanceRaw": "158274311909841407773", - "balanceFormatted": "158.274311909841407773", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xac30bd0389b6c21cb1dd3aa7743654246c2b3f44", - "etherscanUrl": "https://etherscan.io/address/0xac30bd0389b6c21cb1dd3aa7743654246c2b3f44", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd06d3098cf06ae4cdabe03d12b6cf9878528c613", - "balanceRaw": "158266577273077956957", - "balanceFormatted": "158.266577273077956957", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd06d3098cf06ae4cdabe03d12b6cf9878528c613", - "etherscanUrl": "https://etherscan.io/address/0xd06d3098cf06ae4cdabe03d12b6cf9878528c613", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x82c0bd9c20379ae7d08bd74bd7afb2a18c6dbd43", - "balanceRaw": "157610433959471106745", - "balanceFormatted": "157.610433959471106745", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": "biluk.eth", - "ensAvatarUrl": null, - "primaryAddress": "0x82c0bd9c20379ae7d08bd74bd7afb2a18c6dbd43", - "etherscanUrl": "https://etherscan.io/address/0x82c0bd9c20379ae7d08bd74bd7afb2a18c6dbd43", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x96973f7b83a3c785d94e0a6d8712174abb81b748", - "balanceRaw": "157490000000000000000", - "balanceFormatted": "157.49", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x96973f7b83a3c785d94e0a6d8712174abb81b748", - "etherscanUrl": "https://etherscan.io/address/0x96973f7b83a3c785d94e0a6d8712174abb81b748", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_927545", - "balanceRaw": "157354905347500222802", - "balanceFormatted": "157.354905347500222802", - "lastTransferAt": null, - "username": "hanjiangxue", - "displayName": "Hanjiangxue", - "fid": 927545, - "followers": 8, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3803beed-79dc-4f6f-ee17-d178ce047800/rectcrop3", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x46847175e69ae63c97cb21217d385d9336548ee2", - "etherscanUrl": "https://etherscan.io/address/0x46847175e69ae63c97cb21217d385d9336548ee2", - "xHandle": "chiqianguai2024", - "xUrl": "https://twitter.com/chiqianguai2024", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x434d36f32abed3f7937fe0be88dc1b0eb9381244", - "balanceRaw": "157217353068712558592", - "balanceFormatted": "157.217353068712558592", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x434d36f32abed3f7937fe0be88dc1b0eb9381244", - "etherscanUrl": "https://etherscan.io/address/0x434d36f32abed3f7937fe0be88dc1b0eb9381244", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8a6b54f6212673af30c2135838db29b5c79b86bb", - "balanceRaw": "157165036103289783575", - "balanceFormatted": "157.165036103289783575", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8a6b54f6212673af30c2135838db29b5c79b86bb", - "etherscanUrl": "https://etherscan.io/address/0x8a6b54f6212673af30c2135838db29b5c79b86bb", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8dfe2ea92f5abd8c039bfca0c0744c0e2e87e1f2", - "balanceRaw": "156636909794641771531", - "balanceFormatted": "156.636909794641771531", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8dfe2ea92f5abd8c039bfca0c0744c0e2e87e1f2", - "etherscanUrl": "https://etherscan.io/address/0x8dfe2ea92f5abd8c039bfca0c0744c0e2e87e1f2", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x97d3ff5517f1bae16bc97d7d72b72fcab3c50e5c", - "balanceRaw": "156241804388873715580", - "balanceFormatted": "156.24180438887371558", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x97d3ff5517f1bae16bc97d7d72b72fcab3c50e5c", - "etherscanUrl": "https://etherscan.io/address/0x97d3ff5517f1bae16bc97d7d72b72fcab3c50e5c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x53a560dc781dd9cecdf14d60e8225a17ef4ac4e3", - "balanceRaw": "155463106604722386958", - "balanceFormatted": "155.463106604722386958", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x53a560dc781dd9cecdf14d60e8225a17ef4ac4e3", - "etherscanUrl": "https://etherscan.io/address/0x53a560dc781dd9cecdf14d60e8225a17ef4ac4e3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xed702abc9fe8a256931b38a49804a538fdbc5472", - "balanceRaw": "154281333695001798327", - "balanceFormatted": "154.281333695001798327", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xed702abc9fe8a256931b38a49804a538fdbc5472", - "etherscanUrl": "https://etherscan.io/address/0xed702abc9fe8a256931b38a49804a538fdbc5472", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xeb5c0f3279a1316b3ffdaf0eadc5b1f97cda6e21", - "balanceRaw": "154060849498032574641", - "balanceFormatted": "154.060849498032574641", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xeb5c0f3279a1316b3ffdaf0eadc5b1f97cda6e21", - "etherscanUrl": "https://etherscan.io/address/0xeb5c0f3279a1316b3ffdaf0eadc5b1f97cda6e21", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_534655", - "balanceRaw": "153742251690630984302", - "balanceFormatted": "153.742251690630984302", - "lastTransferAt": null, - "username": "matizday", - "displayName": "dayma", - "fid": 534655, - "followers": 5, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6f01e891-84a4-444d-131a-2e8af25e5700/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x254a4c6bb86bb5c182c1ae8252340da789dc668c", - "etherscanUrl": "https://etherscan.io/address/0x254a4c6bb86bb5c182c1ae8252340da789dc668c", - "xHandle": "bjbjbjb87", - "xUrl": "https://twitter.com/bjbjbjb87", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbd95cb7e3574b5ce2ffff32fad346623bc626de0", - "balanceRaw": "153698334110442159352", - "balanceFormatted": "153.698334110442159352", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbd95cb7e3574b5ce2ffff32fad346623bc626de0", - "etherscanUrl": "https://etherscan.io/address/0xbd95cb7e3574b5ce2ffff32fad346623bc626de0", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc215d9d9dad342d662a4e34e06603f2dc0d4280b", - "balanceRaw": "153326952780000000000", - "balanceFormatted": "153.32695278", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc215d9d9dad342d662a4e34e06603f2dc0d4280b", - "etherscanUrl": "https://etherscan.io/address/0xc215d9d9dad342d662a4e34e06603f2dc0d4280b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9167902a148075f61fa03957e5f4196d18aee5a7", - "balanceRaw": "152854000000000000000", - "balanceFormatted": "152.854", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9167902a148075f61fa03957e5f4196d18aee5a7", - "etherscanUrl": "https://etherscan.io/address/0x9167902a148075f61fa03957e5f4196d18aee5a7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb24371bc248a25ff788e21a6e90f6b81b9dca565", - "balanceRaw": "152398664490360180285", - "balanceFormatted": "152.398664490360180285", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb24371bc248a25ff788e21a6e90f6b81b9dca565", - "etherscanUrl": "https://etherscan.io/address/0xb24371bc248a25ff788e21a6e90f6b81b9dca565", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_493537", - "balanceRaw": "151800447658045482251", - "balanceFormatted": "151.800447658045482251", - "lastTransferAt": null, - "username": "grif", - "displayName": "Grif", - "fid": 493537, - "followers": 409, - "pfpUrl": "https://res.cloudinary.com/base-app/image/upload/f_m3u8,f_png/v1757513217/video_uploads/1a2c0bcd-6efa-4420-ac52-8423d451b9e1.png", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x511227c444d741779414b6f35782da01cc88414c", - "etherscanUrl": "https://etherscan.io/address/0x511227c444d741779414b6f35782da01cc88414c", - "xHandle": "grif_gg", - "xUrl": "https://twitter.com/grif_gg", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xdcd8b900985e278399ba9d79b255b740ea53acb2", - "balanceRaw": "151489135906467918721", - "balanceFormatted": "151.489135906467918721", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdcd8b900985e278399ba9d79b255b740ea53acb2", - "etherscanUrl": "https://etherscan.io/address/0xdcd8b900985e278399ba9d79b255b740ea53acb2", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xdeeb527b2038f90cf5bb8b578fa2ae76df1f07db", - "balanceRaw": "151404123667667057181", - "balanceFormatted": "151.404123667667057181", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdeeb527b2038f90cf5bb8b578fa2ae76df1f07db", - "etherscanUrl": "https://etherscan.io/address/0xdeeb527b2038f90cf5bb8b578fa2ae76df1f07db", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_528533", - "balanceRaw": "151386784244401558517", - "balanceFormatted": "151.386784244401558517", - "lastTransferAt": null, - "username": "whatliving", - "displayName": "whatliving", - "fid": 528533, - "followers": 13, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/696283c3-72b2-4660-5190-9b81bfe4da00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x12c6729a923d1b11aa378f994f4257acaca26123", - "etherscanUrl": "https://etherscan.io/address/0x12c6729a923d1b11aa378f994f4257acaca26123", - "xHandle": "chochogadi", - "xUrl": "https://twitter.com/chochogadi", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc7cbc179e141765dcd88c5e2dce173fe0b4a54a0", - "balanceRaw": "151230967534973017875", - "balanceFormatted": "151.230967534973017875", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc7cbc179e141765dcd88c5e2dce173fe0b4a54a0", - "etherscanUrl": "https://etherscan.io/address/0xc7cbc179e141765dcd88c5e2dce173fe0b4a54a0", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x20a49ad2f989971ea3c4e93a956784ee1960532a", - "balanceRaw": "150665170000000000000", - "balanceFormatted": "150.66517", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x20a49ad2f989971ea3c4e93a956784ee1960532a", - "etherscanUrl": "https://etherscan.io/address/0x20a49ad2f989971ea3c4e93a956784ee1960532a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3ff2c0df347571b35376dca9897eb79b8947a572", - "balanceRaw": "150251000000000000000", - "balanceFormatted": "150.251", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3ff2c0df347571b35376dca9897eb79b8947a572", - "etherscanUrl": "https://etherscan.io/address/0x3ff2c0df347571b35376dca9897eb79b8947a572", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_336022", - "balanceRaw": "150248491334823851352", - "balanceFormatted": "150.248491334823851352", - "lastTransferAt": null, - "username": "degen-chad", - "displayName": "degen chad", - "fid": 336022, - "followers": 8646, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f329718c-178d-4802-025c-5a9f7edff400/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3c80786afdd12c81b6d6a876263a6159318aba09", - "etherscanUrl": "https://etherscan.io/address/0x3c80786afdd12c81b6d6a876263a6159318aba09", - "xHandle": "lococrypto420", - "xUrl": "https://twitter.com/lococrypto420", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7972793603343be73882c7891a84c5f3a85fde12", - "balanceRaw": "150000121000000000000", - "balanceFormatted": "150.000121", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7972793603343be73882c7891a84c5f3a85fde12", - "etherscanUrl": "https://etherscan.io/address/0x7972793603343be73882c7891a84c5f3a85fde12", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd18f58f4604bc2f50eea20f0b40b7fbc994c328c", - "balanceRaw": "150000000000000000000", - "balanceFormatted": "150", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd18f58f4604bc2f50eea20f0b40b7fbc994c328c", - "etherscanUrl": "https://etherscan.io/address/0xd18f58f4604bc2f50eea20f0b40b7fbc994c328c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x11f0758d1de21bbc69309221eec6be570c660322", - "balanceRaw": "150000000000000000000", - "balanceFormatted": "150", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x11f0758d1de21bbc69309221eec6be570c660322", - "etherscanUrl": "https://etherscan.io/address/0x11f0758d1de21bbc69309221eec6be570c660322", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf74d811aa89555411e257d1d92c95360c8dffa7e", - "balanceRaw": "150000000000000000000", - "balanceFormatted": "150", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf74d811aa89555411e257d1d92c95360c8dffa7e", - "etherscanUrl": "https://etherscan.io/address/0xf74d811aa89555411e257d1d92c95360c8dffa7e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbe612324ad54ed349545881e30ea438a5b93ae10", - "balanceRaw": "149483442284105263025", - "balanceFormatted": "149.483442284105263025", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbe612324ad54ed349545881e30ea438a5b93ae10", - "etherscanUrl": "https://etherscan.io/address/0xbe612324ad54ed349545881e30ea438a5b93ae10", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x03c25c81e480bc73db92bd71b04c25167891d426", - "balanceRaw": "148855267426099827779", - "balanceFormatted": "148.855267426099827779", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x03c25c81e480bc73db92bd71b04c25167891d426", - "etherscanUrl": "https://etherscan.io/address/0x03c25c81e480bc73db92bd71b04c25167891d426", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_2982", - "balanceRaw": "148479727944144830667", - "balanceFormatted": "148.479727944144830667", - "lastTransferAt": null, - "username": "jacy", - "displayName": "sparkz", - "fid": 2982, - "followers": 10657, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/63552b96-faf4-4480-7775-a93631aaa100/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xde6eff062895c1669c4920f96206e57c8c11a6e5", - "etherscanUrl": "https://etherscan.io/address/0xde6eff062895c1669c4920f96206e57c8c11a6e5", - "xHandle": "hawaiianft", - "xUrl": "https://twitter.com/hawaiianft", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb7fd91de6c9da239f27fcf690a420c495c2896a8", - "balanceRaw": "148314389624602335318", - "balanceFormatted": "148.314389624602335318", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb7fd91de6c9da239f27fcf690a420c495c2896a8", - "etherscanUrl": "https://etherscan.io/address/0xb7fd91de6c9da239f27fcf690a420c495c2896a8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9b43826a70578444bc82ffd2886f8a9a4615eeeb", - "balanceRaw": "148262960398305635556", - "balanceFormatted": "148.262960398305635556", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9b43826a70578444bc82ffd2886f8a9a4615eeeb", - "etherscanUrl": "https://etherscan.io/address/0x9b43826a70578444bc82ffd2886f8a9a4615eeeb", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf173f3395d08488f548b71bcf0d46884b3c40b78", - "balanceRaw": "147704519778409621369", - "balanceFormatted": "147.704519778409621369", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf173f3395d08488f548b71bcf0d46884b3c40b78", - "etherscanUrl": "https://etherscan.io/address/0xf173f3395d08488f548b71bcf0d46884b3c40b78", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc23406d8e43ee59b8b6411e067c9afab918930f2", - "balanceRaw": "147695114684773656944", - "balanceFormatted": "147.695114684773656944", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc23406d8e43ee59b8b6411e067c9afab918930f2", - "etherscanUrl": "https://etherscan.io/address/0xc23406d8e43ee59b8b6411e067c9afab918930f2", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xfabe7703ed620b4c8c78085008cb7dd4d27fb83e", - "balanceRaw": "147512164905044548922", - "balanceFormatted": "147.512164905044548922", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfabe7703ed620b4c8c78085008cb7dd4d27fb83e", - "etherscanUrl": "https://etherscan.io/address/0xfabe7703ed620b4c8c78085008cb7dd4d27fb83e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5651e5445e13d742eb1a43d98c8cbbfa7a4a947a", - "balanceRaw": "146542856413215077430", - "balanceFormatted": "146.54285641321507743", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5651e5445e13d742eb1a43d98c8cbbfa7a4a947a", - "etherscanUrl": "https://etherscan.io/address/0x5651e5445e13d742eb1a43d98c8cbbfa7a4a947a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd3048bdef8e2d82073c49f72411110b4867c5c85", - "balanceRaw": "145672823128481117517", - "balanceFormatted": "145.672823128481117517", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd3048bdef8e2d82073c49f72411110b4867c5c85", - "etherscanUrl": "https://etherscan.io/address/0xd3048bdef8e2d82073c49f72411110b4867c5c85", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd35d6ad3c584c0f56abff7e6b1572f162c24551a", - "balanceRaw": "145050042593499155525", - "balanceFormatted": "145.050042593499155525", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd35d6ad3c584c0f56abff7e6b1572f162c24551a", - "etherscanUrl": "https://etherscan.io/address/0xd35d6ad3c584c0f56abff7e6b1572f162c24551a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_344232", - "balanceRaw": "145000000000000000000", - "balanceFormatted": "145", - "lastTransferAt": null, - "username": "n-n", - "displayName": "Robert Hicken", - "fid": 344232, - "followers": 93, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5f5707f9-6d79-41e6-19ce-dced244e8500/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x96fa16a17d4fd4683c159110e51826e13fc1a5b4", - "etherscanUrl": "https://etherscan.io/address/0x96fa16a17d4fd4683c159110e51826e13fc1a5b4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_4447", - "balanceRaw": "144914797929308360041", - "balanceFormatted": "144.914797929308360041", - "lastTransferAt": null, - "username": "lost", - "displayName": "", - "fid": 4447, - "followers": 1234, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6136c918-13a9-41f1-68f2-3fa7f8952a00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7ca28d283189e7b4ff57a8ea6ede2b7b5c604efe", - "etherscanUrl": "https://etherscan.io/address/0x7ca28d283189e7b4ff57a8ea6ede2b7b5c604efe", - "xHandle": "gabe_ragland", - "xUrl": "https://twitter.com/gabe_ragland", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9795d21237e0e7c5ea1b67daa6ddbfbcf18fa096", - "balanceRaw": "144573221936344011069", - "balanceFormatted": "144.573221936344011069", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9795d21237e0e7c5ea1b67daa6ddbfbcf18fa096", - "etherscanUrl": "https://etherscan.io/address/0x9795d21237e0e7c5ea1b67daa6ddbfbcf18fa096", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xda8ac544d0f4bd677e15df539d87116f5bec7e18", - "balanceRaw": "143159717437980340250", - "balanceFormatted": "143.15971743798034025", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xda8ac544d0f4bd677e15df539d87116f5bec7e18", - "etherscanUrl": "https://etherscan.io/address/0xda8ac544d0f4bd677e15df539d87116f5bec7e18", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xdcfb68df7f9538d6c9058bbb1a7b5147c6534fe2", - "balanceRaw": "142588159475887268413", - "balanceFormatted": "142.588159475887268413", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdcfb68df7f9538d6c9058bbb1a7b5147c6534fe2", - "etherscanUrl": "https://etherscan.io/address/0xdcfb68df7f9538d6c9058bbb1a7b5147c6534fe2", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7d77fd5cd3d3332e13efa9b8d291cfd8748ed1dd", - "balanceRaw": "142527487152529431944", - "balanceFormatted": "142.527487152529431944", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7d77fd5cd3d3332e13efa9b8d291cfd8748ed1dd", - "etherscanUrl": "https://etherscan.io/address/0x7d77fd5cd3d3332e13efa9b8d291cfd8748ed1dd", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x445d60f90bd643d4490b32ebe507dce7c08f0097", - "balanceRaw": "142089682393926019569", - "balanceFormatted": "142.089682393926019569", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x445d60f90bd643d4490b32ebe507dce7c08f0097", - "etherscanUrl": "https://etherscan.io/address/0x445d60f90bd643d4490b32ebe507dce7c08f0097", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_3642", - "balanceRaw": "141813300448949230469", - "balanceFormatted": "141.813300448949230469", - "lastTransferAt": null, - "username": "toadyhawk.eth", - "displayName": "Toady Hawk", - "fid": 3642, - "followers": 156716, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/961c607f-84d6-4973-4a2a-bcba6ed82c00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x49fcf7fd8bced178a3c65d3341f61821eef76423", - "etherscanUrl": "https://etherscan.io/address/0x49fcf7fd8bced178a3c65d3341f61821eef76423", - "xHandle": "toady_hawk", - "xUrl": "https://twitter.com/toady_hawk", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x813b5475e8555f26ab7f10bade69694d18312a75", - "balanceRaw": "141798525019254406236", - "balanceFormatted": "141.798525019254406236", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x813b5475e8555f26ab7f10bade69694d18312a75", - "etherscanUrl": "https://etherscan.io/address/0x813b5475e8555f26ab7f10bade69694d18312a75", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xba80c7e11b8a6a3ac2a557192a5e3cb0ad05101a", - "balanceRaw": "141749527142533211581", - "balanceFormatted": "141.749527142533211581", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xba80c7e11b8a6a3ac2a557192a5e3cb0ad05101a", - "etherscanUrl": "https://etherscan.io/address/0xba80c7e11b8a6a3ac2a557192a5e3cb0ad05101a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x98f607fdff4f21044456dfef4a7739a91619fd53", - "balanceRaw": "141640469102220226150", - "balanceFormatted": "141.64046910222022615", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x98f607fdff4f21044456dfef4a7739a91619fd53", - "etherscanUrl": "https://etherscan.io/address/0x98f607fdff4f21044456dfef4a7739a91619fd53", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x40da5fcd492c2e05eba34679ceaeabfb113a7470", - "balanceRaw": "141606439667585254397", - "balanceFormatted": "141.606439667585254397", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x40da5fcd492c2e05eba34679ceaeabfb113a7470", - "etherscanUrl": "https://etherscan.io/address/0x40da5fcd492c2e05eba34679ceaeabfb113a7470", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_277725", - "balanceRaw": "140543956958303978723", - "balanceFormatted": "140.543956958303978723", - "lastTransferAt": null, - "username": "cruiser", - "displayName": "Cruiser 🔵🎩", - "fid": 277725, - "followers": 55, - "pfpUrl": "https://i.imgur.com/qHlW0QG.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb4d7227660d233f61cb1c4f16e35998266e20aef", - "etherscanUrl": "https://etherscan.io/address/0xb4d7227660d233f61cb1c4f16e35998266e20aef", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x71e32deb2f83ef8785468d77373a3c4d2dcfe098", - "balanceRaw": "140078086921983361024", - "balanceFormatted": "140.078086921983361024", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x71e32deb2f83ef8785468d77373a3c4d2dcfe098", - "etherscanUrl": "https://etherscan.io/address/0x71e32deb2f83ef8785468d77373a3c4d2dcfe098", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x55caeb84763e26c564f853111456ea45943f70f4", - "balanceRaw": "139463428874940529403", - "balanceFormatted": "139.463428874940529403", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x55caeb84763e26c564f853111456ea45943f70f4", - "etherscanUrl": "https://etherscan.io/address/0x55caeb84763e26c564f853111456ea45943f70f4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xac6c213cf5877cfb49b28f1c907cb889e77f8b99", - "balanceRaw": "139218388538452633907", - "balanceFormatted": "139.218388538452633907", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xac6c213cf5877cfb49b28f1c907cb889e77f8b99", - "etherscanUrl": "https://etherscan.io/address/0xac6c213cf5877cfb49b28f1c907cb889e77f8b99", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xac124b80a7e8af2f0aca5718ad9a1e5e58355339", - "balanceRaw": "138656550664145971989", - "balanceFormatted": "138.656550664145971989", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xac124b80a7e8af2f0aca5718ad9a1e5e58355339", - "etherscanUrl": "https://etherscan.io/address/0xac124b80a7e8af2f0aca5718ad9a1e5e58355339", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x57060205e2b7d29a84970b0471aea03ab6b97c20", - "balanceRaw": "138268304275911167744", - "balanceFormatted": "138.268304275911167744", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x57060205e2b7d29a84970b0471aea03ab6b97c20", - "etherscanUrl": "https://etherscan.io/address/0x57060205e2b7d29a84970b0471aea03ab6b97c20", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5581a582b431250d206b113d38f4af67722a06b1", - "balanceRaw": "138115442859460310779", - "balanceFormatted": "138.115442859460310779", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5581a582b431250d206b113d38f4af67722a06b1", - "etherscanUrl": "https://etherscan.io/address/0x5581a582b431250d206b113d38f4af67722a06b1", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xdd73867db0b7cf9e5b075a6f98bd260e2389a538", - "balanceRaw": "137613417542483061135", - "balanceFormatted": "137.613417542483061135", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdd73867db0b7cf9e5b075a6f98bd260e2389a538", - "etherscanUrl": "https://etherscan.io/address/0xdd73867db0b7cf9e5b075a6f98bd260e2389a538", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xdc3e0f31ada1e5fc816bc0e09b7c2ef0ea1ee13a", - "balanceRaw": "137306775583666648088", - "balanceFormatted": "137.306775583666648088", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdc3e0f31ada1e5fc816bc0e09b7c2ef0ea1ee13a", - "etherscanUrl": "https://etherscan.io/address/0xdc3e0f31ada1e5fc816bc0e09b7c2ef0ea1ee13a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x174595f33cc26aab42b73af8d5077af5c0ad714c", - "balanceRaw": "137251674412474135796", - "balanceFormatted": "137.251674412474135796", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x174595f33cc26aab42b73af8d5077af5c0ad714c", - "etherscanUrl": "https://etherscan.io/address/0x174595f33cc26aab42b73af8d5077af5c0ad714c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x303909d1e0b22d61b2de4b2ecb2e62599945639a", - "balanceRaw": "136925996609642111613", - "balanceFormatted": "136.925996609642111613", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x303909d1e0b22d61b2de4b2ecb2e62599945639a", - "etherscanUrl": "https://etherscan.io/address/0x303909d1e0b22d61b2de4b2ecb2e62599945639a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe0ac7f572b922330915c220b103bf7eeca2fdfaa", - "balanceRaw": "136611834239001360754", - "balanceFormatted": "136.611834239001360754", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe0ac7f572b922330915c220b103bf7eeca2fdfaa", - "etherscanUrl": "https://etherscan.io/address/0xe0ac7f572b922330915c220b103bf7eeca2fdfaa", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5f7399231a591370a28f9f263e55d29000654807", - "balanceRaw": "136589372254839792290", - "balanceFormatted": "136.58937225483979229", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5f7399231a591370a28f9f263e55d29000654807", - "etherscanUrl": "https://etherscan.io/address/0x5f7399231a591370a28f9f263e55d29000654807", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc827d4d5e16dd54e32fbdbc0d82c5ae4870525ea", - "balanceRaw": "136369643110543542485", - "balanceFormatted": "136.369643110543542485", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc827d4d5e16dd54e32fbdbc0d82c5ae4870525ea", - "etherscanUrl": "https://etherscan.io/address/0xc827d4d5e16dd54e32fbdbc0d82c5ae4870525ea", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x570460f07684ff86fdccd9feda62e5a08ff190cf", - "balanceRaw": "136133300000000000000", - "balanceFormatted": "136.1333", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x570460f07684ff86fdccd9feda62e5a08ff190cf", - "etherscanUrl": "https://etherscan.io/address/0x570460f07684ff86fdccd9feda62e5a08ff190cf", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7a7c6aab33e9c41428c82411ef8e8b22ebac8cac", - "balanceRaw": "136024574028471680727", - "balanceFormatted": "136.024574028471680727", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7a7c6aab33e9c41428c82411ef8e8b22ebac8cac", - "etherscanUrl": "https://etherscan.io/address/0x7a7c6aab33e9c41428c82411ef8e8b22ebac8cac", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x032929f94a343719cccde5be302b530f787b1370", - "balanceRaw": "135432353397823033528", - "balanceFormatted": "135.432353397823033528", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x032929f94a343719cccde5be302b530f787b1370", - "etherscanUrl": "https://etherscan.io/address/0x032929f94a343719cccde5be302b530f787b1370", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_420816", - "balanceRaw": "135431989216126417402", - "balanceFormatted": "135.431989216126417402", - "lastTransferAt": null, - "username": "yigitm", - "displayName": "0xyigit.base.eth", - "fid": 420816, - "followers": 607, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/43c5b974-4342-410d-7147-48f98d94c100/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa69a63496296e6b04777ad41f6d3605bbeb574d1", - "etherscanUrl": "https://etherscan.io/address/0xa69a63496296e6b04777ad41f6d3605bbeb574d1", - "xHandle": "yigitm44", - "xUrl": "https://twitter.com/yigitm44", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xdc59da89633fbd3bd27bd22e5acede444c394beb", - "balanceRaw": "135000055139105779184", - "balanceFormatted": "135.000055139105779184", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdc59da89633fbd3bd27bd22e5acede444c394beb", - "etherscanUrl": "https://etherscan.io/address/0xdc59da89633fbd3bd27bd22e5acede444c394beb", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x25848b8e80e1537f377c1405227d1503ca141fde", - "balanceRaw": "134994826525481162586", - "balanceFormatted": "134.994826525481162586", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x25848b8e80e1537f377c1405227d1503ca141fde", - "etherscanUrl": "https://etherscan.io/address/0x25848b8e80e1537f377c1405227d1503ca141fde", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_10174", - "balanceRaw": "134507769803971841751", - "balanceFormatted": "134.507769803971841751", - "lastTransferAt": null, - "username": "cryptowenmoon.eth", - "displayName": "Ray F. 🎩", - "fid": 10174, - "followers": 12587, - "pfpUrl": "https://i.imgur.com/tABKvo7.gif", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x52bd76703e654b70fc5ee1fca02ea2725f60117b", - "etherscanUrl": "https://etherscan.io/address/0x52bd76703e654b70fc5ee1fca02ea2725f60117b", - "xHandle": "crypto_wenmoon", - "xUrl": "https://twitter.com/crypto_wenmoon", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb8605388826a3fc1adc94bad37772210c42b940b", - "balanceRaw": "132710420913335676938", - "balanceFormatted": "132.710420913335676938", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb8605388826a3fc1adc94bad37772210c42b940b", - "etherscanUrl": "https://etherscan.io/address/0xb8605388826a3fc1adc94bad37772210c42b940b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6d0b62171e5c8e3731005170139250091dbe9e85", - "balanceRaw": "132248556096292559872", - "balanceFormatted": "132.248556096292559872", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6d0b62171e5c8e3731005170139250091dbe9e85", - "etherscanUrl": "https://etherscan.io/address/0x6d0b62171e5c8e3731005170139250091dbe9e85", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc043eb6d5f2a6c571beb1178ea6f4c0e467f0cdc", - "balanceRaw": "131856439319250065779", - "balanceFormatted": "131.856439319250065779", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc043eb6d5f2a6c571beb1178ea6f4c0e467f0cdc", - "etherscanUrl": "https://etherscan.io/address/0xc043eb6d5f2a6c571beb1178ea6f4c0e467f0cdc", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5cc18f9d8cd51dcb2a26c7c0baf273e5d3143be4", - "balanceRaw": "131750659629614033845", - "balanceFormatted": "131.750659629614033845", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5cc18f9d8cd51dcb2a26c7c0baf273e5d3143be4", - "etherscanUrl": "https://etherscan.io/address/0x5cc18f9d8cd51dcb2a26c7c0baf273e5d3143be4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xff068c48ef0ea3b02cc8f0cda44d486c10c1f09e", - "balanceRaw": "131717982726768430954", - "balanceFormatted": "131.717982726768430954", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xff068c48ef0ea3b02cc8f0cda44d486c10c1f09e", - "etherscanUrl": "https://etherscan.io/address/0xff068c48ef0ea3b02cc8f0cda44d486c10c1f09e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf0ac3d5d902108c8a94890619dfe068bda5c1aaa", - "balanceRaw": "131591187096621719909", - "balanceFormatted": "131.591187096621719909", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf0ac3d5d902108c8a94890619dfe068bda5c1aaa", - "etherscanUrl": "https://etherscan.io/address/0xf0ac3d5d902108c8a94890619dfe068bda5c1aaa", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb1fe8d7274efcad6571f11465709a7df0521b2a9", - "balanceRaw": "130095235673011789191", - "balanceFormatted": "130.095235673011789191", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb1fe8d7274efcad6571f11465709a7df0521b2a9", - "etherscanUrl": "https://etherscan.io/address/0xb1fe8d7274efcad6571f11465709a7df0521b2a9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8f2c60ba450c5acf58ea06feb38cfacfe0d9c78a", - "balanceRaw": "129609984757642569348", - "balanceFormatted": "129.609984757642569348", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8f2c60ba450c5acf58ea06feb38cfacfe0d9c78a", - "etherscanUrl": "https://etherscan.io/address/0x8f2c60ba450c5acf58ea06feb38cfacfe0d9c78a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf829bdab49a80dbcefec333beff4d2aa5db1fcef", - "balanceRaw": "129461683318771372032", - "balanceFormatted": "129.461683318771372032", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf829bdab49a80dbcefec333beff4d2aa5db1fcef", - "etherscanUrl": "https://etherscan.io/address/0xf829bdab49a80dbcefec333beff4d2aa5db1fcef", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x912dd653050cef3e2401f2f9f35487284cac676f", - "balanceRaw": "129340833279088478112", - "balanceFormatted": "129.340833279088478112", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x912dd653050cef3e2401f2f9f35487284cac676f", - "etherscanUrl": "https://etherscan.io/address/0x912dd653050cef3e2401f2f9f35487284cac676f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8c5a5114d03de55a2c3e9c499906d838de17dd94", - "balanceRaw": "129305207372476264447", - "balanceFormatted": "129.305207372476264447", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8c5a5114d03de55a2c3e9c499906d838de17dd94", - "etherscanUrl": "https://etherscan.io/address/0x8c5a5114d03de55a2c3e9c499906d838de17dd94", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb2bf9f21e1c6eb66af815a4bd9e8371ab0b57771", - "balanceRaw": "129294093822374117586", - "balanceFormatted": "129.294093822374117586", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb2bf9f21e1c6eb66af815a4bd9e8371ab0b57771", - "etherscanUrl": "https://etherscan.io/address/0xb2bf9f21e1c6eb66af815a4bd9e8371ab0b57771", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf8992a1f4e302460bd49a1646ee315d810235212", - "balanceRaw": "129161495728769042285", - "balanceFormatted": "129.161495728769042285", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf8992a1f4e302460bd49a1646ee315d810235212", - "etherscanUrl": "https://etherscan.io/address/0xf8992a1f4e302460bd49a1646ee315d810235212", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbf7f315c20d087eda496aea4217b56ef02f0c8d1", - "balanceRaw": "128836016192791023396", - "balanceFormatted": "128.836016192791023396", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbf7f315c20d087eda496aea4217b56ef02f0c8d1", - "etherscanUrl": "https://etherscan.io/address/0xbf7f315c20d087eda496aea4217b56ef02f0c8d1", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe7ce6e78938265dd4d238144998fbfd872b19da7", - "balanceRaw": "128806618722561729879", - "balanceFormatted": "128.806618722561729879", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe7ce6e78938265dd4d238144998fbfd872b19da7", - "etherscanUrl": "https://etherscan.io/address/0xe7ce6e78938265dd4d238144998fbfd872b19da7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5151cf6e6e61597dad5f9cb524251677405e89ba", - "balanceRaw": "128287714687209632638", - "balanceFormatted": "128.287714687209632638", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5151cf6e6e61597dad5f9cb524251677405e89ba", - "etherscanUrl": "https://etherscan.io/address/0x5151cf6e6e61597dad5f9cb524251677405e89ba", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x498af4898ab23f9e1041c93da96e18d6e23c5129", - "balanceRaw": "127775856442879106736", - "balanceFormatted": "127.775856442879106736", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x498af4898ab23f9e1041c93da96e18d6e23c5129", - "etherscanUrl": "https://etherscan.io/address/0x498af4898ab23f9e1041c93da96e18d6e23c5129", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x1f53c0bee870c2e8b28fefd1353edbe6b158a845", - "balanceRaw": "127728653512147121435", - "balanceFormatted": "127.728653512147121435", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1f53c0bee870c2e8b28fefd1353edbe6b158a845", - "etherscanUrl": "https://etherscan.io/address/0x1f53c0bee870c2e8b28fefd1353edbe6b158a845", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_532277", - "balanceRaw": "127687065564541641522", - "balanceFormatted": "127.687065564541641522", - "lastTransferAt": null, - "username": "bestofmatiz", - "displayName": "bestofka", - "fid": 532277, - "followers": 22, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/2d45be7d-6d74-4637-798b-ef1248ab8000/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x36f10b6e4f81dbbc483d261a2fb291dedef95da2", - "etherscanUrl": "https://etherscan.io/address/0x36f10b6e4f81dbbc483d261a2fb291dedef95da2", - "xHandle": "matimzw", - "xUrl": "https://twitter.com/matimzw", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x90bfbf266bb1429c72cac64d9de3d420b8dd5b3e", - "balanceRaw": "127565400240324314336", - "balanceFormatted": "127.565400240324314336", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x90bfbf266bb1429c72cac64d9de3d420b8dd5b3e", - "etherscanUrl": "https://etherscan.io/address/0x90bfbf266bb1429c72cac64d9de3d420b8dd5b3e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x33b467c8cff395e8da9e6b47ef52eb472ce61d09", - "balanceRaw": "126128576796704239546", - "balanceFormatted": "126.128576796704239546", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x33b467c8cff395e8da9e6b47ef52eb472ce61d09", - "etherscanUrl": "https://etherscan.io/address/0x33b467c8cff395e8da9e6b47ef52eb472ce61d09", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x4369c1744577a68c294c56a4d55178680706d955", - "balanceRaw": "126115017314630196057", - "balanceFormatted": "126.115017314630196057", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4369c1744577a68c294c56a4d55178680706d955", - "etherscanUrl": "https://etherscan.io/address/0x4369c1744577a68c294c56a4d55178680706d955", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3cfb36966765b3db57e5566cbbea1db189c04152", - "balanceRaw": "125863356669305370869", - "balanceFormatted": "125.863356669305370869", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3cfb36966765b3db57e5566cbbea1db189c04152", - "etherscanUrl": "https://etherscan.io/address/0x3cfb36966765b3db57e5566cbbea1db189c04152", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xec3d72a4c0b6f819fb39c99b56c46d757bf9d59d", - "balanceRaw": "125662000000000000000", - "balanceFormatted": "125.662", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xec3d72a4c0b6f819fb39c99b56c46d757bf9d59d", - "etherscanUrl": "https://etherscan.io/address/0xec3d72a4c0b6f819fb39c99b56c46d757bf9d59d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x36cd28fe44208c6bf8c09d97c8acd0ec996e4dc3", - "balanceRaw": "125502897357363836000", - "balanceFormatted": "125.502897357363836", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x36cd28fe44208c6bf8c09d97c8acd0ec996e4dc3", - "etherscanUrl": "https://etherscan.io/address/0x36cd28fe44208c6bf8c09d97c8acd0ec996e4dc3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x81ead9025a005929658bd3ee5c0437c3849edf7d", - "balanceRaw": "125163646251911103488", - "balanceFormatted": "125.163646251911103488", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x81ead9025a005929658bd3ee5c0437c3849edf7d", - "etherscanUrl": "https://etherscan.io/address/0x81ead9025a005929658bd3ee5c0437c3849edf7d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_323746", - "balanceRaw": "125000000026413932206", - "balanceFormatted": "125.000000026413932206", - "lastTransferAt": null, - "username": "jackwyldes.eth", - "displayName": "JackWyldes", - "fid": 323746, - "followers": 4289, - "pfpUrl": "https://tba-mobile.mypinata.cloud/ipfs/QmZCPS637XHZDuzZTm29vsM8TQ6pcG6gbdRzoEnoxmw9sB?pinataGatewayToken=PMz6RFTDuk-300OttNnb_U0PSKbbXQdzLmUqdiEq7lesXcsVK8TK7S5GoOtxRGl2", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa06521e841707f81f4644fc6acc3ff75205461a0", - "etherscanUrl": "https://etherscan.io/address/0xa06521e841707f81f4644fc6acc3ff75205461a0", - "xHandle": "jackwyldes", - "xUrl": "https://twitter.com/jackwyldes", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6ad1a91ca59cf12d58c5f81dd737e8081c7c6e64", - "balanceRaw": "124887876688499928175", - "balanceFormatted": "124.887876688499928175", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6ad1a91ca59cf12d58c5f81dd737e8081c7c6e64", - "etherscanUrl": "https://etherscan.io/address/0x6ad1a91ca59cf12d58c5f81dd737e8081c7c6e64", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_129", - "balanceRaw": "124725788920657622188", - "balanceFormatted": "124.725788920657622188", - "lastTransferAt": null, - "username": "phil", - "displayName": "phil", - "fid": 129, - "followers": 466528, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/fa551416-4bdd-4e0f-63f3-08f2b82dc700/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x83d6202e0bc39b516d4b8b1770fbe3e4d7561baf", - "etherscanUrl": "https://etherscan.io/address/0x83d6202e0bc39b516d4b8b1770fbe3e4d7561baf", - "xHandle": "philmohun", - "xUrl": "https://twitter.com/philmohun", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8175732f812af3aff7cf5f73dfcf947c9eef279c", - "balanceRaw": "124664159615057993048", - "balanceFormatted": "124.664159615057993048", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8175732f812af3aff7cf5f73dfcf947c9eef279c", - "etherscanUrl": "https://etherscan.io/address/0x8175732f812af3aff7cf5f73dfcf947c9eef279c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa7358e93b653ad1220f93931e360c3e086fdf5d0", - "balanceRaw": "124257076311737180352", - "balanceFormatted": "124.257076311737180352", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa7358e93b653ad1220f93931e360c3e086fdf5d0", - "etherscanUrl": "https://etherscan.io/address/0xa7358e93b653ad1220f93931e360c3e086fdf5d0", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_226545", - "balanceRaw": "124015694501789074515", - "balanceFormatted": "124.015694501789074515", - "lastTransferAt": null, - "username": "nullable.eth", - "displayName": " nullable.eth 🎩", - "fid": 226545, - "followers": 23, - "pfpUrl": "https://i.imgur.com/moq9klf.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdfa3078a54b9232b75b3baf88804b711b2463943", - "etherscanUrl": "https://etherscan.io/address/0xdfa3078a54b9232b75b3baf88804b711b2463943", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xacb38898840ad9e4b6058179329b610dda1d2861", - "balanceRaw": "122956335316716278071", - "balanceFormatted": "122.956335316716278071", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xacb38898840ad9e4b6058179329b610dda1d2861", - "etherscanUrl": "https://etherscan.io/address/0xacb38898840ad9e4b6058179329b610dda1d2861", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xcab72c950d3971baf129392edf644a6cb4a18be1", - "balanceRaw": "122813437167903729247", - "balanceFormatted": "122.813437167903729247", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcab72c950d3971baf129392edf644a6cb4a18be1", - "etherscanUrl": "https://etherscan.io/address/0xcab72c950d3971baf129392edf644a6cb4a18be1", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa6094d47862fcd0cf36fff16b848a6a95785c587", - "balanceRaw": "122808865725869399187", - "balanceFormatted": "122.808865725869399187", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa6094d47862fcd0cf36fff16b848a6a95785c587", - "etherscanUrl": "https://etherscan.io/address/0xa6094d47862fcd0cf36fff16b848a6a95785c587", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_8952", - "balanceRaw": "122614010719967362593", - "balanceFormatted": "122.614010719967362593", - "lastTransferAt": null, - "username": "littlethings", - "displayName": "Lit", - "fid": 8952, - "followers": 2054, - "pfpUrl": "https://ipfs.decentralized-content.com/ipfs/bafkreif7iwwghepeq3o337rh6llrpvbi6ege2pgxnnpqvfw4nwrok2xgd4", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3226ab225e471eebc833e44520945d0ce25f8bb5", - "etherscanUrl": "https://etherscan.io/address/0x3226ab225e471eebc833e44520945d0ce25f8bb5", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xee367d7e1151fc6d22f505738954d426eea9136d", - "balanceRaw": "122436922597849118608", - "balanceFormatted": "122.436922597849118608", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xee367d7e1151fc6d22f505738954d426eea9136d", - "etherscanUrl": "https://etherscan.io/address/0xee367d7e1151fc6d22f505738954d426eea9136d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf4c28fb811d760db6f522c3b2533024bad8ca285", - "balanceRaw": "122264467817504330267", - "balanceFormatted": "122.264467817504330267", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf4c28fb811d760db6f522c3b2533024bad8ca285", - "etherscanUrl": "https://etherscan.io/address/0xf4c28fb811d760db6f522c3b2533024bad8ca285", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x066169e6ddf389de6fbcb7fc074ddb01a76e24c7", - "balanceRaw": "122212591807705382912", - "balanceFormatted": "122.212591807705382912", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x066169e6ddf389de6fbcb7fc074ddb01a76e24c7", - "etherscanUrl": "https://etherscan.io/address/0x066169e6ddf389de6fbcb7fc074ddb01a76e24c7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd27f76c5a7367d12dd29af36479a75b12e1edd58", - "balanceRaw": "121509957319412400128", - "balanceFormatted": "121.509957319412400128", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd27f76c5a7367d12dd29af36479a75b12e1edd58", - "etherscanUrl": "https://etherscan.io/address/0xd27f76c5a7367d12dd29af36479a75b12e1edd58", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xde260d46793c0dad685cf44eba58e4b5800592b5", - "balanceRaw": "121109663660011798689", - "balanceFormatted": "121.109663660011798689", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xde260d46793c0dad685cf44eba58e4b5800592b5", - "etherscanUrl": "https://etherscan.io/address/0xde260d46793c0dad685cf44eba58e4b5800592b5", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd4bb1902240be8361c28fa35c406f56d22246e60", - "balanceRaw": "121033536144420000660", - "balanceFormatted": "121.03353614442000066", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd4bb1902240be8361c28fa35c406f56d22246e60", - "etherscanUrl": "https://etherscan.io/address/0xd4bb1902240be8361c28fa35c406f56d22246e60", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7a07405a35aae882a29eeef652f0cb0533dcd003", - "balanceRaw": "120248649124877806952", - "balanceFormatted": "120.248649124877806952", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7a07405a35aae882a29eeef652f0cb0533dcd003", - "etherscanUrl": "https://etherscan.io/address/0x7a07405a35aae882a29eeef652f0cb0533dcd003", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf535738f24a8ed5f4432aac2863360f9256ae33c", - "balanceRaw": "120021382902707198720", - "balanceFormatted": "120.02138290270719872", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf535738f24a8ed5f4432aac2863360f9256ae33c", - "etherscanUrl": "https://etherscan.io/address/0xf535738f24a8ed5f4432aac2863360f9256ae33c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xcfc6bf754f3557a36d826613c6cd11d7ea5a7b32", - "balanceRaw": "120006279364994440692", - "balanceFormatted": "120.006279364994440692", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcfc6bf754f3557a36d826613c6cd11d7ea5a7b32", - "etherscanUrl": "https://etherscan.io/address/0xcfc6bf754f3557a36d826613c6cd11d7ea5a7b32", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x909b8635dfa669230424dc3b71e7eb88a70b2859", - "balanceRaw": "119433025062791922481", - "balanceFormatted": "119.433025062791922481", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x909b8635dfa669230424dc3b71e7eb88a70b2859", - "etherscanUrl": "https://etherscan.io/address/0x909b8635dfa669230424dc3b71e7eb88a70b2859", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xccdc603a336270d63962dad04f3bd8b81143f81a", - "balanceRaw": "118948993837553047760", - "balanceFormatted": "118.94899383755304776", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xccdc603a336270d63962dad04f3bd8b81143f81a", - "etherscanUrl": "https://etherscan.io/address/0xccdc603a336270d63962dad04f3bd8b81143f81a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xfb68b6509c263d7cced7f874ee022f91edb398bf", - "balanceRaw": "118290970177113203944", - "balanceFormatted": "118.290970177113203944", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfb68b6509c263d7cced7f874ee022f91edb398bf", - "etherscanUrl": "https://etherscan.io/address/0xfb68b6509c263d7cced7f874ee022f91edb398bf", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5698af96beeb62e1fba97724090dde2983af0785", - "balanceRaw": "118178398496647572231", - "balanceFormatted": "118.178398496647572231", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5698af96beeb62e1fba97724090dde2983af0785", - "etherscanUrl": "https://etherscan.io/address/0x5698af96beeb62e1fba97724090dde2983af0785", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x12478d1a60a910c9cbffb90648766a2bdd5918f5", - "balanceRaw": "118113356767114416727", - "balanceFormatted": "118.113356767114416727", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x12478d1a60a910c9cbffb90648766a2bdd5918f5", - "etherscanUrl": "https://etherscan.io/address/0x12478d1a60a910c9cbffb90648766a2bdd5918f5", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_263333", - "balanceRaw": "118112836438606120672", - "balanceFormatted": "118.112836438606120672", - "lastTransferAt": null, - "username": "clfx.eth", - "displayName": "colfax", - "fid": 263333, - "followers": 15434, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/712e3b0a-f619-4541-28d4-5d57f9f10e00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x310f92fdb4e499cfa8146c86e0e1ad5ce9e36a4b", - "etherscanUrl": "https://etherscan.io/address/0x310f92fdb4e499cfa8146c86e0e1ad5ce9e36a4b", - "xHandle": "tbdzip", - "xUrl": "https://twitter.com/tbdzip", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xabb33725d50446f0b5e6e10196e309cc0e4ababe", - "balanceRaw": "117781059167161093185", - "balanceFormatted": "117.781059167161093185", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xabb33725d50446f0b5e6e10196e309cc0e4ababe", - "etherscanUrl": "https://etherscan.io/address/0xabb33725d50446f0b5e6e10196e309cc0e4ababe", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc3f6b6c4309ba9a9887216dbebf39fac78406696", - "balanceRaw": "117776376990226863606", - "balanceFormatted": "117.776376990226863606", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc3f6b6c4309ba9a9887216dbebf39fac78406696", - "etherscanUrl": "https://etherscan.io/address/0xc3f6b6c4309ba9a9887216dbebf39fac78406696", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_6643", - "balanceRaw": "117773860162866482762", - "balanceFormatted": "117.773860162866482762", - "lastTransferAt": null, - "username": "kredwyn", - "displayName": "kredwyn", - "fid": 6643, - "followers": 33, - "pfpUrl": "https://i.seadn.io/gcs/files/b66f6626cd3c243f09c333237c75fe41.jpg?w=500&auto=format", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc0c336147c15eb2beb196416c4aac50bba8bf00d", - "etherscanUrl": "https://etherscan.io/address/0xc0c336147c15eb2beb196416c4aac50bba8bf00d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x982ff67bbb6bea229b0ffd3037f11ba8d7090b53", - "balanceRaw": "117514488325378377750", - "balanceFormatted": "117.51448832537837775", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x982ff67bbb6bea229b0ffd3037f11ba8d7090b53", - "etherscanUrl": "https://etherscan.io/address/0x982ff67bbb6bea229b0ffd3037f11ba8d7090b53", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x02a7a68de9f3977c860d4b4b25c5040497e87f50", - "balanceRaw": "117473368697642311689", - "balanceFormatted": "117.473368697642311689", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x02a7a68de9f3977c860d4b4b25c5040497e87f50", - "etherscanUrl": "https://etherscan.io/address/0x02a7a68de9f3977c860d4b4b25c5040497e87f50", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe6accd697958c089474a48fcf78eeff63a98b0a1", - "balanceRaw": "116721874414925119714", - "balanceFormatted": "116.721874414925119714", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe6accd697958c089474a48fcf78eeff63a98b0a1", - "etherscanUrl": "https://etherscan.io/address/0xe6accd697958c089474a48fcf78eeff63a98b0a1", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x4cfec81930d87c0ec2d38a4819d05a756756d85b", - "balanceRaw": "116082297903543271810", - "balanceFormatted": "116.08229790354327181", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4cfec81930d87c0ec2d38a4819d05a756756d85b", - "etherscanUrl": "https://etherscan.io/address/0x4cfec81930d87c0ec2d38a4819d05a756756d85b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x2388a78bb6d3638ec2740bc0aed53489ffcd567f", - "balanceRaw": "115805537527426387082", - "balanceFormatted": "115.805537527426387082", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2388a78bb6d3638ec2740bc0aed53489ffcd567f", - "etherscanUrl": "https://etherscan.io/address/0x2388a78bb6d3638ec2740bc0aed53489ffcd567f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_4415", - "balanceRaw": "115636788297124746725", - "balanceFormatted": "115.636788297124746725", - "lastTransferAt": null, - "username": "jakub", - "displayName": "Jakub", - "fid": 4415, - "followers": 4380, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/3ea8c2b7-62ff-4de7-e962-f538a53b4600/rectcrop3", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf7841bc36405092b7661f5c9b01864c622bc445e", - "etherscanUrl": "https://etherscan.io/address/0xf7841bc36405092b7661f5c9b01864c622bc445e", - "xHandle": "jakub_rusiecki", - "xUrl": "https://twitter.com/jakub_rusiecki", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe9a7b62ebf1779e09c3a193e2a77d4e7411a974b", - "balanceRaw": "115537019129308461869", - "balanceFormatted": "115.537019129308461869", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe9a7b62ebf1779e09c3a193e2a77d4e7411a974b", - "etherscanUrl": "https://etherscan.io/address/0xe9a7b62ebf1779e09c3a193e2a77d4e7411a974b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1077510", - "balanceRaw": "115527719797952802137", - "balanceFormatted": "115.527719797952802137", - "lastTransferAt": null, - "username": "gon0x.eth", - "displayName": "Gon", - "fid": 1077510, - "followers": 385, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/379eb665-8c25-4c62-4d83-060429575200/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc002ad4e9b1dab521216be7a8f5936f2f363cbcc", - "etherscanUrl": "https://etherscan.io/address/0xc002ad4e9b1dab521216be7a8f5936f2f363cbcc", - "xHandle": "gon0x_", - "xUrl": "https://twitter.com/gon0x_", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x425240bba92d6d49012531b16e9cbc3266e011e1", - "balanceRaw": "115000000010000000000", - "balanceFormatted": "115.00000001", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x425240bba92d6d49012531b16e9cbc3266e011e1", - "etherscanUrl": "https://etherscan.io/address/0x425240bba92d6d49012531b16e9cbc3266e011e1", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x279a75b40eda7acc5d38b80802b6a2f541fd142f", - "balanceRaw": "114921578376144432160", - "balanceFormatted": "114.92157837614443216", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x279a75b40eda7acc5d38b80802b6a2f541fd142f", - "etherscanUrl": "https://etherscan.io/address/0x279a75b40eda7acc5d38b80802b6a2f541fd142f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x4d7295704db4ac8fe1bbe43c6ff2eeab665eaff6", - "balanceRaw": "114750492310661534266", - "balanceFormatted": "114.750492310661534266", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4d7295704db4ac8fe1bbe43c6ff2eeab665eaff6", - "etherscanUrl": "https://etherscan.io/address/0x4d7295704db4ac8fe1bbe43c6ff2eeab665eaff6", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8654e3e9dd4c136d31d022a79c8590a008a21ae8", - "balanceRaw": "114614552043819401216", - "balanceFormatted": "114.614552043819401216", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8654e3e9dd4c136d31d022a79c8590a008a21ae8", - "etherscanUrl": "https://etherscan.io/address/0x8654e3e9dd4c136d31d022a79c8590a008a21ae8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x02bc0a30878a7f3f5d74081f77a5ad34e651c6f0", - "balanceRaw": "114357176797156105700", - "balanceFormatted": "114.3571767971561057", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x02bc0a30878a7f3f5d74081f77a5ad34e651c6f0", - "etherscanUrl": "https://etherscan.io/address/0x02bc0a30878a7f3f5d74081f77a5ad34e651c6f0", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xfe40c2cdf4e2bf2545b93f2f81011b048ae78599", - "balanceRaw": "114258382917920130981", - "balanceFormatted": "114.258382917920130981", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfe40c2cdf4e2bf2545b93f2f81011b048ae78599", - "etherscanUrl": "https://etherscan.io/address/0xfe40c2cdf4e2bf2545b93f2f81011b048ae78599", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0f1f7b965cb1fd03ea0f0af55811c0dec0844bef", - "balanceRaw": "114114755526992345572", - "balanceFormatted": "114.114755526992345572", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0f1f7b965cb1fd03ea0f0af55811c0dec0844bef", - "etherscanUrl": "https://etherscan.io/address/0x0f1f7b965cb1fd03ea0f0af55811c0dec0844bef", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x877ec2d9718226ef47856e6a98370f65ffec3ecd", - "balanceRaw": "113962860200184337446", - "balanceFormatted": "113.962860200184337446", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x877ec2d9718226ef47856e6a98370f65ffec3ecd", - "etherscanUrl": "https://etherscan.io/address/0x877ec2d9718226ef47856e6a98370f65ffec3ecd", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xde64ddc0c056d00d05e8d3809924e01f8f92ade2", - "balanceRaw": "113625546179445455912", - "balanceFormatted": "113.625546179445455912", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xde64ddc0c056d00d05e8d3809924e01f8f92ade2", - "etherscanUrl": "https://etherscan.io/address/0xde64ddc0c056d00d05e8d3809924e01f8f92ade2", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd8f66e79beb1c64378f811c3e4fa7942e58ce3aa", - "balanceRaw": "113564892340938940664", - "balanceFormatted": "113.564892340938940664", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd8f66e79beb1c64378f811c3e4fa7942e58ce3aa", - "etherscanUrl": "https://etherscan.io/address/0xd8f66e79beb1c64378f811c3e4fa7942e58ce3aa", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_342042", - "balanceRaw": "113362939647383652304", - "balanceFormatted": "113.362939647383652304", - "lastTransferAt": null, - "username": "onepercent", - "displayName": "JG.base.eth", - "fid": 342042, - "followers": 141, - "pfpUrl": "https://i.imgur.com/badBlv6.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x73a1db5ec7567bff1b220f2acab9ca031cb0236a", - "etherscanUrl": "https://etherscan.io/address/0x73a1db5ec7567bff1b220f2acab9ca031cb0236a", - "xHandle": "onepercent_rich", - "xUrl": "https://twitter.com/onepercent_rich", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb83c0b720a019c4292f2d7ec8f2a1653fbfd142b", - "balanceRaw": "112883389878538042978", - "balanceFormatted": "112.883389878538042978", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb83c0b720a019c4292f2d7ec8f2a1653fbfd142b", - "etherscanUrl": "https://etherscan.io/address/0xb83c0b720a019c4292f2d7ec8f2a1653fbfd142b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7db02a48f67dd541b36555b1a24b170c06363d92", - "balanceRaw": "112303363834036170997", - "balanceFormatted": "112.303363834036170997", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7db02a48f67dd541b36555b1a24b170c06363d92", - "etherscanUrl": "https://etherscan.io/address/0x7db02a48f67dd541b36555b1a24b170c06363d92", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xadb12c4da80fe284f71510f59f2fce13a9ed3b5c", - "balanceRaw": "112232182166430613226", - "balanceFormatted": "112.232182166430613226", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xadb12c4da80fe284f71510f59f2fce13a9ed3b5c", - "etherscanUrl": "https://etherscan.io/address/0xadb12c4da80fe284f71510f59f2fce13a9ed3b5c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5dde89ae15d6186cc8c65298e6975c28ddd7375e", - "balanceRaw": "111808745673001631100", - "balanceFormatted": "111.8087456730016311", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5dde89ae15d6186cc8c65298e6975c28ddd7375e", - "etherscanUrl": "https://etherscan.io/address/0x5dde89ae15d6186cc8c65298e6975c28ddd7375e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe6472133973da6b78d28e187f92273a3240f6182", - "balanceRaw": "111749185433564821504", - "balanceFormatted": "111.749185433564821504", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe6472133973da6b78d28e187f92273a3240f6182", - "etherscanUrl": "https://etherscan.io/address/0xe6472133973da6b78d28e187f92273a3240f6182", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9d10d6d4297a7dca03d123f417a9a4d6259c6144", - "balanceRaw": "111535428420192486148", - "balanceFormatted": "111.535428420192486148", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9d10d6d4297a7dca03d123f417a9a4d6259c6144", - "etherscanUrl": "https://etherscan.io/address/0x9d10d6d4297a7dca03d123f417a9a4d6259c6144", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7867101442b8cc9f8551203f6bdb5d2ccc097255", - "balanceRaw": "111526413302406205358", - "balanceFormatted": "111.526413302406205358", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7867101442b8cc9f8551203f6bdb5d2ccc097255", - "etherscanUrl": "https://etherscan.io/address/0x7867101442b8cc9f8551203f6bdb5d2ccc097255", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x219867ed424e168335778bf56dcd7f047a436b16", - "balanceRaw": "111416079243586773668", - "balanceFormatted": "111.416079243586773668", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x219867ed424e168335778bf56dcd7f047a436b16", - "etherscanUrl": "https://etherscan.io/address/0x219867ed424e168335778bf56dcd7f047a436b16", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1428114", - "balanceRaw": "111411451419254732414", - "balanceFormatted": "111.411451419254732414", - "lastTransferAt": null, - "username": "kryptarion", - "displayName": "kryptarion", - "fid": 1428114, - "followers": 28, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0d0b745c-4d5d-4b3a-f029-ad8ff7d79700/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x033f35a020fbdaa7108bd8e509fd50a88a9915de", - "etherscanUrl": "https://etherscan.io/address/0x033f35a020fbdaa7108bd8e509fd50a88a9915de", - "xHandle": "kryptar1on", - "xUrl": "https://twitter.com/kryptar1on", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xdd8f672057e2d56382ac547d48efc3df8cd8b486", - "balanceRaw": "111257189621656686831", - "balanceFormatted": "111.257189621656686831", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xdd8f672057e2d56382ac547d48efc3df8cd8b486", - "etherscanUrl": "https://etherscan.io/address/0xdd8f672057e2d56382ac547d48efc3df8cd8b486", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1436", - "balanceRaw": "111000000000000000000", - "balanceFormatted": "111", - "lastTransferAt": null, - "username": "karmawav", - "displayName": "karma.wav ↑ 🌊", - "fid": 1436, - "followers": 1123, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ad10868b-4c7f-4c8a-7d73-aa056f243700/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xefac5804c8d699503c5a6409ebde96336a48f683", - "etherscanUrl": "https://etherscan.io/address/0xefac5804c8d699503c5a6409ebde96336a48f683", - "xHandle": "karmawav", - "xUrl": "https://twitter.com/karmawav", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_432186", - "balanceRaw": "110019320876544467362", - "balanceFormatted": "110.019320876544467362", - "lastTransferAt": null, - "username": "air-iick", - "displayName": "Air-iick", - "fid": 432186, - "followers": 114, - "pfpUrl": "https://i.imgur.com/4VZPIt1.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc193f33d2fedf22e7292a1bc3bd50aebad99ad45", - "etherscanUrl": "https://etherscan.io/address/0xc193f33d2fedf22e7292a1bc3bd50aebad99ad45", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xab2223d5c9bf563f8c571ca88444156dce45effb", - "balanceRaw": "110000000000000000000", - "balanceFormatted": "110", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xab2223d5c9bf563f8c571ca88444156dce45effb", - "etherscanUrl": "https://etherscan.io/address/0xab2223d5c9bf563f8c571ca88444156dce45effb", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_348393", - "balanceRaw": "109915364426296395111", - "balanceFormatted": "109.915364426296395111", - "lastTransferAt": null, - "username": "jaejae", - "displayName": "JaeJae🎩", - "fid": 348393, - "followers": 557, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/268cfef8-0f05-4c61-a132-5f29106c2e00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x969504f16c3766eaddbe454e94ff2847c90873c0", - "etherscanUrl": "https://etherscan.io/address/0x969504f16c3766eaddbe454e94ff2847c90873c0", - "xHandle": "dign2209", - "xUrl": "https://twitter.com/dign2209", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3db1729be4e9453e888e21c838740258c77217a8", - "balanceRaw": "109482678343694762897", - "balanceFormatted": "109.482678343694762897", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3db1729be4e9453e888e21c838740258c77217a8", - "etherscanUrl": "https://etherscan.io/address/0x3db1729be4e9453e888e21c838740258c77217a8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x1e6864addbe433553d0edaf70d041201aa3f2da4", - "balanceRaw": "108364012207319287098", - "balanceFormatted": "108.364012207319287098", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1e6864addbe433553d0edaf70d041201aa3f2da4", - "etherscanUrl": "https://etherscan.io/address/0x1e6864addbe433553d0edaf70d041201aa3f2da4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbba16c4685e6822de099541ef64e533b0ed5bad0", - "balanceRaw": "108010882582039913350", - "balanceFormatted": "108.01088258203991335", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbba16c4685e6822de099541ef64e533b0ed5bad0", - "etherscanUrl": "https://etherscan.io/address/0xbba16c4685e6822de099541ef64e533b0ed5bad0", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa72cb04e07b19fe5bc0b6192f4f60a355661ae3d", - "balanceRaw": "107977695422101631942", - "balanceFormatted": "107.977695422101631942", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa72cb04e07b19fe5bc0b6192f4f60a355661ae3d", - "etherscanUrl": "https://etherscan.io/address/0xa72cb04e07b19fe5bc0b6192f4f60a355661ae3d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7451d7dc1570f2be4aa3eca8d482dff117570ab7", - "balanceRaw": "107572777886393611111", - "balanceFormatted": "107.572777886393611111", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7451d7dc1570f2be4aa3eca8d482dff117570ab7", - "etherscanUrl": "https://etherscan.io/address/0x7451d7dc1570f2be4aa3eca8d482dff117570ab7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_252090", - "balanceRaw": "107531934410000000000", - "balanceFormatted": "107.53193441", - "lastTransferAt": null, - "username": "dolan", - "displayName": "Dolan", - "fid": 252090, - "followers": 246, - "pfpUrl": "https://i.imgur.com/IXPp3S3.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb98e6b716a6b1aa0d55a6e64481a93828712616f", - "etherscanUrl": "https://etherscan.io/address/0xb98e6b716a6b1aa0d55a6e64481a93828712616f", - "xHandle": "dolan_62", - "xUrl": "https://twitter.com/dolan_62", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x36847bd027558556a1092ed91cfd865cf4a8eef2", - "balanceRaw": "106729739813717044983", - "balanceFormatted": "106.729739813717044983", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x36847bd027558556a1092ed91cfd865cf4a8eef2", - "etherscanUrl": "https://etherscan.io/address/0x36847bd027558556a1092ed91cfd865cf4a8eef2", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xac1565a7fd8923652c2b8b49b6b2b66b3be4fccc", - "balanceRaw": "106576306265424866708", - "balanceFormatted": "106.576306265424866708", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xac1565a7fd8923652c2b8b49b6b2b66b3be4fccc", - "etherscanUrl": "https://etherscan.io/address/0xac1565a7fd8923652c2b8b49b6b2b66b3be4fccc", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_17635", - "balanceRaw": "106249852205213290763", - "balanceFormatted": "106.249852205213290763", - "lastTransferAt": null, - "username": "hamdi", - "displayName": "hamdi", - "fid": 17635, - "followers": 2524, - "pfpUrl": "https://i.imgur.com/ZF0nco8.png", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9efbbfe1dd1f27432b9eee8777af0844c27744a8", - "etherscanUrl": "https://etherscan.io/address/0x9efbbfe1dd1f27432b9eee8777af0844c27744a8", - "xHandle": "allam_hamdi", - "xUrl": "https://twitter.com/allam_hamdi", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc62505f5138edb515b09389d4dbfa36a16f9cb2c", - "balanceRaw": "106105113861211769937", - "balanceFormatted": "106.105113861211769937", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc62505f5138edb515b09389d4dbfa36a16f9cb2c", - "etherscanUrl": "https://etherscan.io/address/0xc62505f5138edb515b09389d4dbfa36a16f9cb2c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x62bb9b2c4b7d7a27d168b413fc0e56a5e751dd8a", - "balanceRaw": "105704317210363990918", - "balanceFormatted": "105.704317210363990918", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x62bb9b2c4b7d7a27d168b413fc0e56a5e751dd8a", - "etherscanUrl": "https://etherscan.io/address/0x62bb9b2c4b7d7a27d168b413fc0e56a5e751dd8a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0c07ae3103327589adada281a803852788af7fb4", - "balanceRaw": "105631147940824412175", - "balanceFormatted": "105.631147940824412175", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0c07ae3103327589adada281a803852788af7fb4", - "etherscanUrl": "https://etherscan.io/address/0x0c07ae3103327589adada281a803852788af7fb4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_12", - "balanceRaw": "105544650997176228019", - "balanceFormatted": "105.544650997176228019", - "lastTransferAt": null, - "username": "linda", - "displayName": "Linda Xie", - "fid": 12, - "followers": 451973, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6aa18817-6238-4b25-086f-1edd0c438f00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x54d06a08ffdf44258a9f9bd2f4b4cdd6692a900f", - "etherscanUrl": "https://etherscan.io/address/0x54d06a08ffdf44258a9f9bd2f4b4cdd6692a900f", - "xHandle": "ljxie", - "xUrl": "https://twitter.com/ljxie", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf45e5fab3911a53d14f110367a12b4dea375b2a9", - "balanceRaw": "105091602976022923578", - "balanceFormatted": "105.091602976022923578", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf45e5fab3911a53d14f110367a12b4dea375b2a9", - "etherscanUrl": "https://etherscan.io/address/0xf45e5fab3911a53d14f110367a12b4dea375b2a9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_2802", - "balanceRaw": "104761235261297134545", - "balanceFormatted": "104.761235261297134545", - "lastTransferAt": null, - "username": "garrett", - "displayName": "Garrett", - "fid": 2802, - "followers": 59339, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c131c034-0090-4610-45a4-acda4b805000/rectcrop3", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xceab0087c5fbc22fb19293bd0be5fa9b23789da9", - "etherscanUrl": "https://etherscan.io/address/0xceab0087c5fbc22fb19293bd0be5fa9b23789da9", - "xHandle": "gskrovina", - "xUrl": "https://twitter.com/gskrovina", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_532272", - "balanceRaw": "104312867377384831470", - "balanceFormatted": "104.31286737738483147", - "lastTransferAt": null, - "username": "umacherry", - "displayName": "umacherry", - "fid": 532272, - "followers": 8, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/72868476-2326-40a9-0b3e-dcc290700000/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x710a1b3f0882222654321ed0eb581bd57c60c91d", - "etherscanUrl": "https://etherscan.io/address/0x710a1b3f0882222654321ed0eb581bd57c60c91d", - "xHandle": "mzwang8", - "xUrl": "https://twitter.com/mzwang8", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd8136b670844cc527b2ce138875f6e5e3313a677", - "balanceRaw": "104062489596966901068", - "balanceFormatted": "104.062489596966901068", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd8136b670844cc527b2ce138875f6e5e3313a677", - "etherscanUrl": "https://etherscan.io/address/0xd8136b670844cc527b2ce138875f6e5e3313a677", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x04daf66e45a254ed7dc3e86b8ba2030cc33d349e", - "balanceRaw": "103729445221519254406", - "balanceFormatted": "103.729445221519254406", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x04daf66e45a254ed7dc3e86b8ba2030cc33d349e", - "etherscanUrl": "https://etherscan.io/address/0x04daf66e45a254ed7dc3e86b8ba2030cc33d349e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xfbc3692deeb263f39c3a30d6d0c0496d97686011", - "balanceRaw": "102983278198101737260", - "balanceFormatted": "102.98327819810173726", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfbc3692deeb263f39c3a30d6d0c0496d97686011", - "etherscanUrl": "https://etherscan.io/address/0xfbc3692deeb263f39c3a30d6d0c0496d97686011", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3daeebc0e636205bb9ffd23a23871d6d067f3b6b", - "balanceRaw": "102762019457667213822", - "balanceFormatted": "102.762019457667213822", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3daeebc0e636205bb9ffd23a23871d6d067f3b6b", - "etherscanUrl": "https://etherscan.io/address/0x3daeebc0e636205bb9ffd23a23871d6d067f3b6b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0173f0dcdbd82fc3a55b5c822dfee2653d245181", - "balanceRaw": "102679988677695865940", - "balanceFormatted": "102.67998867769586594", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0173f0dcdbd82fc3a55b5c822dfee2653d245181", - "etherscanUrl": "https://etherscan.io/address/0x0173f0dcdbd82fc3a55b5c822dfee2653d245181", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc4e868a1af4ac4efbf42054f324754ed90837d9c", - "balanceRaw": "102663255303400981465", - "balanceFormatted": "102.663255303400981465", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc4e868a1af4ac4efbf42054f324754ed90837d9c", - "etherscanUrl": "https://etherscan.io/address/0xc4e868a1af4ac4efbf42054f324754ed90837d9c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xcd4f5fccdab360d4e156d87f45290a6f806ab66e", - "balanceRaw": "102534496423737440167", - "balanceFormatted": "102.534496423737440167", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcd4f5fccdab360d4e156d87f45290a6f806ab66e", - "etherscanUrl": "https://etherscan.io/address/0xcd4f5fccdab360d4e156d87f45290a6f806ab66e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7e04765a05d08d45006d0db9aea670dfc2db4500", - "balanceRaw": "102484423636427945821", - "balanceFormatted": "102.484423636427945821", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7e04765a05d08d45006d0db9aea670dfc2db4500", - "etherscanUrl": "https://etherscan.io/address/0x7e04765a05d08d45006d0db9aea670dfc2db4500", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x32de6b4f63fdff34673245efb5e03495a8fa80b7", - "balanceRaw": "102325402274852304675", - "balanceFormatted": "102.325402274852304675", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x32de6b4f63fdff34673245efb5e03495a8fa80b7", - "etherscanUrl": "https://etherscan.io/address/0x32de6b4f63fdff34673245efb5e03495a8fa80b7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6f24eb89b046145b68ff55ca51342609ea436341", - "balanceRaw": "102233240451483189248", - "balanceFormatted": "102.233240451483189248", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6f24eb89b046145b68ff55ca51342609ea436341", - "etherscanUrl": "https://etherscan.io/address/0x6f24eb89b046145b68ff55ca51342609ea436341", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x12ed741096c2fac04ca10df54ea04f281def6b01", - "balanceRaw": "102062415473727171272", - "balanceFormatted": "102.062415473727171272", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x12ed741096c2fac04ca10df54ea04f281def6b01", - "etherscanUrl": "https://etherscan.io/address/0x12ed741096c2fac04ca10df54ea04f281def6b01", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x70ff9e06a4d9682d28c12614a5bb2d79ef1c330b", - "balanceRaw": "101983392981294304519", - "balanceFormatted": "101.983392981294304519", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x70ff9e06a4d9682d28c12614a5bb2d79ef1c330b", - "etherscanUrl": "https://etherscan.io/address/0x70ff9e06a4d9682d28c12614a5bb2d79ef1c330b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0c6defed38ca5cb6b4737c6df1b6906885ee3460", - "balanceRaw": "101892709895852441587", - "balanceFormatted": "101.892709895852441587", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0c6defed38ca5cb6b4737c6df1b6906885ee3460", - "etherscanUrl": "https://etherscan.io/address/0x0c6defed38ca5cb6b4737c6df1b6906885ee3460", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xebcda5b80f62dd4dd2a96357b42bb6facbf30267", - "balanceRaw": "101843380436700273689", - "balanceFormatted": "101.843380436700273689", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xebcda5b80f62dd4dd2a96357b42bb6facbf30267", - "etherscanUrl": "https://etherscan.io/address/0xebcda5b80f62dd4dd2a96357b42bb6facbf30267", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x05f640935f9d55a5a768580c972811b4021e9ca8", - "balanceRaw": "101632882923636749520", - "balanceFormatted": "101.63288292363674952", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x05f640935f9d55a5a768580c972811b4021e9ca8", - "etherscanUrl": "https://etherscan.io/address/0x05f640935f9d55a5a768580c972811b4021e9ca8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9fd197f67538f6f23a5cb4d114cbf07db0aa76e0", - "balanceRaw": "101338741383964354618", - "balanceFormatted": "101.338741383964354618", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9fd197f67538f6f23a5cb4d114cbf07db0aa76e0", - "etherscanUrl": "https://etherscan.io/address/0x9fd197f67538f6f23a5cb4d114cbf07db0aa76e0", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0ad573d7630dcb93b1fe872f83929c4b86767570", - "balanceRaw": "101325670667429300189", - "balanceFormatted": "101.325670667429300189", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0ad573d7630dcb93b1fe872f83929c4b86767570", - "etherscanUrl": "https://etherscan.io/address/0x0ad573d7630dcb93b1fe872f83929c4b86767570", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb20d7bc56cd3dea3eed4eb2f329a38bcfea789db", - "balanceRaw": "101090868318587835807", - "balanceFormatted": "101.090868318587835807", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb20d7bc56cd3dea3eed4eb2f329a38bcfea789db", - "etherscanUrl": "https://etherscan.io/address/0xb20d7bc56cd3dea3eed4eb2f329a38bcfea789db", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x1450a71f667e4a1a85870c7338366c88acac5e68", - "balanceRaw": "100847419977362701422", - "balanceFormatted": "100.847419977362701422", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1450a71f667e4a1a85870c7338366c88acac5e68", - "etherscanUrl": "https://etherscan.io/address/0x1450a71f667e4a1a85870c7338366c88acac5e68", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9da51024376a60ee499a1ceb9f47b0a29c7f3c77", - "balanceRaw": "100832337336253382800", - "balanceFormatted": "100.8323373362533828", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9da51024376a60ee499a1ceb9f47b0a29c7f3c77", - "etherscanUrl": "https://etherscan.io/address/0x9da51024376a60ee499a1ceb9f47b0a29c7f3c77", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_194896", - "balanceRaw": "100658602828806738856", - "balanceFormatted": "100.658602828806738856", - "lastTransferAt": null, - "username": "berkeleyverse.eth", - "displayName": "Greggs", - "fid": 194896, - "followers": 14, - "pfpUrl": "https://i.imgur.com/BduZYfl.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc05ebcd099739a6dcd0b0a9f7d7f4f9d34c9122f", - "etherscanUrl": "https://etherscan.io/address/0xc05ebcd099739a6dcd0b0a9f7d7f4f9d34c9122f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_889982", - "balanceRaw": "100518086292774697955", - "balanceFormatted": "100.518086292774697955", - "lastTransferAt": null, - "username": "virility", - "displayName": "Shillian Wakespeare", - "fid": 889982, - "followers": 473, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f759ca13-34a8-4595-afd4-53706736a300/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x567d1518fb0c812905d967da7071aa649904a877", - "etherscanUrl": "https://etherscan.io/address/0x567d1518fb0c812905d967da7071aa649904a877", - "xHandle": "noomnole", - "xUrl": "https://twitter.com/noomnole", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x2040e8eef43ca5e2e56e1926eff049dfd49f5daf", - "balanceRaw": "100513698891953225589", - "balanceFormatted": "100.513698891953225589", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2040e8eef43ca5e2e56e1926eff049dfd49f5daf", - "etherscanUrl": "https://etherscan.io/address/0x2040e8eef43ca5e2e56e1926eff049dfd49f5daf", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_246594", - "balanceRaw": "100510602926927441742", - "balanceFormatted": "100.510602926927441742", - "lastTransferAt": null, - "username": "birell.eth", - "displayName": "Birell.eth", - "fid": 246594, - "followers": 328, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/aacee75e-54aa-4d95-a41b-02ec31559a00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x127418d15dc59f772c5f5a29786b737f22d00de7", - "etherscanUrl": "https://etherscan.io/address/0x127418d15dc59f772c5f5a29786b737f22d00de7", - "xHandle": "jasonkim8091", - "xUrl": "https://twitter.com/jasonkim8091", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf0e08065f6a6e37ab420c791408c03fb632ad86e", - "balanceRaw": "100508536807943591538", - "balanceFormatted": "100.508536807943591538", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf0e08065f6a6e37ab420c791408c03fb632ad86e", - "etherscanUrl": "https://etherscan.io/address/0xf0e08065f6a6e37ab420c791408c03fb632ad86e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_307720", - "balanceRaw": "100388239975125800656", - "balanceFormatted": "100.388239975125800656", - "lastTransferAt": null, - "username": "hbj", - "displayName": "hbj🌎☮️", - "fid": 307720, - "followers": 567, - "pfpUrl": "https://i.imgur.com/Fucrq1p.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5cc7a44b7d632aac61dde0d65265a46f5b0cc31e", - "etherscanUrl": "https://etherscan.io/address/0x5cc7a44b7d632aac61dde0d65265a46f5b0cc31e", - "xHandle": "hanbonjovi", - "xUrl": "https://twitter.com/hanbonjovi", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa3b0b241fcad58ee8ccfddcb850bbf47ef697aac", - "balanceRaw": "100323299503982149503", - "balanceFormatted": "100.323299503982149503", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa3b0b241fcad58ee8ccfddcb850bbf47ef697aac", - "etherscanUrl": "https://etherscan.io/address/0xa3b0b241fcad58ee8ccfddcb850bbf47ef697aac", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x54e3aaee75c0d023171b22d18ea231c06aa86ec6", - "balanceRaw": "100212730198523925280", - "balanceFormatted": "100.21273019852392528", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x54e3aaee75c0d023171b22d18ea231c06aa86ec6", - "etherscanUrl": "https://etherscan.io/address/0x54e3aaee75c0d023171b22d18ea231c06aa86ec6", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbc4b5e67677cb6413ec6215540e0bc26ca9c9ac9", - "balanceRaw": "100158468829904724561", - "balanceFormatted": "100.158468829904724561", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbc4b5e67677cb6413ec6215540e0bc26ca9c9ac9", - "etherscanUrl": "https://etherscan.io/address/0xbc4b5e67677cb6413ec6215540e0bc26ca9c9ac9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x1f8c95f696881e8fc5fab47111e0a403f95dd2c9", - "balanceRaw": "100072403505387468510", - "balanceFormatted": "100.07240350538746851", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1f8c95f696881e8fc5fab47111e0a403f95dd2c9", - "etherscanUrl": "https://etherscan.io/address/0x1f8c95f696881e8fc5fab47111e0a403f95dd2c9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x4b410ae7661774a9db543e2af24c712c12ea267b", - "balanceRaw": "100066460237068531322", - "balanceFormatted": "100.066460237068531322", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4b410ae7661774a9db543e2af24c712c12ea267b", - "etherscanUrl": "https://etherscan.io/address/0x4b410ae7661774a9db543e2af24c712c12ea267b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5b586acacb1c60f9fcdb27f7bc89f12a3d6844fc", - "balanceRaw": "100055015751070422592", - "balanceFormatted": "100.055015751070422592", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5b586acacb1c60f9fcdb27f7bc89f12a3d6844fc", - "etherscanUrl": "https://etherscan.io/address/0x5b586acacb1c60f9fcdb27f7bc89f12a3d6844fc", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1626", - "balanceRaw": "100014910031002328997", - "balanceFormatted": "100.014910031002328997", - "lastTransferAt": null, - "username": "mcbain", - "displayName": "McBain", - "fid": 1626, - "followers": 142663, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/6b5f14ba-508a-4090-abca-dae88b6e6d00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x88a99ce848eed55f8ec3df4e8dfa9d1fcca6e2b0", - "etherscanUrl": "https://etherscan.io/address/0x88a99ce848eed55f8ec3df4e8dfa9d1fcca6e2b0", - "xHandle": "grahammcbain", - "xUrl": "https://twitter.com/grahammcbain", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xaf3366d72bc77817a61b28cdd7989530bcffe571", - "balanceRaw": "100006383455950873613", - "balanceFormatted": "100.006383455950873613", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xaf3366d72bc77817a61b28cdd7989530bcffe571", - "etherscanUrl": "https://etherscan.io/address/0xaf3366d72bc77817a61b28cdd7989530bcffe571", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9eaf1997c8687b2b1453c9989a1c6cde1915fef3", - "balanceRaw": "100000003901308388215", - "balanceFormatted": "100.000003901308388215", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9eaf1997c8687b2b1453c9989a1c6cde1915fef3", - "etherscanUrl": "https://etherscan.io/address/0x9eaf1997c8687b2b1453c9989a1c6cde1915fef3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x69239747a8329cfd117c673a7673152f32cfb834", - "balanceRaw": "100000000135684911753", - "balanceFormatted": "100.000000135684911753", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x69239747a8329cfd117c673a7673152f32cfb834", - "etherscanUrl": "https://etherscan.io/address/0x69239747a8329cfd117c673a7673152f32cfb834", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x611d656a52958fc4b2c8cc2bcbd54b1715aff7df", - "balanceRaw": "100000000000000000000", - "balanceFormatted": "100", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x611d656a52958fc4b2c8cc2bcbd54b1715aff7df", - "etherscanUrl": "https://etherscan.io/address/0x611d656a52958fc4b2c8cc2bcbd54b1715aff7df", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xca3459c85009d657393717d7e0072a42f0c69733", - "balanceRaw": "100000000000000000000", - "balanceFormatted": "100", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xca3459c85009d657393717d7e0072a42f0c69733", - "etherscanUrl": "https://etherscan.io/address/0xca3459c85009d657393717d7e0072a42f0c69733", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x88e23b89411dcf30e318acd8d4c3971a7f316b51", - "balanceRaw": "100000000000000000000", - "balanceFormatted": "100", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x88e23b89411dcf30e318acd8d4c3971a7f316b51", - "etherscanUrl": "https://etherscan.io/address/0x88e23b89411dcf30e318acd8d4c3971a7f316b51", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xba8c73b1d1a57bee10e3c246242881f7c23b546a", - "balanceRaw": "100000000000000000000", - "balanceFormatted": "100", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xba8c73b1d1a57bee10e3c246242881f7c23b546a", - "etherscanUrl": "https://etherscan.io/address/0xba8c73b1d1a57bee10e3c246242881f7c23b546a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xca7c76c8925b4425ddfdbb7396772544f4a4f3ea", - "balanceRaw": "100000000000000000000", - "balanceFormatted": "100", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xca7c76c8925b4425ddfdbb7396772544f4a4f3ea", - "etherscanUrl": "https://etherscan.io/address/0xca7c76c8925b4425ddfdbb7396772544f4a4f3ea", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa7eda27a9aa3f0d7dea6554c7cfe96778e980449", - "balanceRaw": "100000000000000000000", - "balanceFormatted": "100", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa7eda27a9aa3f0d7dea6554c7cfe96778e980449", - "etherscanUrl": "https://etherscan.io/address/0xa7eda27a9aa3f0d7dea6554c7cfe96778e980449", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa024e6894aab8d604d49a389b444375587113978", - "balanceRaw": "100000000000000000000", - "balanceFormatted": "100", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa024e6894aab8d604d49a389b444375587113978", - "etherscanUrl": "https://etherscan.io/address/0xa024e6894aab8d604d49a389b444375587113978", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_9856", - "balanceRaw": "99816030878095950450", - "balanceFormatted": "99.81603087809595045", - "lastTransferAt": null, - "username": "0xluo.eth", - "displayName": "0xLuo", - "fid": 9856, - "followers": 65223, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ff52f093-1881-444b-64d6-edfd5804b800/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x97e302e2638eb8ac6572598c559e56c7a9b39cdf", - "etherscanUrl": "https://etherscan.io/address/0x97e302e2638eb8ac6572598c559e56c7a9b39cdf", - "xHandle": "0xluo", - "xUrl": "https://twitter.com/0xluo", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xcd1f60f14ff8564b4b0a9a1a8e63a23de44594b3", - "balanceRaw": "99689701553832542080", - "balanceFormatted": "99.68970155383254208", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcd1f60f14ff8564b4b0a9a1a8e63a23de44594b3", - "etherscanUrl": "https://etherscan.io/address/0xcd1f60f14ff8564b4b0a9a1a8e63a23de44594b3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_16405", - "balanceRaw": "99659846531543273021", - "balanceFormatted": "99.659846531543273021", - "lastTransferAt": null, - "username": "chuckstock", - "displayName": "chuckstock", - "fid": 16405, - "followers": 19121, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b25619df-aa17-456e-3847-528f45688d00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x75d7b7686e110392ca33a6d67c253c3b76258160", - "etherscanUrl": "https://etherscan.io/address/0x75d7b7686e110392ca33a6d67c253c3b76258160", - "xHandle": "0xchuckstock", - "xUrl": "https://twitter.com/0xchuckstock", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa1fce7436e751555bc891b7025be591d5bde483d", - "balanceRaw": "99488542087873145284", - "balanceFormatted": "99.488542087873145284", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa1fce7436e751555bc891b7025be591d5bde483d", - "etherscanUrl": "https://etherscan.io/address/0xa1fce7436e751555bc891b7025be591d5bde483d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x38b26de05af6606c2273d8722f8fda8c067afe56", - "balanceRaw": "99474002904573768314", - "balanceFormatted": "99.474002904573768314", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x38b26de05af6606c2273d8722f8fda8c067afe56", - "etherscanUrl": "https://etherscan.io/address/0x38b26de05af6606c2273d8722f8fda8c067afe56", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7290ac43c0f50e18b7059f11dd1973028d9b67d6", - "balanceRaw": "99004842553938237461", - "balanceFormatted": "99.004842553938237461", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7290ac43c0f50e18b7059f11dd1973028d9b67d6", - "etherscanUrl": "https://etherscan.io/address/0x7290ac43c0f50e18b7059f11dd1973028d9b67d6", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe47410edb39dde7e5ec96e0314a693008cc66658", - "balanceRaw": "98853155884266347475", - "balanceFormatted": "98.853155884266347475", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe47410edb39dde7e5ec96e0314a693008cc66658", - "etherscanUrl": "https://etherscan.io/address/0xe47410edb39dde7e5ec96e0314a693008cc66658", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8448592ca4bdd6a9ed1fdd98f61ef0de76bb5a07", - "balanceRaw": "98348472882561630232", - "balanceFormatted": "98.348472882561630232", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8448592ca4bdd6a9ed1fdd98f61ef0de76bb5a07", - "etherscanUrl": "https://etherscan.io/address/0x8448592ca4bdd6a9ed1fdd98f61ef0de76bb5a07", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x752611dabf305477990d8c060b72c293034ffd33", - "balanceRaw": "98332062243697999119", - "balanceFormatted": "98.332062243697999119", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x752611dabf305477990d8c060b72c293034ffd33", - "etherscanUrl": "https://etherscan.io/address/0x752611dabf305477990d8c060b72c293034ffd33", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb0340088c4d8484e5b206ab5cc042d9fa9ba7b7e", - "balanceRaw": "98229155716939939653", - "balanceFormatted": "98.229155716939939653", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb0340088c4d8484e5b206ab5cc042d9fa9ba7b7e", - "etherscanUrl": "https://etherscan.io/address/0xb0340088c4d8484e5b206ab5cc042d9fa9ba7b7e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x468bc6e0f2745e1f0137d107093d9071c762b76d", - "balanceRaw": "98176409385288861133", - "balanceFormatted": "98.176409385288861133", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x468bc6e0f2745e1f0137d107093d9071c762b76d", - "etherscanUrl": "https://etherscan.io/address/0x468bc6e0f2745e1f0137d107093d9071c762b76d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_380256", - "balanceRaw": "97538499136103787544", - "balanceFormatted": "97.538499136103787544", - "lastTransferAt": null, - "username": "rohito", - "displayName": "rosu.base.eth", - "fid": 380256, - "followers": 494, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/59be92a8-9e9d-4a8d-63b7-f71c0b80f600/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x54c0d25047d0f71e408cec9afbfc34d642173484", - "etherscanUrl": "https://etherscan.io/address/0x54c0d25047d0f71e408cec9afbfc34d642173484", - "xHandle": "rosugm", - "xUrl": "https://twitter.com/rosugm", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_12048", - "balanceRaw": "97318243287819875866", - "balanceFormatted": "97.318243287819875866", - "lastTransferAt": null, - "username": "mathew.eth", - "displayName": "MattwithouttheT 🎩", - "fid": 12048, - "followers": 8791, - "pfpUrl": "https://ipfs.decentralized-content.com/ipfs/QmPzEHt4mfTGHAoGz5TbaVwD4FTqMPV2qiZ6414s5HmqMi", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa26cac6a82d76936f7bff0b53d25fa26ff893706", - "etherscanUrl": "https://etherscan.io/address/0xa26cac6a82d76936f7bff0b53d25fa26ff893706", - "xHandle": "aubergineemoji", - "xUrl": "https://twitter.com/aubergineemoji", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8160506265592a20eb84852de94c92cab0568625", - "balanceRaw": "96824166733626637172", - "balanceFormatted": "96.824166733626637172", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8160506265592a20eb84852de94c92cab0568625", - "etherscanUrl": "https://etherscan.io/address/0x8160506265592a20eb84852de94c92cab0568625", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb49bf45d93450c4c2db93dcdab95b1ea3611872c", - "balanceRaw": "96682728031692778496", - "balanceFormatted": "96.682728031692778496", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb49bf45d93450c4c2db93dcdab95b1ea3611872c", - "etherscanUrl": "https://etherscan.io/address/0xb49bf45d93450c4c2db93dcdab95b1ea3611872c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb44406147e7b39cbc6f697c6002331c33d334f85", - "balanceRaw": "96583594025695830815", - "balanceFormatted": "96.583594025695830815", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb44406147e7b39cbc6f697c6002331c33d334f85", - "etherscanUrl": "https://etherscan.io/address/0xb44406147e7b39cbc6f697c6002331c33d334f85", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3291a4913458be74228cabce82944abf390cb16f", - "balanceRaw": "96528000000000000000", - "balanceFormatted": "96.528", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3291a4913458be74228cabce82944abf390cb16f", - "etherscanUrl": "https://etherscan.io/address/0x3291a4913458be74228cabce82944abf390cb16f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_2095", - "balanceRaw": "96086452506802835540", - "balanceFormatted": "96.08645250680283554", - "lastTransferAt": null, - "username": "ting", - "displayName": "ting.⌐◨-◨", - "fid": 2095, - "followers": 1686, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/161ff709-f2f4-4ed3-8bae-fb899c69d300/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf0a0a0c0d8c22567ec5b24b17db4af8c12db2bcf", - "etherscanUrl": "https://etherscan.io/address/0xf0a0a0c0d8c22567ec5b24b17db4af8c12db2bcf", - "xHandle": "ting", - "xUrl": "https://twitter.com/ting", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_12785", - "balanceRaw": "95821939285757858860", - "balanceFormatted": "95.82193928575785886", - "lastTransferAt": null, - "username": "bvdaniel", - "displayName": "bvdaniel.base.eth", - "fid": 12785, - "followers": 2671, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ed191ed8-dc7b-45a3-993f-346e75ead200/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa2b0405506b13f180e83a66a9903a839b6cbcecd", - "etherscanUrl": "https://etherscan.io/address/0xa2b0405506b13f180e83a66a9903a839b6cbcecd", - "xHandle": "bvdani_el", - "xUrl": "https://twitter.com/bvdani_el", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x261ea25c628db22245eea559afdde44b6d289493", - "balanceRaw": "95293392775153983709", - "balanceFormatted": "95.293392775153983709", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x261ea25c628db22245eea559afdde44b6d289493", - "etherscanUrl": "https://etherscan.io/address/0x261ea25c628db22245eea559afdde44b6d289493", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_12342", - "balanceRaw": "95218748974743696928", - "balanceFormatted": "95.218748974743696928", - "lastTransferAt": null, - "username": "bigshotklim", - "displayName": "⌐◨-◨ BiGSHOT ⌐◨-◨", - "fid": 12342, - "followers": 3882, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7eaaea43-5f63-4e8a-5ae6-78b47230bd00/rectcrop3", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa406ff89d9542464ab21cb0cba1d85ebd4f3dfa4", - "etherscanUrl": "https://etherscan.io/address/0xa406ff89d9542464ab21cb0cba1d85ebd4f3dfa4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf94bc972cc66bf349d6d4eb473c352de80df75d6", - "balanceRaw": "95145238045776118573", - "balanceFormatted": "95.145238045776118573", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf94bc972cc66bf349d6d4eb473c352de80df75d6", - "etherscanUrl": "https://etherscan.io/address/0xf94bc972cc66bf349d6d4eb473c352de80df75d6", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1135756", - "balanceRaw": "94272206230736401936", - "balanceFormatted": "94.272206230736401936", - "lastTransferAt": null, - "username": "lunacycap", - "displayName": "lunacy", - "fid": 1135756, - "followers": 574, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/069284c2-4a6a-4011-388b-a2cff4770200/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3705e88a72d2e486bb7ed832bff54763637efed0", - "etherscanUrl": "https://etherscan.io/address/0x3705e88a72d2e486bb7ed832bff54763637efed0", - "xHandle": "lunacycap", - "xUrl": "https://twitter.com/lunacycap", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_10218", - "balanceRaw": "94215382753377596985", - "balanceFormatted": "94.215382753377596985", - "lastTransferAt": null, - "username": "scorz.eth", - "displayName": "scorz", - "fid": 10218, - "followers": 1215, - "pfpUrl": "https://i.imgur.com/HkY4bS8.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa1e16c39671937ee49b759805b565f1f15a56839", - "etherscanUrl": "https://etherscan.io/address/0xa1e16c39671937ee49b759805b565f1f15a56839", - "xHandle": "scorzeth", - "xUrl": "https://twitter.com/scorzeth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x865f368e735f5afbfb6d31585b8a79bfa97ca5f6", - "balanceRaw": "93941671625941375295", - "balanceFormatted": "93.941671625941375295", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x865f368e735f5afbfb6d31585b8a79bfa97ca5f6", - "etherscanUrl": "https://etherscan.io/address/0x865f368e735f5afbfb6d31585b8a79bfa97ca5f6", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa2f23d5595bce7264fab148b8e3c9456d5f65e7e", - "balanceRaw": "93838893302057355875", - "balanceFormatted": "93.838893302057355875", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa2f23d5595bce7264fab148b8e3c9456d5f65e7e", - "etherscanUrl": "https://etherscan.io/address/0xa2f23d5595bce7264fab148b8e3c9456d5f65e7e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6abeb41f64d66b60eafea043f9a2de2baa2bcfe4", - "balanceRaw": "93653347685786298710", - "balanceFormatted": "93.65334768578629871", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6abeb41f64d66b60eafea043f9a2de2baa2bcfe4", - "etherscanUrl": "https://etherscan.io/address/0x6abeb41f64d66b60eafea043f9a2de2baa2bcfe4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbd2895dc008c31ba7b2e9177ebec4e7b048acb86", - "balanceRaw": "93002130894813803148", - "balanceFormatted": "93.002130894813803148", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbd2895dc008c31ba7b2e9177ebec4e7b048acb86", - "etherscanUrl": "https://etherscan.io/address/0xbd2895dc008c31ba7b2e9177ebec4e7b048acb86", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x12d2b8ac38c59758a062a9f757f2740461779439", - "balanceRaw": "92912375519837300513", - "balanceFormatted": "92.912375519837300513", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x12d2b8ac38c59758a062a9f757f2740461779439", - "etherscanUrl": "https://etherscan.io/address/0x12d2b8ac38c59758a062a9f757f2740461779439", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd6d2506909814177a57cd5ee369c193c63ee3629", - "balanceRaw": "92242985780841818793", - "balanceFormatted": "92.242985780841818793", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd6d2506909814177a57cd5ee369c193c63ee3629", - "etherscanUrl": "https://etherscan.io/address/0xd6d2506909814177a57cd5ee369c193c63ee3629", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_5494", - "balanceRaw": "92116371645155457088", - "balanceFormatted": "92.116371645155457088", - "lastTransferAt": null, - "username": "jachian", - "displayName": "Jason", - "fid": 5494, - "followers": 18659, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/83de6628-ebdc-4625-318e-40231eb37900/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8a5827705d3c7a27864086e2d10f7e0f233e6b7a", - "etherscanUrl": "https://etherscan.io/address/0x8a5827705d3c7a27864086e2d10f7e0f233e6b7a", - "xHandle": "jachian22", - "xUrl": "https://twitter.com/jachian22", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_434644", - "balanceRaw": "91911520240839546550", - "balanceFormatted": "91.91152024083954655", - "lastTransferAt": null, - "username": "malrangcow", - "displayName": "MalrangCow", - "fid": 434644, - "followers": 3071, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/68105e96-d2fd-47e7-3fad-b13db4dab800/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe12a3ba747d1390b0ee23ca4d55cc704a1b54d11", - "etherscanUrl": "https://etherscan.io/address/0xe12a3ba747d1390b0ee23ca4d55cc704a1b54d11", - "xHandle": "kimssickssick", - "xUrl": "https://twitter.com/kimssickssick", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_99", - "balanceRaw": "91863788618997632512", - "balanceFormatted": "91.863788618997632512", - "lastTransferAt": null, - "username": "jesse.base.eth", - "displayName": "Jesse Pollak", - "fid": 99, - "followers": 574998, - "pfpUrl": "https://tba-mobile.mypinata.cloud/ipfs/QmdD5hZrJz1CDVLs2Tg5JLnNR8hjWFVZmMqVWRAWoWYFJd?pinataGatewayToken=3nq0UVhtd3rYmgYDdb1I9qv7rHsw-_DzwdWkZPRQ-QW1avFI9dCS8knaSfq_R5_q", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2211d1d0020daea8039e46cf1367962070d77da9", - "etherscanUrl": "https://etherscan.io/address/0x2211d1d0020daea8039e46cf1367962070d77da9", - "xHandle": "jessepollak", - "xUrl": "https://twitter.com/jessepollak", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_344203", - "balanceRaw": "91615476702149811202", - "balanceFormatted": "91.615476702149811202", - "lastTransferAt": null, - "username": "dany69.eth", - "displayName": "Danny", - "fid": 344203, - "followers": 4472, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/cc20ca39-55f4-4f4e-b1f1-dcc019896d00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7fee69f604fd485c7ea21ba3180df282fdfe8c58", - "etherscanUrl": "https://etherscan.io/address/0x7fee69f604fd485c7ea21ba3180df282fdfe8c58", - "xHandle": "shad0wash", - "xUrl": "https://twitter.com/shad0wash", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8d55510f872e1f574c64b74613295265abd63fbc", - "balanceRaw": "91178798536287608469", - "balanceFormatted": "91.178798536287608469", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8d55510f872e1f574c64b74613295265abd63fbc", - "etherscanUrl": "https://etherscan.io/address/0x8d55510f872e1f574c64b74613295265abd63fbc", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa251520154ca342f0b1d702bf5a56f78c982405b", - "balanceRaw": "91121475835034469893", - "balanceFormatted": "91.121475835034469893", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa251520154ca342f0b1d702bf5a56f78c982405b", - "etherscanUrl": "https://etherscan.io/address/0xa251520154ca342f0b1d702bf5a56f78c982405b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6729cf84cb2bba86aebf65bb08408ad776069548", - "balanceRaw": "90843826560255421661", - "balanceFormatted": "90.843826560255421661", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6729cf84cb2bba86aebf65bb08408ad776069548", - "etherscanUrl": "https://etherscan.io/address/0x6729cf84cb2bba86aebf65bb08408ad776069548", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x140c9fa014db55645a4be8d84e0e8ab432928106", - "balanceRaw": "90599755949413748327", - "balanceFormatted": "90.599755949413748327", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x140c9fa014db55645a4be8d84e0e8ab432928106", - "etherscanUrl": "https://etherscan.io/address/0x140c9fa014db55645a4be8d84e0e8ab432928106", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbea33ec0b1bbf093f3eb985e6970f121dda38a20", - "balanceRaw": "90451341108496050247", - "balanceFormatted": "90.451341108496050247", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbea33ec0b1bbf093f3eb985e6970f121dda38a20", - "etherscanUrl": "https://etherscan.io/address/0xbea33ec0b1bbf093f3eb985e6970f121dda38a20", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6c7ac5594a1110008a157071edf07ff38a29af9e", - "balanceRaw": "90450764903504622485", - "balanceFormatted": "90.450764903504622485", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6c7ac5594a1110008a157071edf07ff38a29af9e", - "etherscanUrl": "https://etherscan.io/address/0x6c7ac5594a1110008a157071edf07ff38a29af9e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x65c9874a0107c5553c92430a7aa3863a79d543fd", - "balanceRaw": "90345066948215331109", - "balanceFormatted": "90.345066948215331109", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x65c9874a0107c5553c92430a7aa3863a79d543fd", - "etherscanUrl": "https://etherscan.io/address/0x65c9874a0107c5553c92430a7aa3863a79d543fd", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x07b94700e51d1a750ea2b808427e1a3df8891770", - "balanceRaw": "90226350653358920875", - "balanceFormatted": "90.226350653358920875", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x07b94700e51d1a750ea2b808427e1a3df8891770", - "etherscanUrl": "https://etherscan.io/address/0x07b94700e51d1a750ea2b808427e1a3df8891770", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_486100", - "balanceRaw": "90064304754793456844", - "balanceFormatted": "90.064304754793456844", - "lastTransferAt": null, - "username": "clarkamoto", - "displayName": "Silver Surfer", - "fid": 486100, - "followers": 15, - "pfpUrl": "https://i.imgur.com/0IQR3qB.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x30c742d33786cafd4287129868da988e33ecddd3", - "etherscanUrl": "https://etherscan.io/address/0x30c742d33786cafd4287129868da988e33ecddd3", - "xHandle": "clarkdavinpart", - "xUrl": "https://twitter.com/clarkdavinpart", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7e088fd5312edd6a6a9203e743d09c134af1a29c", - "balanceRaw": "90000000000000000000", - "balanceFormatted": "90", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7e088fd5312edd6a6a9203e743d09c134af1a29c", - "etherscanUrl": "https://etherscan.io/address/0x7e088fd5312edd6a6a9203e743d09c134af1a29c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x97abb2b50c67efabb1114266d6fb8a543fe23bbf", - "balanceRaw": "89992000000000000000", - "balanceFormatted": "89.992", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x97abb2b50c67efabb1114266d6fb8a543fe23bbf", - "etherscanUrl": "https://etherscan.io/address/0x97abb2b50c67efabb1114266d6fb8a543fe23bbf", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x46808ab27e74044f99f253db6fb5e5d2e7dd967f", - "balanceRaw": "89633269843034281100", - "balanceFormatted": "89.6332698430342811", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x46808ab27e74044f99f253db6fb5e5d2e7dd967f", - "etherscanUrl": "https://etherscan.io/address/0x46808ab27e74044f99f253db6fb5e5d2e7dd967f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_188231", - "balanceRaw": "89107365878473201757", - "balanceFormatted": "89.107365878473201757", - "lastTransferAt": null, - "username": "!188231", - "displayName": null, - "fid": 188231, - "followers": 0, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9bfd4132de6018f23d2fa2a09130c47d65c2a7df", - "etherscanUrl": "https://etherscan.io/address/0x9bfd4132de6018f23d2fa2a09130c47d65c2a7df", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x4ac523187901c75879f25918aef09ba8a0adcdef", - "balanceRaw": "88838379365324099068", - "balanceFormatted": "88.838379365324099068", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4ac523187901c75879f25918aef09ba8a0adcdef", - "etherscanUrl": "https://etherscan.io/address/0x4ac523187901c75879f25918aef09ba8a0adcdef", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xfc5ba0b61fa9a3bf14ec38c690e91f6f8eca73b2", - "balanceRaw": "88509151276005955716", - "balanceFormatted": "88.509151276005955716", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfc5ba0b61fa9a3bf14ec38c690e91f6f8eca73b2", - "etherscanUrl": "https://etherscan.io/address/0xfc5ba0b61fa9a3bf14ec38c690e91f6f8eca73b2", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x93544552e104b6ca5286b6829b5bd82fcd2393d2", - "balanceRaw": "88136352727472715774", - "balanceFormatted": "88.136352727472715774", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x93544552e104b6ca5286b6829b5bd82fcd2393d2", - "etherscanUrl": "https://etherscan.io/address/0x93544552e104b6ca5286b6829b5bd82fcd2393d2", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa83ca1f025f99aa3642ccf613e79569c32645c01", - "balanceRaw": "88004619838244609712", - "balanceFormatted": "88.004619838244609712", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa83ca1f025f99aa3642ccf613e79569c32645c01", - "etherscanUrl": "https://etherscan.io/address/0xa83ca1f025f99aa3642ccf613e79569c32645c01", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xcf9d00ad1706098683588dff19c1bc7d16ae2cb2", - "balanceRaw": "87667219066860322326", - "balanceFormatted": "87.667219066860322326", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcf9d00ad1706098683588dff19c1bc7d16ae2cb2", - "etherscanUrl": "https://etherscan.io/address/0xcf9d00ad1706098683588dff19c1bc7d16ae2cb2", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf09395a315c6721eccdb548444def3f1b0622f32", - "balanceRaw": "87515469431441580487", - "balanceFormatted": "87.515469431441580487", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf09395a315c6721eccdb548444def3f1b0622f32", - "etherscanUrl": "https://etherscan.io/address/0xf09395a315c6721eccdb548444def3f1b0622f32", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_11124", - "balanceRaw": "87221043248234903623", - "balanceFormatted": "87.221043248234903623", - "lastTransferAt": null, - "username": "ds8", - "displayName": "dusan", - "fid": 11124, - "followers": 19284, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/7cfc6af7-1eb5-4287-5818-afd214f45b00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc2926a39d0d88d4c1b227deb933022f1492e818c", - "etherscanUrl": "https://etherscan.io/address/0xc2926a39d0d88d4c1b227deb933022f1492e818c", - "xHandle": "great_imposter", - "xUrl": "https://twitter.com/great_imposter", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9bd967bd4682d6e90d73bb0efe2d8eb5475e234b", - "balanceRaw": "87058921431869444076", - "balanceFormatted": "87.058921431869444076", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9bd967bd4682d6e90d73bb0efe2d8eb5475e234b", - "etherscanUrl": "https://etherscan.io/address/0x9bd967bd4682d6e90d73bb0efe2d8eb5475e234b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x049c48d6e1619cc9aad5005aa3508a33b4f4a114", - "balanceRaw": "86955751062608540913", - "balanceFormatted": "86.955751062608540913", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x049c48d6e1619cc9aad5005aa3508a33b4f4a114", - "etherscanUrl": "https://etherscan.io/address/0x049c48d6e1619cc9aad5005aa3508a33b4f4a114", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_534632", - "balanceRaw": "86737107929077859975", - "balanceFormatted": "86.737107929077859975", - "lastTransferAt": null, - "username": "vipprime", - "displayName": "vipprime", - "fid": 534632, - "followers": 12, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/86c6cf62-92c6-4eba-31ce-60185f8eb100/rectcrop3", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xba046be9fbf97aab4846a738744f36f2f2cbeaff", - "etherscanUrl": "https://etherscan.io/address/0xba046be9fbf97aab4846a738744f36f2f2cbeaff", - "xHandle": "waytang5", - "xUrl": "https://twitter.com/waytang5", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8d0a93719244d70a2bf084efd657ac08b19fdfc9", - "balanceRaw": "86702854556675391488", - "balanceFormatted": "86.702854556675391488", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8d0a93719244d70a2bf084efd657ac08b19fdfc9", - "etherscanUrl": "https://etherscan.io/address/0x8d0a93719244d70a2bf084efd657ac08b19fdfc9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x15b253c7032a4416d611b423ba48cd4850fec521", - "balanceRaw": "86654288450395836754", - "balanceFormatted": "86.654288450395836754", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x15b253c7032a4416d611b423ba48cd4850fec521", - "etherscanUrl": "https://etherscan.io/address/0x15b253c7032a4416d611b423ba48cd4850fec521", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x39ec1b954be9a361929126257d3a4f08e2326f2d", - "balanceRaw": "86597380133733775452", - "balanceFormatted": "86.597380133733775452", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x39ec1b954be9a361929126257d3a4f08e2326f2d", - "etherscanUrl": "https://etherscan.io/address/0x39ec1b954be9a361929126257d3a4f08e2326f2d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x680de71ab50e9cc0fbeb21610977c1b6832e3649", - "balanceRaw": "86361010124278522320", - "balanceFormatted": "86.36101012427852232", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x680de71ab50e9cc0fbeb21610977c1b6832e3649", - "etherscanUrl": "https://etherscan.io/address/0x680de71ab50e9cc0fbeb21610977c1b6832e3649", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_191231", - "balanceRaw": "86273469433864847137", - "balanceFormatted": "86.273469433864847137", - "lastTransferAt": null, - "username": "ruthn", - "displayName": "Ruthn", - "fid": 191231, - "followers": 575, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9fefbd69-a757-4915-838c-643379d5dc00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7e179e77a0c7ba04ac0d02e136c9d6516660c80e", - "etherscanUrl": "https://etherscan.io/address/0x7e179e77a0c7ba04ac0d02e136c9d6516660c80e", - "xHandle": "goodp1ne", - "xUrl": "https://twitter.com/goodp1ne", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf8a3a22ea8152a4d3d5a9acc33bc0c88639ff401", - "balanceRaw": "85830572371732265762", - "balanceFormatted": "85.830572371732265762", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf8a3a22ea8152a4d3d5a9acc33bc0c88639ff401", - "etherscanUrl": "https://etherscan.io/address/0xf8a3a22ea8152a4d3d5a9acc33bc0c88639ff401", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6fc8fe5b6c9505fbcddd83844b774c26b7bcfc7c", - "balanceRaw": "85817677764151486889", - "balanceFormatted": "85.817677764151486889", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6fc8fe5b6c9505fbcddd83844b774c26b7bcfc7c", - "etherscanUrl": "https://etherscan.io/address/0x6fc8fe5b6c9505fbcddd83844b774c26b7bcfc7c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8383b9a512f1c0e57cd015b076372b2844859e57", - "balanceRaw": "85640268633596225666", - "balanceFormatted": "85.640268633596225666", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8383b9a512f1c0e57cd015b076372b2844859e57", - "etherscanUrl": "https://etherscan.io/address/0x8383b9a512f1c0e57cd015b076372b2844859e57", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x21578a4c790d4cf1cb22a7751649166e22342af9", - "balanceRaw": "85255603938840538625", - "balanceFormatted": "85.255603938840538625", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x21578a4c790d4cf1cb22a7751649166e22342af9", - "etherscanUrl": "https://etherscan.io/address/0x21578a4c790d4cf1cb22a7751649166e22342af9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8f5e31a0dc294a282f21c2eaf393976fc8cfa33a", - "balanceRaw": "85237210165580337692", - "balanceFormatted": "85.237210165580337692", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8f5e31a0dc294a282f21c2eaf393976fc8cfa33a", - "etherscanUrl": "https://etherscan.io/address/0x8f5e31a0dc294a282f21c2eaf393976fc8cfa33a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0898fd54c43633e262425996b5971f5fd86b9b35", - "balanceRaw": "85163349640248009553", - "balanceFormatted": "85.163349640248009553", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0898fd54c43633e262425996b5971f5fd86b9b35", - "etherscanUrl": "https://etherscan.io/address/0x0898fd54c43633e262425996b5971f5fd86b9b35", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xecbb2d5e0ad6a5a9907d5c6a0049f5c9bb8b79be", - "balanceRaw": "85016245652266432902", - "balanceFormatted": "85.016245652266432902", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xecbb2d5e0ad6a5a9907d5c6a0049f5c9bb8b79be", - "etherscanUrl": "https://etherscan.io/address/0xecbb2d5e0ad6a5a9907d5c6a0049f5c9bb8b79be", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf8c28eb904fce4c6a60862b56b1b2946bce6ecf2", - "balanceRaw": "84697908322950949966", - "balanceFormatted": "84.697908322950949966", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf8c28eb904fce4c6a60862b56b1b2946bce6ecf2", - "etherscanUrl": "https://etherscan.io/address/0xf8c28eb904fce4c6a60862b56b1b2946bce6ecf2", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xaf64f38f934b8944bfed3577aaf89bb9635192ab", - "balanceRaw": "84621926564453845617", - "balanceFormatted": "84.621926564453845617", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xaf64f38f934b8944bfed3577aaf89bb9635192ab", - "etherscanUrl": "https://etherscan.io/address/0xaf64f38f934b8944bfed3577aaf89bb9635192ab", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3dd3d85a6b92aaebb2fc15bea36eb23d4886ecc5", - "balanceRaw": "84517866071235333814", - "balanceFormatted": "84.517866071235333814", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3dd3d85a6b92aaebb2fc15bea36eb23d4886ecc5", - "etherscanUrl": "https://etherscan.io/address/0x3dd3d85a6b92aaebb2fc15bea36eb23d4886ecc5", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6679ce32f4973bd4f0a0e7ca3da2097350df334d", - "balanceRaw": "84393009416772085664", - "balanceFormatted": "84.393009416772085664", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6679ce32f4973bd4f0a0e7ca3da2097350df334d", - "etherscanUrl": "https://etherscan.io/address/0x6679ce32f4973bd4f0a0e7ca3da2097350df334d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_311845", - "balanceRaw": "84056351688403420897", - "balanceFormatted": "84.056351688403420897", - "lastTransferAt": null, - "username": "reisub.eth", - "displayName": "Bas↑", - "fid": 311845, - "followers": 8200, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/550b332a-097f-49b6-ffcf-6a5d720ba100/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x11415669286d998fd85a2a6146c41f6be41a794d", - "etherscanUrl": "https://etherscan.io/address/0x11415669286d998fd85a2a6146c41f6be41a794d", - "xHandle": "reisubbas", - "xUrl": "https://twitter.com/reisubbas", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7a28dbb2e71b4eb7ed47dcfaa0b689fc6c3283ae", - "balanceRaw": "83604811946092334778", - "balanceFormatted": "83.604811946092334778", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7a28dbb2e71b4eb7ed47dcfaa0b689fc6c3283ae", - "etherscanUrl": "https://etherscan.io/address/0x7a28dbb2e71b4eb7ed47dcfaa0b689fc6c3283ae", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0c9c5aa6d36813167000e0ff627de0fabd0c40a1", - "balanceRaw": "83338030060525656778", - "balanceFormatted": "83.338030060525656778", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0c9c5aa6d36813167000e0ff627de0fabd0c40a1", - "etherscanUrl": "https://etherscan.io/address/0x0c9c5aa6d36813167000e0ff627de0fabd0c40a1", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3a687de5c9a4f8b54a8c800fdff9a418f3898378", - "balanceRaw": "82960704559187229434", - "balanceFormatted": "82.960704559187229434", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3a687de5c9a4f8b54a8c800fdff9a418f3898378", - "etherscanUrl": "https://etherscan.io/address/0x3a687de5c9a4f8b54a8c800fdff9a418f3898378", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_290857", - "balanceRaw": "82702313907413968500", - "balanceFormatted": "82.7023139074139685", - "lastTransferAt": null, - "username": "0xbr0", - "displayName": "0xbr0", - "fid": 290857, - "followers": 2603, - "pfpUrl": "https://ipfs.decentralized-content.com/ipfs/bafkreieq6xkg3wo2l5kooroaehay7grr2blwyvzhn76onjpb2tqtrlkeiq", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6c4e0c3796a4c3c447e7bd502db11f05184e55c1", - "etherscanUrl": "https://etherscan.io/address/0x6c4e0c3796a4c3c447e7bd502db11f05184e55c1", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x2d5b665a27e2388b09a29b07107eaacc6e543113", - "balanceRaw": "82468660128667979366", - "balanceFormatted": "82.468660128667979366", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2d5b665a27e2388b09a29b07107eaacc6e543113", - "etherscanUrl": "https://etherscan.io/address/0x2d5b665a27e2388b09a29b07107eaacc6e543113", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_17555", - "balanceRaw": "82387648598878193480", - "balanceFormatted": "82.38764859887819348", - "lastTransferAt": null, - "username": "jrs", - "displayName": "Jonathan", - "fid": 17555, - "followers": 2410, - "pfpUrl": "https://i.imgur.com/jF6786O.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9034d63cc2710df8756b5680593245289933f58a", - "etherscanUrl": "https://etherscan.io/address/0x9034d63cc2710df8756b5680593245289933f58a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_265428", - "balanceRaw": "82153897498411713385", - "balanceFormatted": "82.153897498411713385", - "lastTransferAt": null, - "username": "tungweb3", - "displayName": "Tungweb3 華流", - "fid": 265428, - "followers": 78, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/029e0bba-9965-47bf-67f5-c932b0703c00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x022f4770e49d442250cb0abfbea61eb48cab9333", - "etherscanUrl": "https://etherscan.io/address/0x022f4770e49d442250cb0abfbea61eb48cab9333", - "xHandle": "tungweb3", - "xUrl": "https://twitter.com/tungweb3", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_4227", - "balanceRaw": "81721249357843002808", - "balanceFormatted": "81.721249357843002808", - "lastTransferAt": null, - "username": "tjkawa", - "displayName": "TJ Kawamura", - "fid": 4227, - "followers": 336, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5a5f0428-2c45-4177-deaa-de4a6bc4cb00/rectcrop3", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1c205ff5c9f247bc985a11e208fd1727d4d74b43", - "etherscanUrl": "https://etherscan.io/address/0x1c205ff5c9f247bc985a11e208fd1727d4d74b43", - "xHandle": "tj_kawa", - "xUrl": "https://twitter.com/tj_kawa", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_4327", - "balanceRaw": "81702536777646937133", - "balanceFormatted": "81.702536777646937133", - "lastTransferAt": null, - "username": "cojo.eth", - "displayName": "Cojo 💭", - "fid": 4327, - "followers": 129937, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/01439454-0146-4af1-de3d-697661637300/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcaaa26c5498de67466e6823ef69718feb04c2952", - "etherscanUrl": "https://etherscan.io/address/0xcaaa26c5498de67466e6823ef69718feb04c2952", - "xHandle": "cojo_eth", - "xUrl": "https://twitter.com/cojo_eth", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x095a9fb7a06a5a9e28f0fcbe7ce04abd38017091", - "balanceRaw": "81409323005328978633", - "balanceFormatted": "81.409323005328978633", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x095a9fb7a06a5a9e28f0fcbe7ce04abd38017091", - "etherscanUrl": "https://etherscan.io/address/0x095a9fb7a06a5a9e28f0fcbe7ce04abd38017091", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xac89e080b7aff490a6756fa320ebd55417415de8", - "balanceRaw": "80986563686344822439", - "balanceFormatted": "80.986563686344822439", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xac89e080b7aff490a6756fa320ebd55417415de8", - "etherscanUrl": "https://etherscan.io/address/0xac89e080b7aff490a6756fa320ebd55417415de8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x174d5727bdb7fa9b0d4e8be7a5c14d698e7d4b2d", - "balanceRaw": "80766607938005835793", - "balanceFormatted": "80.766607938005835793", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x174d5727bdb7fa9b0d4e8be7a5c14d698e7d4b2d", - "etherscanUrl": "https://etherscan.io/address/0x174d5727bdb7fa9b0d4e8be7a5c14d698e7d4b2d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd88c41ccb22e8d808c7eef78a261a727b74224d4", - "balanceRaw": "80701340156023659680", - "balanceFormatted": "80.70134015602365968", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd88c41ccb22e8d808c7eef78a261a727b74224d4", - "etherscanUrl": "https://etherscan.io/address/0xd88c41ccb22e8d808c7eef78a261a727b74224d4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x37a576896cfca6b97c3b243cc7eda3a102db982d", - "balanceRaw": "80089227992267123400", - "balanceFormatted": "80.0892279922671234", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x37a576896cfca6b97c3b243cc7eda3a102db982d", - "etherscanUrl": "https://etherscan.io/address/0x37a576896cfca6b97c3b243cc7eda3a102db982d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbbb5008da7ef90a416a4389fdad3872d6896cff9", - "balanceRaw": "80003848577598611991", - "balanceFormatted": "80.003848577598611991", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbbb5008da7ef90a416a4389fdad3872d6896cff9", - "etherscanUrl": "https://etherscan.io/address/0xbbb5008da7ef90a416a4389fdad3872d6896cff9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_7730", - "balanceRaw": "80000000000000000000", - "balanceFormatted": "80", - "lastTransferAt": null, - "username": "rishabhbansal", - "displayName": "Rishabh Bansal", - "fid": 7730, - "followers": 35, - "pfpUrl": "https://i.imgur.com/BLPeM5C.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa16f60250eba1b9a9fa2e9ed609e302383fd9c82", - "etherscanUrl": "https://etherscan.io/address/0xa16f60250eba1b9a9fa2e9ed609e302383fd9c82", - "xHandle": "rishabhbansal97", - "xUrl": "https://twitter.com/rishabhbansal97", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x614cef4a37c61daca1627537ba213691217278a6", - "balanceRaw": "79997767516489959359", - "balanceFormatted": "79.997767516489959359", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x614cef4a37c61daca1627537ba213691217278a6", - "etherscanUrl": "https://etherscan.io/address/0x614cef4a37c61daca1627537ba213691217278a6", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x913d9688882b2e57e8277d727cc5fd99fee0922c", - "balanceRaw": "79807937844557483437", - "balanceFormatted": "79.807937844557483437", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x913d9688882b2e57e8277d727cc5fd99fee0922c", - "etherscanUrl": "https://etherscan.io/address/0x913d9688882b2e57e8277d727cc5fd99fee0922c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xda4bcc6a2f9a1815348918e2d4ad9d1355856d6e", - "balanceRaw": "79730874893321377263", - "balanceFormatted": "79.730874893321377263", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xda4bcc6a2f9a1815348918e2d4ad9d1355856d6e", - "etherscanUrl": "https://etherscan.io/address/0xda4bcc6a2f9a1815348918e2d4ad9d1355856d6e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xff6f0b67ca7244f81786436788bfd8bf8328e791", - "balanceRaw": "79651229855577542190", - "balanceFormatted": "79.65122985557754219", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xff6f0b67ca7244f81786436788bfd8bf8328e791", - "etherscanUrl": "https://etherscan.io/address/0xff6f0b67ca7244f81786436788bfd8bf8328e791", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xed5d4a44a1a6f716fb121d9af149a9460ee69c02", - "balanceRaw": "79556333729441137794", - "balanceFormatted": "79.556333729441137794", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xed5d4a44a1a6f716fb121d9af149a9460ee69c02", - "etherscanUrl": "https://etherscan.io/address/0xed5d4a44a1a6f716fb121d9af149a9460ee69c02", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7b6f24bd87ad9d8f7bc0107769a50a20fc833844", - "balanceRaw": "79490281630858445116", - "balanceFormatted": "79.490281630858445116", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7b6f24bd87ad9d8f7bc0107769a50a20fc833844", - "etherscanUrl": "https://etherscan.io/address/0x7b6f24bd87ad9d8f7bc0107769a50a20fc833844", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5e1efeeec722c33298672dbd453b47754726f911", - "balanceRaw": "79380707366444892294", - "balanceFormatted": "79.380707366444892294", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5e1efeeec722c33298672dbd453b47754726f911", - "etherscanUrl": "https://etherscan.io/address/0x5e1efeeec722c33298672dbd453b47754726f911", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7b8cc1c88d1f754f51ae97a512c5212e27f9948b", - "balanceRaw": "79369167944749237642", - "balanceFormatted": "79.369167944749237642", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7b8cc1c88d1f754f51ae97a512c5212e27f9948b", - "etherscanUrl": "https://etherscan.io/address/0x7b8cc1c88d1f754f51ae97a512c5212e27f9948b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xcdd86f25a793b55fb70fd86f9502a17afb746fa1", - "balanceRaw": "79264184110685342247", - "balanceFormatted": "79.264184110685342247", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcdd86f25a793b55fb70fd86f9502a17afb746fa1", - "etherscanUrl": "https://etherscan.io/address/0xcdd86f25a793b55fb70fd86f9502a17afb746fa1", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x10dfe5ed945fbb91b49ddd1810cf267420a8062d", - "balanceRaw": "79195127724652473246", - "balanceFormatted": "79.195127724652473246", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x10dfe5ed945fbb91b49ddd1810cf267420a8062d", - "etherscanUrl": "https://etherscan.io/address/0x10dfe5ed945fbb91b49ddd1810cf267420a8062d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xfff1f0a197022b16fb04128bd2c6c4b85eed335e", - "balanceRaw": "79062793550565597873", - "balanceFormatted": "79.062793550565597873", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfff1f0a197022b16fb04128bd2c6c4b85eed335e", - "etherscanUrl": "https://etherscan.io/address/0xfff1f0a197022b16fb04128bd2c6c4b85eed335e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_271329", - "balanceRaw": "78797913825910027721", - "balanceFormatted": "78.797913825910027721", - "lastTransferAt": null, - "username": "!271329", - "displayName": null, - "fid": 271329, - "followers": 0, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb44b476a7f6a83b5e0559433b0d3ad374a0c1682", - "etherscanUrl": "https://etherscan.io/address/0xb44b476a7f6a83b5e0559433b0d3ad374a0c1682", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb01b6810075a425f0e7ff26f7dbb688e38e1daf2", - "balanceRaw": "78089387382307676883", - "balanceFormatted": "78.089387382307676883", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb01b6810075a425f0e7ff26f7dbb688e38e1daf2", - "etherscanUrl": "https://etherscan.io/address/0xb01b6810075a425f0e7ff26f7dbb688e38e1daf2", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3629c9526a3dacd2bcafc85a5082c0d9b138ca98", - "balanceRaw": "77768025874452399620", - "balanceFormatted": "77.76802587445239962", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3629c9526a3dacd2bcafc85a5082c0d9b138ca98", - "etherscanUrl": "https://etherscan.io/address/0x3629c9526a3dacd2bcafc85a5082c0d9b138ca98", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x4a9e40d550676984c85888905c37eaa54e8d086e", - "balanceRaw": "77752096895546268975", - "balanceFormatted": "77.752096895546268975", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4a9e40d550676984c85888905c37eaa54e8d086e", - "etherscanUrl": "https://etherscan.io/address/0x4a9e40d550676984c85888905c37eaa54e8d086e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7e75607ece8a4ccd7887aeee4abdedfd70aa2ae1", - "balanceRaw": "77488660596319413814", - "balanceFormatted": "77.488660596319413814", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7e75607ece8a4ccd7887aeee4abdedfd70aa2ae1", - "etherscanUrl": "https://etherscan.io/address/0x7e75607ece8a4ccd7887aeee4abdedfd70aa2ae1", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x869cf0b4275c42efe4ea6854bd3aadf1038434aa", - "balanceRaw": "77332865763331459653", - "balanceFormatted": "77.332865763331459653", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x869cf0b4275c42efe4ea6854bd3aadf1038434aa", - "etherscanUrl": "https://etherscan.io/address/0x869cf0b4275c42efe4ea6854bd3aadf1038434aa", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xaba709c0c605f01d4baca31c8aba50248fade27b", - "balanceRaw": "77310165641728285030", - "balanceFormatted": "77.31016564172828503", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xaba709c0c605f01d4baca31c8aba50248fade27b", - "etherscanUrl": "https://etherscan.io/address/0xaba709c0c605f01d4baca31c8aba50248fade27b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf4654c162a256758ded7bc9305333e2f4c209849", - "balanceRaw": "77211270209590928679", - "balanceFormatted": "77.211270209590928679", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf4654c162a256758ded7bc9305333e2f4c209849", - "etherscanUrl": "https://etherscan.io/address/0xf4654c162a256758ded7bc9305333e2f4c209849", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x17e325b1647ab6d61f668cc266eb94590fef7ef4", - "balanceRaw": "77206862152683906505", - "balanceFormatted": "77.206862152683906505", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x17e325b1647ab6d61f668cc266eb94590fef7ef4", - "etherscanUrl": "https://etherscan.io/address/0x17e325b1647ab6d61f668cc266eb94590fef7ef4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0b5489942688f16e6cd80c4bd7100cbf9ecdc161", - "balanceRaw": "77170076124913299565", - "balanceFormatted": "77.170076124913299565", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0b5489942688f16e6cd80c4bd7100cbf9ecdc161", - "etherscanUrl": "https://etherscan.io/address/0x0b5489942688f16e6cd80c4bd7100cbf9ecdc161", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x04b2572fc5031af5956970d1a3a2705c4aff3ad4", - "balanceRaw": "77112018271769343644", - "balanceFormatted": "77.112018271769343644", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x04b2572fc5031af5956970d1a3a2705c4aff3ad4", - "etherscanUrl": "https://etherscan.io/address/0x04b2572fc5031af5956970d1a3a2705c4aff3ad4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x77335d2c99a22cad98e2bfe2a682076e7ce7a974", - "balanceRaw": "76789402351536855950", - "balanceFormatted": "76.78940235153685595", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x77335d2c99a22cad98e2bfe2a682076e7ce7a974", - "etherscanUrl": "https://etherscan.io/address/0x77335d2c99a22cad98e2bfe2a682076e7ce7a974", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_533", - "balanceRaw": "76744929013285194106", - "balanceFormatted": "76.744929013285194106", - "lastTransferAt": null, - "username": "alexpaden", - "displayName": "shoni.eth", - "fid": 533, - "followers": 63440, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/52fe8f75-1edc-4ec7-7580-a100d43a8700/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8f655ca3ad2629dd38de8cafa7c20d748a4ec2c2", - "etherscanUrl": "https://etherscan.io/address/0x8f655ca3ad2629dd38de8cafa7c20d748a4ec2c2", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xad8fcc19e9ab4edf72c0a6c7693d1a70542bfd3d", - "balanceRaw": "76248573618352545337", - "balanceFormatted": "76.248573618352545337", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xad8fcc19e9ab4edf72c0a6c7693d1a70542bfd3d", - "etherscanUrl": "https://etherscan.io/address/0xad8fcc19e9ab4edf72c0a6c7693d1a70542bfd3d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd1349b97b644537f96f72e0572c3cc960b8ae672", - "balanceRaw": "76242688833123308394", - "balanceFormatted": "76.242688833123308394", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd1349b97b644537f96f72e0572c3cc960b8ae672", - "etherscanUrl": "https://etherscan.io/address/0xd1349b97b644537f96f72e0572c3cc960b8ae672", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_4375", - "balanceRaw": "76054405443241700108", - "balanceFormatted": "76.054405443241700108", - "lastTransferAt": null, - "username": "resipsa", - "displayName": "res ipsa ☺︎", - "fid": 4375, - "followers": 8914, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/ff4bb513-6e84-46ad-3340-6a864966d000/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x355b1a9f48f48bdd2b77607c6aef4a7acd0ddacf", - "etherscanUrl": "https://etherscan.io/address/0x355b1a9f48f48bdd2b77607c6aef4a7acd0ddacf", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8b3453b96c6267dbf22f559fb8dd9f522722d065", - "balanceRaw": "75935889651138996465", - "balanceFormatted": "75.935889651138996465", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8b3453b96c6267dbf22f559fb8dd9f522722d065", - "etherscanUrl": "https://etherscan.io/address/0x8b3453b96c6267dbf22f559fb8dd9f522722d065", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_190391", - "balanceRaw": "75670972924805586812", - "balanceFormatted": "75.670972924805586812", - "lastTransferAt": null, - "username": "zeronoledger", - "displayName": "Zeronoledger 🎩", - "fid": 190391, - "followers": 71, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/d91bfc83-c6e0-4b00-5305-ee60dc07a500/rectcrop3", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x39766ef16062502a5dcd6ac72f40d6b3bed8ed73", - "etherscanUrl": "https://etherscan.io/address/0x39766ef16062502a5dcd6ac72f40d6b3bed8ed73", - "xHandle": "zeronoledger", - "xUrl": "https://twitter.com/zeronoledger", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbd3ad03cf720cf508f82b799cc4013958615e443", - "balanceRaw": "75420457004545433103", - "balanceFormatted": "75.420457004545433103", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbd3ad03cf720cf508f82b799cc4013958615e443", - "etherscanUrl": "https://etherscan.io/address/0xbd3ad03cf720cf508f82b799cc4013958615e443", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xfec3a995588a4bc72bed6a47f6c3a61946030001", - "balanceRaw": "75311511602070250860", - "balanceFormatted": "75.31151160207025086", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfec3a995588a4bc72bed6a47f6c3a61946030001", - "etherscanUrl": "https://etherscan.io/address/0xfec3a995588a4bc72bed6a47f6c3a61946030001", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3e18ebb703d4fafb3f96deefc49a302877c8c2c7", - "balanceRaw": "75299619732539329339", - "balanceFormatted": "75.299619732539329339", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3e18ebb703d4fafb3f96deefc49a302877c8c2c7", - "etherscanUrl": "https://etherscan.io/address/0x3e18ebb703d4fafb3f96deefc49a302877c8c2c7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_270067", - "balanceRaw": "75259592168632536439", - "balanceFormatted": "75.259592168632536439", - "lastTransferAt": null, - "username": "in2themetaverse", - "displayName": "Wat", - "fid": 270067, - "followers": 30, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/5ea549ad-67a5-4c20-cda3-8eb341232900/rectcrop3", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x20d4db3be3eb78de24a7be06f8cc5f8ee4ce388e", - "etherscanUrl": "https://etherscan.io/address/0x20d4db3be3eb78de24a7be06f8cc5f8ee4ce388e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x91397a787a0856ad57dbbeff3e90e661b75d52e6", - "balanceRaw": "75152489582581430593", - "balanceFormatted": "75.152489582581430593", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x91397a787a0856ad57dbbeff3e90e661b75d52e6", - "etherscanUrl": "https://etherscan.io/address/0x91397a787a0856ad57dbbeff3e90e661b75d52e6", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xfdafc9af71da16ed0d669ac275408b9c0f496168", - "balanceRaw": "75138299428177984488", - "balanceFormatted": "75.138299428177984488", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfdafc9af71da16ed0d669ac275408b9c0f496168", - "etherscanUrl": "https://etherscan.io/address/0xfdafc9af71da16ed0d669ac275408b9c0f496168", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xfcf93df2a2d0667f5222c1c6bc08229434cfb608", - "balanceRaw": "75000000000000000000", - "balanceFormatted": "75", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xfcf93df2a2d0667f5222c1c6bc08229434cfb608", - "etherscanUrl": "https://etherscan.io/address/0xfcf93df2a2d0667f5222c1c6bc08229434cfb608", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa8682e543e29e3cc67979df189d8c6594c3b3fcc", - "balanceRaw": "74693532605215493798", - "balanceFormatted": "74.693532605215493798", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa8682e543e29e3cc67979df189d8c6594c3b3fcc", - "etherscanUrl": "https://etherscan.io/address/0xa8682e543e29e3cc67979df189d8c6594c3b3fcc", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7337b39e75280ff35c56ea8cdf70a97425d27318", - "balanceRaw": "74682763708368427650", - "balanceFormatted": "74.68276370836842765", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7337b39e75280ff35c56ea8cdf70a97425d27318", - "etherscanUrl": "https://etherscan.io/address/0x7337b39e75280ff35c56ea8cdf70a97425d27318", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5ac9ae25d5e0f64088b467e02cea93fbc341f1b7", - "balanceRaw": "74655245353746233822", - "balanceFormatted": "74.655245353746233822", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5ac9ae25d5e0f64088b467e02cea93fbc341f1b7", - "etherscanUrl": "https://etherscan.io/address/0x5ac9ae25d5e0f64088b467e02cea93fbc341f1b7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8c3c014d116c5e18264830dc2a7ee12ea2c3360b", - "balanceRaw": "74598294339910226688", - "balanceFormatted": "74.598294339910226688", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8c3c014d116c5e18264830dc2a7ee12ea2c3360b", - "etherscanUrl": "https://etherscan.io/address/0x8c3c014d116c5e18264830dc2a7ee12ea2c3360b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x1c331b954c8b16a0f462f71444f314e1a5f14b3c", - "balanceRaw": "74549258282972877040", - "balanceFormatted": "74.54925828297287704", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1c331b954c8b16a0f462f71444f314e1a5f14b3c", - "etherscanUrl": "https://etherscan.io/address/0x1c331b954c8b16a0f462f71444f314e1a5f14b3c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_484278", - "balanceRaw": "74248600698440763800", - "balanceFormatted": "74.2486006984407638", - "lastTransferAt": null, - "username": "artyooom", - "displayName": "artyooom", - "fid": 484278, - "followers": 6, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/60588de8-c16c-42a8-e22e-c52b71445000/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3820e04f4d00a06338b66e9a1c67bc13b7d069d5", - "etherscanUrl": "https://etherscan.io/address/0x3820e04f4d00a06338b66e9a1c67bc13b7d069d5", - "xHandle": "rinalliled11066", - "xUrl": "https://twitter.com/rinalliled11066", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x4f54f422bc807b750f16a69548d3f9b281dc4d48", - "balanceRaw": "73761693893766414281", - "balanceFormatted": "73.761693893766414281", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4f54f422bc807b750f16a69548d3f9b281dc4d48", - "etherscanUrl": "https://etherscan.io/address/0x4f54f422bc807b750f16a69548d3f9b281dc4d48", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc4f432db1b2f7f668ad723d348bc600eafbafc9a", - "balanceRaw": "73758088572287117227", - "balanceFormatted": "73.758088572287117227", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc4f432db1b2f7f668ad723d348bc600eafbafc9a", - "etherscanUrl": "https://etherscan.io/address/0xc4f432db1b2f7f668ad723d348bc600eafbafc9a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_17296", - "balanceRaw": "73664367196389839520", - "balanceFormatted": "73.66436719638983952", - "lastTransferAt": null, - "username": "york", - "displayName": "York", - "fid": 17296, - "followers": 2190, - "pfpUrl": "https://i.imgur.com/5BoOWqi.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa706f5c72d30f27f798cc4e163f7d9c720a781b9", - "etherscanUrl": "https://etherscan.io/address/0xa706f5c72d30f27f798cc4e163f7d9c720a781b9", - "xHandle": "realyorkz", - "xUrl": "https://twitter.com/realyorkz", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x53d13c26dc785bd4f1ee91e7321b828b1573e2a2", - "balanceRaw": "73492993574735822606", - "balanceFormatted": "73.492993574735822606", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x53d13c26dc785bd4f1ee91e7321b828b1573e2a2", - "etherscanUrl": "https://etherscan.io/address/0x53d13c26dc785bd4f1ee91e7321b828b1573e2a2", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5c3f8fdc2c99cd27576f36d13446f33c58e67a24", - "balanceRaw": "73434811228936117058", - "balanceFormatted": "73.434811228936117058", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5c3f8fdc2c99cd27576f36d13446f33c58e67a24", - "etherscanUrl": "https://etherscan.io/address/0x5c3f8fdc2c99cd27576f36d13446f33c58e67a24", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xcfe07703e150737d8dbe9265205fb05fd12ffde2", - "balanceRaw": "73295276443091908148", - "balanceFormatted": "73.295276443091908148", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcfe07703e150737d8dbe9265205fb05fd12ffde2", - "etherscanUrl": "https://etherscan.io/address/0xcfe07703e150737d8dbe9265205fb05fd12ffde2", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5b6732f098d27d3f97fd3b8e3cbb194dd3c9d324", - "balanceRaw": "73176813933969420587", - "balanceFormatted": "73.176813933969420587", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5b6732f098d27d3f97fd3b8e3cbb194dd3c9d324", - "etherscanUrl": "https://etherscan.io/address/0x5b6732f098d27d3f97fd3b8e3cbb194dd3c9d324", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf3adeb4f96ee65d847ed625b727e28172ce8a3bd", - "balanceRaw": "73155654574385418633", - "balanceFormatted": "73.155654574385418633", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf3adeb4f96ee65d847ed625b727e28172ce8a3bd", - "etherscanUrl": "https://etherscan.io/address/0xf3adeb4f96ee65d847ed625b727e28172ce8a3bd", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd297dae8361911dbcc34a202ab6852e983c83f8f", - "balanceRaw": "73066771564365077712", - "balanceFormatted": "73.066771564365077712", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd297dae8361911dbcc34a202ab6852e983c83f8f", - "etherscanUrl": "https://etherscan.io/address/0xd297dae8361911dbcc34a202ab6852e983c83f8f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_240470", - "balanceRaw": "72993396709317820885", - "balanceFormatted": "72.993396709317820885", - "lastTransferAt": null, - "username": "design3r", - "displayName": "Designer", - "fid": 240470, - "followers": 57, - "pfpUrl": "https://i.imgur.com/XtRDxzT.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb7a72f7ad85b96b2a4665a308da0f5ca027ab05e", - "etherscanUrl": "https://etherscan.io/address/0xb7a72f7ad85b96b2a4665a308da0f5ca027ab05e", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb01b1bb5903d79316979984ef4944b63debe764b", - "balanceRaw": "72839158626627998354", - "balanceFormatted": "72.839158626627998354", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb01b1bb5903d79316979984ef4944b63debe764b", - "etherscanUrl": "https://etherscan.io/address/0xb01b1bb5903d79316979984ef4944b63debe764b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3ff94c85e607cd07df0b76cc97c40a78d7d03290", - "balanceRaw": "72472773249004692272", - "balanceFormatted": "72.472773249004692272", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3ff94c85e607cd07df0b76cc97c40a78d7d03290", - "etherscanUrl": "https://etherscan.io/address/0x3ff94c85e607cd07df0b76cc97c40a78d7d03290", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7e5b58a2058d5c656b7f149c1676a960058b9f0b", - "balanceRaw": "72060650201118597137", - "balanceFormatted": "72.060650201118597137", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7e5b58a2058d5c656b7f149c1676a960058b9f0b", - "etherscanUrl": "https://etherscan.io/address/0x7e5b58a2058d5c656b7f149c1676a960058b9f0b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xcc0ba860281a1f31b30c805b0f8210f6106cc14b", - "balanceRaw": "71500198984288973502", - "balanceFormatted": "71.500198984288973502", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcc0ba860281a1f31b30c805b0f8210f6106cc14b", - "etherscanUrl": "https://etherscan.io/address/0xcc0ba860281a1f31b30c805b0f8210f6106cc14b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf118e0df659b83215734c0b05991c039a86ec6bf", - "balanceRaw": "71493639721456990847", - "balanceFormatted": "71.493639721456990847", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf118e0df659b83215734c0b05991c039a86ec6bf", - "etherscanUrl": "https://etherscan.io/address/0xf118e0df659b83215734c0b05991c039a86ec6bf", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x206660e3b00ae1d51c1158a823a859250e94e99c", - "balanceRaw": "71407570587347082968", - "balanceFormatted": "71.407570587347082968", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x206660e3b00ae1d51c1158a823a859250e94e99c", - "etherscanUrl": "https://etherscan.io/address/0x206660e3b00ae1d51c1158a823a859250e94e99c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0c0582b9874c0b37f0ef1b3785906fc5fe9ca9e3", - "balanceRaw": "71099449093336469715", - "balanceFormatted": "71.099449093336469715", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0c0582b9874c0b37f0ef1b3785906fc5fe9ca9e3", - "etherscanUrl": "https://etherscan.io/address/0x0c0582b9874c0b37f0ef1b3785906fc5fe9ca9e3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xda495e7690e87b6d2b85286e369cb02b46f77196", - "balanceRaw": "71089869062806029474", - "balanceFormatted": "71.089869062806029474", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xda495e7690e87b6d2b85286e369cb02b46f77196", - "etherscanUrl": "https://etherscan.io/address/0xda495e7690e87b6d2b85286e369cb02b46f77196", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x66e678dc7cb7033f2e52d6f943892d3099ababf9", - "balanceRaw": "71061233958504842170", - "balanceFormatted": "71.06123395850484217", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x66e678dc7cb7033f2e52d6f943892d3099ababf9", - "etherscanUrl": "https://etherscan.io/address/0x66e678dc7cb7033f2e52d6f943892d3099ababf9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_571167", - "balanceRaw": "70997616178549611590", - "balanceFormatted": "70.99761617854961159", - "lastTransferAt": null, - "username": "beeperbase", - "displayName": "Masta Apes", - "fid": 571167, - "followers": 11, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/c5cda360-b0a9-4fe5-7019-3db1848ff400/rectcrop3", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa033994ec61afdcb90c84ff0271e8123bad0e471", - "etherscanUrl": "https://etherscan.io/address/0xa033994ec61afdcb90c84ff0271e8123bad0e471", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_377066", - "balanceRaw": "70960972024915638238", - "balanceFormatted": "70.960972024915638238", - "lastTransferAt": null, - "username": "zhanglu.eth", - "displayName": "zhanglu.eth", - "fid": 377066, - "followers": 95, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f6f88ed0-fdf5-418e-969e-191946273b00/original", - "ensName": "zhanglu.eth", - "ensAvatarUrl": null, - "primaryAddress": "0xb269db3b90703d2f65e37904cef5793c5da0e80b", - "etherscanUrl": "https://etherscan.io/address/0xb269db3b90703d2f65e37904cef5793c5da0e80b", - "xHandle": "craaie1", - "xUrl": "https://twitter.com/craaie1", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xcf6fabc42faf1e14d3fa0d0a98461f160c8e5a09", - "balanceRaw": "70957801079061526856", - "balanceFormatted": "70.957801079061526856", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcf6fabc42faf1e14d3fa0d0a98461f160c8e5a09", - "etherscanUrl": "https://etherscan.io/address/0xcf6fabc42faf1e14d3fa0d0a98461f160c8e5a09", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x42c1d5a03388f32bee82ec3636e75bc5891897c4", - "balanceRaw": "70896367133432281916", - "balanceFormatted": "70.896367133432281916", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x42c1d5a03388f32bee82ec3636e75bc5891897c4", - "etherscanUrl": "https://etherscan.io/address/0x42c1d5a03388f32bee82ec3636e75bc5891897c4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9c061333a8655a4846636e3f1857be1abe8401a5", - "balanceRaw": "70860945274566027392", - "balanceFormatted": "70.860945274566027392", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9c061333a8655a4846636e3f1857be1abe8401a5", - "etherscanUrl": "https://etherscan.io/address/0x9c061333a8655a4846636e3f1857be1abe8401a5", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa96f99188020744509e29ab29fbda1e9ef1eaee3", - "balanceRaw": "70618497546813111512", - "balanceFormatted": "70.618497546813111512", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa96f99188020744509e29ab29fbda1e9ef1eaee3", - "etherscanUrl": "https://etherscan.io/address/0xa96f99188020744509e29ab29fbda1e9ef1eaee3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7aa41c5121f19ca052c78a213e04298bca3de7d9", - "balanceRaw": "70588017667674654197", - "balanceFormatted": "70.588017667674654197", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7aa41c5121f19ca052c78a213e04298bca3de7d9", - "etherscanUrl": "https://etherscan.io/address/0x7aa41c5121f19ca052c78a213e04298bca3de7d9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x99c36f37b6f546abcbdc798230e95f46efaf9dfc", - "balanceRaw": "70510914277080607758", - "balanceFormatted": "70.510914277080607758", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x99c36f37b6f546abcbdc798230e95f46efaf9dfc", - "etherscanUrl": "https://etherscan.io/address/0x99c36f37b6f546abcbdc798230e95f46efaf9dfc", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6d749657eef45af46fa2b7c81a6a4c7636f45d64", - "balanceRaw": "70268735248831073796", - "balanceFormatted": "70.268735248831073796", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6d749657eef45af46fa2b7c81a6a4c7636f45d64", - "etherscanUrl": "https://etherscan.io/address/0x6d749657eef45af46fa2b7c81a6a4c7636f45d64", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe44de38002d00c0c95ef0722193034f5f3a79763", - "balanceRaw": "70262119404745411887", - "balanceFormatted": "70.262119404745411887", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe44de38002d00c0c95ef0722193034f5f3a79763", - "etherscanUrl": "https://etherscan.io/address/0xe44de38002d00c0c95ef0722193034f5f3a79763", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0526bb9e8fa4696def544dd3d5563d7f73190029", - "balanceRaw": "70222005824305123000", - "balanceFormatted": "70.222005824305123", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0526bb9e8fa4696def544dd3d5563d7f73190029", - "etherscanUrl": "https://etherscan.io/address/0x0526bb9e8fa4696def544dd3d5563d7f73190029", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x0df60e53959504e2aeeca46a5a2e72e860a3666d", - "balanceRaw": "70204787477863563604", - "balanceFormatted": "70.204787477863563604", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x0df60e53959504e2aeeca46a5a2e72e860a3666d", - "etherscanUrl": "https://etherscan.io/address/0x0df60e53959504e2aeeca46a5a2e72e860a3666d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_245985", - "balanceRaw": "70122731699077666415", - "balanceFormatted": "70.122731699077666415", - "lastTransferAt": null, - "username": "kaspa0811", - "displayName": "kaspa0811", - "fid": 245985, - "followers": 673, - "pfpUrl": "https://i.imgur.com/zH87bWs.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x800ffd44997e28d5197ff8bdf4b297bed767b156", - "etherscanUrl": "https://etherscan.io/address/0x800ffd44997e28d5197ff8bdf4b297bed767b156", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_227886", - "balanceRaw": "70071508726618411079", - "balanceFormatted": "70.071508726618411079", - "lastTransferAt": null, - "username": "grit", - "displayName": "GRIT", - "fid": 227886, - "followers": 13061, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/a8ffa43c-c9a6-4565-0c5f-a4d4eabca600/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd7e1e42010d91506d52f01bfe0757d81a226a0b4", - "etherscanUrl": "https://etherscan.io/address/0xd7e1e42010d91506d52f01bfe0757d81a226a0b4", - "xHandle": "gritcult", - "xUrl": "https://twitter.com/gritcult", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x2f55e238c9984227d0f7eb61a5eda825ec535aae", - "balanceRaw": "70046067235432984302", - "balanceFormatted": "70.046067235432984302", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2f55e238c9984227d0f7eb61a5eda825ec535aae", - "etherscanUrl": "https://etherscan.io/address/0x2f55e238c9984227d0f7eb61a5eda825ec535aae", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xef56490cf84c3da2452fb19a992c25ce073b3479", - "balanceRaw": "70013488132267105373", - "balanceFormatted": "70.013488132267105373", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xef56490cf84c3da2452fb19a992c25ce073b3479", - "etherscanUrl": "https://etherscan.io/address/0xef56490cf84c3da2452fb19a992c25ce073b3479", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x53a9c678e14eff2aa217941dd63d9de462c75cf4", - "balanceRaw": "70000000000000000000", - "balanceFormatted": "70", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x53a9c678e14eff2aa217941dd63d9de462c75cf4", - "etherscanUrl": "https://etherscan.io/address/0x53a9c678e14eff2aa217941dd63d9de462c75cf4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x56d3bda3e1f4bc779e558960b29d37112d74cd39", - "balanceRaw": "70000000000000000000", - "balanceFormatted": "70", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x56d3bda3e1f4bc779e558960b29d37112d74cd39", - "etherscanUrl": "https://etherscan.io/address/0x56d3bda3e1f4bc779e558960b29d37112d74cd39", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x96373eb9db0c4da8b1cfc700a37f5897c69df5af", - "balanceRaw": "69598654755967974832", - "balanceFormatted": "69.598654755967974832", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x96373eb9db0c4da8b1cfc700a37f5897c69df5af", - "etherscanUrl": "https://etherscan.io/address/0x96373eb9db0c4da8b1cfc700a37f5897c69df5af", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x4745a3e3d41761a531f62a1b9a3fcf6d5fac5eae", - "balanceRaw": "69499362958586643460", - "balanceFormatted": "69.49936295858664346", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4745a3e3d41761a531f62a1b9a3fcf6d5fac5eae", - "etherscanUrl": "https://etherscan.io/address/0x4745a3e3d41761a531f62a1b9a3fcf6d5fac5eae", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x819bcaa68624f84ab30abb1b352b3a62628d4cdc", - "balanceRaw": "69326992839376374411", - "balanceFormatted": "69.326992839376374411", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x819bcaa68624f84ab30abb1b352b3a62628d4cdc", - "etherscanUrl": "https://etherscan.io/address/0x819bcaa68624f84ab30abb1b352b3a62628d4cdc", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x2cc0b3c73aadae4ecb736cc493a907ca25f6c1b1", - "balanceRaw": "68992038989304954114", - "balanceFormatted": "68.992038989304954114", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2cc0b3c73aadae4ecb736cc493a907ca25f6c1b1", - "etherscanUrl": "https://etherscan.io/address/0x2cc0b3c73aadae4ecb736cc493a907ca25f6c1b1", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9793a103a18af2c704bbfe2a485c862820adb87b", - "balanceRaw": "68984886399397816746", - "balanceFormatted": "68.984886399397816746", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9793a103a18af2c704bbfe2a485c862820adb87b", - "etherscanUrl": "https://etherscan.io/address/0x9793a103a18af2c704bbfe2a485c862820adb87b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xa34d6b4c4be7eb2bd1b1ba3a3aab126ccb942dd4", - "balanceRaw": "68887836035298292717", - "balanceFormatted": "68.887836035298292717", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xa34d6b4c4be7eb2bd1b1ba3a3aab126ccb942dd4", - "etherscanUrl": "https://etherscan.io/address/0xa34d6b4c4be7eb2bd1b1ba3a3aab126ccb942dd4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_897481", - "balanceRaw": "68741065110714488048", - "balanceFormatted": "68.741065110714488048", - "lastTransferAt": null, - "username": "garyma", - "displayName": "GaryMa", - "fid": 897481, - "followers": 7, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/29db9d9b-bd33-49b2-271c-4a27e8948200/rectcrop3", - "ensName": "garyma.eth", - "ensAvatarUrl": null, - "primaryAddress": "0xb924f68aae1a544366f93ea453f9da56f239c88b", - "etherscanUrl": "https://etherscan.io/address/0xb924f68aae1a544366f93ea453f9da56f239c88b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x2109d16da3a83611e88a90d43c4c927f82b429ae", - "balanceRaw": "68648477129029008991", - "balanceFormatted": "68.648477129029008991", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x2109d16da3a83611e88a90d43c4c927f82b429ae", - "etherscanUrl": "https://etherscan.io/address/0x2109d16da3a83611e88a90d43c4c927f82b429ae", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_86", - "balanceRaw": "68616790311146888840", - "balanceFormatted": "68.61679031114688884", - "lastTransferAt": null, - "username": "steakbitcoin.eth", - "displayName": "Dustin Moring", - "fid": 86, - "followers": 1433, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/69b50e80-a2c3-4c29-2533-e168614d5e00/rectcrop3", - "ensName": "steakbitcoin.eth", - "ensAvatarUrl": null, - "primaryAddress": "0x7c01d14f10db1df1a70f2b3eb8a93962c345fcf1", - "etherscanUrl": "https://etherscan.io/address/0x7c01d14f10db1df1a70f2b3eb8a93962c345fcf1", - "xHandle": "dustinmoring", - "xUrl": "https://twitter.com/dustinmoring", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_1113654", - "balanceRaw": "68535038605107806072", - "balanceFormatted": "68.535038605107806072", - "lastTransferAt": null, - "username": "firmjeff", - "displayName": "Jeffrey (Computer Operator)", - "fid": 1113654, - "followers": 602, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/cc49a4b0-7d3a-4e1e-e294-6754aad60c00/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7f48ae30095013130bf181773a2c360722c29242", - "etherscanUrl": "https://etherscan.io/address/0x7f48ae30095013130bf181773a2c360722c29242", - "xHandle": "thefirmjeff", - "xUrl": "https://twitter.com/thefirmjeff", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xd73b6ad2fc2e4a5319dff7f5aa8c9287aaa9f1a3", - "balanceRaw": "68429594345879239272", - "balanceFormatted": "68.429594345879239272", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xd73b6ad2fc2e4a5319dff7f5aa8c9287aaa9f1a3", - "etherscanUrl": "https://etherscan.io/address/0xd73b6ad2fc2e4a5319dff7f5aa8c9287aaa9f1a3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xba8ef337f2bc5efc7e3c7b33a9ecf2f89cfe4a2c", - "balanceRaw": "68292238541836534276", - "balanceFormatted": "68.292238541836534276", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xba8ef337f2bc5efc7e3c7b33a9ecf2f89cfe4a2c", - "etherscanUrl": "https://etherscan.io/address/0xba8ef337f2bc5efc7e3c7b33a9ecf2f89cfe4a2c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3fcf39eca3a6277f9d7c4aa6764c89e325135da8", - "balanceRaw": "68265178147854180763", - "balanceFormatted": "68.265178147854180763", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3fcf39eca3a6277f9d7c4aa6764c89e325135da8", - "etherscanUrl": "https://etherscan.io/address/0x3fcf39eca3a6277f9d7c4aa6764c89e325135da8", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x854cf23a60cdbc1dd833f20ce508264d54989a9a", - "balanceRaw": "68129882748046312914", - "balanceFormatted": "68.129882748046312914", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x854cf23a60cdbc1dd833f20ce508264d54989a9a", - "etherscanUrl": "https://etherscan.io/address/0x854cf23a60cdbc1dd833f20ce508264d54989a9a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x47655c3b13dd14a54f8ae3cf17cfda12f7f91cd7", - "balanceRaw": "67945062512810601341", - "balanceFormatted": "67.945062512810601341", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x47655c3b13dd14a54f8ae3cf17cfda12f7f91cd7", - "etherscanUrl": "https://etherscan.io/address/0x47655c3b13dd14a54f8ae3cf17cfda12f7f91cd7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x93d138c902aadce90958e78f00f9ea40226de573", - "balanceRaw": "67558128206616537516", - "balanceFormatted": "67.558128206616537516", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x93d138c902aadce90958e78f00f9ea40226de573", - "etherscanUrl": "https://etherscan.io/address/0x93d138c902aadce90958e78f00f9ea40226de573", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x30993356d3994784c37e2901b790df4071fcc890", - "balanceRaw": "67400275382257062470", - "balanceFormatted": "67.40027538225706247", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x30993356d3994784c37e2901b790df4071fcc890", - "etherscanUrl": "https://etherscan.io/address/0x30993356d3994784c37e2901b790df4071fcc890", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbbb7e4fd168f1f2bf573599ecf63d2083ec392ed", - "balanceRaw": "67224580474508626576", - "balanceFormatted": "67.224580474508626576", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbbb7e4fd168f1f2bf573599ecf63d2083ec392ed", - "etherscanUrl": "https://etherscan.io/address/0xbbb7e4fd168f1f2bf573599ecf63d2083ec392ed", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc5844aceac95d63ab4e7a38e3fed9777ad1946d5", - "balanceRaw": "67152339158612096074", - "balanceFormatted": "67.152339158612096074", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc5844aceac95d63ab4e7a38e3fed9777ad1946d5", - "etherscanUrl": "https://etherscan.io/address/0xc5844aceac95d63ab4e7a38e3fed9777ad1946d5", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x675b29b816eda75d921e3033e9037549e185d4e4", - "balanceRaw": "67000000000000000000", - "balanceFormatted": "67", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x675b29b816eda75d921e3033e9037549e185d4e4", - "etherscanUrl": "https://etherscan.io/address/0x675b29b816eda75d921e3033e9037549e185d4e4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x4de90cc3f8cc8ce1cbf3c30a9f7111f67bcff285", - "balanceRaw": "66942671089143183581", - "balanceFormatted": "66.942671089143183581", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4de90cc3f8cc8ce1cbf3c30a9f7111f67bcff285", - "etherscanUrl": "https://etherscan.io/address/0x4de90cc3f8cc8ce1cbf3c30a9f7111f67bcff285", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf3ac7cc1d51fe5d580ca4cc7143a83f7cb17fe33", - "balanceRaw": "66868548168410523068", - "balanceFormatted": "66.868548168410523068", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf3ac7cc1d51fe5d580ca4cc7143a83f7cb17fe33", - "etherscanUrl": "https://etherscan.io/address/0xf3ac7cc1d51fe5d580ca4cc7143a83f7cb17fe33", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x4d678f4662b0c8454ff80a7089e0ed1f879a585b", - "balanceRaw": "66803219490505711684", - "balanceFormatted": "66.803219490505711684", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x4d678f4662b0c8454ff80a7089e0ed1f879a585b", - "etherscanUrl": "https://etherscan.io/address/0x4d678f4662b0c8454ff80a7089e0ed1f879a585b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x372d991eb63bdc4ed118fcdeed27e7d0a729f2fc", - "balanceRaw": "66620291634027701011", - "balanceFormatted": "66.620291634027701011", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x372d991eb63bdc4ed118fcdeed27e7d0a729f2fc", - "etherscanUrl": "https://etherscan.io/address/0x372d991eb63bdc4ed118fcdeed27e7d0a729f2fc", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_212532", - "balanceRaw": "66488832304170397683", - "balanceFormatted": "66.488832304170397683", - "lastTransferAt": null, - "username": "!212532", - "displayName": null, - "fid": 212532, - "followers": 0, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x39efcac054b978053b1febaf98b11d6f0ee6dd41", - "etherscanUrl": "https://etherscan.io/address/0x39efcac054b978053b1febaf98b11d6f0ee6dd41", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_214447", - "balanceRaw": "66073397116114486661", - "balanceFormatted": "66.073397116114486661", - "lastTransferAt": null, - "username": "yes2crypto.eth", - "displayName": "YES2Crypto 🎩 🟪🟡", - "fid": 214447, - "followers": 21249, - "pfpUrl": "https://i.imgur.com/dBoVmnG.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe068579e46ca32a7ed3d51f217fcc5a0d829fca0", - "etherscanUrl": "https://etherscan.io/address/0xe068579e46ca32a7ed3d51f217fcc5a0d829fca0", - "xHandle": "yes2crypto1", - "xUrl": "https://twitter.com/yes2crypto1", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xec7097711548ec53e96828f0052caec7315a8362", - "balanceRaw": "65911626518389645470", - "balanceFormatted": "65.91162651838964547", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xec7097711548ec53e96828f0052caec7315a8362", - "etherscanUrl": "https://etherscan.io/address/0xec7097711548ec53e96828f0052caec7315a8362", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x6ca424d6582c92fa69d5b24ca493b6664179f53c", - "balanceRaw": "65572783965415973493", - "balanceFormatted": "65.572783965415973493", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6ca424d6582c92fa69d5b24ca493b6664179f53c", - "etherscanUrl": "https://etherscan.io/address/0x6ca424d6582c92fa69d5b24ca493b6664179f53c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xc854907fcddb0a50ad8fcee46b7935c9cc01dc03", - "balanceRaw": "65096540156488229595", - "balanceFormatted": "65.096540156488229595", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc854907fcddb0a50ad8fcee46b7935c9cc01dc03", - "etherscanUrl": "https://etherscan.io/address/0xc854907fcddb0a50ad8fcee46b7935c9cc01dc03", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_188294", - "balanceRaw": "65009193255557027297", - "balanceFormatted": "65.009193255557027297", - "lastTransferAt": null, - "username": "cryptofortochka", - "displayName": "CryptoFortochka.base.eth", - "fid": 188294, - "followers": 1955, - "pfpUrl": "https://i.imgur.com/YbMFVED.jpg", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x6c7298bc25ce5c0d5a072091a657040106a7ddca", - "etherscanUrl": "https://etherscan.io/address/0x6c7298bc25ce5c0d5a072091a657040106a7ddca", - "xHandle": "keeperssd", - "xUrl": "https://twitter.com/keeperssd", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xbe2aa8cca5beb324c85cb65252a683eed918c49b", - "balanceRaw": "65000000000000000083", - "balanceFormatted": "65.000000000000000083", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xbe2aa8cca5beb324c85cb65252a683eed918c49b", - "etherscanUrl": "https://etherscan.io/address/0xbe2aa8cca5beb324c85cb65252a683eed918c49b", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x99a318ca087f8b0d6296cd87a8e7f1955d6c1866", - "balanceRaw": "64959366641526581681", - "balanceFormatted": "64.959366641526581681", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x99a318ca087f8b0d6296cd87a8e7f1955d6c1866", - "etherscanUrl": "https://etherscan.io/address/0x99a318ca087f8b0d6296cd87a8e7f1955d6c1866", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb16246c92a0b95d64a89b872f6d763f71cb5bcf9", - "balanceRaw": "64894217745404435694", - "balanceFormatted": "64.894217745404435694", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb16246c92a0b95d64a89b872f6d763f71cb5bcf9", - "etherscanUrl": "https://etherscan.io/address/0xb16246c92a0b95d64a89b872f6d763f71cb5bcf9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5290075a7b70a859ef45b1913c9f2357280e20e0", - "balanceRaw": "64840432487835618674", - "balanceFormatted": "64.840432487835618674", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5290075a7b70a859ef45b1913c9f2357280e20e0", - "etherscanUrl": "https://etherscan.io/address/0x5290075a7b70a859ef45b1913c9f2357280e20e0", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xb66e0d13f9c099e2e185fb5dafb8f59f0dc744a3", - "balanceRaw": "64780473400926539264", - "balanceFormatted": "64.780473400926539264", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xb66e0d13f9c099e2e185fb5dafb8f59f0dc744a3", - "etherscanUrl": "https://etherscan.io/address/0xb66e0d13f9c099e2e185fb5dafb8f59f0dc744a3", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3798bd9990db87e7388d2c31bac0c91d7094a9fe", - "balanceRaw": "64649266103247128004", - "balanceFormatted": "64.649266103247128004", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3798bd9990db87e7388d2c31bac0c91d7094a9fe", - "etherscanUrl": "https://etherscan.io/address/0x3798bd9990db87e7388d2c31bac0c91d7094a9fe", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_534680", - "balanceRaw": "64425662790661169157", - "balanceFormatted": "64.425662790661169157", - "lastTransferAt": null, - "username": "aiur", - "displayName": "auurrr", - "fid": 534680, - "followers": 11, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/f9c44011-8f8e-468d-4308-f918b938d500/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xda9009a9fd2150e38562886336a3268859ed8155", - "etherscanUrl": "https://etherscan.io/address/0xda9009a9fd2150e38562886336a3268859ed8155", - "xHandle": "run_zealot", - "xUrl": "https://twitter.com/run_zealot", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x03f40b0ffa3c7a0fdf1098a1b4f856081e31179f", - "balanceRaw": "64270596806108040008", - "balanceFormatted": "64.270596806108040008", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x03f40b0ffa3c7a0fdf1098a1b4f856081e31179f", - "etherscanUrl": "https://etherscan.io/address/0x03f40b0ffa3c7a0fdf1098a1b4f856081e31179f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5be7fdfbffeb5cf3a24bd3f350a7265bda0b5b02", - "balanceRaw": "64097006576129000000", - "balanceFormatted": "64.097006576129", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5be7fdfbffeb5cf3a24bd3f350a7265bda0b5b02", - "etherscanUrl": "https://etherscan.io/address/0x5be7fdfbffeb5cf3a24bd3f350a7265bda0b5b02", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_211186", - "balanceRaw": "64000045321072781772", - "balanceFormatted": "64.000045321072781772", - "lastTransferAt": null, - "username": "0xgymleader", - "displayName": "Ray", - "fid": 211186, - "followers": 8824, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/42057236-b8e9-4ef6-b809-4339c2086600/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x79cc495a6b7b5120430d875af1e31c32705a2712", - "etherscanUrl": "https://etherscan.io/address/0x79cc495a6b7b5120430d875af1e31c32705a2712", - "xHandle": "avocado_papi", - "xUrl": "https://twitter.com/avocado_papi", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_3346", - "balanceRaw": "63826299274332819026", - "balanceFormatted": "63.826299274332819026", - "lastTransferAt": null, - "username": "haole", - "displayName": "Haole", - "fid": 3346, - "followers": 16779, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/0b61cfa2-f9b0-4d21-452e-3bfa88779e00/rectcrop3", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xc6278fa6b38f429e58c4821dd61fc8fc6b20cc4f", - "etherscanUrl": "https://etherscan.io/address/0xc6278fa6b38f429e58c4821dd61fc8fc6b20cc4f", - "xHandle": "0xhaole", - "xUrl": "https://twitter.com/0xhaole", - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xe7958e5e481ebb8c079c58535dbd9c10b1c35f29", - "balanceRaw": "63710972810854368598", - "balanceFormatted": "63.710972810854368598", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xe7958e5e481ebb8c079c58535dbd9c10b1c35f29", - "etherscanUrl": "https://etherscan.io/address/0xe7958e5e481ebb8c079c58535dbd9c10b1c35f29", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xcf85946b004cc8453b067299a28dfb1de5ea33ba", - "balanceRaw": "63670194069905270394", - "balanceFormatted": "63.670194069905270394", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xcf85946b004cc8453b067299a28dfb1de5ea33ba", - "etherscanUrl": "https://etherscan.io/address/0xcf85946b004cc8453b067299a28dfb1de5ea33ba", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x96b0425c29ab7664d80c4754b681f5907172ec7c", - "balanceRaw": "63614135689217926519", - "balanceFormatted": "63.614135689217926519", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x96b0425c29ab7664d80c4754b681f5907172ec7c", - "etherscanUrl": "https://etherscan.io/address/0x96b0425c29ab7664d80c4754b681f5907172ec7c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x1204f8ba2c563cb289d7e9732430b8c824c5e5e0", - "balanceRaw": "63555812983350414556", - "balanceFormatted": "63.555812983350414556", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x1204f8ba2c563cb289d7e9732430b8c824c5e5e0", - "etherscanUrl": "https://etherscan.io/address/0x1204f8ba2c563cb289d7e9732430b8c824c5e5e0", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x23e5ce857e1b970bb63ce7379dba94678f421392", - "balanceRaw": "63219548003598924601", - "balanceFormatted": "63.219548003598924601", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x23e5ce857e1b970bb63ce7379dba94678f421392", - "etherscanUrl": "https://etherscan.io/address/0x23e5ce857e1b970bb63ce7379dba94678f421392", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x8570dadda6094b9e309e922bd784f324493da2c9", - "balanceRaw": "63184170115318491006", - "balanceFormatted": "63.184170115318491006", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x8570dadda6094b9e309e922bd784f324493da2c9", - "etherscanUrl": "https://etherscan.io/address/0x8570dadda6094b9e309e922bd784f324493da2c9", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9070a27459173abc768e023d0838908b537fd7c7", - "balanceRaw": "63139478371865682429", - "balanceFormatted": "63.139478371865682429", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9070a27459173abc768e023d0838908b537fd7c7", - "etherscanUrl": "https://etherscan.io/address/0x9070a27459173abc768e023d0838908b537fd7c7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xf39f07e3ad2ce01e2ef826074e41d721d04d4f2f", - "balanceRaw": "63093765884466999888", - "balanceFormatted": "63.093765884466999888", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xf39f07e3ad2ce01e2ef826074e41d721d04d4f2f", - "etherscanUrl": "https://etherscan.io/address/0xf39f07e3ad2ce01e2ef826074e41d721d04d4f2f", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x64e94a4b2fd134f4ac6a6f50d4b909d477507708", - "balanceRaw": "62950614516641944960", - "balanceFormatted": "62.95061451664194496", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x64e94a4b2fd134f4ac6a6f50d4b909d477507708", - "etherscanUrl": "https://etherscan.io/address/0x64e94a4b2fd134f4ac6a6f50d4b909d477507708", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x9967b1e5ae9c9335c571084c49d91ccf3f339bdd", - "balanceRaw": "62768666198575218709", - "balanceFormatted": "62.768666198575218709", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x9967b1e5ae9c9335c571084c49d91ccf3f339bdd", - "etherscanUrl": "https://etherscan.io/address/0x9967b1e5ae9c9335c571084c49d91ccf3f339bdd", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x5fb61612fdd87ab753ca6b76e811e3c6dba1936d", - "balanceRaw": "62032341101064107874", - "balanceFormatted": "62.032341101064107874", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x5fb61612fdd87ab753ca6b76e811e3c6dba1936d", - "etherscanUrl": "https://etherscan.io/address/0x5fb61612fdd87ab753ca6b76e811e3c6dba1936d", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "fc_fid_194418", - "balanceRaw": "61740099886947776498", - "balanceFormatted": "61.740099886947776498", - "lastTransferAt": null, - "username": "lilou", - "displayName": "Lilou", - "fid": 194418, - "followers": 268, - "pfpUrl": "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/414517b0-ccee-4279-d28f-a9e90f8f9500/original", - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x65399efb5c2a2b66666930b5b3add78414e3828a", - "etherscanUrl": "https://etherscan.io/address/0x65399efb5c2a2b66666930b5b3add78414e3828a", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x7fb7350a682b2587cffde67f06505a5d71590159", - "balanceRaw": "61655608773630023638", - "balanceFormatted": "61.655608773630023638", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x7fb7350a682b2587cffde67f06505a5d71590159", - "etherscanUrl": "https://etherscan.io/address/0x7fb7350a682b2587cffde67f06505a5d71590159", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xff64de5e1b35fccf31787d3f53dd6cf1ef56122c", - "balanceRaw": "61452226628679409994", - "balanceFormatted": "61.452226628679409994", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xff64de5e1b35fccf31787d3f53dd6cf1ef56122c", - "etherscanUrl": "https://etherscan.io/address/0xff64de5e1b35fccf31787d3f53dd6cf1ef56122c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x3ccac7039f086df4b90e3f194f5404828c79c4b7", - "balanceRaw": "61387264144392294437", - "balanceFormatted": "61.387264144392294437", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x3ccac7039f086df4b90e3f194f5404828c79c4b7", - "etherscanUrl": "https://etherscan.io/address/0x3ccac7039f086df4b90e3f194f5404828c79c4b7", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0x29d5d519ff451eb411db50f7fdc455dcf85380e4", - "balanceRaw": "61349129533629595100", - "balanceFormatted": "61.3491295336295951", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0x29d5d519ff451eb411db50f7fdc455dcf85380e4", - "etherscanUrl": "https://etherscan.io/address/0x29d5d519ff451eb411db50f7fdc455dcf85380e4", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - }, - { - "address": "0xed3d8c67401b913f1fddd137023d355a54392e5c", - "balanceRaw": "61341781054517116429", - "balanceFormatted": "61.341781054517116429", - "lastTransferAt": null, - "username": null, - "displayName": null, - "fid": null, - "followers": null, - "pfpUrl": null, - "ensName": null, - "ensAvatarUrl": null, - "primaryAddress": "0xed3d8c67401b913f1fddd137023d355a54392e5c", - "etherscanUrl": "https://etherscan.io/address/0xed3d8c67401b913f1fddd137023d355a54392e5c", - "xHandle": null, - "xUrl": null, - "githubHandle": null, - "githubUrl": null - } - ], - "tokenSymbol": null, - "tokenDecimals": 18, - "lastUpdatedTimestamp": "2025-11-17T21:46:18.757Z", - "lastFetchSettings": { - "source": "tokenHolders", - "network": "base", - "contractAddress": "0x1bc0c42215582d5a085795f4badbac3ff36d1bcb", - "assetType": "token" - } -} diff --git a/src/config/clanker/initialSpaces/index.ts b/src/config/clanker/initialSpaces/index.ts deleted file mode 100644 index 74ff4e240..000000000 --- a/src/config/clanker/initialSpaces/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Export the initial space creators from nouns config -export { default as createInitialProfileSpaceConfigForFid } from './initialProfileSpace'; -export { default as createInitialChannelSpaceConfig } from './initialChannelSpace'; -export { default as createInitialTokenSpaceConfigForAddress } from './initialTokenSpace'; -export { default as createInitalProposalSpaceConfigForProposalId } from './initialProposalSpace'; -export { default as INITIAL_HOMEBASE_CONFIG } from './initialHomebase'; diff --git a/src/config/clanker/initialSpaces/initialChannelSpace.ts b/src/config/clanker/initialSpaces/initialChannelSpace.ts deleted file mode 100644 index 3980bab28..000000000 --- a/src/config/clanker/initialSpaces/initialChannelSpace.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { SpaceConfig } from "@/app/(spaces)/Space"; -import { FilterType, FeedType } from "@neynar/nodejs-sdk/build/api"; -import { cloneDeep } from "lodash"; -import { getLayoutConfig } from "@/common/utils/layoutFormatUtils"; -import { INITIAL_SPACE_CONFIG_EMPTY } from "../../initialSpaceConfig"; - -const INITIAL_CHANNEL_SPACE_CONFIG = cloneDeep(INITIAL_SPACE_CONFIG_EMPTY); -INITIAL_CHANNEL_SPACE_CONFIG.tabNames = ["Channel"]; - -const createInitialChannelSpaceConfig = ( - channelId: string, -): Omit => { - const config = cloneDeep(INITIAL_CHANNEL_SPACE_CONFIG); - - config.fidgetInstanceDatums = { - "feed:channel": { - config: { - editable: false, - settings: { - feedType: FeedType.Filter, - filterType: FilterType.ChannelId, - channel: channelId, - }, - data: {}, - }, - fidgetType: "feed", - id: "feed:channel", - }, - }; - - const layoutItems = [ - { - w: 6, - h: 8, - x: 0, - y: 0, - i: "feed:channel", - minW: 4, - maxW: 20, - minH: 6, - maxH: 12, - moved: false, - static: false, - }, - ]; - - const layoutConfig = getLayoutConfig(config.layoutDetails); - layoutConfig.layout = layoutItems; - - config.tabNames = ["Channel"]; - - return config; -}; - -export default createInitialChannelSpaceConfig; diff --git a/src/config/clanker/initialSpaces/initialHomebase.ts b/src/config/clanker/initialSpaces/initialHomebase.ts deleted file mode 100644 index b03ea8812..000000000 --- a/src/config/clanker/initialSpaces/initialHomebase.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { SpaceConfig } from "@/app/(spaces)/Space"; -import DEFAULT_THEME from "@/common/lib/theme/defaultTheme"; - -const tutorialText = ` -### 🖌️ Click the paintbrush in the bottom-left corner to open Customization Mode - -### Add Fidgets -1. Click the blue **+** button. -2. Drag a Fidget to an open spot on the grid. -3. Click Save - -(after saving, scroll down here for more instructions) - -![Add Fidget2](https://space.mypinata.cloud/ipfs/bafkreiczpd2bzyoboj6uxr65kta5cmg3bziveq2nz5egx4fuxr2nmkthru) - -### Customize Fidgets -1. From customization mode, click any Fidget on the grid to open its settings. -2. Click 'Style' to customize a fidget's look. Any Fidget styles set to "Theme" inherit their look from the Tab's theme. - -![EditFidget](https://space.mypinata.cloud/ipfs/bafybeihcjkbcljxr4ttgt6xcxmsc4m4qvw62hkolifmd3t5pdc7kvj5nji) - -### Arrange Fidgets -- **Move:** Drag from the center -![move fidget](https://space.mypinata.cloud/ipfs/QmYWvdpdiyKwjVAqjhcFTBkiTUnc8rF4p2EGg3C4sTRsr6) -- **Resize:** Drag from an edge or corner -![Resize Fidget](https://space.mypinata.cloud/ipfs/bafybeifmssfizx5xjmmqyc6wwqxgs2xdhhbdtnvldtj276mfdul3eacmwu) -- **Stash in Fidget Tray:** Click a fidget then click ⇱ to save it for later. -![image](https://space.mypinata.cloud/ipfs/bafkreigy7ymnuwertvr6bn4bmwfe3vu3vvw4r6ekkzv6prs4zwlibsjtya) -- **Delete:** Click a fidget then click X it to delete it forever. -![image](https://space.mypinata.cloud/ipfs/bafkreieucvjlovm7wq5ftcvvvcv52sujnqqjwji5epoigb2ycumhmgrtfy) - -### Customize Theme -- **Templates:** Select a pre-made Theme. Then, customize it further to make it your own. -- **Style:** Set a background color for the Tab, or set the default styles for all Fidgets on the Tab. -- **Fonts:** Set the default header and body fonts for Fidgets on the Tab. -- **Code:** Add HTML/CSS to fully customize the Tab's background, or generate a custom background with a prompt. - -![Edit Theme2](https://space.mypinata.cloud/ipfs/bafybeietizt4vgyaiv62ytn25ztanusjmbcw6iwm3v2gedcpipr6koxh3e) - -### Customize Music -Add a soundtrack to each Tab. Search for or paste the link to any song or playlist on YouTube, or select a music NFT. - -![customize music](https://space.mypinata.cloud/ipfs/bafkreigtslrp3kjj42gp25bxd5nubs47qx22ivj3pkm3tc7j3fbqt3b5hy) - -### Homebase vs. Space -**Your Space** is your public profile that everyone can see. -**Your Homebase** is a private dashboard that only you can see. - -You can use the same tricks and Fidgets to customize them both. Use your **Homebase** to access the content, communities, and functionality you love, and use your **Space** to share the content and functionality you love with your friends. - -### Questions or feedback? - -Tag [@nounspacetom](https://nounspace.com/s/nounspacetom) in a cast or join our [Discord](https://discord.gg/H8EYnEmj6q). - -### Happy customizing! -`; - -const clankerManagerFidgetID = "ClankerManager:clanker-manager"; - -const layoutID = ""; -const createInitialHomebaseConfig = (userAddress?: string): SpaceConfig => { - return { - layoutID, - layoutDetails: { - layoutConfig: { - layout: [ - { - w: 6, - h: 8, - x: 0, - y: 0, - i: clankerManagerFidgetID, - moved: false, - static: false, - }, - ], - }, - layoutFidget: "grid", - }, - theme: DEFAULT_THEME, - fidgetInstanceDatums: { - [clankerManagerFidgetID]: { - config: { - editable: true, - settings: { - deployerAddress: userAddress || "", - rewardRecipientAddress: "", - accentColor: "#2563eb", - primaryFontFamily: "var(--user-theme-font)", - primaryFontColor: "var(--user-theme-font-color)", - secondaryFontFamily: "var(--user-theme-headings-font)", - secondaryFontColor: "var(--user-theme-headings-font-color)", - background: "var(--user-theme-fidget-background)", - fidgetBorderColor: "var(--user-theme-fidget-border-color)", - fidgetBorderWidth: "var(--user-theme-fidget-border-width)", - fidgetShadow: "var(--user-theme-fidget-shadow)", - }, - data: {}, - }, - fidgetType: "ClankerManager", - id: clankerManagerFidgetID, - }, - }, - isEditable: false, - fidgetTrayContents: [], - }; -}; - -// Export both function and default constant for backward compatibility -const INITIAL_HOMEBASE_CONFIG = createInitialHomebaseConfig(); -export default INITIAL_HOMEBASE_CONFIG; -export { createInitialHomebaseConfig }; \ No newline at end of file diff --git a/src/config/clanker/initialSpaces/initialProfileSpace.ts b/src/config/clanker/initialSpaces/initialProfileSpace.ts deleted file mode 100644 index 6d874fe06..000000000 --- a/src/config/clanker/initialSpaces/initialProfileSpace.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { SpaceConfig } from "@/app/(spaces)/Space"; -import { FeedType, FilterType } from "@neynar/nodejs-sdk/build/api"; -import { cloneDeep } from "lodash"; -import { getLayoutConfig } from "@/common/utils/layoutFormatUtils"; -import { INITIAL_SPACE_CONFIG_EMPTY } from "../../initialSpaceConfig"; - -// Set default tabNames for profile spaces -const INITIAL_PROFILE_SPACE_CONFIG = cloneDeep(INITIAL_SPACE_CONFIG_EMPTY); -INITIAL_PROFILE_SPACE_CONFIG.tabNames = ["Profile"]; - -const createInitialProfileSpaceConfigForFid = ( - fid: number, - username?: string, - walletAddress?: string, -): Omit => { - const config = cloneDeep(INITIAL_PROFILE_SPACE_CONFIG); - config.fidgetInstanceDatums = { - "feed:profile": { - config: { - editable: false, - settings: { - feedType: FeedType.Filter, - users: fid, - filterType: FilterType.Fids, - }, - data: {}, - }, - fidgetType: "feed", - id: "feed:profile", - }, - "Portfolio:cd627e89-d661-4255-8c4c-2242a950e93e": { - config: { - editable: false, - settings: { - trackType: "farcaster", - farcasterUsername: username ?? "", - walletAddresses: "", - }, - data: {}, - }, - fidgetType: "Portfolio", - id: "Portfolio:cd627e89-d661-4255-8c4c-2242a950e93e", - }, - "ClankerManager:clanker-manager": { - config: { - editable: false, - settings: { - deployerAddress: walletAddress || "", - rewardRecipientAddress: "", - accentColor: "#2563eb", - primaryFontFamily: "var(--user-theme-font)", - primaryFontColor: "var(--user-theme-font-color)", - secondaryFontFamily: "var(--user-theme-headings-font)", - secondaryFontColor: "var(--user-theme-headings-font-color)", - background: "var(--user-theme-fidget-background)", - fidgetBorderColor: "var(--user-theme-fidget-border-color)", - fidgetBorderWidth: "var(--user-theme-fidget-border-width)", - fidgetShadow: "var(--user-theme-fidget-shadow)", - }, - data: {}, - }, - fidgetType: "ClankerManager", - id: "ClankerManager:clanker-manager", - }, - }; - const layoutItems = [ - { - w: 4, - h: 8, - x: 0, - y: 0, - i: "feed:profile", - minW: 4, - maxW: 36, - minH: 6, - maxH: 36, - moved: false, - static: false, - }, - { - w: 4, - h: 8, - x: 4, - y: 0, - i: "Portfolio:cd627e89-d661-4255-8c4c-2242a950e93e", - minW: 3, - maxW: 36, - minH: 3, - maxH: 36, - moved: false, - static: false, - }, - { - w: 4, - h: 8, - x: 8, - y: 0, - i: "ClankerManager:clanker-manager", - minW: 3, - maxW: 36, - minH: 3, - maxH: 36, - moved: false, - static: false, - }, - ]; - - // Set the layout configuration - const layoutConfig = getLayoutConfig(config.layoutDetails); - layoutConfig.layout = layoutItems; - - // Set default tab names - config.tabNames = ["Profile"]; - - return config; -}; - -export default createInitialProfileSpaceConfigForFid; diff --git a/src/config/clanker/initialSpaces/initialProposalSpace.ts b/src/config/clanker/initialSpaces/initialProposalSpace.ts deleted file mode 100644 index e555345a0..000000000 --- a/src/config/clanker/initialSpaces/initialProposalSpace.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { SpaceConfig } from "@/app/(spaces)/Space"; -import { cloneDeep } from "lodash"; -import { INITIAL_SPACE_CONFIG_EMPTY } from "../../initialSpaceConfig"; -import { Address } from "viem"; - -export const createInitalProposalSpaceConfigForProposalId = ( - proposalId: string, - proposerAddress: Address -): Omit => { - const config = cloneDeep(INITIAL_SPACE_CONFIG_EMPTY); - - config.fidgetInstanceDatums = { - "iframe:2f0a1c7b-da0c-474c-ad30-59915d0096b1": { - config: { - editable: true, - data: {}, - settings: { - url: `https://www.nouns.camp/proposals/${proposalId}?tab=description`, - showOnMobile: true, - customMobileDisplayName: "Proposal", - background: "var(--user-theme-fidget-background)", - fidgetBorderWidth: "var(--user-theme-fidget-border-width)", - fidgetBorderColor: "var(--user-theme-fidget-border-color)", - fidgetShadow: "var(--user-theme-fidget-shadow)", - }, - }, - fidgetType: "iframe", - id: "iframe:2f0a1c7b-da0c-474c-ad30-59915d0096b1", - }, - "iframe:10e88b10-b999-4ddc-a577-bd0eeb6bc76d": { - config: { - editable: true, - data: {}, - settings: { - url: "https://euphonious-kulfi-5e5a30.netlify.app/?id=" + proposalId, - showOnMobile: true, - customMobileDisplayName: "TLDR", - background: "var(--user-theme-fidget-background)", - fidgetBorderWidth: "var(--user-theme-fidget-border-width)", - fidgetBorderColor: "var(--user-theme-fidget-border-color)", - fidgetShadow: "var(--user-theme-fidget-shadow)", - }, - }, - fidgetType: "iframe", - id: "iframe:10e88b10-b999-4ddc-a577-bd0eeb6bc76d", - }, - "iframe:1afc071b-ce6b-4527-9419-f2e057a9fb0a": { - config: { - editable: true, - data: {}, - settings: { - url: `https://www.nouns.camp/proposals/${proposalId}?tab=activity`, - showOnMobile: true, - customMobileDisplayName: "Activity", - background: "var(--user-theme-fidget-background)", - fidgetBorderWidth: "var(--user-theme-fidget-border-width)", - fidgetBorderColor: "var(--user-theme-fidget-border-color)", - fidgetShadow: "var(--user-theme-fidget-shadow)", - }, - }, - fidgetType: "iframe", - id: "iframe:1afc071b-ce6b-4527-9419-f2e057a9fb0a", - }, - "iframe:ffb3cd56-3203-4b94-b842-adab9a7eabc9": { - config: { - editable: true, - data: {}, - settings: { - url: `https://chat-fidget.vercel.app/?room=prop%20${proposalId}%20chat&owner=${proposerAddress}`, - showOnMobile: true, - customMobileDisplayName: "Chat", - background: "var(--user-theme-fidget-background)", - fidgetBorderWidth: "var(--user-theme-fidget-border-width)", - fidgetBorderColor: "var(--user-theme-fidget-border-color)", - fidgetShadow: "var(--user-theme-fidget-shadow)", - }, - }, - fidgetType: "iframe", - id: "iframe:ffb3cd56-3203-4b94-b842-adab9a7eabc9", - }, - }; - - config.layoutDetails = { - layoutConfig: { - layout: [ - { - w: 4, - h: 10, - x: 0, - y: 0, - i: "iframe:2f0a1c7b-da0c-474c-ad30-59915d0096b1", - minW: 2, - maxW: 36, - minH: 2, - maxH: 36, - moved: false, - static: false, - resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], - isBounded: false, - }, - { - w: 4, - h: 6, - x: 4, - y: 0, - i: "iframe:10e88b10-b999-4ddc-a577-bd0eeb6bc76d", - minW: 2, - maxW: 36, - minH: 2, - maxH: 36, - moved: false, - static: false, - resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], - }, - { - w: 4, - h: 10, - x: 8, - y: 0, - i: "iframe:1afc071b-ce6b-4527-9419-f2e057a9fb0a", - minW: 2, - maxW: 36, - minH: 2, - maxH: 36, - moved: false, - static: false, - resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], - isBounded: false, - }, - { - w: 4, - h: 4, - x: 4, - y: 6, - i: "iframe:ffb3cd56-3203-4b94-b842-adab9a7eabc9", - minW: 2, - maxW: 36, - minH: 2, - maxH: 36, - moved: false, - static: false, - resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], - isBounded: false, - }, - ], - }, - layoutFidget: "default-layout-fidget", // Assign a valid string value - }; - - config.theme = { - id: "Homebase-Tab 10-Theme", - name: "Homebase-Tab 10-Theme", - properties: { - background: "#ffffff", - backgroundHTML: ` - - - - Nouns DAO Animated Background - - - -`, - fidgetBackground: "#ffffff", - fidgetBorderColor: "#eeeeee", - fidgetBorderWidth: "1px", - fidgetShadow: "none", - font: "Inter", - fontColor: "#000000", - headingsFont: "Londrina Solid", - headingsFontColor: "#000000", - musicURL: "https://www.youtube.com/watch?v=dMXlZ4y7OK4&t=1804", - fidgetBorderRadius: "12px", - gridSpacing: "16", - }, - }; - - return config; -}; - -export default createInitalProposalSpaceConfigForProposalId; diff --git a/src/config/clanker/initialSpaces/initialTokenSpace.ts b/src/config/clanker/initialSpaces/initialTokenSpace.ts deleted file mode 100644 index c5073ffc7..000000000 --- a/src/config/clanker/initialSpaces/initialTokenSpace.ts +++ /dev/null @@ -1,445 +0,0 @@ -import { SpaceConfig } from "@/app/(spaces)/Space"; -import { cloneDeep } from "lodash"; -import { INITIAL_SPACE_CONFIG_EMPTY } from "../../initialSpaceConfig"; -import { getNetworkWithId } from "@/common/lib/utils/networks"; -import { EtherScanChainName } from "../../../constants/etherscanChainIds"; -import { getGeckoUrl } from "@/common/lib/utils/links"; -import { Address } from "viem"; -import { getLayoutConfig } from "@/common/utils/layoutFormatUtils"; - -export const createInitialTokenSpaceConfigForAddress = ( - address: string, - castHash: string | null, - casterFid: string | null, - symbol: string, - isClankerToken: boolean, - network: EtherScanChainName = "base", -): Omit => { - const config = cloneDeep(INITIAL_SPACE_CONFIG_EMPTY); - - config.fidgetInstanceDatums = { - "Swap:f9e0259a-4524-4b37-a261-9f3be26d4af1": { - config: { - data: {}, - editable: true, - settings: { - defaultBuyToken: address, - defaultSellToken: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - fromChain: getNetworkWithId(network), - toChain: getNetworkWithId(network), - size: 0.8, - }, - }, - fidgetType: "Swap", - id: "Swap:f9e0259a-4524-4b37-a261-9f3be26d4af1", - }, - ...(isClankerToken && - castHash && - casterFid && - castHash !== "clank.fun deployment" && { - "cast:9c63b80e-bd46-4c8e-9e4e-c6facc41bf71": { - config: { - data: {}, - editable: true, - settings: { - background: "var(--user-theme-fidget-background)", - castHash: castHash, - casterFid: casterFid, - fidgetBorderColor: "var(--user-theme-fidget-border-color)", - fidgetBorderWidth: "var(--user-theme-fidget-border-width)", - fidgetShadow: "var(--user-theme-fidget-shadow)", - }, - }, - fidgetType: "cast", - id: "cast:9c63b80e-bd46-4c8e-9e4e-c6facc41bf71", - }, - }), - "feed:3de67742-56f2-402c-b751-7e769cdcfc56": { - config: { - data: {}, - editable: true, - settings: { - Xhandle: "thenounspace", - background: "var(--user-theme-fidget-background)", - feedType: "filter", - keyword: `$${symbol}`, - fidgetBorderColor: "var(--user-theme-fidget-border-color)", - fidgetBorderWidth: "var(--user-theme-fidget-border-width)", - fidgetShadow: "var(--user-theme-fidget-shadow)", - filterType: "keyword", - fontColor: "var(--user-theme-font-color)", - fontFamily: "var(--user-theme-font)", - }, - }, - fidgetType: "feed", - id: "feed:3de67742-56f2-402c-b751-7e769cdcfc56", - }, - "Market:733222fa-38f8-4343-9fa2-6646bb47dde0": { - config: { - data: {}, - editable: true, - settings: { - background: "var(--user-theme-fidget-background)", - fidgetBorderColor: "var(--user-theme-fidget-border-color)", - fidgetBorderWidth: "var(--user-theme-fidget-border-width)", - fidgetShadow: "var(--user-theme-fidget-shadow)", - chain: getNetworkWithId(network), - token: address, - dataSource: "geckoterminal", - theme: "light", - size: 1, - }, - }, - - fidgetType: "Market", - id: "Market:733222fa-38f8-4343-9fa2-6646bb47dde0", - }, - "links:5b4c8b73-416d-4842-9dc5-12fc186d8f57": { - config: { - data: {}, - editable: true, - settings: { - DescriptionColor: "black", - HeaderColor: "black", - background: "rgba(255, 255, 255, 0.5)", - css: "", - fidgetBorderColor: "var(--user-theme-fidget-border-color)", - fidgetBorderWidth: "var(--user-theme-fidget-border-width)", - fidgetShadow: "var(--user-theme-fidget-shadow)", - headingsFontFamily: "'__Inter_d65c78', '__Inter_Fallback_d65c78'", - itemBackground: "#e0eeff", - links: [ - ...(isClankerToken - ? [ - { - avatar: - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAMAAABF0y+mAAAAM1BMVEX////q5PfUxe7o4fbMu+uPadTGs+jKueqJYNHEseiJYtKOZ9PJt+rUx+7MuuvPwOvm3vVnLuiEAAAAXUlEQVR4Ac3QAxaAQAAE0LXR/S+bXdNDWuOvSSGBMsYhCikVRG2MxejcFZoHcXoDQCF9gBiMURC1cfYzpDFSiEnKAHF6w4TuiMscs0bt+69JQyW8VyvkOVeH6p/QAF54BSckEkJ8AAAAAElFTkSuQmCC", - description: "", - text: "Clanker.world", - url: `https://www.clanker.world/clanker/${address}`, - }, - ] - : []), - { - avatar: - "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBwgHBgkIBwgKCgkLDRYPDQwMDRsUFRAWIB0iIiAdHx8kKDQsJCYxJx8fLT0tMTU3Ojo6Iys/RD84QzQ5OjcBCgoKDQwNGg8PGjclHyU3Nzc3Nzc3Ljc3Nzc3Nzg3Nzc3NzctKzc3Nzc3Nzc3Kzc3Nzc3Nzc3ODc3Nzc3Nys3N//AABEIABwAHAMBIgACEQEDEQH/xAAaAAABBQEAAAAAAAAAAAAAAAAHAgQFBggD/8QAKRAAAgEDAgQFBQAAAAAAAAAAAQIDAAURBDEGEiFREyJBccEHFKHR8P/EABgBAAIDAAAAAAAAAAAAAAAAAAIDAQQF/8QAHhEAAQQDAAMAAAAAAAAAAAAAAgABAxEEEiETQVH/2gAMAwEAAhEDEQA/ABZGperdZOBdRcYQ87SpI4ykMcfM2O5HxTbgPQQ6ozayXDNAwCIdgd8n4o4/TZJ5LNNrdVpmgaedhEHGHMa4AJ925j7YrekIYofI7WnuVMs/3zhi4WdpDPEJIY3MbTRMHVG6eV8E8jdR5WwagmBBxR6v3FMV5F5suj0/2ulaR4JplQBpzjDMD+O+3WgHqX8Od05geViMjY0stmBiNqtRfOrvYb3PaNWJYTlT0dDs47H9+lGvhLjKRLa7W9hNC4OI5D1gf+9NjvWeSxpxBrJ4VYRyFQRg4O4qvDliw6SNYoNr46vfGHE6xNJpdFJmRifFlXv6gfJoftJzMSaS7s5yxyaTScnLKcr9Ib+L/9k=", - description: "", - text: "GeckoTerminal", - url: getGeckoUrl(address as Address, network), - }, - { - avatar: - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAEGElEQVR4AbWWA5AcXRSFX2ytbe/+DNccx6vYtm3bTjm2jUGyVg/CRmzb6dwXdO12LaYHt+oMu/s797zbQMYWy7JVMzIyYkaPHjstOTlln1SmKBCJpDfFYukNqUxe2D459cCwESOmnz17Nh62rY4sVXfu3LGZOHHiJIlUdsfG1v577Tr12PLUyMaOFUuk9ydPnjqNJEl7k8HQRZWVK1f2a9Y8/GmduvU5gLHC+zRr3uL50uXLB+P0BMFfvnzZqHvPXkegY8Fgvho2smW79ehx8vXr17bGRu4qkyl05oL5kkikV+7fv+9RYecyOQe3uKRyBTZhV+aa9+7d95CloXx179HrRKkzsWLF6t4wwd/NBSQmibW7d+9OP3DgSFuscRMmbIY5KDETK1asGFQCjgckPDzysSU63LNnT2f+9SMqKuZ98W2aNw9//ubNGwduo6lTp46rW6+BRSI+efJkMuJVTEzca/4pOmHSpKnc2sNwMEIgdvaObOs2bTPaJaccxFK0bFXQoKGN0QawRCLJPWBXQ3B5Dbe1cxC09smpaSdh50rFYq4sb9kqV4gB3ARUMBo9evQkoTF36dJ1Ix+SnJy6V4iB6OjYV2CgAUpLS99dTtTf/fwDPvv5B3729PT+CmtnEQNOzq7fVq9e3QnhgqteXmlwX1//L0eOHGkFLp2x4CLlDXfBU+YawMs9d+78idyfIrGk1AFMSEy6iXi1YMGigeYYcHRy+Y6n/8/8qAiyK0qCe3qpBhJENxCvli1b1s9UAz4+fp8XLFo0/A9cSVxvqSq6rkVSqVxnbQPDho2gt27dKvvzPeMyFaYirj9TEuRF1LZd8hFrG4Cunf58Vhbd8FIR1E2InwUTO9CAQYPmW9kAVxcIOgDAzC84SEtORAcOH5bBZdgiBmDC2UKtNrE0uJKgoyDyhxwcS8tE43jqRsfEPjfXwPLly6fjO2op8VeCqIcC/GNxOHy/m5+fXw3hGjhw8HIhBrp3774GGVFKLekGsMMYyJdaS07nNtTr9b4BgcEfStzXE0UM4tWuXbu6QNRnzp8/H1oe2GBgq8P6DgTQ89Lg0P3Ls3rGkXeRWTA8MCjko5u757d//v3//dix45aUFmd54N1wZwVwK1UReYkH5WtUWY9ljiAvUH0koHYbDNXVBNlLWXSdqAAM3V+/cPw4WQOZWziN8zqqsVpLLeFNdzmibmYS11xNhp7SauvAQeJVWnoexHzZOCh3zt/O0FJhRsM2wimiKSRDNFo6WU1Qs2FqVXCgN4KgHJzKO1fIeCIhhc9RiHYCOH8rHMrpA+w/L/POnVrI1FLnUu5woGWg1wLA7yC1DcpLlB+yVGWTZH2NlkmBjtbCwTNg6h/BHHwF2FeY/qdwtcuB/zapdXSnzEt3bJCR9QPwKOxl9MLyXAAAAABJRU5ErkJggg==", - description: "", - text: "BaseScan", - url: `https://basescan.org/address/${address}`, - }, - ], - title: `$${symbol} Links`, - viewMode: "list", - }, - }, - fidgetType: "links", - id: "links:5b4c8b73-416d-4842-9dc5-12fc186d8f57", - }, - "Chat:09528872-6659-460e-bb25-0c200cccb0ec": { - config: { - data: {}, - editable: true, - settings: { - background: "var(--user-theme-fidget-background)", - fidgetBorderColor: "var(--user-theme-fidget-border-color)", - fidgetBorderWidth: "var(--user-theme-fidget-border-width)", - fidgetShadow: "var(--user-theme-fidget-shadow)", - roomName: address, - }, - }, - fidgetType: "Chat", - id: "Chat:09528872-6659-460e-bb25-0c200cccb0ec", - }, - }; - - const newLayout = isClankerToken && - castHash && - casterFid && - castHash !== "clank.fun deployment" - ? [ - { - h: 6, - i: "Chat:09528872-6659-460e-bb25-0c200cccb0ec", - maxH: 36, - maxW: 36, - minH: 2, - minW: 2, - moved: false, - resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], - static: false, - w: 4, - x: 4, - y: 4, - }, - { - h: 8, - i: "feed:3de67742-56f2-402c-b751-7e769cdcfc56", - maxH: 36, - maxW: 36, - minH: 2, - minW: 4, - moved: false, - resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], - static: false, - w: 4, - x: 8, - y: 0, - }, - { - h: 5, - i: "Swap:f9e0259a-4524-4b37-a261-9f3be26d4af1", - maxH: 36, - maxW: 36, - minH: 3, - minW: 2, - moved: false, - resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], - static: false, - w: 4, - x: 0, - y: 5, - }, - { - h: 5, - i: "Market:733222fa-38f8-4343-9fa2-6646bb47dde0", - maxH: 36, - maxW: 36, - minH: 2, - minW: 2, - moved: false, - resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], - static: false, - w: 4, - x: 0, - y: 0, - }, - { - h: 2, - i: "links:5b4c8b73-416d-4842-9dc5-12fc186d8f57", - maxH: 36, - maxW: 36, - minH: 2, - minW: 2, - moved: false, - resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], - static: false, - w: 4, - x: 8, - y: 8, - }, - { - h: 4, - i: "cast:9c63b80e-bd46-4c8e-9e4e-c6facc41bf71", - maxH: 4, - maxW: 12, - minH: 1, - minW: 3, - moved: false, - resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], - static: false, - w: 4, - x: 4, - y: 0, - }, - ] - : [ - { - h: 8, - i: "Chat:09528872-6659-460e-bb25-0c200cccb0ec", - maxH: 36, - maxW: 36, - minH: 2, - minW: 2, - moved: false, - resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], - static: false, - w: 4, - x: 4, - y: 0, - }, - { - h: 2, - i: "links:5b4c8b73-416d-4842-9dc5-12fc186d8f57", - maxH: 36, - maxW: 36, - minH: 2, - minW: 2, - moved: false, - resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], - static: false, - w: 4, - x: 4, - y: 8, - }, - { - h: 10, - i: "feed:3de67742-56f2-402c-b751-7e769cdcfc56", - maxH: 36, - maxW: 36, - minH: 2, - minW: 4, - moved: false, - resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], - static: false, - w: 4, - x: 8, - y: 0, - }, - { - h: 5, - i: "Market:733222fa-38f8-4343-9fa2-6646bb47dde0", - maxH: 36, - maxW: 36, - minH: 2, - minW: 2, - moved: false, - resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], - static: false, - w: 4, - x: 0, - y: 0, - }, - { - h: 5, - i: "Swap:f9e0259a-4524-4b37-a261-9f3be26d4af1", - maxH: 36, - maxW: 36, - minH: 3, - minW: 2, - moved: false, - resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"], - static: false, - w: 4, - x: 0, - y: 5, - }, - ]; - - // Set the layout configuration - const layoutConfig = getLayoutConfig(config.layoutDetails); - layoutConfig.layout = newLayout; - - config.theme = { - id: "default", - name: "Default", - properties: { - background: "#ffffff", - backgroundHTML: ` - - - - - - Aurora Borealis (Light Theme) - - - -
-
- - - `, - fidgetBackground: "#ffffffb0", // equivalent to rgba(255, 255, 255, 0.69) - fidgetBorderColor: "#eeeeee", // equivalent to rgba(238, 238, 238, 1) - fidgetBorderWidth: "0", - fidgetShadow: "none", - font: "Inter", - fontColor: "#000000", - headingsFont: "Inter", - headingsFontColor: "#000000", - musicURL: "https://www.youtube.com/watch?v=dMXlZ4y7OK4&t=1804", - fidgetBorderRadius: "12px", - gridSpacing: "16", - }, - }; - - return config; -}; - -export default createInitialTokenSpaceConfigForAddress; \ No newline at end of file diff --git a/src/config/createExplorePageConfig.ts b/src/config/createExplorePageConfig.ts index bdc3909ba..34bdd0df0 100644 --- a/src/config/createExplorePageConfig.ts +++ b/src/config/createExplorePageConfig.ts @@ -1,6 +1,6 @@ import DEFAULT_THEME from "@/common/lib/theme/defaultTheme"; import type { - ExplorePageConfig, + NavPageConfig, TabConfig, } from "./systemConfig"; import type { @@ -182,7 +182,7 @@ export const createExplorePageConfig = ({ defaultTokenNetwork = "mainnet", channelNetwork = "base", preloadedDirectoryData, -}: CreateExplorePageConfigOptions): ExplorePageConfig => { +}: CreateExplorePageConfigOptions): NavPageConfig => { const tabEntries: Array<{ key: string; config: TabConfig }> = []; const seenTabNames = new Set(); diff --git a/src/config/example/index.ts b/src/config/example/index.ts index c8f5cb242..2b4627489 100644 --- a/src/config/example/index.ts +++ b/src/config/example/index.ts @@ -7,10 +7,3 @@ export { exampleFidgets } from './example.fidgets'; export { exampleHomePage } from './example.home'; export { exampleExplorePage } from './example.explore'; export { exampleUI } from './example.ui'; - -// Export the initial space creators (used at runtime) -export { default as createInitialProfileSpaceConfigForFid } from './initialSpaces/profile'; -export { default as createInitialChannelSpaceConfig } from './initialSpaces/channel'; -export { default as createInitialTokenSpaceConfigForAddress } from './initialSpaces/token'; -export { default as createInitalProposalSpaceConfigForProposalId } from './initialSpaces/proposal'; -export { default as INITIAL_HOMEBASE_CONFIG } from './initialSpaces/homebase'; diff --git a/src/config/example/initialSpaces/channel.ts b/src/config/example/initialSpaces/channel.ts deleted file mode 100644 index 68235ca19..000000000 --- a/src/config/example/initialSpaces/channel.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { SpaceConfig } from "@/app/(spaces)/Space"; -import { FilterType, FeedType } from "@neynar/nodejs-sdk/build/api"; -import { cloneDeep } from "lodash"; -import { getLayoutConfig } from "@/common/utils/layoutFormatUtils"; -import { INITIAL_SPACE_CONFIG_EMPTY } from "../../initialSpaceConfig"; - -const INITIAL_CHANNEL_SPACE_CONFIG = cloneDeep(INITIAL_SPACE_CONFIG_EMPTY); -INITIAL_CHANNEL_SPACE_CONFIG.tabNames = ["Channel"]; - -const createInitialChannelSpaceConfig = ( - channelId: string, -): Omit => { - const config = cloneDeep(INITIAL_CHANNEL_SPACE_CONFIG); - - config.fidgetInstanceDatums = { - "feed:channel": { - config: { - editable: false, - settings: { - feedType: FeedType.Filter, - filterType: FilterType.ChannelId, - channel: channelId, - }, - data: {}, - }, - fidgetType: "feed", - id: "feed:channel", - }, - "text:channel-info": { - config: { - editable: false, - settings: { - content: "Welcome to this Example Community channel!", - fontSize: "16px", - textAlign: "center" - }, - data: {}, - }, - fidgetType: "text", - id: "text:channel-info", - }, - }; - - const layoutItems = [ - { - w: 12, - h: 8, - x: 0, - y: 0, - i: "text:channel-info", - minW: 4, - maxW: 20, - minH: 2, - maxH: 6, - moved: false, - static: false, - }, - { - w: 6, - h: 8, - x: 0, - y: 8, - i: "feed:channel", - minW: 4, - maxW: 20, - minH: 6, - maxH: 12, - moved: false, - static: false, - }, - ]; - - // Set the layout configuration - const layoutConfig = getLayoutConfig(config.layoutDetails); - layoutConfig.layout = layoutItems; - - // Set default tab names - config.tabNames = ["Channel"]; - - return config; -}; - -export default createInitialChannelSpaceConfig; diff --git a/src/config/example/initialSpaces/homebase.ts b/src/config/example/initialSpaces/homebase.ts deleted file mode 100644 index 4726ae5b3..000000000 --- a/src/config/example/initialSpaces/homebase.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { SpaceConfig } from "@/app/(spaces)/Space"; -import DEFAULT_THEME from "@/common/lib/theme/defaultTheme"; - -const tutorialText = ` -### 🖌️ Click the paintbrush in the bottom-left corner to open Customization Mode - -### Add Fidgets -1. Click the blue **+** button. -2. Drag a Fidget to an open spot on the grid. -3. Click Save - -(after saving, scroll down here for more instructions) - -### Customize Fidgets -1. From customization mode, click any Fidget on the grid to open its settings. -2. Click 'Style' to customize a fidget's look. Any Fidget styles set to "Theme" inherit their look from the Tab's theme. - -### Arrange Fidgets -- **Move:** Drag from the center -- **Resize:** Drag from an edge or corner -- **Stash in Fidget Tray:** Click a fidget then click ⇱ to save it for later. -- **Delete:** Click a fidget then click X it to delete it forever. - -### Create New Tabs -1. Click the **+** button in the tab bar to create a new tab. -2. Name your tab and click Save. -3. Add fidgets to your new tab! - -### Themes -Click the paintbrush icon to open customization mode, then click the **Theme** button to change your space's appearance. - -### Need Help? -Visit our documentation or join our community for support! - ---- - -**Welcome to Example Community!** This is your personal space where you can organize fidgets however you'd like. -`; - -const INITIAL_HOMEBASE_CONFIG: SpaceConfig = { - fidgetInstanceDatums: { - "text:tutorial": { - config: { - data: {}, - editable: false, - settings: { - content: tutorialText, - fontSize: "14px", - textAlign: "left", - background: "var(--user-theme-fidget-background)", - fidgetBorderWidth: "var(--user-theme-fidget-border-width)", - fidgetBorderColor: "var(--user-theme-fidget-border-color)", - fidgetShadow: "var(--user-theme-fidget-shadow)", - }, - }, - fidgetType: "text", - id: "text:tutorial", - }, - }, - layoutID: "homebase-layout", - layoutDetails: { - layoutConfig: { - layout: [ - { - w: 12, - h: 12, - x: 0, - y: 0, - i: "text:tutorial", - minW: 4, - maxW: 36, - minH: 6, - maxH: 36, - moved: false, - static: false, - }, - ], - }, - layoutFidget: "grid", - }, - fidgetTrayContents: [], - theme: DEFAULT_THEME, - timestamp: new Date().toISOString(), - tabNames: ["Homebase"], - isEditable: true, -}; - -export default INITIAL_HOMEBASE_CONFIG; diff --git a/src/config/example/initialSpaces/index.ts b/src/config/example/initialSpaces/index.ts deleted file mode 100644 index 2c0c465c2..000000000 --- a/src/config/example/initialSpaces/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Export the initial space creators from example config -export { default as createInitialProfileSpaceConfigForFid } from './profile'; -export { default as createInitialChannelSpaceConfig } from './channel'; -export { default as createInitialTokenSpaceConfigForAddress } from './token'; -export { default as createInitalProposalSpaceConfigForProposalId } from './proposal'; -export { default as INITIAL_HOMEBASE_CONFIG } from './homebase'; diff --git a/src/config/example/initialSpaces/profile.ts b/src/config/example/initialSpaces/profile.ts deleted file mode 100644 index eaa5f753f..000000000 --- a/src/config/example/initialSpaces/profile.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { SpaceConfig } from "@/app/(spaces)/Space"; -import { FeedType, FilterType } from "@neynar/nodejs-sdk/build/api"; -import { cloneDeep } from "lodash"; -import { getLayoutConfig } from "@/common/utils/layoutFormatUtils"; -import { INITIAL_SPACE_CONFIG_EMPTY } from "../../initialSpaceConfig"; - -// Set default tabNames for profile spaces -const INITIAL_PROFILE_SPACE_CONFIG = cloneDeep(INITIAL_SPACE_CONFIG_EMPTY); -INITIAL_PROFILE_SPACE_CONFIG.tabNames = ["Profile"]; - -const createInitialProfileSpaceConfigForFid = ( - fid: number, - username?: string, -): Omit => { - const config = cloneDeep(INITIAL_PROFILE_SPACE_CONFIG); - config.fidgetInstanceDatums = { - "feed:profile": { - config: { - editable: false, - settings: { - feedType: FeedType.Filter, - users: fid, - filterType: FilterType.Fids, - }, - data: {}, - }, - fidgetType: "feed", - id: "feed:profile", - }, - "text:welcome": { - config: { - editable: false, - settings: { - content: `Welcome to Example Community, ${username || 'friend'}!`, - fontSize: "18px", - textAlign: "left" - }, - data: {}, - }, - fidgetType: "text", - id: "text:welcome", - }, - }; - const layoutItems = [ - { - w: 6, - h: 8, - x: 0, - y: 0, - i: "feed:profile", - minW: 4, - maxW: 36, - minH: 6, - maxH: 36, - moved: false, - static: false, - }, - { - w: 6, - h: 8, - x: 7, - y: 0, - i: "text:welcome", - minW: 3, - maxW: 36, - minH: 3, - maxH: 36, - moved: false, - static: false, - }, - ]; - - // Set the layout configuration - const layoutConfig = getLayoutConfig(config.layoutDetails); - layoutConfig.layout = layoutItems; - - // Set default tab names - config.tabNames = ["Profile"]; - - return config; -}; - -export default createInitialProfileSpaceConfigForFid; diff --git a/src/config/example/initialSpaces/proposal.ts b/src/config/example/initialSpaces/proposal.ts deleted file mode 100644 index f85890c54..000000000 --- a/src/config/example/initialSpaces/proposal.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { SpaceConfig } from "@/app/(spaces)/Space"; -import { cloneDeep } from "lodash"; -import { INITIAL_SPACE_CONFIG_EMPTY } from "../../initialSpaceConfig"; -import { Address } from "viem"; - -export const createInitalProposalSpaceConfigForProposalId = ( - proposalId: string, - proposerAddress: Address -): Omit => { - const config = cloneDeep(INITIAL_SPACE_CONFIG_EMPTY); - config.tabNames = ["Proposal"]; - - config.fidgetInstanceDatums = { - "iframe:proposal-description": { - config: { - editable: true, - data: {}, - settings: { - url: `https://governance.example.com/proposals/${proposalId}`, - showOnMobile: true, - customMobileDisplayName: "Proposal", - background: "var(--user-theme-fidget-background)", - fidgetBorderWidth: "var(--user-theme-fidget-border-width)", - fidgetBorderColor: "var(--user-theme-fidget-border-color)", - fidgetShadow: "var(--user-theme-fidget-shadow)", - }, - }, - fidgetType: "iframe", - id: "iframe:proposal-description", - }, - "text:proposal-info": { - config: { - editable: false, - data: {}, - settings: { - content: "Example Community Proposal Information", - fontSize: "16px", - textAlign: "center" - }, - }, - fidgetType: "text", - id: "text:proposal-info", - }, - }; - - const layoutItems = [ - { - w: 12, - h: 6, - x: 0, - y: 0, - i: "iframe:proposal-description", - minW: 4, - maxW: 36, - minH: 4, - maxH: 36, - moved: false, - static: false, - }, - { - w: 12, - h: 4, - x: 0, - y: 6, - i: "text:proposal-info", - minW: 4, - maxW: 36, - minH: 2, - maxH: 36, - moved: false, - static: false, - }, - ]; - - config.layoutDetails.layoutConfig.layout = layoutItems; - - return config; -}; - -export default createInitalProposalSpaceConfigForProposalId; diff --git a/src/config/example/initialSpaces/token.ts b/src/config/example/initialSpaces/token.ts deleted file mode 100644 index 63081e4cc..000000000 --- a/src/config/example/initialSpaces/token.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { SpaceConfig } from "@/app/(spaces)/Space"; -import { cloneDeep } from "lodash"; -import { INITIAL_SPACE_CONFIG_EMPTY } from "../../initialSpaceConfig"; -import { EtherScanChainName } from "../../../constants/etherscanChainIds"; -import { getGeckoUrl } from "@/common/lib/utils/links"; -import { Address } from "viem"; -import { getLayoutConfig } from "@/common/utils/layoutFormatUtils"; - -export const createInitialTokenSpaceConfigForAddress = ( - address: Address, - network: EtherScanChainName, - castHash?: string, - casterFid?: number, -): Omit => { - const config = cloneDeep(INITIAL_SPACE_CONFIG_EMPTY); - config.tabNames = ["Token"]; - - config.fidgetInstanceDatums = { - "market:token-market": { - config: { - data: {}, - editable: true, - settings: { - tokenAddress: address, - network: network, - geckoUrl: getGeckoUrl(address, network), - }, - }, - fidgetType: "market", - id: "market:token-market", - }, - "text:token-info": { - config: { - data: {}, - editable: false, - settings: { - content: "Token information and market data", - fontSize: "16px", - textAlign: "center" - }, - }, - fidgetType: "text", - id: "text:token-info", - }, - }; - - const layoutItems = [ - { - w: 6, - h: 8, - x: 0, - y: 0, - i: "market:token-market", - minW: 4, - maxW: 36, - minH: 6, - maxH: 36, - moved: false, - static: false, - }, - { - w: 6, - h: 8, - x: 7, - y: 0, - i: "text:token-info", - minW: 4, - maxW: 36, - minH: 6, - maxH: 36, - moved: false, - static: false, - }, - ]; - - if (castHash && casterFid) { - config.fidgetInstanceDatums["cast:example-cast"] = { - config: { - data: {}, - editable: true, - settings: { - background: "var(--user-theme-fidget-background)", - castHash: castHash, - casterFid: casterFid, - fidgetBorderColor: "var(--user-theme-fidget-border-color)", - fidgetBorderWidth: "var(--user-theme-fidget-border-width)", - fidgetShadow: "var(--user-theme-fidget-shadow)", - }, - }, - fidgetType: "cast", - id: "cast:example-cast", - }; - layoutItems.push({ - w: 12, - h: 6, - x: 0, - y: 8, - i: "cast:example-cast", - minW: 4, - maxW: 36, - minH: 4, - maxH: 36, - moved: false, - static: false, - }); - } - - // Set the layout configuration - const layoutConfig = getLayoutConfig(config.layoutDetails); - layoutConfig.layout = layoutItems; - - return config; -}; - -export default createInitialTokenSpaceConfigForAddress; diff --git a/src/config/index.ts b/src/config/index.ts index 374420ce1..953676644 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -69,121 +69,34 @@ export async function loadSystemConfig(context?: ConfigLoadContext): Promise { - switch (resolveCommunity()) { - case 'clanker': - return clankerCreateInitialProfileSpaceConfigForFid(fid, username, walletAddress); - case 'example': - return exampleCreateInitialProfileSpaceConfigForFid(fid, username); - case 'nouns': - default: - return nounsCreateInitialProfileSpaceConfigForFid(fid, username); - } -}; - -export const createInitialChannelSpaceConfig = (channelId: string) => { - switch (resolveCommunity()) { - case 'clanker': - return clankerCreateInitialChannelSpaceConfig(channelId); - case 'example': - return exampleCreateInitialChannelSpaceConfig(channelId); - case 'nouns': - default: - return nounsCreateInitialChannelSpaceConfig(channelId); - } -}; - -export const createInitialTokenSpaceConfigForAddress = ( - ...args: any[] -) => { - switch (resolveCommunity()) { - case 'clanker': - return (clankerCreateInitialTokenSpaceConfigForAddress as any)(...args); - case 'example': - return (exampleCreateInitialTokenSpaceConfigForAddress as any)(...args); - case 'nouns': - default: - return (nounsCreateInitialTokenSpaceConfigForAddress as any)(...args); - } -}; - -// Maintain the historical (typo) API used by consumers -export const createInitalProposalSpaceConfigForProposalId = ( - ...args: any[] -) => { - switch (resolveCommunity()) { - case 'clanker': - // clanker uses the corrected spelling under the hood - return (clankerCreateInitialProposalSpaceConfigForProposalId as any)(...args); - case 'example': - return (exampleCreateInitalProposalSpaceConfigForProposalId as any)(...args); - case 'nouns': - default: - return (nounsCreateInitalProposalSpaceConfigForProposalId as any)(...args); - } -}; - -// Resolve the initial homebase config at module load based on the active community -export const INITIAL_HOMEBASE_CONFIG = (() => { - switch (resolveCommunity()) { - case 'clanker': - return clankerINITIAL_HOMEBASE_CONFIG; - case 'example': - return exampleINITIAL_HOMEBASE_CONFIG; - case 'nouns': - default: - return nounsINITIAL_HOMEBASE_CONFIG; - } -})(); - -// Function to create initial homebase config with user-specific data (e.g., wallet address) -export const createInitialHomebaseConfig = (userAddress?: string) => { - switch (resolveCommunity()) { - case 'clanker': - return clankerCreateInitialHomebaseConfig(userAddress); - case 'example': - return exampleINITIAL_HOMEBASE_CONFIG; - case 'nouns': - default: - return nounsINITIAL_HOMEBASE_CONFIG; - } -}; - // Export initial space config export { INITIAL_SPACE_CONFIG_EMPTY } from './initialSpaceConfig'; diff --git a/src/config/loaders/runtimeLoader.ts b/src/config/loaders/runtimeLoader.ts index afe8ca4c9..0e9bca475 100644 --- a/src/config/loaders/runtimeLoader.ts +++ b/src/config/loaders/runtimeLoader.ts @@ -63,13 +63,10 @@ export class RuntimeConfigLoader implements ConfigLoader { ); } - // Map pages object to homePage/explorePage for backward compatibility // Add themes from shared file (themes are not in database) const mappedConfig: SystemConfig = { ...dbConfig, theme: themes, // Themes come from shared file - homePage: dbConfig.pages?.['home'] || dbConfig.homePage || null, - explorePage: dbConfig.pages?.['explore'] || dbConfig.explorePage || null, }; return mappedConfig as SystemConfig; diff --git a/src/config/systemConfig.ts b/src/config/systemConfig.ts index 6b94e392d..df776d357 100644 --- a/src/config/systemConfig.ts +++ b/src/config/systemConfig.ts @@ -30,13 +30,8 @@ export interface SystemConfig { theme: ThemeConfig; community: CommunityConfig; fidgets: FidgetConfig; - homePage: HomePageConfig | null; // Nullable for navigation-space approach - explorePage: ExplorePageConfig | null; // Nullable for navigation-space approach navigation?: NavigationConfig; ui?: UIConfig; - pages?: { - [key: string]: HomePageConfig | ExplorePageConfig; - }; // For database config structure } export interface UIConfig { @@ -138,7 +133,7 @@ export interface FidgetConfig { disabled: string[]; } -export interface HomePageConfig { +export interface NavPageConfig { defaultTab: string; tabOrder: string[]; tabs: { @@ -156,8 +151,6 @@ export interface HomePageConfig { }; } -export type ExplorePageConfig = HomePageConfig; - export interface NavigationConfig { items: NavigationItem[]; logoTooltip?: LogoTooltipConfig; From fc1b61c2cb6463ad2098ef40f90b4f3e908539ba Mon Sep 17 00:00:00 2001 From: Jesse Paterson Date: Tue, 2 Dec 2025 15:06:29 -0600 Subject: [PATCH 118/155] Updated to have server pass systemConfig to client as props --- docs/CONFIGURATION.md | 35 +- docs/INTEGRATIONS/SUPABASE.md | 2 + .../CONFIGURATION/ARCHITECTURE_OVERVIEW.md | 521 ++++++++++++++++++ docs/SYSTEMS/CONFIGURATION/OVERVIEW.md | 367 ------------ src/app/layout.tsx | 14 +- .../components/molecules/BrandHeader.tsx | 10 +- .../organisms/ClientMobileHeaderWrapper.tsx | 11 +- .../organisms/ClientSidebarWrapper.tsx | 9 +- .../components/organisms/MobileHeader.tsx | 11 +- .../components/organisms/Navigation.tsx | 19 +- src/common/components/organisms/Sidebar.tsx | 9 +- src/common/lib/hooks/useUIColors.ts | 18 +- src/config/index.ts | 21 +- src/config/loaders/utils.ts | 69 +-- ...0251129172847_create_community_configs.sql | 1 + 15 files changed, 668 insertions(+), 449 deletions(-) create mode 100644 docs/SYSTEMS/CONFIGURATION/ARCHITECTURE_OVERVIEW.md delete mode 100644 docs/SYSTEMS/CONFIGURATION/OVERVIEW.md diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 43d40cde1..578d0dee4 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -5,7 +5,7 @@ Nounspace uses a **database-backed configuration system** with **domain-based mu ## Database-Backed Configuration System For complete documentation on how the configuration system works, see: -- **[Configuration System Overview](SYSTEMS/CONFIGURATION/OVERVIEW.md)** - Complete description of the database-backed configuration system +- **[Architecture Overview](SYSTEMS/CONFIGURATION/ARCHITECTURE_OVERVIEW.md)** - Complete description of the database-backed configuration system ## Environment Variables @@ -52,9 +52,30 @@ NEXT_PUBLIC_WEBSITE_URL=http://localhost:3000 ## Configuration Loading -The application uses **runtime loading** from Supabase. Config is fetched from the database at runtime based on the request domain, enabling multi-tenant support. +The application uses **server-only runtime loading** from Supabase. Config is fetched from the database at runtime based on the request domain, enabling multi-tenant support. -See [Configuration System Overview](SYSTEMS/CONFIGURATION/OVERVIEW.md) for details. +### Server-Only Architecture + +**Important:** `loadSystemConfig()` is **server-only** and can only be called from Server Components. Client components receive config via the `systemConfig` prop. + +**Pattern:** +```typescript +// ✅ Server Component +export default async function MyServerComponent() { + const systemConfig = await loadSystemConfig(); + return ; +} + +// ✅ Client Component +"use client"; +type Props = { systemConfig: SystemConfig }; +export function MyClientComponent({ systemConfig }: Props) { + const { brand, assets } = systemConfig; + // Use config... +} +``` + +See [Architecture Overview](SYSTEMS/CONFIGURATION/ARCHITECTURE_OVERVIEW.md) for complete details. ## Configuration Structure @@ -89,7 +110,7 @@ src/config/ └── index.ts # Main configuration loader ``` -**Note:** Themes are stored in `shared/themes.ts` and pages (homePage/explorePage) are stored as Spaces in Supabase Storage. See [Configuration System Overview](SYSTEMS/CONFIGURATION/OVERVIEW.md) for details. +**Note:** Themes are stored in `shared/themes.ts` and pages (homePage/explorePage) are stored as Spaces in Supabase Storage. See [Architecture Overview](SYSTEMS/CONFIGURATION/ARCHITECTURE_OVERVIEW.md) for details. ## Domain-Based Multi-Tenant Configuration @@ -109,6 +130,10 @@ Browser Request (example.nounspace.com) Middleware (detects domain, sets x-community-id header) ↓ Server Component (reads header, loads config) + ├─ Calls: await loadSystemConfig() ← SERVER-ONLY + └─ Passes systemConfig as prop to Client Components + ↓ +Client Components receive systemConfig prop ↓ Renders with correct community config ``` @@ -157,7 +182,7 @@ To add a new community configuration: 4. **Create navigation spaces**: If your community has navigation pages, create `navPage` type spaces in `spaceRegistrations` and upload their configs to Storage 5. **Update seed data**: Add seed data in `supabase/seed.sql` for the new community -See [Configuration System Overview](SYSTEMS/CONFIGURATION/OVERVIEW.md) for detailed instructions. +See [Architecture Overview](SYSTEMS/CONFIGURATION/ARCHITECTURE_OVERVIEW.md) for detailed instructions. ## Development vs Production diff --git a/docs/INTEGRATIONS/SUPABASE.md b/docs/INTEGRATIONS/SUPABASE.md index 014754b37..b2944f680 100644 --- a/docs/INTEGRATIONS/SUPABASE.md +++ b/docs/INTEGRATIONS/SUPABASE.md @@ -92,6 +92,7 @@ CREATE INDEX idx_spaces_public ON spaces(is_public) WHERE is_public = TRUE; ```sql -- Get active community configuration +-- Returns the most recently updated published config for a community CREATE OR REPLACE FUNCTION get_active_community_config( p_community_id VARCHAR(50) ) @@ -114,6 +115,7 @@ BEGIN FROM "public"."community_configs" WHERE "community_id" = p_community_id AND "is_published" = true + ORDER BY "updated_at" DESC LIMIT 1; RETURN v_config; diff --git a/docs/SYSTEMS/CONFIGURATION/ARCHITECTURE_OVERVIEW.md b/docs/SYSTEMS/CONFIGURATION/ARCHITECTURE_OVERVIEW.md new file mode 100644 index 000000000..d768bc497 --- /dev/null +++ b/docs/SYSTEMS/CONFIGURATION/ARCHITECTURE_OVERVIEW.md @@ -0,0 +1,521 @@ +# Architecture Overview + +## Executive Summary + +This document provides a comprehensive overview of the Nounspace configuration architecture. The system has been refactored from a static, build-time TypeScript-based configuration system to a **dynamic, database-backed, multi-tenant runtime configuration system** that supports domain-based community detection. + +## Core Architectural Principles + +1. **Server-Only Config Loading**: `loadSystemConfig()` is server-only and uses `await headers()` API +2. **Prop-Based Config Passing**: Client components receive config via `systemConfig` prop from Server Components +3. **Runtime Configuration Loading**: All community configs are loaded from Supabase at request time +4. **Multi-Tenant Support**: Single deployment serves multiple communities via domain-based routing +5. **Separation of Concerns**: Configs, themes, and pages are stored in different locations +6. **Dynamic Navigation**: Navigation pages are stored as Spaces in Supabase Storage +7. **Simplified Space Creators**: All communities use Nouns implementations for initial space creation + +--- + +## Architecture Layers + +### 1. Request Flow & Domain Detection + +``` +Browser Request (example.nounspace.com) + ↓ +Next.js Middleware (middleware.ts) + ├─ Detects domain from headers + ├─ Resolves community ID (example.nounspace.com → "example") + └─ Sets headers: x-community-id, x-detected-domain + ↓ +Server Component (layout.tsx, page.tsx, etc.) + ├─ Reads headers (async headers() API) + ├─ Calls await loadSystemConfig() ← SERVER-ONLY + ├─ Loads config from database + └─ Passes systemConfig as prop to Client Components + ↓ +Client Components + ├─ Receive systemConfig prop + └─ Use config data (brand, assets, navigation, etc.) + ↓ +Renders with community-specific config +``` + +**Key Point:** Config loading is **server-only**. Client components never call `loadSystemConfig()` directly - they receive config via props. + +**Key Files:** +- `middleware.ts` - Domain detection and header injection +- `src/config/loaders/registry.ts` - Domain → community ID resolution +- `src/config/loaders/utils.ts` - Context building and community ID resolution + +### 2. Configuration Loading System + +#### Server-Only Architecture + +**Important:** `loadSystemConfig()` is **server-only** and can only be called from Server Components or Server Actions. Client components receive config via props. + +#### Configuration Loader Architecture + +``` +loadSystemConfig(context?) - SERVER-ONLY + ↓ +buildContext() - Builds ConfigLoadContext + ├─ Reads x-community-id, x-detected-domain headers (server-only) + └─ Falls back to env vars if needed + ↓ +resolveCommunityId(context) - Priority order: + 1. Explicit context.communityId + 2. NEXT_PUBLIC_TEST_COMMUNITY (dev only) + 3. Domain resolution (from middleware headers) + 4. NEXT_PUBLIC_COMMUNITY (fallback) + ↓ +RuntimeConfigLoader.load(context) + ├─ Fetches from Supabase RPC: get_active_community_config() + ├─ Validates config structure + ├─ Merges with shared themes (from shared/themes.ts) + └─ Returns SystemConfig +``` + +#### Prop Passing Pattern + +``` +Server Component (loads config) + ↓ systemConfig prop +Client Wrapper Component + ↓ systemConfig prop +Client Component (uses config) + ↓ systemConfig prop (if needed) +Child Client Components +``` + +**Example:** +```typescript +// ✅ CORRECT: Server Component +export default async function RootLayout() { + const systemConfig = await loadSystemConfig(); // Server-only + return ; +} + +// ❌ WRONG: Client Component +"use client"; +export function MyComponent() { + const config = loadSystemConfig(); // ERROR: Can't use server APIs +} +``` + +**Key Files:** +- `src/config/index.ts` - Main config loading entry point +- `src/config/loaders/runtimeLoader.ts` - Database config loader +- `src/config/loaders/types.ts` - Type definitions +- `src/config/loaders/utils.ts` - Utility functions + +#### Community ID Resolution Priority + +1. **Explicit Context** (`context.communityId`) - Highest priority +2. **Development Override** (`NEXT_PUBLIC_TEST_COMMUNITY`) - For local testing +3. **Domain Resolution** - From middleware headers or window.location +4. **Environment Variable** (`NEXT_PUBLIC_COMMUNITY`) - Final fallback + +### 3. Database Schema + +#### `community_configs` Table + +```sql +CREATE TABLE community_configs ( + id UUID PRIMARY KEY, + community_id VARCHAR(50) NOT NULL UNIQUE, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT now(), + brand_config JSONB NOT NULL, -- Brand identity + assets_config JSONB NOT NULL, -- Asset paths + community_config JSONB NOT NULL, -- Community integration + fidgets_config JSONB NOT NULL, -- Enabled/disabled fidgets + navigation_config JSONB, -- Navigation items (with spaceId refs) + ui_config JSONB, -- UI colors + is_published BOOLEAN DEFAULT true +); +``` + +#### Database Function: `get_active_community_config` + +```sql +-- Returns most recently updated published config for a community +-- Orders by updated_at DESC for deterministic results +SELECT jsonb_build_object( + 'brand', brand_config, + 'assets', assets_config, + 'community', community_config, + 'fidgets', fidgets_config, + 'navigation', navigation_config, + 'ui', ui_config +) +FROM community_configs +WHERE community_id = p_community_id + AND is_published = true +ORDER BY updated_at DESC +LIMIT 1; +``` + +**Key Features:** +- Deterministic ordering by `updated_at DESC` +- Only returns published configs +- Returns most recent version if multiple exist + +### 4. Configuration Structure + +#### SystemConfig Interface + +```typescript +interface SystemConfig { + brand: BrandConfig; // From database + assets: AssetConfig; // From database + theme: ThemeConfig; // From shared/themes.ts (NOT in database) + community: CommunityConfig; // From database + fidgets: FidgetConfig; // From database + navigation?: NavigationConfig; // From database (with spaceId refs) + ui?: UIConfig; // From database +} +``` + +#### What's Stored Where + +| Component | Storage Location | Notes | +|-----------|-----------------|-------| +| **Brand Config** | Database (`brand_config`) | Display name, tagline, description | +| **Assets Config** | Database (`assets_config`) | Logo paths, favicon, OG images | +| **Community Config** | Database (`community_config`) | URLs, social handles, contracts, tokens | +| **Fidgets Config** | Database (`fidgets_config`) | Enabled/disabled fidget IDs | +| **Navigation Config** | Database (`navigation_config`) | Navigation items with `spaceId` refs | +| **UI Config** | Database (`ui_config`) | Primary colors, hover states | +| **Themes** | `src/config/shared/themes.ts` | Shared across all communities | +| **Navigation Pages** | Supabase Storage (`spaces` bucket) | Stored as Spaces, referenced by `spaceId` | + +### 5. Navigation Pages as Spaces + +#### Concept + +Navigation pages (like `/home` and `/explore`) are **not** stored in the database config. Instead, they are stored as **Spaces** in Supabase Storage and referenced by navigation items via `spaceId`. + +#### Navigation Item Structure + +```typescript +interface NavigationItem { + id: string; + label: string; + href: string; // e.g., "/home" + icon?: 'home' | 'explore' | ...; + spaceId?: string; // Reference to Space in storage +} +``` + +#### Space Storage Structure + +``` +spaces/ + {spaceId}/ + tabOrder ← JSON: { tabOrder: ["Nouns", "Socials", ...] } + tabs/ + Nouns ← SpaceConfig JSON (fidgets, layout, etc.) + Socials ← SpaceConfig JSON + ... +``` + +#### Loading Flow + +``` +User navigates to /home + ↓ +Middleware: Sets x-community-id header + ↓ +NavPage Server Component (page.tsx) + ├─ Step 1: Load SystemConfig + │ └─ Gets navigation items from database + │ └─ Finds nav item: { href: "/home", spaceId: "abc-123-def" } + │ + ├─ Step 2: Load Space from Storage + │ └─ Downloads: spaces/abc-123-def/tabOrder + │ └─ Downloads: spaces/abc-123-def/tabs/Nouns + │ └─ Downloads: spaces/abc-123-def/tabs/Socials + │ └─ Constructs NavPageConfig + │ + ├─ Step 3: Redirect to default tab (if no tab specified) + │ └─ Redirects to: /home/Nouns + │ + └─ Step 4: Render with tab + └─ Passes NavPageConfig to NavPageClient + ↓ +NavPageClient (Client Component) + ├─ Receives: pageConfig, activeTabName, navSlug props + ├─ Extracts active tab config from pageConfig.tabs + ├─ Creates TabBar component + └─ Renders SpacePage with tab config +``` + +#### Detailed Navigation Page Flow + +**When user visits `/home`:** + +1. **Middleware runs first:** + - Detects domain: `example.nounspace.com` + - Sets header: `x-community-id: "example"` + +2. **NavPage Server Component:** + - Loads `SystemConfig` → gets navigation items + - Finds nav item with `href="/home"` → extracts `spaceId` + - Loads Space from Supabase Storage: + - Downloads `tabOrder` file + - Downloads each tab config file + - Constructs `NavPageConfig` object + - If no tab specified → redirects to `/home/{defaultTab}` + +3. **NavPage runs again with tab:** + - Loads `SystemConfig` again + - Loads Space from Storage again + - Validates tab exists + - Passes `NavPageConfig` to `NavPageClient` + +4. **NavPageClient (Client Component):** + - Receives `pageConfig` prop (NavPageConfig) + - Extracts active tab config + - Renders `SpacePage` with tab content + +**Storage Structure:** +``` +Supabase Storage (spaces bucket): + spaces/ + {spaceId}/ + tabOrder ← SignedFile: { tabOrder: ["Nouns", "Socials"] } + tabs/ + Nouns ← SignedFile: SpaceConfig JSON + Socials ← SignedFile: SpaceConfig JSON +``` + +**Database References:** +```json +// In community_configs.navigation_config: +{ + "items": [ + { + "id": "home", + "href": "/home", + "spaceId": "abc-123-def" ← References Space in storage + } + ] +} +``` + +**Key Files:** +- `src/app/[navSlug]/[[...tabName]]/page.tsx` - Dynamic navigation page handler +- `src/app/[navSlug]/[[...tabName]]/NavPageClient.tsx` - Client component for rendering +- `src/config/systemConfig.ts` - `NavPageConfig` type definition + +### 6. Space Creators + +#### Simplified Architecture + +All communities now use **Nouns implementations** for initial space creation. The space creator functions are synchronous and directly re-export Nouns implementations. + +```typescript +// All communities use Nouns implementations +export const createInitialProfileSpaceConfigForFid = nounsCreateInitialProfileSpaceConfigForFid; +export const createInitialChannelSpaceConfig = nounsCreateInitialChannelSpaceConfig; +export const createInitialTokenSpaceConfigForAddress = nounsCreateInitialTokenSpaceConfigForAddress; +export const createInitalProposalSpaceConfigForProposalId = nounsCreateInitalProposalSpaceConfigForProposalId; +export const INITIAL_HOMEBASE_CONFIG = nounsINITIAL_HOMEBASE_CONFIG; +``` + +**Key Files:** +- `src/config/index.ts` - Re-exports Nouns implementations +- `src/config/nouns/initialSpaces/` - Nouns space creator implementations + +--- + +## Key Architectural Changes + +### Removed Components + +1. **Build-Time Config Loading** - All configs now load at runtime +2. **Static Config Fallbacks** - No fallback to TypeScript configs +3. **Community-Specific Space Creators** - All use Nouns implementations +4. **HomePageConfig/ExplorePageConfig in SystemConfig** - Moved to Spaces +5. **Factory Pattern for Config Loaders** - Simplified to single runtime loader + +### Added Components + +1. **Middleware-Based Domain Detection** - Centralized domain resolution +2. **Runtime Database Loading** - All configs from Supabase +3. **Navigation-Space References** - Pages stored as Spaces +4. **NavPageConfig Type** - Unified type for navigation pages +5. **Deterministic Database Function** - Orders by `updated_at DESC` + +### Simplified Components + +1. **Config Loading** - Single `RuntimeConfigLoader` (no factory) +2. **Space Creators** - Synchronous, Nouns-only implementations +3. **Type System** - `NavPageConfig` replaces `HomePageConfig | ExplorePageConfig` +4. **Community Resolution** - Clear priority order + +--- + +## Data Flow Examples + +### Example 1: Loading Config for `example.nounspace.com` + +``` +1. Request arrives at middleware.ts + └─ Domain: "example.nounspace.com" + └─ Resolves to: "example" + └─ Sets header: x-community-id = "example" + +2. Server Component calls loadSystemConfig() + └─ Reads x-community-id header: "example" + └─ Calls RuntimeConfigLoader.load({ communityId: "example" }) + +3. RuntimeConfigLoader + └─ Calls Supabase RPC: get_active_community_config("example") + └─ Receives JSONB config from database + └─ Merges with themes from shared/themes.ts + └─ Returns SystemConfig + +4. Component renders with example community config +``` + +### Example 2: Navigating to `/home` Page + +``` +1. Request: example.nounspace.com/home + └─ Middleware detects domain → sets x-community-id: "example" + +2. NavPage Server Component loads + └─ Calls: await loadSystemConfig() + └─ Gets navigation items from database + └─ Finds: { href: "/home", spaceId: "abc-123-def" } + +3. NavPage loads Space from Supabase Storage + └─ Downloads: spaces/abc-123-def/tabOrder + └─ Downloads: spaces/abc-123-def/tabs/Nouns + └─ Downloads: spaces/abc-123-def/tabs/Socials + └─ Constructs NavPageConfig: + { + defaultTab: "Nouns", + tabOrder: ["Nouns", "Socials"], + tabs: { "Nouns": {...}, "Socials": {...} } + } + +4. Redirects to default tab: /home/Nouns + +5. NavPage runs again with tab + └─ Loads SystemConfig and Space again + └─ Validates "Nouns" tab exists + └─ Passes NavPageConfig to NavPageClient + +6. NavPageClient (Client Component) renders + └─ Receives pageConfig prop + └─ Extracts tab config: pageConfig.tabs["Nouns"] + └─ Creates TabBar component + └─ Renders SpacePage with tab content +``` + +### Example 3: Component Hierarchy & Prop Flow + +``` +RootLayout (Server Component) +├─ await loadSystemConfig() ← SERVER-ONLY +├─ ClientMobileHeaderWrapper (Client) +│ └─ systemConfig prop +│ └─ MobileHeader (Client) +│ ├─ systemConfig prop +│ ├─ BrandHeader (Client) ← uses systemConfig.assets +│ └─ Navigation (Client) ← uses systemConfig.navigation +│ +└─ ClientSidebarWrapper (Client) + └─ systemConfig prop + └─ Sidebar (Client) + └─ systemConfig prop + └─ Navigation (Client) ← uses systemConfig.navigation +``` + +--- + +## Environment Variables + +### Required + +- `NEXT_PUBLIC_SUPABASE_URL` - Supabase project URL +- `NEXT_PUBLIC_SUPABASE_ANON_KEY` - Supabase anon key (for runtime loading) +- `SUPABASE_SERVICE_ROLE_KEY` - Service role key (for seeding/admin operations) + +### Optional + +- `NEXT_PUBLIC_COMMUNITY` - Community ID fallback (defaults to 'nouns') +- `NEXT_PUBLIC_TEST_COMMUNITY` - Override for local testing (development only) + +--- + +## Testing & Development + +### Local Testing + +1. **Localhost Subdomains**: `example.localhost:3000` → detects "example" +2. **Environment Override**: `NEXT_PUBLIC_TEST_COMMUNITY=example npm run dev` +3. **Direct Community**: `NEXT_PUBLIC_COMMUNITY=nouns npm run dev` + +### Production + +- Domain-based detection: `example.nounspace.com` → "example" +- Falls back to `NEXT_PUBLIC_COMMUNITY` if domain can't be resolved + +--- + +## Benefits + +1. **Multi-Tenant Support** - Single deployment serves multiple communities +2. **Dynamic Updates** - Config changes without rebuild +3. **Domain-Based Routing** - Automatic community detection +4. **Unified Architecture** - Pages are Spaces, consistent with existing system +5. **Shared Themes** - Single source of truth, no duplication +6. **Simplified Codebase** - Removed build-time complexity +7. **Deterministic Loading** - Database function orders by `updated_at DESC` +8. **Server-Client Separation** - Clear boundaries, no client-side server API calls +9. **Type Safety** - SystemConfig type flows through props +10. **Performance** - Config loaded once at root, reused throughout app +11. **No Hydration Issues** - No client-side domain detection + +--- + +## Related Files + +### Core Configuration +- `src/config/index.ts` - Main config loader +- `src/config/systemConfig.ts` - Type definitions +- `src/config/loaders/runtimeLoader.ts` - Database loader +- `src/config/loaders/utils.ts` - Utility functions +- `src/config/loaders/registry.ts` - Domain resolution +- `src/config/shared/themes.ts` - Shared themes + +### Routing & Navigation +- `middleware.ts` - Domain detection and header injection +- `src/app/[navSlug]/[[...tabName]]/page.tsx` - Dynamic navigation (Server Component) +- `src/app/[navSlug]/[[...tabName]]/NavPageClient.tsx` - Client component for rendering +- `src/app/layout.tsx` - Root layout that loads config and passes to client components +- `src/common/components/organisms/ClientSidebarWrapper.tsx` - Client wrapper for Sidebar +- `src/common/components/organisms/ClientMobileHeaderWrapper.tsx` - Client wrapper for MobileHeader + +### Database +- `supabase/migrations/20251129172847_create_community_configs.sql` - Schema +- `scripts/seed-community-configs.ts` - Seeding script +- `scripts/seed-navpage-spaces.ts` - Navigation page seeding + +### Space Creators +- `src/config/nouns/initialSpaces/` - Nouns implementations +- `src/config/index.ts` - Re-exports + +--- + +## Future Considerations + +1. **Versioning**: Database function supports multiple versions (orders by `updated_at`) +2. **Caching**: Could add caching layer for frequently accessed configs +3. **Admin UI**: Could build admin interface for config updates +4. **Validation**: Could add JSON schema validation for configs +5. **Rollback**: Could add version history and rollback capabilities + diff --git a/docs/SYSTEMS/CONFIGURATION/OVERVIEW.md b/docs/SYSTEMS/CONFIGURATION/OVERVIEW.md deleted file mode 100644 index 33033b425..000000000 --- a/docs/SYSTEMS/CONFIGURATION/OVERVIEW.md +++ /dev/null @@ -1,367 +0,0 @@ -# Database-Backed Configuration System - -## Overview - -Nounspace uses a database-backed configuration system with **domain-based multi-tenant support**. Community configurations are stored in Supabase and loaded dynamically at runtime based on the request domain, enabling a single deployment to serve multiple communities. - -## Architecture - -``` -┌─────────────────┐ -│ Browser │ -│ (Request) │ -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ Middleware │ -│ (Edge Runtime) │ -│ - Detects domain│ -│ - Sets headers │ -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ ┌──────────────────┐ -│ Server Component│─────▶│ Config Loader │ -│ │ │ (Runtime) │ -└─────────────────┘ └─────────┬────────┘ - │ - ▼ - ┌──────────────────┐ - │ Database │ - │ (Runtime) │ - └──────────────────┘ -``` - -## Configuration Storage - -### Database Table - -Configurations are stored in the `community_configs` table: - -```sql -CREATE TABLE "public"."community_configs" ( - "id" UUID PRIMARY KEY, - "community_id" VARCHAR(50) NOT NULL UNIQUE, - "updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), - "brand_config" JSONB NOT NULL, -- Brand identity - "assets_config" JSONB NOT NULL, -- Asset paths - "community_config" JSONB NOT NULL, -- Community integration data - "fidgets_config" JSONB NOT NULL, -- Enabled/disabled fidgets - "navigation_config" JSONB, -- Navigation items (with spaceId refs) - "ui_config" JSONB, -- UI colors - "is_published" BOOLEAN DEFAULT true -); -``` - -### Config Sections - -- **`brand_config`**: Display name, tagline, description, mini app tags -- **`assets_config`**: Logo paths (main, icon, favicon, og image, splash) -- **`community_config`**: URLs, social handles, governance links, tokens, contracts -- **`fidgets_config`**: Enabled and disabled fidget IDs -- **`navigation_config`**: Navigation items with optional `spaceId` references -- **`ui_config`**: Primary colors, hover states, cast button colors - -### What's Not in the Database - -- **Themes**: Stored in `src/config/shared/themes.ts` (shared across communities) -- **Pages** (homePage/explorePage): Stored as Spaces in Supabase Storage, referenced by navigation items - -## Configuration Loading - -All communities use runtime loading from Supabase: - -**Process:** -1. **Middleware Detects Domain** - ```typescript - // middleware.ts - const domain = request.headers.get('host'); // "example.nounspace.com" - const communityId = resolveCommunityFromDomain(domain); // "example" - response.headers.set('x-community-id', communityId); - ``` - -2. **Server Component Reads Header** - ```typescript - // Server Component - const headersList = await headers(); - const communityId = headersList.get('x-community-id'); - ``` - -3. **Fetch Config from Database** (at request time) - ```typescript - // src/config/loaders/runtimeLoader.ts - const { data } = await supabase - .rpc('get_active_community_config', { p_community_id: communityId }) - .single(); - return data; - ``` - -**Benefits:** -- Multi-tenant support (different domains → different communities) -- Single deployment serves all communities -- Config can be updated without rebuild -- Dynamic configuration updates - -### Database Function - -The `get_active_community_config(community_id)` function returns a combined JSON object: - -```json -{ - "brand": { /* brand_config */ }, - "assets": { /* assets_config */ }, - "community": { /* community_config */ }, - "fidgets": { /* fidgets_config */ }, - "navigation": { /* navigation_config */ }, - "ui": { /* ui_config */ } -} -``` - -## Navigation Pages as Spaces - -### Concept - -Navigation pages (like `/home` and `/explore`) are stored as Spaces in Supabase Storage, not in the database config. Navigation items reference these Spaces via `spaceId`. - -### Storage Structure - -**Space Registration** (in `spaceRegistrations` table): -- `spaceType = 'navPage'` -- `fid = NULL` (system-owned) -- `identityPublicKey = 'system'` -- `signature = 'system-seed'` - -**Space Config Files** (in Supabase Storage bucket `spaces`): -``` -spaces/ - {spaceId}/ - tabOrder ← JSON: { tabOrder: ["Nouns", "Socials", ...] } - tabs/ - Nouns ← SpaceConfig JSON (fidgets, layout, etc.) - Socials ← SpaceConfig JSON - ... -``` - -**File Format** (SignedFile wrapper): -```json -{ - "fileData": "{...SpaceConfig JSON as string...}", - "fileType": "json", - "isEncrypted": false, - "timestamp": "2024-01-01T00:00:00Z", - "publicKey": "nounspace", - "signature": "not applicable, machine generated file" -} -``` - -### Navigation Reference - -Navigation items reference Spaces: - -```typescript -{ - id: 'home', - label: 'Home', - href: '/home', - icon: 'home', - spaceId: 'uuid-of-home-space' // ← References Space -} -``` - -## Dynamic Routing - -### Route Handler - -The `/[navSlug]/[[...tabName]]/page.tsx` route handles all navigation-backed pages: - -- `/home` → redirects to `/home/{defaultTab}` -- `/home/Nouns` → renders Nouns tab -- `/explore` → redirects to `/explore/{defaultTab}` -- `/explore/Featured` → renders Featured tab - -### Request Flow - -1. User visits `/home` -2. Route handler finds navigation item by slug -3. If `spaceId` exists, loads Space from Storage: - - Fetches `{spaceId}/tabOrder` to get tab order - - Fetches each `{spaceId}/tabs/{tabName}` for tab configs - - Reconstructs `PageConfig` format -4. If no tab specified, redirects to default tab -5. Renders `NavPageClient` with tabs - -### Space Loading Function - -```typescript -async function loadSpaceAsPageConfig(spaceId: string): Promise { - // 1. Fetch tab order - const { data: tabOrderData } = await supabase.storage - .from('spaces') - .download(`${spaceId}/tabOrder`); - - const tabOrderFile = JSON.parse(await tabOrderData.text()) as SignedFile; - const tabOrderObj = JSON.parse(tabOrderFile.fileData); - const tabOrder = tabOrderObj.tabOrder; - - // 2. Fetch each tab config - const tabs = {}; - for (const tabName of tabOrder) { - const { data: tabData } = await supabase.storage - .from('spaces') - .download(`${spaceId}/tabs/${tabName}`); - - const tabFile = JSON.parse(await tabData.text()) as SignedFile; - const tabConfig = JSON.parse(tabFile.fileData); - tabs[tabName] = tabConfig; - } - - // 3. Reconstruct PageConfig - return { - defaultTab: tabOrder[0], - tabOrder, - tabs, - layout: { /* defaults */ } - }; -} -``` - -## Shared Themes - -Themes are stored in `src/config/shared/themes.ts` and shared across all communities: - -```typescript -export const themes = { - default: { /* ... */ }, - nounish: { /* ... */ }, - gradientAndWave: { /* ... */ }, - // ... all 10 themes -}; -``` - -All communities import from this shared file, reducing duplication and config size. - -## Configuration Size - -The database config is ~2.8 KB (down from ~29 KB) by: -- Moving themes to shared file (5.5 KB saved) -- Moving pages to Spaces (20.6 KB saved) - -This size reduction makes the environment variable approach viable. - -## Request Flow - -### Complete Flow Example - -**User visits:** `https://example.nounspace.com/home` - -1. **Middleware** (Edge Runtime) - - Extracts domain: `example.nounspace.com` - - Resolves community ID: `example` - - Sets headers: `x-community-id: example`, `x-detected-domain: example.nounspace.com` - -2. **Server Component** - - Reads `x-community-id` header - - Calls `await loadSystemConfig()` - - Fetches config from database for the detected community - - Renders page with correct config - -3. **Client Component** - - Uses `window.location.hostname` for domain detection - - Config loading is always async (from database) - -### Config Loader - -```typescript -// src/config/index.ts -export async function loadSystemConfig(context?: ConfigLoadContext): Promise { - // Server-side: reads from middleware-set headers - // Client-side: uses window.location.hostname - - const factory = getConfigLoaderFactory(); - const loader = factory.getLoader(context); - - // Always uses runtime loader (fetches from database) - return await loader.load(context); -} -``` - -### Component Usage - -**Server Components** (must await): -```typescript -// Server Component -import { loadSystemConfig } from '@/config'; - -export default async function Layout() { - const config = await loadSystemConfig(); - return
{config.brand.displayName}
; -} -``` - -**Client Components** (always async): -```typescript -// Client Component -import { loadSystemConfig } from '@/config'; - -function Navigation() { - const config = await loadSystemConfig(); // Always async (from database) -} -``` - -**React Hook**: -```typescript -import { useSystemConfig } from '@/common/lib/hooks/useSystemConfig'; - -function Navigation() { - const config = useSystemConfig(); - // Use config.brand, config.navigation, etc. -} -``` - -## Environment Variables - -**Required:** -- `NEXT_PUBLIC_SUPABASE_URL` - Supabase project URL -- `NEXT_PUBLIC_SUPABASE_ANON_KEY` - Supabase anon key (for runtime loading) -- `SUPABASE_SERVICE_ROLE_KEY` - Service role key (for seeding) - -**Optional:** -- `NEXT_PUBLIC_COMMUNITY` - Community ID (defaults to 'nouns', used as fallback) -- `NEXT_PUBLIC_TEST_COMMUNITY` - Override for local testing (development only) - -## Domain Resolution - -The system automatically resolves community ID from domain: - -- `example.nounspace.com` → `example` -- `clanker.nounspace.com` → `clanker` -- `example.localhost:3000` → `example` (for local testing) - -**Priority order:** -1. Middleware-set header (`x-community-id`) -2. Development override (`NEXT_PUBLIC_TEST_COMMUNITY`) -3. Domain resolution -4. Environment variable (`NEXT_PUBLIC_COMMUNITY`) - -## Benefits - -- **Multi-Tenant Support** - Single deployment serves multiple communities -- **Domain-Based Routing** - Automatic community detection from domain -- **Admin Updates** - Configs can be updated via database -- **Dynamic Updates** - Config changes without rebuild -- **Small Config** - Only ~2.8 KB -- **Unified Architecture** - Pages are Spaces, consistent with existing system -- **Shared Themes** - Single source of truth, no duplication - -## Related Files - -- **Database**: `supabase/migrations/20251129172847_create_community_configs.sql` -- **Middleware**: `middleware.ts` - Domain detection and header setting -- **Build Config**: `next.config.mjs` - Downloads assets at build time -- **Config Loaders**: `src/config/loaders/` - Strategy pattern implementation -- **Config Loader**: `src/config/index.ts` - Main config loading function -- **Route Handler**: `src/app/[navSlug]/[[...tabName]]/page.tsx` - Dynamic navigation -- **Space Seeding**: `scripts/seed-all.ts` - Unified seeding script -- **Shared Themes**: `src/config/shared/themes.ts` - Theme definitions - diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 285e1f719..e6e2d31fa 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,7 +4,7 @@ import "@/styles/globals.css"; import '@coinbase/onchainkit/styles.css'; import Providers from "@/common/providers"; import { SpeedInsights } from "@vercel/speed-insights/next"; -import { loadSystemConfig } from "@/config"; +import { loadSystemConfig, SystemConfig } from "@/config"; import ClientMobileHeaderWrapper from "@/common/components/organisms/ClientMobileHeaderWrapper"; import ClientSidebarWrapper from "@/common/components/organisms/ClientSidebarWrapper"; import type { Metadata } from 'next' // Migrating next/head @@ -71,34 +71,36 @@ export async function generateMetadata(): Promise { // And a public key. If valid, we can prerender as if it is that user signed in // This will allow us to prerender some logged in state since we will know what user it is -export default function RootLayout({ +export default async function RootLayout({ children, }: { children: React.ReactNode; }) { + const systemConfig = await loadSystemConfig(); + return ( - {sidebarLayout(children)} + {sidebarLayout(children, systemConfig)} ); } -const sidebarLayout = (page: React.ReactNode) => { +const sidebarLayout = (page: React.ReactNode, systemConfig: SystemConfig) => { return ( <>
{/* App Navigation Bar */}
- +
{/* Main Content with Sidebar */}
- +
{page}
diff --git a/src/common/components/molecules/BrandHeader.tsx b/src/common/components/molecules/BrandHeader.tsx index ed9d7e981..f62fcbc93 100644 --- a/src/common/components/molecules/BrandHeader.tsx +++ b/src/common/components/molecules/BrandHeader.tsx @@ -9,12 +9,16 @@ import { import { Tooltip, TooltipArrow } from "@radix-ui/react-tooltip"; import { FaExternalLinkAlt } from "react-icons/fa"; import { Londrina_Solid } from "next/font/google"; -import { loadSystemConfig } from "@/config"; +import { SystemConfig } from "@/config"; const Londrina = Londrina_Solid({ subsets: ["latin"], weight: "400" }); -const BrandHeader = () => { - const { assets, brand, navigation } = loadSystemConfig(); +type BrandHeaderProps = { + systemConfig: SystemConfig; +}; + +const BrandHeader = ({ systemConfig }: BrandHeaderProps) => { + const { assets, brand, navigation } = systemConfig; const logoTooltip = navigation?.logoTooltip; const logoSrc = assets.logos.icon || assets.logos.main; diff --git a/src/common/components/organisms/ClientMobileHeaderWrapper.tsx b/src/common/components/organisms/ClientMobileHeaderWrapper.tsx index cb0e8572d..943d92950 100644 --- a/src/common/components/organisms/ClientMobileHeaderWrapper.tsx +++ b/src/common/components/organisms/ClientMobileHeaderWrapper.tsx @@ -2,10 +2,15 @@ import React from "react"; import MobileHeader from "./MobileHeader"; +import { SystemConfig } from "@/config"; -export const ClientMobileHeaderWrapper: React.FC = React.memo( - function ClientMobileHeaderWrapper() { - return ; +type ClientMobileHeaderWrapperProps = { + systemConfig: SystemConfig; +}; + +export const ClientMobileHeaderWrapper: React.FC = React.memo( + function ClientMobileHeaderWrapper({ systemConfig }) { + return ; } ); diff --git a/src/common/components/organisms/ClientSidebarWrapper.tsx b/src/common/components/organisms/ClientSidebarWrapper.tsx index f5ed0deeb..6eed7c536 100644 --- a/src/common/components/organisms/ClientSidebarWrapper.tsx +++ b/src/common/components/organisms/ClientSidebarWrapper.tsx @@ -2,13 +2,18 @@ import React from "react"; import Sidebar from "./Sidebar"; +import { SystemConfig } from "@/config"; + +type ClientSidebarWrapperProps = { + systemConfig: SystemConfig; +}; /** * Client-side wrapper for Sidebar * This enables the server component in layout.tsx to properly use client components */ -export const ClientSidebarWrapper: React.FC = () => { - return ; +export const ClientSidebarWrapper: React.FC = ({ systemConfig }) => { + return ; }; export default ClientSidebarWrapper; diff --git a/src/common/components/organisms/MobileHeader.tsx b/src/common/components/organisms/MobileHeader.tsx index 1e0509584..03c16e390 100644 --- a/src/common/components/organisms/MobileHeader.tsx +++ b/src/common/components/organisms/MobileHeader.tsx @@ -23,14 +23,18 @@ import { } from "../molecules/CastModalHelpers"; import { toFarcasterCdnUrl } from "@/common/lib/utils/farcasterCdn"; import { useUIColors } from "@/common/lib/hooks/useUIColors"; +import { SystemConfig } from "@/config"; +type MobileHeaderProps = { + systemConfig: SystemConfig; +}; -const MobileHeader = () => { +const MobileHeader = ({ systemConfig }: MobileHeaderProps) => { const setModalOpen = useAppStore((state) => state.setup.setModalOpen); const isLoggedIn = useAppStore((state) => state.getIsAccountReady()); const isInitializing = useAppStore((state) => state.getIsInitializing()); - const uiColors = useUIColors(); + const uiColors = useUIColors({ systemConfig }); const { setEditMode, sidebarEditable } = useSidebarContext(); @@ -255,12 +259,13 @@ const MobileHeader = () => {
{isLoggedIn ? userAvatar : menuButton}
- +
{actionButton}
; type NavProps = { + systemConfig: SystemConfig; isEditable: boolean; enterEditMode?: () => void; mobile?: boolean; onNavigate?: () => void; }; -const NavIconBadge: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const uiColors = useUIColors(); +const NavIconBadge: React.FC<{ + children: React.ReactNode; + systemConfig: SystemConfig; +}> = ({ children, systemConfig }) => { + const uiColors = useUIColors({ systemConfig }); return ( = ({ children }) => const Navigation = React.memo( ({ + systemConfig, isEditable, enterEditMode, mobile = false, @@ -97,7 +102,7 @@ const Navigation = React.memo( const logout = useLogout(); const notificationBadgeText = useNotificationBadgeText(); const pathname = usePathname(); - const { community, navigation, ui } = loadSystemConfig(); + const { community, navigation, ui } = systemConfig; const discordUrl = community?.urls?.discord || "https://discord.gg/eYQeXU2WuH"; // Get cast button colors from config, with fallback to blue @@ -303,7 +308,7 @@ const Navigation = React.memo( rel={openInNewTab ? "noopener noreferrer" : undefined} target={openInNewTab ? "_blank" : undefined} > - {badgeText && {badgeText}} + {badgeText && {badgeText}} {!shrunk && {label}} @@ -329,7 +334,7 @@ const Navigation = React.memo( )} onClick={onClick} > - {badgeText && {badgeText}} + {badgeText && {badgeText}}
From 57d9c9b4a4030a3dbde05b680c4aa10491e592dd Mon Sep 17 00:00:00 2001 From: Willy Ogorzaly Date: Tue, 30 Dec 2025 15:01:52 -0600 Subject: [PATCH 153/155] Make cast modal scrollable within viewport --- src/common/components/molecules/Modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/components/molecules/Modal.tsx b/src/common/components/molecules/Modal.tsx index d8ba7c69d..ca4d9b7a1 100644 --- a/src/common/components/molecules/Modal.tsx +++ b/src/common/components/molecules/Modal.tsx @@ -55,7 +55,7 @@ const Modal = ({ "pointer-events-auto data-[state=open]:animate-contentShow bg-background", "w-[100vw] max-w-[600px] rounded-[10px] p-[25px]", "shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none", - "relative overflow-visible", + "relative min-h-[320px] max-h-[90vh] overflow-y-auto overflow-x-visible", )} // Fixes issue causing grid items to remain draggable behind open modal onMouseDown={(e) => e.stopPropagation()} From 3450d5e3add0c1101a82bd852145201253574e61 Mon Sep 17 00:00:00 2001 From: Willy Ogorzaly Date: Tue, 30 Dec 2025 15:12:31 -0600 Subject: [PATCH 154/155] Render images without OG card chrome --- src/fidgets/farcaster/components/CreateCast.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/fidgets/farcaster/components/CreateCast.tsx b/src/fidgets/farcaster/components/CreateCast.tsx index e015d1a02..98c0ffcfe 100644 --- a/src/fidgets/farcaster/components/CreateCast.tsx +++ b/src/fidgets/farcaster/components/CreateCast.tsx @@ -45,6 +45,7 @@ import { FarcasterCastIdEmbed, FarcasterEmbed, isFarcasterUrlEmbed } from "../ut import { ChannelPicker } from "./channelPicker"; import FrameV2Embed from "./Embeds/FrameV2Embed"; import VideoEmbed from "./Embeds/VideoEmbed"; +import CreateCastImage from "./Embeds/createCastImage"; import EmbededCast from "./Embeds/EmbededCast"; import { useSharedData } from "@/common/providers/SharedDataProvider"; import { @@ -613,6 +614,10 @@ const CreateCast: React.FC = ({ const embedUrl = item.embed.url; const domain = getHostnameFromUrl(embedUrl); + if (isImageUrl(embedUrl)) { + return ; + } + if (item.metadata?.video && embedUrl) { return ; } From 6e0e67a31d60e73c95647ce9e8f5d2b16c0dd873 Mon Sep 17 00:00:00 2001 From: Willy Ogorzaly Date: Tue, 30 Dec 2025 15:40:29 -0600 Subject: [PATCH 155/155] Improve Farcaster embed validation and caching (#1639) --- src/common/providers/SharedDataProvider.tsx | 17 +++++++++++++---- src/fidgets/farcaster/components/CreateCast.tsx | 9 +++++++-- .../farcaster/utils/embedNormalization.ts | 10 ++++++++-- src/pages/api/farcaster/neynar/embedMetadata.ts | 4 ---- .../api/farcaster/neynar/publishMessage.ts | 4 ++-- 5 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/common/providers/SharedDataProvider.tsx b/src/common/providers/SharedDataProvider.tsx index 2b600f72f..f817e1bb7 100644 --- a/src/common/providers/SharedDataProvider.tsx +++ b/src/common/providers/SharedDataProvider.tsx @@ -13,6 +13,8 @@ import type { EmbedUrlMetadata } from "@neynar/nodejs-sdk/build/api/models/embed import type { Channel } from "@mod-protocol/farcaster"; import type { FarcasterEmbed } from "@/fidgets/farcaster/utils/embedTypes"; +const MAX_RECENT_EMBEDS = 50; + // Define the types of data that will be shared interface SharedDataContextType { // Cache of recently accessed channels @@ -58,10 +60,17 @@ export const SharedDataProvider: React.FC<{ children: React.ReactNode }> = ({ ch // Adds an embed to the recent embeds cache const addRecentEmbed = useCallback( (url: string, embed: FarcasterEmbed, metadata?: EmbedUrlMetadata) => { - setRecentEmbeds((prev) => ({ - ...prev, - [url]: { embed, metadata }, - })); + setRecentEmbeds((prev) => { + const updated = { ...prev, [url]: { embed, metadata } }; + const keys = Object.keys(updated); + + if (keys.length > MAX_RECENT_EMBEDS) { + const [oldestKey] = keys; + delete updated[oldestKey]; + } + + return updated; + }); }, [], ); diff --git a/src/fidgets/farcaster/components/CreateCast.tsx b/src/fidgets/farcaster/components/CreateCast.tsx index 98c0ffcfe..940e2f86f 100644 --- a/src/fidgets/farcaster/components/CreateCast.tsx +++ b/src/fidgets/farcaster/components/CreateCast.tsx @@ -194,9 +194,14 @@ const CreateCast: React.FC = ({ const [embedPreviews, setEmbedPreviews] = useState>({}); const [removedEmbeds, setRemovedEmbeds] = useState>(new Set()); const [loadingEmbeds, setLoadingEmbeds] = useState>(new Set()); + const loadingEmbedsRef = useRef>(loadingEmbeds); const [embedErrors, setEmbedErrors] = useState>({}); const { addRecentEmbed, getRecentEmbed } = useSharedData(); + useEffect(() => { + loadingEmbedsRef.current = loadingEmbeds; + }, [loadingEmbeds]); + const isTargetInsideEmojiPicker = useCallback( (event?: Event | CustomEvent<{ originalEvent?: Event }>, fallbackTarget?: EventTarget | null) => { const isInside = (node?: EventTarget | null) => { @@ -721,7 +726,7 @@ const CreateCast: React.FC = ({ return; } - if (loadingEmbeds.has(normalizedUrl)) { + if (loadingEmbedsRef.current.has(normalizedUrl)) { return; } @@ -771,7 +776,7 @@ const CreateCast: React.FC = ({ return () => { timers.forEach((timer) => clearTimeout(timer)); }; - }, [text, embedPreviews, addRecentEmbed, getRecentEmbed, removedEmbeds, loadingEmbeds]); + }, [text, embedPreviews, addRecentEmbed, getRecentEmbed, removedEmbeds]); useEffect(() => { if (!editor) return; diff --git a/src/fidgets/farcaster/utils/embedNormalization.ts b/src/fidgets/farcaster/utils/embedNormalization.ts index c0ceb75e8..e4c35ef5b 100644 --- a/src/fidgets/farcaster/utils/embedNormalization.ts +++ b/src/fidgets/farcaster/utils/embedNormalization.ts @@ -104,11 +104,17 @@ export const validateFarcasterEmbeds = (embeds: unknown[]) => { }; }; +type SanitizeOptions = { + removedKeys?: Set; + limit?: number; + normalized?: FarcasterEmbed[]; +}; + export const sanitizeFarcasterEmbeds = ( embeds: unknown[], - options?: { removedKeys?: Set; limit?: number }, + options?: SanitizeOptions, ) => { - const { normalized } = validateFarcasterEmbeds(embeds); + const normalized = options?.normalized ?? validateFarcasterEmbeds(embeds).normalized; const limit = options?.limit ?? FARCASTER_EMBED_LIMIT; const removedKeys = options?.removedKeys ?? new Set(); diff --git a/src/pages/api/farcaster/neynar/embedMetadata.ts b/src/pages/api/farcaster/neynar/embedMetadata.ts index db73f907e..1fcc82417 100644 --- a/src/pages/api/farcaster/neynar/embedMetadata.ts +++ b/src/pages/api/farcaster/neynar/embedMetadata.ts @@ -4,10 +4,6 @@ import { isAxiosError } from "axios"; import { NextApiRequest, NextApiResponse } from "next"; const embedMetadata = async (req: NextApiRequest, res: NextApiResponse) => { - if (req.method !== "GET") { - return res.status(405).json({ message: "Method not allowed" }); - } - const url = typeof req.query.url === "string" ? req.query.url : ""; if (!url) { return res.status(400).json({ message: "Missing url" }); diff --git a/src/pages/api/farcaster/neynar/publishMessage.ts b/src/pages/api/farcaster/neynar/publishMessage.ts index f3c1404c4..81c9a2c6d 100644 --- a/src/pages/api/farcaster/neynar/publishMessage.ts +++ b/src/pages/api/farcaster/neynar/publishMessage.ts @@ -30,10 +30,10 @@ async function publishMessage(req: NextApiRequest, res: NextApiResponse) { }); } - message.embeds = sanitizeFarcasterEmbeds(message.embeds); + message.embeds = sanitizeFarcasterEmbeds(message.embeds, { normalized }); } - const response = await neynar.publishMessageToFarcaster({body: message}); + const response = await neynar.publishMessageToFarcaster({ body: message }); res.status(200).json(response); } catch (e: any) { if (e.response?.data) {