+
{badges.map((badge) => {
const Icon = BADGE_ICONS[badge.key];
return (
diff --git a/src/components/VerifiedCreatorBadge.test.tsx b/src/components/VerifiedCreatorBadge.test.tsx
new file mode 100644
index 00000000..302ae552
--- /dev/null
+++ b/src/components/VerifiedCreatorBadge.test.tsx
@@ -0,0 +1,51 @@
+import { describe, expect, it } from "vitest";
+import { render, screen } from "@testing-library/react";
+import { VerifiedCreatorBadge } from "@/components/VerifiedCreatorBadge";
+import type { CreatorVerification } from "@/lib/identity";
+
+describe("VerifiedCreatorBadge", () => {
+ it("renders the verified label for a SEP-1 verification", () => {
+ const verification: CreatorVerification = {
+ status: "verified",
+ method: "sep1-toml",
+ domain: "creator.example",
+ stellarTomlUrl: "https://creator.example/.well-known/stellar.toml",
+ };
+ render(
);
+ expect(screen.getByText("Verified creator")).toBeInTheDocument();
+ });
+
+ it("renders nothing for an unverified creator", () => {
+ const verification: CreatorVerification = { status: "unverified" };
+ const { container } = render(
);
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it("renders nothing for an error state", () => {
+ const verification: CreatorVerification = {
+ status: "error",
+ message: "bad sig",
+ };
+ const { container } = render(
);
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it("shows a pending chip for pending verification", () => {
+ const verification: CreatorVerification = { status: "pending" };
+ render(
);
+ expect(screen.getByText("Verification pending")).toBeInTheDocument();
+ });
+
+ it("omits the label in compact mode", () => {
+ const verification: CreatorVerification = {
+ status: "verified",
+ method: "sep12-attestation",
+ name: "Ada",
+ };
+ const { container } = render(
+
,
+ );
+ expect(screen.queryByText("Verified creator")).not.toBeInTheDocument();
+ expect(container.querySelector("span[role='status']")).toBeInTheDocument();
+ });
+});
diff --git a/src/components/VerifiedCreatorBadge.tsx b/src/components/VerifiedCreatorBadge.tsx
new file mode 100644
index 00000000..4554623b
--- /dev/null
+++ b/src/components/VerifiedCreatorBadge.tsx
@@ -0,0 +1,101 @@
+import { BadgeCheck } from "lucide-react";
+import { Tooltip } from "@/components/ui/Tooltip";
+import { cn } from "@/lib/utils";
+import type { CreatorVerification, VerificationMethod } from "@/lib/identity";
+
+const METHOD_LABEL: Record
= {
+ "sep1-toml": "SEP-1 domain identity",
+ "sep12-attestation": "SEP-12 verified identity",
+ "external-link": "Externally verified link",
+};
+
+interface VerifiedCreatorBadgeProps {
+ verification?: CreatorVerification | null;
+ variant?: "compact" | "full";
+ className?: string;
+}
+
+/**
+ * Renders a verified-creator badge driven by SEP-1/SEP-12 verification state.
+ * Shows nothing for unverified/error states; a muted pending chip for `pending`.
+ */
+export function VerifiedCreatorBadge({
+ verification,
+ variant = "full",
+ className,
+}: VerifiedCreatorBadgeProps) {
+ if (!verification || verification.status === "unverified") return null;
+
+ if (verification.status === "pending") {
+ return (
+
+
+ Verification pending
+
+ );
+ }
+
+ if (verification.status === "error") return null;
+
+ const methodLabel = verification.method
+ ? METHOD_LABEL[verification.method]
+ : "Verified creator";
+ const detailLines = [
+ verification.name ? `Name: ${verification.name}` : null,
+ verification.domain ? `Domain: ${verification.domain}` : null,
+ verification.issuedAt
+ ? `Issued: ${new Date(verification.issuedAt).toLocaleDateString()}`
+ : null,
+ ].filter(Boolean) as string[];
+
+ const badgeContent = (
+
+
+ {variant === "full" ? "Verified creator" : null}
+
+ );
+
+ return (
+
+ {methodLabel}
+ {detailLines.map((line) => (
+
+ {line}
+
+ ))}
+ {verification.stellarTomlUrl ? (
+
+ View stellar.toml
+
+ ) : null}
+
+ }
+ >
+
+ {badgeContent}
+
+
+ );
+}
diff --git a/src/hooks/useCreatorVerification.test.ts b/src/hooks/useCreatorVerification.test.ts
new file mode 100644
index 00000000..710d72bf
--- /dev/null
+++ b/src/hooks/useCreatorVerification.test.ts
@@ -0,0 +1,74 @@
+import { describe, expect, it, beforeEach, vi } from "vitest";
+import { renderHook, act, waitFor } from "@testing-library/react";
+import { useCreatorVerification } from "@/hooks/useCreatorVerification";
+import { fetchStellarToml } from "@/lib/identity/stellarToml";
+
+const CREATOR = "GCREATOREXAMPLECREATOREXAMPLECREATOREXAMPLECREATOREXAMPLECREATORX";
+
+const TOML = `SIGNING_KEY = "GORGSIGNINGKEYORGSIGNINGKEYORGSIGNINGKEYORGSIGNINGKEYORGSIGN"
+ACCOUNTS = ["${CREATOR}"]
+`;
+
+describe("useCreatorVerification", () => {
+ beforeEach(() => {
+ localStorage.clear();
+ vi.restoreAllMocks();
+ });
+
+ it("verifies a creator domain via SEP-1 stellar.toml", async () => {
+ vi.stubGlobal(
+ "fetch",
+ vi.fn(async (input: string | URL) => {
+ expect(String(input)).toContain("/.well-known/stellar.toml");
+ return {
+ ok: true,
+ status: 200,
+ text: async () => TOML,
+ } as Response;
+ }),
+ );
+
+ const { result } = renderHook(() => useCreatorVerification(CREATOR));
+
+ expect(result.current.verification).toBeNull();
+
+ await act(async () => {
+ await result.current.verifyDomain("creator.example");
+ });
+
+ await waitFor(() => {
+ expect(result.current.verification?.status).toBe("verified");
+ });
+ expect(result.current.verification?.method).toBe("sep1-toml");
+ });
+
+ it("reports unverified when the account is missing from the toml", async () => {
+ vi.stubGlobal(
+ "fetch",
+ vi.fn(async () => ({
+ ok: true,
+ status: 200,
+ text: async () =>
+ `ACCOUNTS = ["GOTHEROTHEROTHEROTHEROTHEROTHEROTHEROTHEROTHER"]`,
+ })) as typeof fetch,
+ );
+
+ const { result } = renderHook(() =>
+ useCreatorVerification("GABSENTABSENTABSENTABSENTABSENTABSENTABSENT"),
+ );
+
+ await act(async () => {
+ await result.current.verifyDomain("creator.example");
+ });
+
+ await waitFor(() => {
+ expect(result.current.verification?.status).toBe("unverified");
+ });
+ });
+
+ it("rejects non-HTTPS stellar.toml URLs", async () => {
+ await expect(fetchStellarToml("http://insecure.example/.well-known/stellar.toml")).rejects.toThrow(
+ /HTTPS/,
+ );
+ });
+});
diff --git a/src/hooks/useCreatorVerification.ts b/src/hooks/useCreatorVerification.ts
new file mode 100644
index 00000000..f764451e
--- /dev/null
+++ b/src/hooks/useCreatorVerification.ts
@@ -0,0 +1,119 @@
+import { useCallback, useEffect, useState } from "react";
+import {
+ evaluateCreatorVerification,
+ fetchStellarToml,
+ type CreatorVerification,
+} from "@/lib/identity";
+import {
+ clearVerification,
+ getStoredVerification,
+ storeVerification,
+} from "@/lib/identity/store";
+
+function tomlUrlForDomain(domain: string): string {
+ const cleaned = domain
+ .trim()
+ .replace(/^https?:\/\//, "")
+ .replace(/\/+$/, "");
+ return `https://${cleaned}/.well-known/stellar.toml`;
+}
+
+export interface UseCreatorVerificationOptions {
+ /** Optional pre-known stellar.toml URL for this creator (e.g. from the API). */
+ stellarTomlUrl?: string;
+ /** Whether the reputation service already confirmed an external link. */
+ externalVerified?: boolean;
+}
+
+export interface UseCreatorVerificationResult {
+ verification: CreatorVerification | null;
+ isLoading: boolean;
+ error: string | null;
+ /** Run SEP-1/SEP-12 verification against a creator-controlled domain. */
+ verifyDomain: (domain: string) => Promise;
+ reset: () => void;
+}
+
+export function useCreatorVerification(
+ address: string | undefined,
+ options: UseCreatorVerificationOptions = {},
+): UseCreatorVerificationResult {
+ const { stellarTomlUrl, externalVerified } = options;
+ const [verification, setVerification] = useState(
+ address ? getStoredVerification(address) : null,
+ );
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (!address) {
+ setVerification(null);
+ return;
+ }
+ const stored = getStoredVerification(address);
+ if (stored) {
+ setVerification(stored);
+ return;
+ }
+ if (externalVerified) {
+ setVerification({
+ status: "verified",
+ method: "external-link",
+ message: "Verified via an externally confirmed creator link.",
+ });
+ }
+ }, [address, externalVerified]);
+
+ const verifyDomain = useCallback(
+ async (domain: string): Promise => {
+ if (!address) {
+ const missing: CreatorVerification = {
+ status: "error",
+ message: "Connect a wallet to verify your creator identity.",
+ };
+ setError(missing.message ?? "Unable to verify.");
+ setVerification(missing);
+ return missing;
+ }
+
+ setIsLoading(true);
+ setError(null);
+ try {
+ const url = stellarTomlUrl ?? tomlUrlForDomain(domain);
+ const toml = await fetchStellarToml(url);
+ const result = evaluateCreatorVerification({
+ account: address,
+ toml,
+ stellarTomlUrl: url,
+ externalVerified,
+ });
+ if (result.status === "verified") {
+ storeVerification(address, result);
+ }
+ setVerification(result);
+ if (result.status === "error" || result.status === "unverified") {
+ setError(result.message ?? "Verification failed.");
+ }
+ return result;
+ } catch (err) {
+ const message =
+ err instanceof Error ? err.message : "Could not verify creator identity.";
+ const failed: CreatorVerification = { status: "error", message };
+ setError(message);
+ setVerification(failed);
+ return failed;
+ } finally {
+ setIsLoading(false);
+ }
+ },
+ [address, stellarTomlUrl, externalVerified],
+ );
+
+ const reset = useCallback(() => {
+ if (address) clearVerification(address);
+ setVerification(null);
+ setError(null);
+ }, [address]);
+
+ return { verification, isLoading, error, verifyDomain, reset };
+}
diff --git a/src/lib/identity/index.ts b/src/lib/identity/index.ts
new file mode 100644
index 00000000..46eb20f9
--- /dev/null
+++ b/src/lib/identity/index.ts
@@ -0,0 +1,4 @@
+export * from "./types";
+export * from "./stellarToml";
+export * from "./verify";
+export * from "./store";
diff --git a/src/lib/identity/stellarToml.test.ts b/src/lib/identity/stellarToml.test.ts
new file mode 100644
index 00000000..7ff6f2cd
--- /dev/null
+++ b/src/lib/identity/stellarToml.test.ts
@@ -0,0 +1,69 @@
+import { describe, expect, it } from "vitest";
+import {
+ findCreatorInToml,
+ parseStellarToml,
+ toStellarToml,
+} from "@/lib/identity/stellarToml";
+
+const SAMPLE_TOML = `# Stellar info file
+SIGNING_KEY = "GAIGZHHWYKOAPIXKX3TQOLJLFWHYYPVFRXYBL7GBRL3QSEMEVOVQXXDB"
+ACCOUNTS = ["GCREATOREXAMPLECREATOREXAMPLECREATOREXAMPLECREATOREXAMPLECREATORX"]
+VERSION = "0.1.0"
+
+[[VERIFIED_CREATORS]]
+ACCOUNT = "GOTHERCREATOROTHERCREATOROTHERCREATOROTHERCREATOROTHERCREATOROTH"
+NAME = "Ada Lovelace"
+HANDLE = "ada"
+
+[[DOCUMENTATION]]
+ORG_NAME = "Prompt Mint"
+`;
+
+describe("parseStellarToml", () => {
+ it("parses top-level scalars, arrays, and arrays of tables", () => {
+ const parsed = parseStellarToml(SAMPLE_TOML);
+ expect(parsed.SIGNING_KEY).toBe(
+ "GAIGZHHWYKOAPIXKX3TQOLJLFWHYYPVFRXYBL7GBRL3QSEMEVOVQXXDB",
+ );
+ expect(Array.isArray(parsed.ACCOUNTS)).toBe(true);
+ expect(parsed.VERSION).toBe("0.1.0");
+ expect(Array.isArray(parsed.VERIFIED_CREATORS)).toBe(true);
+ });
+
+ it("normalizes to a StellarToml with verified creators", () => {
+ const toml = toStellarToml(parseStellarToml(SAMPLE_TOML));
+ expect(toml.signingKey).toBe(
+ "GAIGZHHWYKOAPIXKX3TQOLJLFWHYYPVFRXYBL7GBRL3QSEMEVOVQXXDB",
+ );
+ expect(toml.accounts).toContain(
+ "GCREATOREXAMPLECREATOREXAMPLECREATOREXAMPLECREATOREXAMPLECREATORX",
+ );
+ expect(toml.verifiedCreators[0]).toMatchObject({
+ account: "GOTHERCREATOROTHERCREATOROTHERCREATOROTHERCREATOROTHERCREATOROTH",
+ name: "Ada Lovelace",
+ });
+ });
+});
+
+describe("findCreatorInToml", () => {
+ const toml = toStellarToml(parseStellarToml(SAMPLE_TOML));
+
+ it("matches an account declared in ACCOUNTS (case-insensitive)", () => {
+ const lower = "gcreatorexamplecreatorexamplecreatorexamplecreatorexamplecreatorx";
+ expect(findCreatorInToml(toml, lower).found).toBe(true);
+ });
+
+ it("matches an account in VERIFIED_CREATORS and returns its name", () => {
+ const other =
+ "GOTHERCREATOROTHERCREATOROTHERCREATOROTHERCREATOROTHERCREATOROTH";
+ const match = findCreatorInToml(toml, other);
+ expect(match.found).toBe(true);
+ expect(match.name).toBe("Ada Lovelace");
+ });
+
+ it("returns not found for an unrelated account", () => {
+ expect(
+ findCreatorInToml(toml, "GUNRELATEDUNRELATEDUNRELATEDUNRELATEDUNRELATEDUNRELATED").found,
+ ).toBe(false);
+ });
+});
diff --git a/src/lib/identity/stellarToml.ts b/src/lib/identity/stellarToml.ts
new file mode 100644
index 00000000..f11c7105
--- /dev/null
+++ b/src/lib/identity/stellarToml.ts
@@ -0,0 +1,165 @@
+import type { StellarToml } from "./types";
+
+/**
+ * Minimal TOML parser covering the subset used by SEP-1 `stellar.toml`
+ * files: top-level key/value pairs, `[table]` sections, `[[array-of-tables]]`
+ * sections, inline arrays of scalars, strings, integers, floats and booleans.
+ * It intentionally does not implement every TOML feature — only what creator
+ * identity verification needs.
+ */
+export function parseStellarToml(text: string): Record {
+ const root: Record = {};
+ let current: Record = root;
+
+ const stripComment = (line: string): string => {
+ let inString = false;
+ let quote = "";
+ for (let i = 0; i < line.length; i++) {
+ const ch = line[i];
+ if (inString) {
+ if (ch === quote) inString = false;
+ } else if (ch === '"' || ch === "'") {
+ inString = true;
+ quote = ch;
+ } else if (ch === "#") {
+ return line.slice(0, i);
+ }
+ }
+ return line;
+ };
+
+ const parseScalar = (raw: string): unknown => {
+ const value = raw.trim();
+ if (
+ (value.startsWith('"') && value.endsWith('"')) ||
+ (value.startsWith("'") && value.endsWith("'"))
+ ) {
+ return value.slice(1, -1);
+ }
+ if (value === "true") return true;
+ if (value === "false") return false;
+ if (value.startsWith("[") && value.endsWith("]")) {
+ const inner = value.slice(1, -1).trim();
+ if (inner === "") return [];
+ return inner.split(",").map((part) => parseScalar(part));
+ }
+ if (/^-?\d+$/.test(value)) return Number(value);
+ if (/^-?\d*\.\d+$/.test(value)) return Number(value);
+ return value;
+ };
+
+ const assign = (key: string, value: unknown) => {
+ current[key] = value;
+ };
+
+ for (const rawLine of text.split(/\r?\n/)) {
+ const line = stripComment(rawLine).trim();
+ if (line === "") continue;
+
+ if (line.startsWith("[[")) {
+ const name = line.slice(2, line.indexOf("]")).trim();
+ const parent: Record = current;
+ const list = (Array.isArray(parent[name]) ? parent[name] : []) as unknown[];
+ const next: Record = {};
+ list.push(next);
+ parent[name] = list;
+ current = next;
+ continue;
+ }
+
+ if (line.startsWith("[")) {
+ const name = line.slice(1, line.indexOf("]")).trim();
+ const segments = name.split(".");
+ let node: Record = root;
+ for (const segment of segments) {
+ if (typeof node[segment] !== "object" || node[segment] === null) {
+ node[segment] = {};
+ }
+ node = node[segment] as Record;
+ }
+ current = node;
+ continue;
+ }
+
+ const eq = line.indexOf("=");
+ if (eq === -1) continue;
+ const key = line.slice(0, eq).trim();
+ const value = line.slice(eq + 1);
+ assign(key, parseScalar(value));
+ }
+
+ return root;
+}
+
+function asStringArray(value: unknown): string[] {
+ if (Array.isArray(value)) {
+ return value
+ .map((item) => (typeof item === "string" ? item : String(item)))
+ .filter(Boolean);
+ }
+ if (typeof value === "string" && value.length > 0) return [value];
+ return [];
+}
+
+/** Normalize a parsed TOML document into the subset we consume. */
+export function toStellarToml(parsed: Record): StellarToml {
+ const verifiedCreatorsRaw = parsed.VERIFIED_CREATORS;
+ const verifiedCreators = Array.isArray(verifiedCreatorsRaw)
+ ? (verifiedCreatorsRaw as Array>).map((entry) => ({
+ account: typeof entry.ACCOUNT === "string" ? entry.ACCOUNT : undefined,
+ name: typeof entry.NAME === "string" ? entry.NAME : undefined,
+ handle: typeof entry.HANDLE === "string" ? entry.HANDLE : undefined,
+ }))
+ : [];
+
+ return {
+ signingKey:
+ typeof parsed.SIGNING_KEY === "string" ? parsed.SIGNING_KEY : undefined,
+ accounts: asStringArray(parsed.ACCOUNTS),
+ verifiedCreators,
+ ...parsed,
+ };
+}
+
+export interface TomlAccountMatch {
+ found: boolean;
+ name?: string;
+ handle?: string;
+}
+
+/** Check whether a creator account is declared in a parsed `stellar.toml`. */
+export function findCreatorInToml(
+ toml: StellarToml,
+ account: string,
+): TomlAccountMatch {
+ const normalized = account.trim().toLowerCase();
+ if (toml.accounts.some((a) => a.trim().toLowerCase() === normalized)) {
+ return { found: true };
+ }
+ for (const creator of toml.verifiedCreators) {
+ if (creator.account && creator.account.trim().toLowerCase() === normalized) {
+ return { found: true, name: creator.name, handle: creator.handle };
+ }
+ }
+ return { found: false };
+}
+
+/** Fetch and parse a SEP-1 `stellar.toml`. Requires an HTTPS origin. */
+export async function fetchStellarToml(url: string): Promise {
+ let parsedUrl: URL;
+ try {
+ parsedUrl = new URL(url);
+ } catch {
+ throw new Error("Invalid stellar.toml URL.");
+ }
+ if (parsedUrl.protocol !== "https:") {
+ throw new Error("stellar.toml must be served over HTTPS.");
+ }
+
+ const response = await fetch(url, { redirect: "error" });
+ if (!response.ok) {
+ throw new Error(`Could not fetch stellar.toml (${response.status}).`);
+ }
+ const text = await response.text();
+ return toStellarToml(parseStellarToml(text));
+}
diff --git a/src/lib/identity/store.ts b/src/lib/identity/store.ts
new file mode 100644
index 00000000..ef1e47c5
--- /dev/null
+++ b/src/lib/identity/store.ts
@@ -0,0 +1,53 @@
+import type { CreatorVerification } from "./types";
+
+/**
+ * Local persistence of verification claims keyed by creator address. SEP-1/SEP-12
+ * verification is fully reproducible from a creator's `stellar.toml`, so this
+ * store only caches the result so the verified badge renders consistently across
+ * the app on the viewer's device. A backend-indexed claim is a follow-up.
+ */
+
+const STORAGE_KEY = "prompthash:creator-verifications";
+
+type VerificationMap = Record;
+
+function readMap(): VerificationMap {
+ if (typeof localStorage === "undefined") return {};
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ return raw ? (JSON.parse(raw) as VerificationMap) : {};
+ } catch {
+ return {};
+ }
+}
+
+function writeMap(map: VerificationMap): void {
+ if (typeof localStorage === "undefined") return;
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(map));
+ } catch {
+ // ignore quota / serialization errors — verification is best-effort cache
+ }
+}
+
+export function getStoredVerification(address: string): CreatorVerification | null {
+ const normalized = address.trim().toLowerCase();
+ return readMap()[normalized] ?? null;
+}
+
+export function storeVerification(
+ address: string,
+ verification: CreatorVerification,
+): void {
+ const normalized = address.trim().toLowerCase();
+ const map = readMap();
+ map[normalized] = verification;
+ writeMap(map);
+}
+
+export function clearVerification(address: string): void {
+ const normalized = address.trim().toLowerCase();
+ const map = readMap();
+ delete map[normalized];
+ writeMap(map);
+}
diff --git a/src/lib/identity/types.ts b/src/lib/identity/types.ts
new file mode 100644
index 00000000..7a867153
--- /dev/null
+++ b/src/lib/identity/types.ts
@@ -0,0 +1,62 @@
+/**
+ * SEP-compatible creator identity & verification types.
+ *
+ * The design follows the Stellar Ecosystem Proposal patterns used across the
+ * network:
+ * - SEP-1 (Stellar Info File): a creator proves control of a domain by
+ * publishing a `stellar.toml` that lists their Stellar account and a
+ * `SIGNING_KEY` used to attest identity.
+ * - SEP-12 (KYC / identity): an attestation object (mirroring a SEP-12
+ * customer record) signed by that `SIGNING_KEY` confirms the creator's
+ * verified legal/display identity.
+ */
+
+export type VerificationStatus =
+ | "verified"
+ | "unverified"
+ | "pending"
+ | "error";
+
+export type VerificationMethod =
+ | "sep1-toml"
+ | "sep12-attestation"
+ | "external-link";
+
+export interface CreatorVerification {
+ status: VerificationStatus;
+ method?: VerificationMethod;
+ domain?: string;
+ name?: string;
+ identityType?: "individual" | "organization";
+ issuedAt?: string;
+ stellarTomlUrl?: string;
+ attestationUrl?: string;
+ message?: string;
+}
+
+/** Parsed subset of a SEP-1 `stellar.toml` we rely on. */
+export interface StellarToml {
+ signingKey?: string;
+ accounts: string[];
+ /** Custom array-of-tables advertising verified creators. */
+ verifiedCreators: Array<{
+ account?: string;
+ name?: string;
+ handle?: string;
+ }>;
+ /** Arbitrary extra keys, preserved for debugging/display. */
+ [key: string]: unknown;
+}
+
+/** SEP-12-style signed identity attestation for a creator. */
+export interface Sep12Attestation {
+ schema: "sep12-creator-v1";
+ domain: string;
+ account: string;
+ name: string;
+ type: "individual" | "organization";
+ status: "VERIFIED" | "PENDING" | "REJECTED";
+ issuedAt: string;
+ attestationUrl?: string;
+ signature: string;
+}
diff --git a/src/lib/identity/verify.test.ts b/src/lib/identity/verify.test.ts
new file mode 100644
index 00000000..72979e7a
--- /dev/null
+++ b/src/lib/identity/verify.test.ts
@@ -0,0 +1,121 @@
+import { describe, expect, it } from "vitest";
+import { Keypair } from "@stellar/stellar-base";
+import {
+ buildAttestationPayload,
+ evaluateCreatorVerification,
+ verifySep12Attestation,
+ verifySep1Identity,
+} from "@/lib/identity/verify";
+import { toStellarToml, parseStellarToml } from "@/lib/identity/stellarToml";
+import type { Sep12Attestation, StellarToml } from "@/lib/identity/types";
+
+function signAttestation(
+ attestation: Sep12Attestation,
+ signingKey: Keypair,
+): Sep12Attestation {
+ const payload = buildAttestationPayload(attestation);
+ const signature = signingKey.sign(Buffer.from(payload, "utf8")).toString("base64");
+ return { ...attestation, signature };
+}
+
+const CREATOR = "GCREATOREXAMPLECREATOREXAMPLECREATOREXAMPLECREATOREXAMPLECREATORX";
+
+const TOML_BASE: StellarToml = toStellarToml(
+ parseStellarToml(
+ `SIGNING_KEY = "GORGSIGNINGKEYORGSIGNINGKEYORGSIGNINGKEYORGSIGNINGKEYORGSIGN"
+ACCOUNTS = ["${CREATOR}"]`,
+ ),
+);
+
+describe("verifySep1Identity", () => {
+ it("is true when the creator account is listed", () => {
+ expect(
+ verifySep1Identity({ account: CREATOR, toml: TOML_BASE }),
+ ).toBe(true);
+ });
+
+ it("is false when the creator account is absent", () => {
+ expect(
+ verifySep1Identity({
+ account: "GABSENTABSENTABSENTABSENTABSENTABSENTABSENT",
+ toml: TOML_BASE,
+ }),
+ ).toBe(false);
+ });
+});
+
+describe("verifySep12Attestation", () => {
+ const orgKey = Keypair.random();
+ const tomlWithKey: StellarToml = {
+ ...TOML_BASE,
+ signingKey: orgKey.publicKey(),
+ };
+
+ const baseAttestation: Sep12Attestation = {
+ schema: "sep12-creator-v1",
+ domain: "creator.example",
+ account: CREATOR,
+ name: "Ada Lovelace",
+ type: "individual",
+ status: "VERIFIED",
+ issuedAt: "2026-08-30T00:00:00.000Z",
+ signature: "",
+ };
+
+ it("verifies a valid signature from the published SIGNING_KEY", () => {
+ const signed = signAttestation(baseAttestation, orgKey);
+ expect(verifySep12Attestation({ attestation: signed, signingKey: orgKey.publicKey() })).toBe(
+ true,
+ );
+ });
+
+ it("rejects a signature from a different key", () => {
+ const signed = signAttestation(baseAttestation, Keypair.random());
+ expect(verifySep12Attestation({ attestation: signed, signingKey: orgKey.publicKey() })).toBe(
+ false,
+ );
+ });
+
+ it("rejects when the status is not VERIFIED", () => {
+ const pending: Sep12Attestation = { ...baseAttestation, status: "PENDING", signature: "" };
+ const signed = signAttestation(pending, orgKey);
+ expect(verifySep12Attestation({ attestation: signed, signingKey: orgKey.publicKey() })).toBe(
+ false,
+ );
+ });
+
+ it("rejects a tampered payload", () => {
+ const signed = signAttestation(baseAttestation, orgKey);
+ const tampered: Sep12Attestation = { ...signed, name: "Evil Hacker" };
+ expect(verifySep12Attestation({ attestation: tampered, signingKey: orgKey.publicKey() })).toBe(
+ false,
+ );
+ });
+
+ it("evaluateCreatorVerification returns sep12-attestation when signed", () => {
+ const signed = signAttestation(baseAttestation, orgKey);
+ const result = evaluateCreatorVerification({
+ account: CREATOR,
+ toml: tomlWithKey,
+ attestation: signed,
+ stellarTomlUrl: "https://creator.example/.well-known/stellar.toml",
+ });
+ expect(result.status).toBe("verified");
+ expect(result.method).toBe("sep12-attestation");
+ expect(result.name).toBe("Ada Lovelace");
+ });
+
+ it("evaluateCreatorVerification returns sep1-toml without an attestation", () => {
+ const result = evaluateCreatorVerification({ account: CREATOR, toml: TOML_BASE });
+ expect(result.status).toBe("verified");
+ expect(result.method).toBe("sep1-toml");
+ });
+
+ it("evaluateCreatorVerification returns unverified when account missing", () => {
+ const result = evaluateCreatorVerification({
+ account: "GABSENTABSENTABSENTABSENTABSENTABSENTABSENT",
+ toml: TOML_BASE,
+ });
+ expect(result.status).toBe("unverified");
+ });
+});
diff --git a/src/lib/identity/verify.ts b/src/lib/identity/verify.ts
new file mode 100644
index 00000000..bb83b2b8
--- /dev/null
+++ b/src/lib/identity/verify.ts
@@ -0,0 +1,147 @@
+import { Keypair } from "@stellar/stellar-base";
+import type {
+ CreatorVerification,
+ Sep12Attestation,
+ StellarToml,
+} from "./types";
+import { findCreatorInToml } from "./stellarToml";
+
+/** Deterministic, signable representation of a SEP-12 attestation. */
+export function buildAttestationPayload(attestation: Sep12Attestation): string {
+ return [
+ `schema=${attestation.schema}`,
+ `domain=${attestation.domain}`,
+ `account=${attestation.account}`,
+ `name=${attestation.name}`,
+ `type=${attestation.type}`,
+ `status=${attestation.status}`,
+ `issuedAt=${attestation.issuedAt}`,
+ ].join("\n");
+}
+
+export function isValidEd25519PublicKey(key?: string): boolean {
+ if (!key) return false;
+ try {
+ Keypair.fromPublicKey(key);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+/**
+ * Verify a SEP-12-style attestation against the `SIGNING_KEY` published in the
+ * creator's `stellar.toml` (SEP-1). The signature is a base64 ed25519
+ * signature over {@link buildAttestationPayload}.
+ */
+export function verifySep12Attestation(params: {
+ attestation: Sep12Attestation;
+ signingKey: string;
+}): boolean {
+ const { attestation, signingKey } = params;
+ if (!isValidEd25519PublicKey(signingKey)) return false;
+ if (attestation.status !== "VERIFIED") return false;
+ let signature: Buffer;
+ try {
+ signature = Buffer.from(attestation.signature, "base64");
+ } catch {
+ return false;
+ }
+ try {
+ const keypair = Keypair.fromPublicKey(signingKey);
+ return keypair.verify(Buffer.from(buildAttestationPayload(attestation), "utf8"), signature);
+ } catch {
+ return false;
+ }
+}
+
+/** SEP-1 account ownership check: is the creator account declared in the TOML? */
+export function verifySep1Identity(params: {
+ account: string;
+ toml: StellarToml;
+}): boolean {
+ return findCreatorInToml(params.toml, params.account).found;
+}
+
+export interface EvaluateVerificationParams {
+ account: string;
+ toml: StellarToml;
+ attestation?: Sep12Attestation;
+ stellarTomlUrl?: string;
+ externalVerified?: boolean;
+}
+
+/**
+ * Combine the SEP-1 account check and optional SEP-12 attestation into a single
+ * creator verification result. Falls back to an `external-link` signal when the
+ * marketplace reputation service has already confirmed a link.
+ */
+export function evaluateCreatorVerification(
+ params: EvaluateVerificationParams,
+): CreatorVerification {
+ const { account, toml, attestation, stellarTomlUrl, externalVerified } = params;
+ const match = findCreatorInToml(toml, account);
+
+ if (!match.found) {
+ if (externalVerified) {
+ return {
+ status: "verified",
+ method: "external-link",
+ stellarTomlUrl,
+ message: "Verified via an externally confirmed creator link.",
+ };
+ }
+ return {
+ status: "unverified",
+ stellarTomlUrl,
+ message: "This Stellar account was not found in the domain's stellar.toml.",
+ };
+ }
+
+ const domain = attestation?.domain ?? extractDomain(stellarTomlUrl) ?? "stellar.toml";
+
+ if (attestation && toml.signingKey) {
+ const valid = verifySep12Attestation({
+ attestation,
+ signingKey: toml.signingKey,
+ });
+ if (!valid) {
+ return {
+ status: "error",
+ method: "sep12-attestation",
+ domain,
+ stellarTomlUrl,
+ message: "Attestation signature did not match the domain signing key.",
+ };
+ }
+ return {
+ status: "verified",
+ method: "sep12-attestation",
+ domain,
+ name: attestation.name,
+ identityType: attestation.type,
+ issuedAt: attestation.issuedAt,
+ attestationUrl: attestation.attestationUrl,
+ stellarTomlUrl,
+ message: "Identity verified via SEP-1 discovery and a signed SEP-12 attestation.",
+ };
+ }
+
+ return {
+ status: "verified",
+ method: "sep1-toml",
+ domain,
+ name: match.name,
+ stellarTomlUrl,
+ message: "Creator account confirmed in the domain's stellar.toml (SEP-1).",
+ };
+}
+
+function extractDomain(stellarTomlUrl?: string): string | undefined {
+ if (!stellarTomlUrl) return undefined;
+ try {
+ return new URL(stellarTomlUrl).hostname;
+ } catch {
+ return undefined;
+ }
+}
diff --git a/src/pages/profile/page.tsx b/src/pages/profile/page.tsx
index 3e6af771..cf6ca79e 100644
--- a/src/pages/profile/page.tsx
+++ b/src/pages/profile/page.tsx
@@ -37,6 +37,8 @@ import { NotificationPreferences } from "@/components/NotificationPreferences";
import { PostVersionUpdate } from "@/components/PostVersionUpdate";
import { CreatorReputationPanel } from "@/components/CreatorReputation";
import { ReputationSummary } from "@/components/ReputationSummary";
+import { CreatorVerificationCard } from "@/components/CreatorVerificationCard";
+import { VerifiedCreatorBadge } from "@/components/VerifiedCreatorBadge";
import { RecentlyViewed } from "@/components/RecentlyViewed";
import { SkeletonCard } from "@/components/Skeleton";
import { Badge } from "@/components/ui/badge";
@@ -45,6 +47,7 @@ import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useWallet } from "@/hooks/useWallet";
import { useWalletBalance } from "@/hooks/useWalletBalance";
+import { useCreatorVerification } from "@/hooks/useCreatorVerification";
import { invalidateAllPromptQueries } from "@/hooks/useContractSync";
import { browserStellarConfig } from "@/lib/stellar/browserConfig";
import {
@@ -799,6 +802,8 @@ export default function ProfilePage() {
const creatorShareUrl =
profileAddress != null ? buildCreatorShareUrl(profileAddress) : null;
+ const publicVerification = useCreatorVerification(profileAddress ?? undefined);
+
const createdQuery = useQuery({
queryKey: ["created-prompts", profileAddress],
queryFn: async () =>
@@ -1066,6 +1071,9 @@ export default function ProfilePage() {
{profileAddress}
+
+
+
{activeListingCount} active listing
{activeListingCount === 1 ? "" : "s"}
@@ -1267,6 +1275,7 @@ export default function ProfilePage() {
)}