From 2ac3a5524703622e4fae9b8ae263af500c7e6672 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 27 Mar 2026 14:52:22 +0000 Subject: [PATCH 1/6] [#294] Cache ERC-8004 agent data in users table Add agent columns to users table (migration 00029) with agent_id, agent_name, agent_description, agent_genre, agent_llm_model, agent_wallet, agent_owner, agent_registered_at. Create API endpoints POST /api/user/agent-register and /api/user/agent-update for persisting agent data after on-chain transactions. AgentRegister and AgentManage now persist to DB after registration, URI updates, wallet changes, and unset operations. Agents page uses DB-first detection with RPC fallback for unknown wallets. detectWriterType() checks DB before RPC. fetchAgentMetadata() reads from DB cache and auto-caches externally registered agents on first RPC lookup. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/actions.ts | 45 +++++++++++- lib/contracts/erc8004.ts | 18 ++++- lib/supabase.ts | 24 +++++++ src/app/agents/page.tsx | 57 +++++++++------ src/app/api/user/agent-register/route.ts | 64 +++++++++++++++++ src/app/api/user/agent-update/route.ts | 71 +++++++++++++++++++ src/components/AgentManage.tsx | 32 +++++++++ src/components/AgentRegister.tsx | 30 +++++++- .../migrations/00029_users_agent_columns.sql | 12 ++++ 9 files changed, 330 insertions(+), 23 deletions(-) create mode 100644 src/app/api/user/agent-register/route.ts create mode 100644 src/app/api/user/agent-update/route.ts create mode 100644 supabase/migrations/00029_users_agent_columns.sql diff --git a/lib/actions.ts b/lib/actions.ts index 2ea2590a..5093f6d4 100644 --- a/lib/actions.ts +++ b/lib/actions.ts @@ -32,11 +32,54 @@ export async function getFarcasterProfile( /** * Server action that resolves ERC-8004 agent metadata from a wallet address. + * Checks DB cache first, falls back to RPC. Caches externally registered agents. */ export async function fetchAgentMetadata( address: string, ): Promise { - return _getAgentMetadata(address as Address); + // DB-first: check cached agent data + const dbUser = await getUserFromDB(address); + if (dbUser?.agent_id != null) { + return { + agentId: String(dbUser.agent_id), + owner: dbUser.agent_owner ?? undefined, + name: dbUser.agent_name ?? "Unknown Agent", + description: dbUser.agent_description ?? "", + genre: dbUser.agent_genre ?? undefined, + llmModel: dbUser.agent_llm_model ?? undefined, + registeredAt: dbUser.agent_registered_at ?? undefined, + }; + } + + // RPC fallback — also cache the result for next time + const meta = await _getAgentMetadata(address as Address); + if (meta && meta.agentId) { + const supabase = createServiceRoleClient(); + if (supabase) { + const normalized = address.toLowerCase(); + const userId = dbUser?.id; + const agentFields = { + agent_id: Number(meta.agentId), + agent_name: meta.name || null, + agent_description: meta.description || null, + agent_genre: meta.genre || null, + agent_llm_model: meta.llmModel || null, + agent_owner: meta.owner?.toLowerCase() || null, + agent_registered_at: meta.registeredAt || null, + agent_wallet: normalized, + }; + try { + if (userId) { + await supabase.from("users").update(agentFields).eq("id", userId); + } else { + await supabase.from("users").insert({ primary_address: normalized, ...agentFields }); + } + } catch { + // Best-effort cache — don't fail the metadata lookup + } + } + } + return meta; } /** diff --git a/lib/contracts/erc8004.ts b/lib/contracts/erc8004.ts index aa7a6fe4..9ac28767 100644 --- a/lib/contracts/erc8004.ts +++ b/lib/contracts/erc8004.ts @@ -1,6 +1,7 @@ import { type Address } from "viem"; import { publicClient } from "../rpc"; import { ERC8004_REGISTRY } from "./constants"; +import { createServiceRoleClient } from "../supabase"; // --------------------------------------------------------------------------- // ABI @@ -182,6 +183,21 @@ export async function detectWriterType( writerAddress: Address ): Promise { try { + // DB-first: check if this address is a cached agent wallet + const supabase = createServiceRoleClient(); + if (supabase) { + const normalized = writerAddress.toLowerCase(); + const { data } = await supabase + .from("users") + .select("agent_id") + .or(`agent_wallet.eq.${normalized},primary_address.eq.${normalized}`) + .not("agent_id", "is", null) + .limit(1) + .single(); + if (data?.agent_id) return 1; + } + + // RPC fallback for agents not yet in DB const agentId = await publicClient.readContract({ address: ERC8004_REGISTRY, abi: erc8004Abi, @@ -190,7 +206,7 @@ export async function detectWriterType( }); return agentId > BigInt(0) ? 1 : 0; } catch { - // Best-effort: default to human if registry query fails + // Best-effort: default to human if both checks fail return 0; } } diff --git a/lib/supabase.ts b/lib/supabase.ts index 382015d4..c9e5d0d2 100644 --- a/lib/supabase.ts +++ b/lib/supabase.ts @@ -422,6 +422,14 @@ export interface Database { quotient_rank: number | null; quotient_labels: Json | null; quotient_updated_at: string | null; + agent_id: number | null; + agent_name: string | null; + agent_description: string | null; + agent_genre: string | null; + agent_llm_model: string | null; + agent_wallet: string | null; + agent_owner: string | null; + agent_registered_at: string | null; stats_fetched_at: string | null; steemhunt_fetched_at: string | null; created_at: string; @@ -457,6 +465,14 @@ export interface Database { quotient_rank?: number | null; quotient_labels?: Json | null; quotient_updated_at?: string | null; + agent_id?: number | null; + agent_name?: string | null; + agent_description?: string | null; + agent_genre?: string | null; + agent_llm_model?: string | null; + agent_wallet?: string | null; + agent_owner?: string | null; + agent_registered_at?: string | null; stats_fetched_at?: string | null; steemhunt_fetched_at?: string | null; created_at?: string; @@ -492,6 +508,14 @@ export interface Database { quotient_rank?: number | null; quotient_labels?: Json | null; quotient_updated_at?: string | null; + agent_id?: number | null; + agent_name?: string | null; + agent_description?: string | null; + agent_genre?: string | null; + agent_llm_model?: string | null; + agent_wallet?: string | null; + agent_owner?: string | null; + agent_registered_at?: string | null; stats_fetched_at?: string | null; steemhunt_fetched_at?: string | null; created_at?: string; diff --git a/src/app/agents/page.tsx b/src/app/agents/page.tsx index 2b6e3b6e..11634b83 100644 --- a/src/app/agents/page.tsx +++ b/src/app/agents/page.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { useAccount, useReadContract } from "wagmi"; -import { zeroAddress } from "viem"; +import { useQuery } from "@tanstack/react-query"; import { ConnectWallet } from "../../components/ConnectWallet"; import { AgentRegister } from "../../components/AgentRegister"; import { AgentManage } from "../../components/AgentManage"; @@ -10,6 +10,8 @@ import { AgentBuild } from "../../components/AgentBuild"; import { AgentDashboard } from "../../components/AgentDashboard"; import { erc8004Abi } from "../../../lib/contracts/erc8004"; import { ERC8004_REGISTRY } from "../../../lib/contracts/constants"; +import type { User } from "../../../lib/supabase"; +import { getUserFromDB } from "../../../lib/actions"; type Tab = "register" | "build" | "dashboard"; @@ -17,52 +19,67 @@ export default function AgentsPage() { const { isConnected, address } = useAccount(); const [tab, setTab] = useState("register"); - // Check if wallet is bound as an agent wallet - const { data: agentIdByWallet, isLoading: walletLoading } = useReadContract({ + // DB-first: check if user has cached agent data + const { data: dbUser, isLoading: dbLoading } = useQuery({ + queryKey: ["db-user-agent", address], + queryFn: () => getUserFromDB(address!), + enabled: !!address, + }); + + const dbAgentId = dbUser?.agent_id; + const dbIsOwner = dbAgentId != null && dbUser?.agent_owner?.toLowerCase() === address?.toLowerCase(); + const dbIsAgentWallet = dbAgentId != null && dbUser?.agent_wallet?.toLowerCase() === address?.toLowerCase(); + const dbDetected = dbAgentId != null; + + // RPC fallback: only if DB has no agent data + const needsRpcFallback = !dbLoading && !dbDetected && !!address; + + const { data: rpcAgentId, isLoading: rpcWalletLoading } = useReadContract({ address: ERC8004_REGISTRY, abi: erc8004Abi, functionName: "agentIdByWallet", args: address ? [address] : undefined, - query: { enabled: !!address }, + query: { enabled: needsRpcFallback }, }); - // Check if wallet owns any agent NFTs (ERC-721 balanceOf) - const { data: nftBalance, isLoading: balanceLoading } = useReadContract({ + const { data: rpcBalance, isLoading: rpcBalanceLoading } = useReadContract({ address: ERC8004_REGISTRY, abi: erc8004Abi, functionName: "balanceOf", args: address ? [address] : undefined, - query: { enabled: !!address }, + query: { enabled: needsRpcFallback }, }); - // If owner, get the first owned token ID - const hasNft = nftBalance !== undefined && nftBalance > BigInt(0); - const { data: ownedTokenId, isLoading: tokenLoading } = useReadContract({ + const rpcHasNft = rpcBalance !== undefined && rpcBalance > BigInt(0); + const { data: rpcOwnedToken, isLoading: rpcTokenLoading } = useReadContract({ address: ERC8004_REGISTRY, abi: erc8004Abi, functionName: "tokenOfOwnerByIndex", args: address ? [address, BigInt(0)] : undefined, - query: { enabled: !!address && hasNft }, + query: { enabled: needsRpcFallback && rpcHasNft }, }); - const isAgentWallet = agentIdByWallet !== undefined && agentIdByWallet > BigInt(0); - const isOwner = hasNft && ownedTokenId !== undefined; - const detectLoading = walletLoading || balanceLoading || (hasNft && tokenLoading); + const rpcIsAgentWallet = rpcAgentId !== undefined && rpcAgentId > BigInt(0); + const rpcIsOwner = rpcHasNft && rpcOwnedToken !== undefined; - // Determine which agentId and role to use + // Combine DB + RPC results let detectedAgentId: bigint | undefined; let detectedRole: "owner" | "agentWallet" | undefined; - if (isOwner) { - detectedAgentId = ownedTokenId; + + if (dbDetected) { + detectedAgentId = BigInt(dbAgentId!); + detectedRole = dbIsOwner ? "owner" : dbIsAgentWallet ? "agentWallet" : "owner"; + } else if (rpcIsOwner) { + detectedAgentId = rpcOwnedToken; detectedRole = "owner"; - } else if (isAgentWallet) { - detectedAgentId = agentIdByWallet; + } else if (rpcIsAgentWallet) { + detectedAgentId = rpcAgentId; detectedRole = "agentWallet"; } const hasExistingAgent = detectedAgentId !== undefined && detectedRole !== undefined; + const detectLoading = dbLoading || (needsRpcFallback && (rpcWalletLoading || rpcBalanceLoading || (rpcHasNft && rpcTokenLoading))); - // Determine the label for the first tab const firstTabLabel = hasExistingAgent ? "Manage" : "Register"; return ( diff --git a/src/app/api/user/agent-register/route.ts b/src/app/api/user/agent-register/route.ts new file mode 100644 index 00000000..610d667a --- /dev/null +++ b/src/app/api/user/agent-register/route.ts @@ -0,0 +1,64 @@ +import { NextRequest, NextResponse } from "next/server"; +import { createServiceRoleClient } from "../../../../../lib/supabase"; + +/** + * POST /api/user/agent-register + * Upserts agent columns on the user row after on-chain registration. + */ +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { walletAddress, agentId, name, description, genre, llmModel, agentWallet, agentOwner } = body; + + if (!walletAddress || typeof walletAddress !== "string" || !agentId) { + return NextResponse.json({ error: "walletAddress and agentId are required" }, { status: 400 }); + } + + const supabase = createServiceRoleClient(); + if (!supabase) { + return NextResponse.json({ error: "Database not configured" }, { status: 500 }); + } + + const normalized = walletAddress.toLowerCase(); + + // Find existing user by verified_addresses or primary_address + const { data: byVerified } = await supabase + .from("users") + .select("id") + .contains("verified_addresses", [normalized]) + .single(); + + const { data: byPrimary } = !byVerified + ? await supabase.from("users").select("id").eq("primary_address", normalized).single() + : { data: byVerified }; + + const existingUser = byVerified ?? byPrimary; + + const agentFields = { + agent_id: Number(agentId), + agent_name: name || null, + agent_description: description || null, + agent_genre: genre || null, + agent_llm_model: llmModel || null, + agent_wallet: agentWallet?.toLowerCase() || null, + agent_owner: (agentOwner || walletAddress).toLowerCase(), + agent_registered_at: new Date().toISOString(), + }; + + if (existingUser) { + await supabase.from("users").update(agentFields).eq("id", existingUser.id); + } else { + await supabase.from("users").insert({ + primary_address: normalized, + ...agentFields, + }); + } + + return NextResponse.json({ ok: true }); + } catch (err) { + return NextResponse.json( + { error: err instanceof Error ? err.message : "Internal error" }, + { status: 500 }, + ); + } +} diff --git a/src/app/api/user/agent-update/route.ts b/src/app/api/user/agent-update/route.ts new file mode 100644 index 00000000..7119ffd3 --- /dev/null +++ b/src/app/api/user/agent-update/route.ts @@ -0,0 +1,71 @@ +import { NextRequest, NextResponse } from "next/server"; +import { createServiceRoleClient } from "../../../../../lib/supabase"; + +/** + * POST /api/user/agent-update + * Updates specific agent columns on the user row after management actions. + */ +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { walletAddress, fields } = body; + + if (!walletAddress || typeof walletAddress !== "string" || !fields || typeof fields !== "object") { + return NextResponse.json({ error: "walletAddress and fields are required" }, { status: 400 }); + } + + const supabase = createServiceRoleClient(); + if (!supabase) { + return NextResponse.json({ error: "Database not configured" }, { status: 500 }); + } + + const normalized = walletAddress.toLowerCase(); + + // Allow only known agent columns + const allowedKeys = [ + "agent_name", "agent_description", "agent_genre", + "agent_llm_model", "agent_wallet", "agent_owner", + ]; + const sanitized: Record = {}; + for (const key of allowedKeys) { + if (key in fields) { + sanitized[key] = fields[key] != null ? String(fields[key]).toLowerCase() : null; + } + } + // Name/description/genre/model should preserve case + for (const key of ["agent_name", "agent_description", "agent_genre", "agent_llm_model"]) { + if (key in fields) { + sanitized[key] = fields[key] || null; + } + } + + if (Object.keys(sanitized).length === 0) { + return NextResponse.json({ error: "No valid fields to update" }, { status: 400 }); + } + + // Find user by verified_addresses or primary_address + const { data: byVerified } = await supabase + .from("users") + .select("id") + .contains("verified_addresses", [normalized]) + .single(); + + const { data: byPrimary } = !byVerified + ? await supabase.from("users").select("id").eq("primary_address", normalized).single() + : { data: byVerified }; + + const existingUser = byVerified ?? byPrimary; + if (!existingUser) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + await supabase.from("users").update(sanitized).eq("id", existingUser.id); + + return NextResponse.json({ ok: true }); + } catch (err) { + return NextResponse.json( + { error: err instanceof Error ? err.message : "Internal error" }, + { status: 500 }, + ); + } +} diff --git a/src/components/AgentManage.tsx b/src/components/AgentManage.tsx index 8d8b4894..cdb1cec6 100644 --- a/src/components/AgentManage.tsx +++ b/src/components/AgentManage.tsx @@ -155,6 +155,20 @@ export function AgentManage({ agentId, role }: AgentManageProps) { await publicClient.waitForTransactionReceipt({ hash }); const parsed = JSON.parse(editUri); setMetadata({ ...metadata!, ...parsed }); + // Persist URI update to DB + fetch("/api/user/agent-update", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + walletAddress: address, + fields: { + agent_name: parsed.name, + agent_description: parsed.description, + agent_genre: parsed.genre || null, + agent_llm_model: parsed.llmModel || null, + }, + }), + }).catch(() => {}); setEditing(false); } catch (err) { setError(err instanceof Error ? err.message : "Failed to update URI"); @@ -175,6 +189,15 @@ export function AgentManage({ agentId, role }: AgentManageProps) { }); setTxHash(hash); await publicClient.waitForTransactionReceipt({ hash }); + // Persist unset to DB + fetch("/api/user/agent-update", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + walletAddress: address, + fields: { agent_wallet: null }, + }), + }).catch(() => {}); } catch (err) { setError(err instanceof Error ? err.message : "Failed to unset wallet"); } finally { @@ -222,6 +245,15 @@ export function AgentManage({ agentId, role }: AgentManageProps) { }); setTxHash(hash); await publicClient.waitForTransactionReceipt({ hash }); + // Persist new wallet binding to DB + fetch("/api/user/agent-update", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + walletAddress: address, + fields: { agent_wallet: newWalletAddr.toLowerCase() }, + }), + }).catch(() => {}); setWalletStep(null); setChangingWallet(false); setNewWalletAddr(""); diff --git a/src/components/AgentRegister.tsx b/src/components/AgentRegister.tsx index 770c0167..ba1101d3 100644 --- a/src/components/AgentRegister.tsx +++ b/src/components/AgentRegister.tsx @@ -91,11 +91,30 @@ export function AgentRegister() { return decoded.eventName === "Registered"; } catch { return false; } }); + let newAgentId: bigint | undefined; if (registeredLog) { const decoded = decodeEventLog({ abi: erc8004Abi, data: registeredLog.data, topics: registeredLog.topics }); - if (decoded.eventName === "Registered") setAgentId(decoded.args.agentId); + if (decoded.eventName === "Registered") { + newAgentId = decoded.args.agentId; + setAgentId(newAgentId); + } } setOwnerAddress(address); + // Persist agent data to DB + const meta = JSON.parse(agentURI); + fetch("/api/user/agent-register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + walletAddress: address, + agentId: newAgentId?.toString(), + name: meta.name, + description: meta.description, + genre: meta.genre, + llmModel: meta.llmModel, + agentOwner: address, + }), + }).catch(() => {}); setStep("3a"); } catch (err) { setError(err instanceof Error ? err.message : "Registration failed"); @@ -139,6 +158,15 @@ export function AgentRegister() { }); setBindTxHash(hash); await publicClient.waitForTransactionReceipt({ hash }); + // Persist wallet binding to DB + fetch("/api/user/agent-update", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + walletAddress: ownerAddress, + fields: { agent_wallet: agentWallet.toLowerCase() }, + }), + }).catch(() => {}); setStep("done"); } catch (err) { setError(err instanceof Error ? err.message : "Wallet binding failed"); diff --git a/supabase/migrations/00029_users_agent_columns.sql b/supabase/migrations/00029_users_agent_columns.sql new file mode 100644 index 00000000..7c1de5de --- /dev/null +++ b/supabase/migrations/00029_users_agent_columns.sql @@ -0,0 +1,12 @@ +-- Add ERC-8004 agent identity columns to users table +ALTER TABLE users ADD COLUMN IF NOT EXISTS agent_id INTEGER UNIQUE; +ALTER TABLE users ADD COLUMN IF NOT EXISTS agent_name TEXT; +ALTER TABLE users ADD COLUMN IF NOT EXISTS agent_description TEXT; +ALTER TABLE users ADD COLUMN IF NOT EXISTS agent_genre TEXT; +ALTER TABLE users ADD COLUMN IF NOT EXISTS agent_llm_model TEXT; +ALTER TABLE users ADD COLUMN IF NOT EXISTS agent_wallet TEXT; +ALTER TABLE users ADD COLUMN IF NOT EXISTS agent_owner TEXT; +ALTER TABLE users ADD COLUMN IF NOT EXISTS agent_registered_at TIMESTAMPTZ; + +CREATE INDEX IF NOT EXISTS idx_users_agent_id ON users (agent_id) WHERE agent_id IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_users_agent_wallet ON users (agent_wallet) WHERE agent_wallet IS NOT NULL; From 4933c86dce74edbb939fba44a37c61bf6fc9dd84 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 27 Mar 2026 14:56:45 +0000 Subject: [PATCH 2/6] [#294] Fix dashboard DB-first detection and agent_wallet/owner lookups AgentDashboard now uses DB-first detection with RPC fallback, matching agents page pattern. API endpoints and getUserFromDB also lookup by agent_wallet and agent_owner columns, so externally registered agents and owner wallets resolve correctly. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/actions.ts | 19 +++++- src/app/api/user/agent-register/route.ts | 12 +++- src/app/api/user/agent-update/route.ts | 12 +++- src/components/AgentDashboard.tsx | 83 ++++++++++++++++-------- 4 files changed, 95 insertions(+), 31 deletions(-) diff --git a/lib/actions.ts b/lib/actions.ts index 5093f6d4..70f048f6 100644 --- a/lib/actions.ts +++ b/lib/actions.ts @@ -108,5 +108,22 @@ export async function getUserFromDB( .eq("primary_address", normalized) .single(); - return byPrimary ?? null; + if (byPrimary) return byPrimary; + + // Also check agent_wallet and agent_owner for externally registered agents + const { data: byAgentWallet } = await supabase + .from("users") + .select("*") + .eq("agent_wallet", normalized) + .single(); + + if (byAgentWallet) return byAgentWallet; + + const { data: byAgentOwner } = await supabase + .from("users") + .select("*") + .eq("agent_owner", normalized) + .single(); + + return byAgentOwner ?? null; } diff --git a/src/app/api/user/agent-register/route.ts b/src/app/api/user/agent-register/route.ts index 610d667a..ca230ed0 100644 --- a/src/app/api/user/agent-register/route.ts +++ b/src/app/api/user/agent-register/route.ts @@ -21,7 +21,7 @@ export async function POST(request: NextRequest) { const normalized = walletAddress.toLowerCase(); - // Find existing user by verified_addresses or primary_address + // Find existing user by verified_addresses, primary_address, agent_wallet, or agent_owner const { data: byVerified } = await supabase .from("users") .select("id") @@ -32,7 +32,15 @@ export async function POST(request: NextRequest) { ? await supabase.from("users").select("id").eq("primary_address", normalized).single() : { data: byVerified }; - const existingUser = byVerified ?? byPrimary; + const { data: byAgentWallet } = !(byVerified ?? byPrimary) + ? await supabase.from("users").select("id").eq("agent_wallet", normalized).single() + : { data: null }; + + const { data: byAgentOwner } = !(byVerified ?? byPrimary ?? byAgentWallet) + ? await supabase.from("users").select("id").eq("agent_owner", normalized).single() + : { data: null }; + + const existingUser = byVerified ?? byPrimary ?? byAgentWallet ?? byAgentOwner; const agentFields = { agent_id: Number(agentId), diff --git a/src/app/api/user/agent-update/route.ts b/src/app/api/user/agent-update/route.ts index 7119ffd3..896785bf 100644 --- a/src/app/api/user/agent-update/route.ts +++ b/src/app/api/user/agent-update/route.ts @@ -43,7 +43,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "No valid fields to update" }, { status: 400 }); } - // Find user by verified_addresses or primary_address + // Find user by verified_addresses, primary_address, agent_wallet, or agent_owner const { data: byVerified } = await supabase .from("users") .select("id") @@ -54,7 +54,15 @@ export async function POST(request: NextRequest) { ? await supabase.from("users").select("id").eq("primary_address", normalized).single() : { data: byVerified }; - const existingUser = byVerified ?? byPrimary; + const { data: byAgentWallet } = !(byVerified ?? byPrimary) + ? await supabase.from("users").select("id").eq("agent_wallet", normalized).single() + : { data: null }; + + const { data: byAgentOwner } = !(byVerified ?? byPrimary ?? byAgentWallet) + ? await supabase.from("users").select("id").eq("agent_owner", normalized).single() + : { data: null }; + + const existingUser = byVerified ?? byPrimary ?? byAgentWallet ?? byAgentOwner; if (!existingUser) { return NextResponse.json({ error: "User not found" }, { status: 404 }); } diff --git a/src/components/AgentDashboard.tsx b/src/components/AgentDashboard.tsx index 8f6586f3..69fa49e6 100644 --- a/src/components/AgentDashboard.tsx +++ b/src/components/AgentDashboard.tsx @@ -5,59 +5,90 @@ import { useQuery } from "@tanstack/react-query"; import Link from "next/link"; import { erc8004Abi } from "../../lib/contracts/erc8004"; import { ERC8004_REGISTRY } from "../../lib/contracts/constants"; +import { getUserFromDB } from "../../lib/actions"; +import type { User } from "../../lib/supabase"; export function AgentDashboard() { const { address } = useAccount(); - // Check if wallet is registered as an agent wallet - const { data: agentIdByWallet, isLoading: walletLoading } = useReadContract({ + // DB-first: check cached agent data + const { data: dbUser, isLoading: dbLoading } = useQuery({ + queryKey: ["db-user-dashboard", address], + queryFn: () => getUserFromDB(address!), + enabled: !!address, + }); + + const dbAgentId = dbUser?.agent_id; + const dbDetected = dbAgentId != null; + const dbIsOwner = dbDetected && dbUser?.agent_owner?.toLowerCase() === address?.toLowerCase(); + const dbIsAgentWallet = dbDetected && dbUser?.agent_wallet?.toLowerCase() === address?.toLowerCase(); + const dbAgentWallet = dbUser?.agent_wallet; + + // RPC fallback: only if DB has no agent data + const needsRpcFallback = !dbLoading && !dbDetected && !!address; + + const { data: rpcAgentId, isLoading: rpcWalletLoading } = useReadContract({ address: ERC8004_REGISTRY, abi: erc8004Abi, functionName: "agentIdByWallet", args: address ? [address] : undefined, - query: { enabled: !!address }, + query: { enabled: needsRpcFallback }, }); - // Check if wallet owns agent NFTs (owner role) - const { data: nftBalance, isLoading: balanceLoading } = useReadContract({ + const { data: rpcBalance, isLoading: rpcBalanceLoading } = useReadContract({ address: ERC8004_REGISTRY, abi: erc8004Abi, functionName: "balanceOf", args: address ? [address] : undefined, - query: { enabled: !!address }, + query: { enabled: needsRpcFallback }, }); - const hasNft = nftBalance !== undefined && nftBalance > BigInt(0); - const { data: ownedTokenId, isLoading: tokenLoading } = useReadContract({ + const rpcHasNft = rpcBalance !== undefined && rpcBalance > BigInt(0); + const { data: rpcOwnedToken, isLoading: rpcTokenLoading } = useReadContract({ address: ERC8004_REGISTRY, abi: erc8004Abi, functionName: "tokenOfOwnerByIndex", args: address ? [address, BigInt(0)] : undefined, - query: { enabled: !!address && hasNft }, + query: { enabled: needsRpcFallback && rpcHasNft }, }); - // Get the agent wallet for the owned token (to query storylines by that address) - const isOwner = hasNft && ownedTokenId !== undefined; - const { data: boundAgentWallet } = useReadContract({ + const rpcIsOwner = rpcHasNft && rpcOwnedToken !== undefined; + const { data: rpcBoundWallet } = useReadContract({ address: ERC8004_REGISTRY, abi: erc8004Abi, functionName: "getAgentWallet", - args: ownedTokenId !== undefined ? [ownedTokenId] : undefined, - query: { enabled: isOwner }, + args: rpcOwnedToken !== undefined ? [rpcOwnedToken] : undefined, + query: { enabled: needsRpcFallback && rpcIsOwner }, }); - const isAgentWallet = agentIdByWallet !== undefined && agentIdByWallet > BigInt(0); - const agentId = isOwner ? ownedTokenId : isAgentWallet ? agentIdByWallet : undefined; - const isAgent = agentId !== undefined; + const rpcIsAgentWallet = rpcAgentId !== undefined && rpcAgentId > BigInt(0); - // Determine the writer address for storyline lookup - // If connected as owner, prefer the bound agent wallet but fall back to owner address - // when the agent wallet is unset (zero address) or missing - const hasValidAgentWallet = - boundAgentWallet && boundAgentWallet !== "0x0000000000000000000000000000000000000000"; - const writerAddress = isOwner - ? hasValidAgentWallet ? (boundAgentWallet as string) : address - : address; + // Combine DB + RPC + let agentId: bigint | undefined; + let isOwner = false; + let isAgentWallet = false; + let writerAddress: string | undefined = address; + + if (dbDetected) { + agentId = BigInt(dbAgentId!); + isOwner = dbIsOwner; + isAgentWallet = dbIsAgentWallet; + // For owner, use cached agent_wallet for storyline lookup + if (dbIsOwner && dbAgentWallet) { + writerAddress = dbAgentWallet; + } + } else if (rpcIsOwner) { + agentId = rpcOwnedToken; + isOwner = true; + const hasValidRpcWallet = rpcBoundWallet && rpcBoundWallet !== "0x0000000000000000000000000000000000000000"; + if (hasValidRpcWallet) writerAddress = rpcBoundWallet as string; + } else if (rpcIsAgentWallet) { + agentId = rpcAgentId; + isAgentWallet = true; + } + + const isAgent = agentId !== undefined; + const detectLoading = dbLoading || (needsRpcFallback && (rpcWalletLoading || rpcBalanceLoading || (rpcHasNft && rpcTokenLoading))); // Fetch agent's storylines from Supabase const { data: storylines, isLoading: storylinesLoading } = useQuery({ @@ -71,7 +102,7 @@ export function AgentDashboard() { enabled: !!writerAddress && isAgent, }); - if (walletLoading || balanceLoading || (hasNft && tokenLoading)) { + if (detectLoading) { return (

Loading agent status...

From c8d66136d12398ac4eed33ede0decba5e372b48c Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 27 Mar 2026 15:02:02 +0000 Subject: [PATCH 3/6] [#294] Prioritize agent_id rows in agent-specific lookups Add getAgentUserFromDB() helper that searches agent_wallet/agent_owner first (with agent_id NOT NULL filter), then falls back to standard address columns. Used by agents page, dashboard, and fetchAgentMetadata. API endpoints also prioritize agent_id rows so owner wallets with separate agent rows resolve correctly. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/actions.ts | 57 ++++++++++++++++++++++-- src/app/agents/page.tsx | 4 +- src/app/api/user/agent-register/route.ts | 33 ++++++++------ src/app/api/user/agent-update/route.ts | 32 ++++++++----- src/components/AgentDashboard.tsx | 5 +-- 5 files changed, 99 insertions(+), 32 deletions(-) diff --git a/lib/actions.ts b/lib/actions.ts index 70f048f6..8505eab1 100644 --- a/lib/actions.ts +++ b/lib/actions.ts @@ -37,8 +37,8 @@ export async function getFarcasterProfile( export async function fetchAgentMetadata( address: string, ): Promise { - // DB-first: check cached agent data - const dbUser = await getUserFromDB(address); + // DB-first: check cached agent data (agent-specific lookup) + const dbUser = await getAgentUserFromDB(address); if (dbUser?.agent_id != null) { return { agentId: String(dbUser.agent_id), @@ -84,7 +84,7 @@ export async function fetchAgentMetadata( /** * Look up a user from the DB by wallet address. - * Searches verified_addresses first, then primary_address for wallet-only users. + * Searches verified_addresses first, then primary_address, then agent columns. */ export async function getUserFromDB( address: string, @@ -127,3 +127,54 @@ export async function getUserFromDB( return byAgentOwner ?? null; } + +/** + * Look up an agent user from the DB, prioritizing rows with agent_id. + * Use this for agent-specific lookups (detection, management, metadata). + */ +export async function getAgentUserFromDB( + address: string, +): Promise { + const supabase = createServiceRoleClient(); + if (!supabase) return null; + + const normalized = address.toLowerCase(); + + // First: find a row with agent_id keyed by agent_wallet or agent_owner + const { data: byAgentWallet } = await supabase + .from("users") + .select("*") + .eq("agent_wallet", normalized) + .not("agent_id", "is", null) + .single(); + + if (byAgentWallet) return byAgentWallet; + + const { data: byAgentOwner } = await supabase + .from("users") + .select("*") + .eq("agent_owner", normalized) + .not("agent_id", "is", null) + .single(); + + if (byAgentOwner) return byAgentOwner; + + // Fallback: check standard address columns for rows with agent_id + const { data: byVerified } = await supabase + .from("users") + .select("*") + .contains("verified_addresses", [normalized]) + .not("agent_id", "is", null) + .single(); + + if (byVerified) return byVerified; + + const { data: byPrimary } = await supabase + .from("users") + .select("*") + .eq("primary_address", normalized) + .not("agent_id", "is", null) + .single(); + + return byPrimary ?? null; +} diff --git a/src/app/agents/page.tsx b/src/app/agents/page.tsx index 11634b83..4533064b 100644 --- a/src/app/agents/page.tsx +++ b/src/app/agents/page.tsx @@ -11,7 +11,7 @@ import { AgentDashboard } from "../../components/AgentDashboard"; import { erc8004Abi } from "../../../lib/contracts/erc8004"; import { ERC8004_REGISTRY } from "../../../lib/contracts/constants"; import type { User } from "../../../lib/supabase"; -import { getUserFromDB } from "../../../lib/actions"; +import { getAgentUserFromDB } from "../../../lib/actions"; type Tab = "register" | "build" | "dashboard"; @@ -22,7 +22,7 @@ export default function AgentsPage() { // DB-first: check if user has cached agent data const { data: dbUser, isLoading: dbLoading } = useQuery({ queryKey: ["db-user-agent", address], - queryFn: () => getUserFromDB(address!), + queryFn: () => getAgentUserFromDB(address!), enabled: !!address, }); diff --git a/src/app/api/user/agent-register/route.ts b/src/app/api/user/agent-register/route.ts index ca230ed0..000ac66c 100644 --- a/src/app/api/user/agent-register/route.ts +++ b/src/app/api/user/agent-register/route.ts @@ -21,26 +21,33 @@ export async function POST(request: NextRequest) { const normalized = walletAddress.toLowerCase(); - // Find existing user by verified_addresses, primary_address, agent_wallet, or agent_owner - const { data: byVerified } = await supabase + // Find existing user — prefer rows with agent_id, then standard address columns + const { data: byAgentWallet } = await supabase .from("users") .select("id") - .contains("verified_addresses", [normalized]) + .eq("agent_wallet", normalized) + .not("agent_id", "is", null) .single(); - const { data: byPrimary } = !byVerified - ? await supabase.from("users").select("id").eq("primary_address", normalized).single() - : { data: byVerified }; + const { data: byAgentOwner } = !byAgentWallet + ? await supabase.from("users").select("id").eq("agent_owner", normalized).not("agent_id", "is", null).single() + : { data: byAgentWallet }; - const { data: byAgentWallet } = !(byVerified ?? byPrimary) - ? await supabase.from("users").select("id").eq("agent_wallet", normalized).single() - : { data: null }; + // Fallback: standard address columns (any row, even without agent_id — we'll update it) + let existingUser = byAgentWallet ?? byAgentOwner; + if (!existingUser) { + const { data: byVerified } = await supabase + .from("users") + .select("id") + .contains("verified_addresses", [normalized]) + .single(); - const { data: byAgentOwner } = !(byVerified ?? byPrimary ?? byAgentWallet) - ? await supabase.from("users").select("id").eq("agent_owner", normalized).single() - : { data: null }; + const { data: byPrimary } = !byVerified + ? await supabase.from("users").select("id").eq("primary_address", normalized).single() + : { data: byVerified }; - const existingUser = byVerified ?? byPrimary ?? byAgentWallet ?? byAgentOwner; + existingUser = byVerified ?? byPrimary; + } const agentFields = { agent_id: Number(agentId), diff --git a/src/app/api/user/agent-update/route.ts b/src/app/api/user/agent-update/route.ts index 896785bf..32f76f76 100644 --- a/src/app/api/user/agent-update/route.ts +++ b/src/app/api/user/agent-update/route.ts @@ -43,26 +43,36 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "No valid fields to update" }, { status: 400 }); } - // Find user by verified_addresses, primary_address, agent_wallet, or agent_owner - const { data: byVerified } = await supabase + // Find user row with agent_id, prioritizing agent-specific columns + const { data: byAgentWallet } = await supabase .from("users") .select("id") - .contains("verified_addresses", [normalized]) + .eq("agent_wallet", normalized) + .not("agent_id", "is", null) .single(); - const { data: byPrimary } = !byVerified - ? await supabase.from("users").select("id").eq("primary_address", normalized).single() - : { data: byVerified }; + const { data: byAgentOwner } = !byAgentWallet + ? await supabase.from("users").select("id").eq("agent_owner", normalized).not("agent_id", "is", null).single() + : { data: byAgentWallet }; - const { data: byAgentWallet } = !(byVerified ?? byPrimary) - ? await supabase.from("users").select("id").eq("agent_wallet", normalized).single() + const { data: byVerified } = !(byAgentWallet ?? byAgentOwner) + ? await supabase.from("users").select("id").contains("verified_addresses", [normalized]).not("agent_id", "is", null).single() : { data: null }; - const { data: byAgentOwner } = !(byVerified ?? byPrimary ?? byAgentWallet) - ? await supabase.from("users").select("id").eq("agent_owner", normalized).single() + const { data: byPrimary } = !(byAgentWallet ?? byAgentOwner ?? byVerified) + ? await supabase.from("users").select("id").eq("primary_address", normalized).not("agent_id", "is", null).single() : { data: null }; - const existingUser = byVerified ?? byPrimary ?? byAgentWallet ?? byAgentOwner; + // Fallback: any matching row (even without agent_id) + let existingUser = byAgentWallet ?? byAgentOwner ?? byVerified ?? byPrimary; + if (!existingUser) { + const { data: anyMatch } = await supabase + .from("users") + .select("id") + .or(`primary_address.eq.${normalized},agent_wallet.eq.${normalized},agent_owner.eq.${normalized}`) + .single(); + existingUser = anyMatch; + } if (!existingUser) { return NextResponse.json({ error: "User not found" }, { status: 404 }); } diff --git a/src/components/AgentDashboard.tsx b/src/components/AgentDashboard.tsx index 69fa49e6..e71fca0a 100644 --- a/src/components/AgentDashboard.tsx +++ b/src/components/AgentDashboard.tsx @@ -5,8 +5,7 @@ import { useQuery } from "@tanstack/react-query"; import Link from "next/link"; import { erc8004Abi } from "../../lib/contracts/erc8004"; import { ERC8004_REGISTRY } from "../../lib/contracts/constants"; -import { getUserFromDB } from "../../lib/actions"; -import type { User } from "../../lib/supabase"; +import { getAgentUserFromDB } from "../../lib/actions"; export function AgentDashboard() { const { address } = useAccount(); @@ -14,7 +13,7 @@ export function AgentDashboard() { // DB-first: check cached agent data const { data: dbUser, isLoading: dbLoading } = useQuery({ queryKey: ["db-user-dashboard", address], - queryFn: () => getUserFromDB(address!), + queryFn: () => getAgentUserFromDB(address!), enabled: !!address, }); From e24760e4298e90b39ae430230cdcea57c3a7e732 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 27 Mar 2026 15:05:13 +0000 Subject: [PATCH 4/6] [#294] Auto-cache externally registered agents on RPC fallback When agents page or dashboard detects an agent via RPC that has no DB record, call fetchAgentMetadata() to persist the data for subsequent visits. Uses a ref guard to fire only once per mount. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/agents/page.tsx | 12 +++++++++++- src/components/AgentDashboard.tsx | 12 +++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/app/agents/page.tsx b/src/app/agents/page.tsx index 4533064b..8542af5c 100644 --- a/src/app/agents/page.tsx +++ b/src/app/agents/page.tsx @@ -1,8 +1,9 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect, useRef } from "react"; import { useAccount, useReadContract } from "wagmi"; import { useQuery } from "@tanstack/react-query"; +import { fetchAgentMetadata } from "../../../lib/actions"; import { ConnectWallet } from "../../components/ConnectWallet"; import { AgentRegister } from "../../components/AgentRegister"; import { AgentManage } from "../../components/AgentManage"; @@ -80,6 +81,15 @@ export default function AgentsPage() { const hasExistingAgent = detectedAgentId !== undefined && detectedRole !== undefined; const detectLoading = dbLoading || (needsRpcFallback && (rpcWalletLoading || rpcBalanceLoading || (rpcHasNft && rpcTokenLoading))); + // Auto-cache: when RPC fallback detects an agent not in DB, persist it + const cachedRef = useRef(false); + useEffect(() => { + if (!dbDetected && hasExistingAgent && address && !cachedRef.current) { + cachedRef.current = true; + fetchAgentMetadata(address).catch(() => {}); + } + }, [dbDetected, hasExistingAgent, address]); + const firstTabLabel = hasExistingAgent ? "Manage" : "Register"; return ( diff --git a/src/components/AgentDashboard.tsx b/src/components/AgentDashboard.tsx index e71fca0a..aedf4161 100644 --- a/src/components/AgentDashboard.tsx +++ b/src/components/AgentDashboard.tsx @@ -1,11 +1,12 @@ "use client"; +import { useEffect, useRef } from "react"; import { useAccount, useReadContract } from "wagmi"; import { useQuery } from "@tanstack/react-query"; import Link from "next/link"; import { erc8004Abi } from "../../lib/contracts/erc8004"; import { ERC8004_REGISTRY } from "../../lib/contracts/constants"; -import { getAgentUserFromDB } from "../../lib/actions"; +import { getAgentUserFromDB, fetchAgentMetadata } from "../../lib/actions"; export function AgentDashboard() { const { address } = useAccount(); @@ -89,6 +90,15 @@ export function AgentDashboard() { const isAgent = agentId !== undefined; const detectLoading = dbLoading || (needsRpcFallback && (rpcWalletLoading || rpcBalanceLoading || (rpcHasNft && rpcTokenLoading))); + // Auto-cache: when RPC fallback detects an agent not in DB, persist it + const cachedRef = useRef(false); + useEffect(() => { + if (!dbDetected && isAgent && address && !cachedRef.current) { + cachedRef.current = true; + fetchAgentMetadata(address).catch(() => {}); + } + }, [dbDetected, isAgent, address]); + // Fetch agent's storylines from Supabase const { data: storylines, isLoading: storylinesLoading } = useQuery({ queryKey: ["agent-storylines", writerAddress], From 6b7c2fa2bb2701a5ec2eddce7e8371561debc7ac Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 27 Mar 2026 15:11:56 +0000 Subject: [PATCH 5/6] [#294] Fix owner-wallet caching with getAgentMetadataById Add getAgentMetadataById() that resolves by agentId directly (fetches URI, owner, and agentWallet in parallel), so owner wallets that aren't the bound agent wallet can still be cached. Add cacheAgentById() server action used by agents page and dashboard auto-cache effect. This covers the case where agentIdByWallet() returns 0 for owner wallets. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/actions.ts | 51 +++++++++++++++++++++++++++++++ lib/contracts/erc8004.ts | 48 +++++++++++++++++++++++++++++ src/app/agents/page.tsx | 8 ++--- src/components/AgentDashboard.tsx | 8 ++--- 4 files changed, 107 insertions(+), 8 deletions(-) diff --git a/lib/actions.ts b/lib/actions.ts index 8505eab1..df4838e1 100644 --- a/lib/actions.ts +++ b/lib/actions.ts @@ -3,6 +3,7 @@ import { lookupByAddress, type FarcasterProfile } from "./farcaster"; import { getAgentMetadata as _getAgentMetadata, + getAgentMetadataById as _getAgentMetadataById, type AgentMetadata, } from "./contracts/erc8004"; import { createServiceRoleClient, type User } from "./supabase"; @@ -82,6 +83,56 @@ export async function fetchAgentMetadata( return meta; } +/** + * Cache an externally registered agent by agentId. + * Use when the wallet is an NFT owner (not the bound agent wallet), + * so agentIdByWallet() wouldn't find it. + */ +export async function cacheAgentById( + walletAddress: string, + agentId: string, +): Promise { + const supabase = createServiceRoleClient(); + if (!supabase) return; + + const normalized = walletAddress.toLowerCase(); + + // Check if already cached + const { data: existing } = await supabase + .from("users") + .select("agent_id") + .eq("agent_id", Number(agentId)) + .single(); + if (existing) return; + + // Fetch metadata from RPC by agentId + const meta = await _getAgentMetadataById(BigInt(agentId)); + if (!meta) return; + + const agentFields = { + agent_id: Number(meta.agentId), + agent_name: meta.name || null, + agent_description: meta.description || null, + agent_genre: meta.genre || null, + agent_llm_model: meta.llmModel || null, + agent_owner: meta.owner?.toLowerCase() || normalized, + agent_wallet: meta.agentWallet?.toLowerCase() || null, + agent_registered_at: meta.registeredAt || null, + }; + + // Find existing user row or create one + const dbUser = await getUserFromDB(walletAddress); + try { + if (dbUser) { + await supabase.from("users").update(agentFields).eq("id", dbUser.id); + } else { + await supabase.from("users").insert({ primary_address: normalized, ...agentFields }); + } + } catch { + // Best-effort + } +} + /** * Look up a user from the DB by wallet address. * Searches verified_addresses first, then primary_address, then agent columns. diff --git a/lib/contracts/erc8004.ts b/lib/contracts/erc8004.ts index 9ac28767..798a67d0 100644 --- a/lib/contracts/erc8004.ts +++ b/lib/contracts/erc8004.ts @@ -14,6 +14,7 @@ import { createServiceRoleClient } from "../supabase"; export interface AgentMetadata { agentId?: string; owner?: string; + agentWallet?: string; name: string; description: string; genre?: string; @@ -281,3 +282,50 @@ export async function getAgentMetadata( return null; } } + +/** + * Resolve ERC-8004 agent metadata by agentId (not by wallet address). + * Use when you already know the agentId (e.g. from balanceOf/tokenOfOwnerByIndex). + */ +export async function getAgentMetadataById( + agentId: bigint, +): Promise { + try { + const [uri, owner, agentWallet] = await Promise.all([ + publicClient.readContract({ + address: ERC8004_REGISTRY, + abi: erc8004Abi, + functionName: "agentURI", + args: [agentId], + }), + publicClient.readContract({ + address: ERC8004_REGISTRY, + abi: erc8004Abi, + functionName: "ownerOf", + args: [agentId], + }).catch(() => undefined), + publicClient.readContract({ + address: ERC8004_REGISTRY, + abi: erc8004Abi, + functionName: "getAgentWallet", + args: [agentId], + }).catch(() => undefined), + ]); + if (!uri) return null; + + const parsed = await resolveAgentURI(uri as string); + return { + agentId: agentId.toString(), + owner: owner as string | undefined, + agentWallet: agentWallet as string | undefined, + name: (parsed.name as string) || "Unknown Agent", + description: (parsed.description as string) || "", + genre: (parsed.genre as string) || undefined, + llmModel: (parsed.llmModel as string) || (parsed.model as string) || undefined, + registeredBy: (parsed.registeredBy as string) || undefined, + registeredAt: (parsed.registeredAt as string) || undefined, + }; + } catch { + return null; + } +} diff --git a/src/app/agents/page.tsx b/src/app/agents/page.tsx index 8542af5c..6554a594 100644 --- a/src/app/agents/page.tsx +++ b/src/app/agents/page.tsx @@ -3,7 +3,7 @@ import { useState, useEffect, useRef } from "react"; import { useAccount, useReadContract } from "wagmi"; import { useQuery } from "@tanstack/react-query"; -import { fetchAgentMetadata } from "../../../lib/actions"; +import { cacheAgentById } from "../../../lib/actions"; import { ConnectWallet } from "../../components/ConnectWallet"; import { AgentRegister } from "../../components/AgentRegister"; import { AgentManage } from "../../components/AgentManage"; @@ -84,11 +84,11 @@ export default function AgentsPage() { // Auto-cache: when RPC fallback detects an agent not in DB, persist it const cachedRef = useRef(false); useEffect(() => { - if (!dbDetected && hasExistingAgent && address && !cachedRef.current) { + if (!dbDetected && hasExistingAgent && address && detectedAgentId && !cachedRef.current) { cachedRef.current = true; - fetchAgentMetadata(address).catch(() => {}); + cacheAgentById(address, detectedAgentId.toString()).catch(() => {}); } - }, [dbDetected, hasExistingAgent, address]); + }, [dbDetected, hasExistingAgent, address, detectedAgentId]); const firstTabLabel = hasExistingAgent ? "Manage" : "Register"; diff --git a/src/components/AgentDashboard.tsx b/src/components/AgentDashboard.tsx index aedf4161..ae1cd0f1 100644 --- a/src/components/AgentDashboard.tsx +++ b/src/components/AgentDashboard.tsx @@ -6,7 +6,7 @@ import { useQuery } from "@tanstack/react-query"; import Link from "next/link"; import { erc8004Abi } from "../../lib/contracts/erc8004"; import { ERC8004_REGISTRY } from "../../lib/contracts/constants"; -import { getAgentUserFromDB, fetchAgentMetadata } from "../../lib/actions"; +import { getAgentUserFromDB, cacheAgentById } from "../../lib/actions"; export function AgentDashboard() { const { address } = useAccount(); @@ -93,11 +93,11 @@ export function AgentDashboard() { // Auto-cache: when RPC fallback detects an agent not in DB, persist it const cachedRef = useRef(false); useEffect(() => { - if (!dbDetected && isAgent && address && !cachedRef.current) { + if (!dbDetected && isAgent && address && agentId && !cachedRef.current) { cachedRef.current = true; - fetchAgentMetadata(address).catch(() => {}); + cacheAgentById(address, agentId.toString()).catch(() => {}); } - }, [dbDetected, isAgent, address]); + }, [dbDetected, isAgent, address, agentId]); // Fetch agent's storylines from Supabase const { data: storylines, isLoading: storylinesLoading } = useQuery({ From 4f689ec710917f2c915aea66d0a51a4b03b0008a Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 27 Mar 2026 15:15:36 +0000 Subject: [PATCH 6/6] [#294] Normalize zero-address agent_wallet to null when caching Prevent caching 0x000...000 as agent_wallet for agents with no bound wallet, which caused dashboard storyline lookups to query the zero address. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/actions.ts b/lib/actions.ts index df4838e1..ccfcd734 100644 --- a/lib/actions.ts +++ b/lib/actions.ts @@ -116,7 +116,7 @@ export async function cacheAgentById( agent_genre: meta.genre || null, agent_llm_model: meta.llmModel || null, agent_owner: meta.owner?.toLowerCase() || normalized, - agent_wallet: meta.agentWallet?.toLowerCase() || null, + agent_wallet: meta.agentWallet && meta.agentWallet !== "0x0000000000000000000000000000000000000000" ? meta.agentWallet.toLowerCase() : null, agent_registered_at: meta.registeredAt || null, };