diff --git a/lib/actions.ts b/lib/actions.ts
index e2085473..63b77e80 100644
--- a/lib/actions.ts
+++ b/lib/actions.ts
@@ -267,3 +267,26 @@ export async function getAgentUserFromDB(
return byPrimary ?? null;
}
+
+/**
+ * For a writer address, check if it's an ERC-8004 agent and return agent info
+ * plus the owner's Farcaster profile (if available).
+ * Returns null only if the address is NOT an agent.
+ */
+export async function getAgentOwnerProfile(
+ writerAddress: string,
+): Promise<{ ownerProfile: FarcasterProfile | null; agentName: string; agentId: number } | null> {
+ "use server";
+ const agentUser = await getAgentUserFromDB(writerAddress);
+ if (!agentUser?.agent_id) return null;
+
+ const ownerProfile = agentUser.agent_owner
+ ? await getFarcasterProfile(agentUser.agent_owner)
+ : null;
+
+ return {
+ ownerProfile,
+ agentName: agentUser.agent_name || `Agent #${agentUser.agent_id}`,
+ agentId: agentUser.agent_id,
+ };
+}
diff --git a/package.json b/package.json
index 4e921a7f..de308ca4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "plotlink",
- "version": "0.1.17",
+ "version": "0.1.18",
"private": true,
"workspaces": [
"packages/*"
diff --git a/src/app/profile/[address]/page.tsx b/src/app/profile/[address]/page.tsx
index ae6294ba..aa2b95d8 100644
--- a/src/app/profile/[address]/page.tsx
+++ b/src/app/profile/[address]/page.tsx
@@ -8,7 +8,7 @@ import { formatUnits, type Address } from "viem";
import Link from "next/link";
import { supabase, type Storyline, type Donation, type TradeHistory, type User } from "../../../../lib/supabase";
import { STORY_FACTORY, RESERVE_LABEL, EXPLORER_URL, MCV2_BOND, PLOT_TOKEN } from "../../../../lib/contracts/constants";
-import { getFullUserProfile } from "../../../../lib/actions";
+import { getFullUserProfile, getFarcasterProfile } from "../../../../lib/actions";
import { truncateAddress } from "../../../../lib/utils";
import { formatPrice, formatSupply } from "../../../../lib/format";
import { getTokenPrice, mcv2BondAbi, erc20Abi, type TokenPriceInfo, get24hPriceChange, getTokenTVL } from "../../../../lib/price";
@@ -237,6 +237,15 @@ function ProfileHeader({
onCooldown: boolean;
cooldownRemaining: number;
}) {
+ // Fetch owner's Farcaster profile for "Operated by" section
+ const ownerAddress = agentMeta?.owner;
+ const hasOwner = !!ownerAddress && ownerAddress.toLowerCase() !== address.toLowerCase();
+ const { data: ownerFcProfile } = useQuery({
+ queryKey: ["owner-fc-profile", ownerAddress],
+ queryFn: () => getFarcasterProfile(ownerAddress!),
+ enabled: hasOwner,
+ });
+
const displayName = agentMeta?.name ?? fcProfile?.displayName ?? null;
const hasFarcaster = dbUser?.fid != null && dbUser?.username != null;
const hasX = dbUser?.twitter != null;
@@ -430,12 +439,19 @@ function ProfileHeader({
)}
- {agentMeta.owner && agentMeta.owner.toLowerCase() !== address.toLowerCase() && (
-
-
Owner:
-
- {agentMeta.owner.slice(0, 6)}...{agentMeta.owner.slice(-4)}
-
+ {hasOwner && (
+
+
Operated by
+
+ {ownerFcProfile?.pfpUrl && (
+ // eslint-disable-next-line @next/next/no-img-element
+

+ )}
+
+ {ownerFcProfile?.displayName || ownerFcProfile?.username || `${ownerAddress!.slice(0, 6)}...${ownerAddress!.slice(-4)}`}
+
+
ERC-8004 Verified
+
)}
diff --git a/src/app/story/[storylineId]/[plotIndex]/page.tsx b/src/app/story/[storylineId]/[plotIndex]/page.tsx
index d8d902ac..041ca1a6 100644
--- a/src/app/story/[storylineId]/[plotIndex]/page.tsx
+++ b/src/app/story/[storylineId]/[plotIndex]/page.tsx
@@ -136,7 +136,7 @@ export default async function PlotDetailPage({ params }: { params: Params }) {
by{" "}
{truncateAddress(sl.writer_address)}}>
-
+
{p.block_timestamp && (
diff --git a/src/app/story/[storylineId]/page.tsx b/src/app/story/[storylineId]/page.tsx
index 9309db02..77d72d5f 100644
--- a/src/app/story/[storylineId]/page.tsx
+++ b/src/app/story/[storylineId]/page.tsx
@@ -364,7 +364,7 @@ function StoryHeader({
Writer
{truncateAddress(storyline.writer_address)}}>
-
+
{storyline.writer_type === 1 &&
}
diff --git a/src/components/StoryCard.tsx b/src/components/StoryCard.tsx
index 6d13c9e7..e9ae5f99 100644
--- a/src/components/StoryCard.tsx
+++ b/src/components/StoryCard.tsx
@@ -116,7 +116,7 @@ export function StoryCard({
{/* Metadata below notebook: author → TVL → rating */}
-
+
{storyline.writer_type === 1 && }
{storyline.token_address && (
diff --git a/src/components/WriterIdentity.tsx b/src/components/WriterIdentity.tsx
index 29d9bb3a..783af2b9 100644
--- a/src/components/WriterIdentity.tsx
+++ b/src/components/WriterIdentity.tsx
@@ -1,21 +1,47 @@
import Link from "next/link";
-import { getFarcasterProfile } from "../../lib/actions";
+import { getFarcasterProfile, getAgentOwnerProfile } from "../../lib/actions";
import { truncateAddress } from "../../lib/utils";
/**
- * Server component that displays a Farcaster identity (avatar + username)
- * when available, falling back to a truncated Ethereum address.
- * Links to the internal profile page at /profile/[address].
+ * Server component that displays a writer identity.
+ * For agents with an owner who has a Farcaster profile, shows "{owner}'s AI Writer".
+ * For agents without owner FID, shows "AI Writer #{id}".
+ * Falls back to Farcaster profile or truncated address for non-agents.
*/
-export async function WriterIdentity({ address }: { address: string }) {
+export async function WriterIdentity({ address, writerType }: { address: string; writerType?: number | null }) {
+ // For agents (or unknown), try owner lookup first
+ if (writerType === 1 || writerType === undefined || writerType === null) {
+ const ownerInfo = await getAgentOwnerProfile(address);
+ if (ownerInfo) {
+ // Agent with owner FID: "{owner}'s AI Writer"
+ if (ownerInfo.ownerProfile) {
+ return (
+
+ {ownerInfo.ownerProfile.pfpUrl && (
+ // eslint-disable-next-line @next/next/no-img-element
+

+ )}
+ {ownerInfo.ownerProfile.displayName || ownerInfo.ownerProfile.username}'s AI Writer
+
+ );
+ }
+ // Agent without owner FID: plain "AI Writer #{id}"
+ return (
+
+ AI Writer #{ownerInfo.agentId}
+
+ );
+ }
+ }
+
const profile = await getFarcasterProfile(address);
if (!profile) {
return (
-
+
{truncateAddress(address)}
);
@@ -28,13 +54,7 @@ export async function WriterIdentity({ address }: { address: string }) {
>
{profile.pfpUrl && (
// eslint-disable-next-line @next/next/no-img-element
-

+

)}
@{profile.username}
diff --git a/src/components/WriterIdentityClient.tsx b/src/components/WriterIdentityClient.tsx
index fb683ad8..e9e42514 100644
--- a/src/components/WriterIdentityClient.tsx
+++ b/src/components/WriterIdentityClient.tsx
@@ -2,72 +2,124 @@
import { useEffect, useState } from "react";
import Link from "next/link";
-import { getFarcasterProfile } from "../../lib/actions";
+import { getFarcasterProfile, getAgentOwnerProfile } from "../../lib/actions";
import { truncateAddress } from "../../lib/utils";
import type { FarcasterProfile } from "../../lib/farcaster";
+interface OwnerInfo {
+ ownerProfile: FarcasterProfile | null;
+ agentName: string;
+ agentId: number;
+}
+
/**
- * Client component that resolves a Farcaster identity via server action.
- * Shows a truncated address while loading, then replaces with avatar + username.
- * Links to the internal profile page at /profile/[address].
+ * Client component that resolves a writer identity via server action.
+ * For agents with an owner who has a Farcaster profile, shows "{owner}'s AI Writer".
+ * Falls back to Farcaster profile or truncated address.
*/
-export function WriterIdentityClient({ address, linkProfile = true }: { address: string; linkProfile?: boolean }) {
+export function WriterIdentityClient({
+ address,
+ linkProfile = true,
+ writerType,
+}: {
+ address: string;
+ linkProfile?: boolean;
+ writerType?: number | null;
+}) {
const [profile, setProfile] = useState
(null);
+ const [ownerInfo, setOwnerInfo] = useState(null);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
let cancelled = false;
- getFarcasterProfile(address).then((p) => {
+
+ async function resolve() {
+ // For agents (or unknown), try owner lookup first
+ if (writerType === 1 || writerType === undefined || writerType === null) {
+ const owner = await getAgentOwnerProfile(address);
+ if (!cancelled && owner) {
+ setOwnerInfo(owner);
+ setLoaded(true);
+ return;
+ }
+ }
+ // Fall back to writer's own Farcaster profile
+ const p = await getFarcasterProfile(address);
if (!cancelled) {
setProfile(p);
setLoaded(true);
}
- });
- return () => {
- cancelled = true;
- };
- }, [address]);
+ }
- const label = !loaded || !profile
- ? truncateAddress(address)
- : null;
+ resolve();
+ return () => { cancelled = true; };
+ }, [address, writerType]);
- if (!loaded || !profile) {
+ if (!loaded) {
+ const label = truncateAddress(address);
if (!linkProfile) return {label};
return (
-
+
{label}
);
}
- const inner = (
-
- {profile.pfpUrl && (
- // eslint-disable-next-line @next/next/no-img-element
-
- )}
- @{profile.username}
-
- );
+ // Agent with owner Farcaster profile: "{owner}'s AI Writer"
+ if (ownerInfo && ownerInfo.ownerProfile) {
+ const inner = (
+
+ {ownerInfo.ownerProfile.pfpUrl && (
+ // eslint-disable-next-line @next/next/no-img-element
+
+ )}
+ {ownerInfo.ownerProfile.displayName || ownerInfo.ownerProfile.username}'s AI Writer
+
+ );
+ if (!linkProfile) return inner;
+ return (
+
+ {inner}
+
+ );
+ }
+
+ // Agent without owner FID: plain "AI Writer #{id}"
+ if (ownerInfo) {
+ const label = `AI Writer #${ownerInfo.agentId}`;
+ if (!linkProfile) return {label};
+ return (
+
+ {label}
+
+ );
+ }
- if (!linkProfile) return inner;
+ // Regular writer with Farcaster profile
+ if (profile) {
+ const inner = (
+
+ {profile.pfpUrl && (
+ // eslint-disable-next-line @next/next/no-img-element
+
+ )}
+ @{profile.username}
+
+ );
+ if (!linkProfile) return inner;
+ return (
+
+ {inner}
+
+ );
+ }
+ // Fallback: truncated address
+ const label = truncateAddress(address);
+ if (!linkProfile) return {label};
return (
-
- {inner}
+
+ {label}
);
}