From 63398e46c8863f03654c254872f80b00d692b7b2 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 27 Mar 2026 15:47:35 +0000 Subject: [PATCH 1/7] [#295] Skip RPC fallback for known non-agent users Add checkUserExists() helper that checks if any DB row exists for a wallet. Agents page and dashboard now distinguish between known users with agent_id=NULL (definitive non-agents, skip RPC) and completely unknown wallets (still eligible for RPC fallback + auto-cache). Eliminates 3+ unnecessary RPC calls per visit for known non-agent users. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/actions.ts | 11 +++++++++++ src/app/agents/page.tsx | 18 ++++++++++++------ src/components/AgentDashboard.tsx | 15 +++++++++++---- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/lib/actions.ts b/lib/actions.ts index ccfcd734..9f77fd8b 100644 --- a/lib/actions.ts +++ b/lib/actions.ts @@ -83,6 +83,17 @@ export async function fetchAgentMetadata( return meta; } +/** + * Check if a user exists in the DB (any row, with or without agent_id). + * Returns true if a known user, false if completely unknown wallet. + */ +export async function checkUserExists( + address: string, +): Promise { + const user = await getUserFromDB(address); + return user !== null; +} + /** * Cache an externally registered agent by agentId. * Use when the wallet is an NFT owner (not the bound agent wallet), diff --git a/src/app/agents/page.tsx b/src/app/agents/page.tsx index 6554a594..6cfe84a9 100644 --- a/src/app/agents/page.tsx +++ b/src/app/agents/page.tsx @@ -3,7 +3,6 @@ import { useState, useEffect, useRef } from "react"; import { useAccount, useReadContract } from "wagmi"; import { useQuery } from "@tanstack/react-query"; -import { cacheAgentById } from "../../../lib/actions"; import { ConnectWallet } from "../../components/ConnectWallet"; import { AgentRegister } from "../../components/AgentRegister"; import { AgentManage } from "../../components/AgentManage"; @@ -11,8 +10,7 @@ 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 { getAgentUserFromDB } from "../../../lib/actions"; +import { getAgentUserFromDB, checkUserExists, cacheAgentById } from "../../../lib/actions"; type Tab = "register" | "build" | "dashboard"; @@ -32,8 +30,16 @@ export default function AgentsPage() { 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; + // Check if user exists in DB at all (even without agent_id) + const { data: userExists, isLoading: userExistsLoading } = useQuery({ + queryKey: ["user-exists", address], + queryFn: () => checkUserExists(address!), + enabled: !!address && !dbLoading && !dbDetected, + }); + + // RPC fallback: only for completely unknown wallets (no DB record at all) + // Known users with agent_id=NULL are definitively non-agents — skip RPC + const needsRpcFallback = !dbLoading && !dbDetected && !userExistsLoading && userExists === false && !!address; const { data: rpcAgentId, isLoading: rpcWalletLoading } = useReadContract({ address: ERC8004_REGISTRY, @@ -79,7 +85,7 @@ export default function AgentsPage() { } const hasExistingAgent = detectedAgentId !== undefined && detectedRole !== undefined; - const detectLoading = dbLoading || (needsRpcFallback && (rpcWalletLoading || rpcBalanceLoading || (rpcHasNft && rpcTokenLoading))); + const detectLoading = dbLoading || (!dbDetected && userExistsLoading) || (needsRpcFallback && (rpcWalletLoading || rpcBalanceLoading || (rpcHasNft && rpcTokenLoading))); // Auto-cache: when RPC fallback detects an agent not in DB, persist it const cachedRef = useRef(false); diff --git a/src/components/AgentDashboard.tsx b/src/components/AgentDashboard.tsx index ae1cd0f1..709f8c80 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, cacheAgentById } from "../../lib/actions"; +import { getAgentUserFromDB, checkUserExists, cacheAgentById } from "../../lib/actions"; export function AgentDashboard() { const { address } = useAccount(); @@ -24,8 +24,15 @@ export function AgentDashboard() { 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; + // Check if user exists in DB at all (even without agent_id) + const { data: userExists, isLoading: userExistsLoading } = useQuery({ + queryKey: ["user-exists-dashboard", address], + queryFn: () => checkUserExists(address!), + enabled: !!address && !dbLoading && !dbDetected, + }); + + // RPC fallback: only for completely unknown wallets (no DB record at all) + const needsRpcFallback = !dbLoading && !dbDetected && !userExistsLoading && userExists === false && !!address; const { data: rpcAgentId, isLoading: rpcWalletLoading } = useReadContract({ address: ERC8004_REGISTRY, @@ -88,7 +95,7 @@ export function AgentDashboard() { } const isAgent = agentId !== undefined; - const detectLoading = dbLoading || (needsRpcFallback && (rpcWalletLoading || rpcBalanceLoading || (rpcHasNft && rpcTokenLoading))); + const detectLoading = dbLoading || (!dbDetected && userExistsLoading) || (needsRpcFallback && (rpcWalletLoading || rpcBalanceLoading || (rpcHasNft && rpcTokenLoading))); // Auto-cache: when RPC fallback detects an agent not in DB, persist it const cachedRef = useRef(false); From 3ec23e1d258b8f1f1ba6054ae52c8b84f96019d9 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 27 Mar 2026 15:52:00 +0000 Subject: [PATCH 2/7] [#295] Add lightweight background check for known non-agents Known non-agents (DB row with agent_id=NULL) now get a single non-blocking agentIdByWallet RPC call to catch external registrations. If positive, auto-caches and updates UI. Avoids the heavy 3-call chain (balanceOf, tokenOfOwnerByIndex, etc.) while still detecting users who registered as agents externally after their PlotLink account was created. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/agents/page.tsx | 28 ++++++++++++++++++++++++++-- src/components/AgentDashboard.tsx | 29 ++++++++++++++++++++++++----- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/app/agents/page.tsx b/src/app/agents/page.tsx index 6cfe84a9..229b0610 100644 --- a/src/app/agents/page.tsx +++ b/src/app/agents/page.tsx @@ -37,8 +37,29 @@ export default function AgentsPage() { enabled: !!address && !dbLoading && !dbDetected, }); - // RPC fallback: only for completely unknown wallets (no DB record at all) - // Known users with agent_id=NULL are definitively non-agents — skip RPC + const knownNonAgent = !dbDetected && !userExistsLoading && userExists === true; + + // Lightweight background check for known non-agents: single agentIdByWallet call + // Catches users who registered externally after creating their PlotLink account + const { data: bgAgentId } = useReadContract({ + address: ERC8004_REGISTRY, + abi: erc8004Abi, + functionName: "agentIdByWallet", + args: address ? [address] : undefined, + query: { enabled: knownNonAgent }, + }); + + // If background check finds an agent, cache it + const bgFoundAgent = bgAgentId !== undefined && bgAgentId > BigInt(0); + const bgCachedRef = useRef(false); + useEffect(() => { + if (bgFoundAgent && address && !bgCachedRef.current) { + bgCachedRef.current = true; + cacheAgentById(address, bgAgentId!.toString()).catch(() => {}); + } + }, [bgFoundAgent, address, bgAgentId]); + + // Full RPC fallback: only for completely unknown wallets (no DB record at all) const needsRpcFallback = !dbLoading && !dbDetected && !userExistsLoading && userExists === false && !!address; const { data: rpcAgentId, isLoading: rpcWalletLoading } = useReadContract({ @@ -76,6 +97,9 @@ export default function AgentsPage() { if (dbDetected) { detectedAgentId = BigInt(dbAgentId!); detectedRole = dbIsOwner ? "owner" : dbIsAgentWallet ? "agentWallet" : "owner"; + } else if (bgFoundAgent) { + detectedAgentId = bgAgentId; + detectedRole = "agentWallet"; } else if (rpcIsOwner) { detectedAgentId = rpcOwnedToken; detectedRole = "owner"; diff --git a/src/components/AgentDashboard.tsx b/src/components/AgentDashboard.tsx index 709f8c80..4c2730df 100644 --- a/src/components/AgentDashboard.tsx +++ b/src/components/AgentDashboard.tsx @@ -31,7 +31,27 @@ export function AgentDashboard() { enabled: !!address && !dbLoading && !dbDetected, }); - // RPC fallback: only for completely unknown wallets (no DB record at all) + const knownNonAgent = !dbDetected && !userExistsLoading && userExists === true; + + // Lightweight background check for known non-agents + const { data: bgAgentId } = useReadContract({ + address: ERC8004_REGISTRY, + abi: erc8004Abi, + functionName: "agentIdByWallet", + args: address ? [address] : undefined, + query: { enabled: knownNonAgent }, + }); + + const bgFoundAgent = bgAgentId !== undefined && bgAgentId > BigInt(0); + const bgCachedRef = useRef(false); + useEffect(() => { + if (bgFoundAgent && address && !bgCachedRef.current) { + bgCachedRef.current = true; + cacheAgentById(address, bgAgentId!.toString()).catch(() => {}); + } + }, [bgFoundAgent, address, bgAgentId]); + + // Full RPC fallback: only for completely unknown wallets (no DB record at all) const needsRpcFallback = !dbLoading && !dbDetected && !userExistsLoading && userExists === false && !!address; const { data: rpcAgentId, isLoading: rpcWalletLoading } = useReadContract({ @@ -80,10 +100,9 @@ export function AgentDashboard() { agentId = BigInt(dbAgentId!); isOwner = dbIsOwner; isAgentWallet = dbIsAgentWallet; - // For owner, use cached agent_wallet for storyline lookup - if (dbIsOwner && dbAgentWallet) { - writerAddress = dbAgentWallet; - } + } else if (bgFoundAgent) { + agentId = bgAgentId; + isAgentWallet = true; } else if (rpcIsOwner) { agentId = rpcOwnedToken; isOwner = true; From d34a0fdd30ab1d16293260d8b847fe0ae0a1f9ac Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 27 Mar 2026 15:55:25 +0000 Subject: [PATCH 3/7] [#295] Zero RPC calls for known non-agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove lightweight background agentIdByWallet check for known non-agents. Users with a DB row and agent_id=NULL now make zero RPC calls — definitively treated as non-agents per issue spec. External agent registrations for existing PlotLink users are handled via profile refresh flow. Full RPC fallback (3+ calls) only fires for completely unknown wallets with no DB record. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/agents/page.tsx | 29 +++-------------------------- src/components/AgentDashboard.tsx | 26 ++------------------------ 2 files changed, 5 insertions(+), 50 deletions(-) diff --git a/src/app/agents/page.tsx b/src/app/agents/page.tsx index 229b0610..8176508e 100644 --- a/src/app/agents/page.tsx +++ b/src/app/agents/page.tsx @@ -37,29 +37,9 @@ export default function AgentsPage() { enabled: !!address && !dbLoading && !dbDetected, }); - const knownNonAgent = !dbDetected && !userExistsLoading && userExists === true; - - // Lightweight background check for known non-agents: single agentIdByWallet call - // Catches users who registered externally after creating their PlotLink account - const { data: bgAgentId } = useReadContract({ - address: ERC8004_REGISTRY, - abi: erc8004Abi, - functionName: "agentIdByWallet", - args: address ? [address] : undefined, - query: { enabled: knownNonAgent }, - }); - - // If background check finds an agent, cache it - const bgFoundAgent = bgAgentId !== undefined && bgAgentId > BigInt(0); - const bgCachedRef = useRef(false); - useEffect(() => { - if (bgFoundAgent && address && !bgCachedRef.current) { - bgCachedRef.current = true; - cacheAgentById(address, bgAgentId!.toString()).catch(() => {}); - } - }, [bgFoundAgent, address, bgAgentId]); - - // Full RPC fallback: only for completely unknown wallets (no DB record at all) + // RPC fallback: only for completely unknown wallets (no DB record at all) + // Known users with agent_id=NULL are definitively non-agents — zero RPC calls + // External registrations are detected via profile refresh (/api/user/onboard) const needsRpcFallback = !dbLoading && !dbDetected && !userExistsLoading && userExists === false && !!address; const { data: rpcAgentId, isLoading: rpcWalletLoading } = useReadContract({ @@ -97,9 +77,6 @@ export default function AgentsPage() { if (dbDetected) { detectedAgentId = BigInt(dbAgentId!); detectedRole = dbIsOwner ? "owner" : dbIsAgentWallet ? "agentWallet" : "owner"; - } else if (bgFoundAgent) { - detectedAgentId = bgAgentId; - detectedRole = "agentWallet"; } else if (rpcIsOwner) { detectedAgentId = rpcOwnedToken; detectedRole = "owner"; diff --git a/src/components/AgentDashboard.tsx b/src/components/AgentDashboard.tsx index 4c2730df..e3e126eb 100644 --- a/src/components/AgentDashboard.tsx +++ b/src/components/AgentDashboard.tsx @@ -31,27 +31,8 @@ export function AgentDashboard() { enabled: !!address && !dbLoading && !dbDetected, }); - const knownNonAgent = !dbDetected && !userExistsLoading && userExists === true; - - // Lightweight background check for known non-agents - const { data: bgAgentId } = useReadContract({ - address: ERC8004_REGISTRY, - abi: erc8004Abi, - functionName: "agentIdByWallet", - args: address ? [address] : undefined, - query: { enabled: knownNonAgent }, - }); - - const bgFoundAgent = bgAgentId !== undefined && bgAgentId > BigInt(0); - const bgCachedRef = useRef(false); - useEffect(() => { - if (bgFoundAgent && address && !bgCachedRef.current) { - bgCachedRef.current = true; - cacheAgentById(address, bgAgentId!.toString()).catch(() => {}); - } - }, [bgFoundAgent, address, bgAgentId]); - - // Full RPC fallback: only for completely unknown wallets (no DB record at all) + // RPC fallback: only for completely unknown wallets (no DB record at all) + // Known users with agent_id=NULL are definitively non-agents — zero RPC calls const needsRpcFallback = !dbLoading && !dbDetected && !userExistsLoading && userExists === false && !!address; const { data: rpcAgentId, isLoading: rpcWalletLoading } = useReadContract({ @@ -100,9 +81,6 @@ export function AgentDashboard() { agentId = BigInt(dbAgentId!); isOwner = dbIsOwner; isAgentWallet = dbIsAgentWallet; - } else if (bgFoundAgent) { - agentId = bgAgentId; - isAgentWallet = true; } else if (rpcIsOwner) { agentId = rpcOwnedToken; isOwner = true; From 3c54a740835b356209ffd6e1ac1e8b16608e16fb Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 27 Mar 2026 15:57:50 +0000 Subject: [PATCH 4/7] [#295] Restore owner writerAddress fallback to cached agent_wallet Fix regression: DB-detected owners now correctly use cached agent_wallet for storyline lookup instead of the connected owner wallet address. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/AgentDashboard.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/AgentDashboard.tsx b/src/components/AgentDashboard.tsx index e3e126eb..d08c3d16 100644 --- a/src/components/AgentDashboard.tsx +++ b/src/components/AgentDashboard.tsx @@ -81,6 +81,10 @@ export function AgentDashboard() { 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; From b08eed03b51498596d4c559f36b4902759cb5218 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 27 Mar 2026 16:01:12 +0000 Subject: [PATCH 5/7] [#295] Add ERC-8004 agent check to profile refresh onboard route When a user refreshes their profile via /api/user/onboard and has no cached agent_id, check the ERC-8004 registry via getAgentMetadata(). If an agent is found, persist agent columns to DB. This provides the sync path for existing PlotLink users who register as agents externally. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/api/user/onboard/route.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/app/api/user/onboard/route.ts b/src/app/api/user/onboard/route.ts index 8ac0beee..2bcffe82 100644 --- a/src/app/api/user/onboard/route.ts +++ b/src/app/api/user/onboard/route.ts @@ -4,6 +4,8 @@ import { getUserByWallet } from "../../../../../lib/farcaster-indexer"; import { lookupByAddress } from "../../../../../lib/farcaster"; import { fetchQuotientScore, isQuotientStale } from "../../../../../lib/quotient"; import { buildUserData } from "../../../../../lib/user-data"; +import { getAgentMetadata } from "../../../../../lib/contracts/erc8004"; +import type { Address } from "viem"; const COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes @@ -110,6 +112,28 @@ export async function POST(request: NextRequest) { quotientData, }); + // Check ERC-8004 agent status if not already cached + if (!existingUser?.agent_id) { + try { + const agentMeta = await getAgentMetadata(normalizedAddress as Address); + if (agentMeta?.agentId) { + Object.assign(userData, { + agent_id: Number(agentMeta.agentId), + agent_name: agentMeta.name || null, + agent_description: agentMeta.description || null, + agent_genre: agentMeta.genre || null, + agent_llm_model: agentMeta.llmModel || null, + agent_owner: agentMeta.owner?.toLowerCase() || null, + agent_wallet: agentMeta.agentWallet && agentMeta.agentWallet !== "0x0000000000000000000000000000000000000000" + ? agentMeta.agentWallet.toLowerCase() : null, + agent_registered_at: agentMeta.registeredAt || null, + }); + } + } catch { + // Non-fatal — agent check is best-effort + } + } + // Upsert — update by existing row identity if (existingUser) { const { data, error } = await supabase From 920687983d845a0034a21a70502e34225f11378b Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 27 Mar 2026 16:03:32 +0000 Subject: [PATCH 6/7] [#295] Add owner-wallet detection to onboard agent check Profile refresh now checks both agentIdByWallet (bound wallet) and balanceOf + tokenOfOwnerByIndex (NFT owner) before falling back to getAgentMetadataById. Covers existing PlotLink users who register as agents externally with a separate or unset bound wallet. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/api/user/onboard/route.ts | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/app/api/user/onboard/route.ts b/src/app/api/user/onboard/route.ts index 2bcffe82..6062648c 100644 --- a/src/app/api/user/onboard/route.ts +++ b/src/app/api/user/onboard/route.ts @@ -4,7 +4,9 @@ import { getUserByWallet } from "../../../../../lib/farcaster-indexer"; import { lookupByAddress } from "../../../../../lib/farcaster"; import { fetchQuotientScore, isQuotientStale } from "../../../../../lib/quotient"; import { buildUserData } from "../../../../../lib/user-data"; -import { getAgentMetadata } from "../../../../../lib/contracts/erc8004"; +import { getAgentMetadata, getAgentMetadataById, erc8004Abi } from "../../../../../lib/contracts/erc8004"; +import { ERC8004_REGISTRY } from "../../../../../lib/contracts/constants"; +import { publicClient } from "../../../../../lib/rpc"; import type { Address } from "viem"; const COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes @@ -115,7 +117,32 @@ export async function POST(request: NextRequest) { // Check ERC-8004 agent status if not already cached if (!existingUser?.agent_id) { try { - const agentMeta = await getAgentMetadata(normalizedAddress as Address); + // Check 1: is this wallet a bound agent wallet? + let agentMeta = await getAgentMetadata(normalizedAddress as Address); + + // Check 2: does this wallet own an agent NFT? (owner with separate bound wallet) + if (!agentMeta) { + const balance = await publicClient.readContract({ + address: ERC8004_REGISTRY, + abi: erc8004Abi, + functionName: "balanceOf", + args: [normalizedAddress as Address], + }).catch(() => BigInt(0)); + + if (balance > BigInt(0)) { + const ownedTokenId = await publicClient.readContract({ + address: ERC8004_REGISTRY, + abi: erc8004Abi, + functionName: "tokenOfOwnerByIndex", + args: [normalizedAddress as Address, BigInt(0)], + }).catch(() => undefined); + + if (ownedTokenId !== undefined) { + agentMeta = await getAgentMetadataById(ownedTokenId); + } + } + } + if (agentMeta?.agentId) { Object.assign(userData, { agent_id: Number(agentMeta.agentId), From eacf0d8ce3005a6db5e324a19c4bac345fd9c3e6 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 27 Mar 2026 16:06:21 +0000 Subject: [PATCH 7/7] [#295] Fetch agentWallet in getAgentMetadata for correct role caching getAgentMetadata() now fetches getAgentWallet() in parallel with agentURI and ownerOf, so the agent_wallet field is populated when caching direct bound-wallet registrations via profile refresh. Prevents misclassification of bound wallets as owners. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/contracts/erc8004.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/contracts/erc8004.ts b/lib/contracts/erc8004.ts index 798a67d0..a0777848 100644 --- a/lib/contracts/erc8004.ts +++ b/lib/contracts/erc8004.ts @@ -251,7 +251,7 @@ export async function getAgentMetadata( }); if (agentId <= BigInt(0)) return null; - const [uri, owner] = await Promise.all([ + const [uri, owner, agentWallet] = await Promise.all([ publicClient.readContract({ address: ERC8004_REGISTRY, abi: erc8004Abi, @@ -264,6 +264,12 @@ export async function getAgentMetadata( functionName: "ownerOf", args: [agentId], }).catch(() => undefined), + publicClient.readContract({ + address: ERC8004_REGISTRY, + abi: erc8004Abi, + functionName: "getAgentWallet", + args: [agentId], + }).catch(() => undefined), ]); if (!uri) return null; @@ -271,6 +277,7 @@ export async function getAgentMetadata( 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,