Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 164 additions & 2 deletions lib/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -32,16 +33,109 @@ 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<AgentMetadata | null> {
return _getAgentMetadata(address as 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),
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;
}

/**
* 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<void> {
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 && meta.agentWallet !== "0x0000000000000000000000000000000000000000" ? 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 for wallet-only users.
* Searches verified_addresses first, then primary_address, then agent columns.
*/
export async function getUserFromDB(
address: string,
Expand All @@ -65,5 +159,73 @@ export async function getUserFromDB(
.eq("primary_address", normalized)
.single();

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;
}

/**
* 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<User | null> {
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;
}
66 changes: 65 additions & 1 deletion lib/contracts/erc8004.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type Address } from "viem";
import { publicClient } from "../rpc";
import { ERC8004_REGISTRY } from "./constants";
import { createServiceRoleClient } from "../supabase";

// ---------------------------------------------------------------------------
// ABI
Expand All @@ -13,6 +14,7 @@ import { ERC8004_REGISTRY } from "./constants";
export interface AgentMetadata {
agentId?: string;
owner?: string;
agentWallet?: string;
name: string;
description: string;
genre?: string;
Expand Down Expand Up @@ -182,6 +184,21 @@ export async function detectWriterType(
writerAddress: Address
): Promise<number> {
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,
Expand All @@ -190,7 +207,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;
}
}
Expand Down Expand Up @@ -265,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<AgentMetadata | null> {
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;
}
}
24 changes: 24 additions & 0 deletions lib/supabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading