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
55 changes: 55 additions & 0 deletions src/app/api/luca/skills/agent-books/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { NextRequest, NextResponse } from "next/server";
import { v1Auth } from "@/lib/v1-auth";
import { buildAgentBooks, getAgentBySlug } from "@/lib/agent-books";
import { ledgerErrorResponse } from "@/lib/api-utils";
import type { TimeRange } from "@/lib/ledger";

export const dynamic = "force-dynamic";

const VALID_PERIODS = new Set<string>(["7d", "14d", "30d", "90d"]);

export async function POST(req: NextRequest) {
const auth = await v1Auth(req);
if (!auth.ok) return auth.response;

const start = Date.now();
const endpoint = "/api/luca/skills/agent-books";

let body: { slug?: string; period?: string };
try {
body = await req.json() as { slug?: string; period?: string };
} catch {
auth.finish(400, Date.now() - start, endpoint);
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}

const slug = (body.slug ?? "").trim().toLowerCase();
if (!slug) {
auth.finish(400, Date.now() - start, endpoint);
return NextResponse.json({ error: "slug is required" }, { status: 400 });
}

const period = (body.period ?? "30d") as TimeRange;
if (!VALID_PERIODS.has(period)) {
auth.finish(400, Date.now() - start, endpoint);
return NextResponse.json({ error: "period must be 7d, 14d, 30d, or 90d" }, { status: 400 });
}

try {
const agent = await getAgentBySlug(slug);
if (!agent) {
auth.finish(404, Date.now() - start, endpoint);
return NextResponse.json(
{ error: `Agent '${slug}' not found. Use registry-check skill to find the correct slug.` },
{ status: 404 },
);
}

const books = await buildAgentBooks(agent, period);
auth.finish(200, Date.now() - start, endpoint);
return NextResponse.json({ skill: "agent-books", ...books });
} catch (error) {
auth.finish(500, Date.now() - start, endpoint);
return ledgerErrorResponse(error);
}
}
119 changes: 119 additions & 0 deletions src/app/api/luca/skills/luca-report/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { NextRequest, NextResponse } from "next/server";
import { v1Auth } from "@/lib/v1-auth";
import { buildAgentBooks, getAgentBySlug } from "@/lib/agent-books";
import { getWalletStableBalance } from "@/lib/treasury-balance";
import { isBooksEligibleWallet } from "@/lib/wallet-eligibility";
import { toSlug } from "@/app/registry/[slug]/slug";
import { ledgerErrorResponse } from "@/lib/api-utils";
import type { TimeRange } from "@/lib/ledger";

export const dynamic = "force-dynamic";

const VALID_PERIODS = new Set<string>(["7d", "14d", "30d", "90d"]);

function treasuryHealth(balance: number, hasWallets: boolean): string {
if (!hasWallets) return "unknown";
if (balance >= 10_000) return "healthy";
if (balance >= 1_000) return "low";
return "critical";
}

export async function POST(req: NextRequest) {
const auth = await v1Auth(req);
if (!auth.ok) return auth.response;

const start = Date.now();
const endpoint = "/api/luca/skills/luca-report";

let body: { slug?: string; period?: string };
try {
body = await req.json() as { slug?: string; period?: string };
} catch {
auth.finish(400, Date.now() - start, endpoint);
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}

const slug = (body.slug ?? "").trim().toLowerCase();
if (!slug) {
auth.finish(400, Date.now() - start, endpoint);
return NextResponse.json({ error: "slug is required" }, { status: 400 });
}

const period = (body.period ?? "30d") as TimeRange;
if (!VALID_PERIODS.has(period)) {
auth.finish(400, Date.now() - start, endpoint);
return NextResponse.json({ error: "period must be 7d, 14d, 30d, or 90d" }, { status: 400 });
}

try {
const agent = await getAgentBySlug(slug);
if (!agent) {
auth.finish(404, Date.now() - start, endpoint);
return NextResponse.json({ error: `Agent '${slug}' not found` }, { status: 404 });
}

const allWallets = agent.wallets ?? [];
const eligibleWallets = allWallets.filter((w) => isBooksEligibleWallet(w, agent.tokenAddress).eligible);
const attributionTier =
eligibleWallets.length > 0 ? "manifest_attributed" :
allWallets.length > 0 ? "discovered" :
"unattributed";

// Books and treasury balances run in parallel
const [booksResult, ...balanceResults] = await Promise.allSettled([
buildAgentBooks(agent, period),
...eligibleWallets.map((w) => getWalletStableBalance(w.address)),
]);

const books = booksResult.status === "fulfilled" ? booksResult.value : null;

const treasuryWallets = eligibleWallets.map((w, i) => {
const balance = balanceResults[i]?.status === "fulfilled" ? (balanceResults[i] as PromiseFulfilledResult<number>).value : 0;
return {
address: w.address.toLowerCase(),
label: w.label,
role: w.role ?? null,
address_type: w.address_type ?? "unknown",
stable_balance_usd: balance,
chain: w.chain ?? "base",
};
});

const totalBalance = Math.round(
treasuryWallets.reduce((s, w) => s + w.stable_balance_usd, 0) * 100,
) / 100;

auth.finish(200, Date.now() - start, endpoint);
return NextResponse.json({
skill: "luca-report",
agent: {
name: agent.name,
slug: toSlug(agent.name),
ecosystem: agent.ecosystem,
verification_status: agent.verificationStatus,
evidence_sources: agent.evidenceSources ?? [],
erc8004_agent_id: agent.erc8004AgentId ?? null,
erc8004_did: agent.erc8004Did ?? null,
website: agent.website ?? null,
x_handle: agent.xHandle ?? null,
},
attribution: {
tier: attributionTier,
books_eligible_wallets: eligibleWallets.length,
total_wallets: allWallets.length,
source: eligibleWallets.length > 0 ? "manifest" : null,
},
books,
treasury: {
wallets: treasuryWallets,
total_stable_balance_usd: totalBalance,
health: treasuryHealth(totalBalance, eligibleWallets.length > 0),
},
summary: books?.attributed ? books.luca_summary : null,
generated_at: new Date().toISOString(),
});
} catch (error) {
auth.finish(500, Date.now() - start, endpoint);
return ledgerErrorResponse(error);
}
}
117 changes: 117 additions & 0 deletions src/app/api/luca/skills/registry-check/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { NextRequest, NextResponse } from "next/server";
import { v1Auth } from "@/lib/v1-auth";
import { getRegistryAgents } from "@/lib/registry-db";
import { isBooksEligibleWallet } from "@/lib/wallet-eligibility";
import { toSlug } from "@/app/registry/[slug]/slug";

export const dynamic = "force-dynamic";

const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/i;

export async function POST(req: NextRequest) {
const auth = await v1Auth(req);
if (!auth.ok) return auth.response;

const start = Date.now();
const endpoint = "/api/luca/skills/registry-check";

let body: { query?: string };
try {
body = await req.json() as { query?: string };
} catch {
auth.finish(400, Date.now() - start, endpoint);
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}

const query = (body.query ?? "").trim();
if (!query) {
auth.finish(400, Date.now() - start, endpoint);
return NextResponse.json({ error: "query is required (agent slug, name, or 0x wallet address)" }, { status: 400 });
}

try {
const { agents } = await getRegistryAgents();
const isAddress = ADDRESS_RE.test(query);
const queryLower = query.toLowerCase();

let matchType: "slug" | "name" | "wallet_address" | null = null;
type Agent = (typeof agents)[0];
let match: Agent | undefined;

if (isAddress) {
match = agents.find((a) =>
(a.wallets ?? []).some((w) => w.address.toLowerCase() === queryLower),
);
if (match) matchType = "wallet_address";
}

if (!match) {
match = agents.find((a) => toSlug(a.name) === queryLower);
if (match) matchType = "slug";
}

if (!match) {
match = agents.find((a) => a.name.toLowerCase().includes(queryLower));
if (match) matchType = "name";
}

if (!match) {
auth.finish(200, Date.now() - start, endpoint);
return NextResponse.json({
skill: "registry-check",
found: false,
query,
agent: null,
generated_at: new Date().toISOString(),
});
}

const wallets = match.wallets ?? [];
const eligibleWallets = wallets.filter((w) => isBooksEligibleWallet(w, match!.tokenAddress).eligible);

const attributionTier =
eligibleWallets.length > 0 ? "manifest_attributed" :
wallets.length > 0 ? "discovered" :
"unattributed";

auth.finish(200, Date.now() - start, endpoint);
return NextResponse.json({
skill: "registry-check",
found: true,
query,
match_type: matchType,
agent: {
name: match.name,
slug: toSlug(match.name),
ecosystem: match.ecosystem,
verification_status: match.verificationStatus,
evidence_sources: match.evidenceSources ?? [],
erc8004_agent_id: match.erc8004AgentId ?? null,
erc8004_did: match.erc8004Did ?? null,
attribution_tier: attributionTier,
books_eligible_wallets: eligibleWallets.length,
total_wallets: wallets.length,
wallets: wallets.map((w) => {
const elig = isBooksEligibleWallet(w, match!.tokenAddress);
return {
address: w.address.toLowerCase(),
label: w.label,
role: w.role ?? null,
address_type: w.address_type ?? "unknown",
evidence_source: w.evidenceSource ?? null,
books_eligible: elig.eligible,
books_ineligibility_reason: elig.eligible ? null : elig.reason,
chain: w.chain ?? "base",
};
}),
},
generated_at: new Date().toISOString(),
});
} catch (e) {
auth.finish(502, Date.now() - start, endpoint);
return NextResponse.json(
{ error: e instanceof Error ? e.message : "Registry check failed" },
{ status: 502 },
);
}
}
104 changes: 104 additions & 0 deletions src/app/api/luca/skills/revenue-analysis/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { NextRequest, NextResponse } from "next/server";
import { v1Auth } from "@/lib/v1-auth";
import { buildAgentBooks, getAgentBySlug } from "@/lib/agent-books";
import { ledgerErrorResponse } from "@/lib/api-utils";
import type { TimeRange } from "@/lib/ledger";

export const dynamic = "force-dynamic";

const VALID_PERIODS = new Set<string>(["7d", "14d", "30d", "90d"]);

export async function POST(req: NextRequest) {
const auth = await v1Auth(req);
if (!auth.ok) return auth.response;

const start = Date.now();
const endpoint = "/api/luca/skills/revenue-analysis";

let body: { slug?: string; period?: string };
try {
body = await req.json() as { slug?: string; period?: string };
} catch {
auth.finish(400, Date.now() - start, endpoint);
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}

const slug = (body.slug ?? "").trim().toLowerCase();
if (!slug) {
auth.finish(400, Date.now() - start, endpoint);
return NextResponse.json({ error: "slug is required" }, { status: 400 });
}

const period = (body.period ?? "30d") as TimeRange;
if (!VALID_PERIODS.has(period)) {
auth.finish(400, Date.now() - start, endpoint);
return NextResponse.json({ error: "period must be 7d, 14d, 30d, or 90d" }, { status: 400 });
}

try {
const agent = await getAgentBySlug(slug);
if (!agent) {
auth.finish(404, Date.now() - start, endpoint);
return NextResponse.json({ error: `Agent '${slug}' not found` }, { status: 404 });
}

const books = await buildAgentBooks(agent, period);

if (!books.attributed) {
auth.finish(200, Date.now() - start, endpoint);
return NextResponse.json({
skill: "revenue-analysis",
agent: books.agent,
period,
attributed: false,
reason: books.reason,
message: books.message ?? null,
});
}

const { financials, classification, confidence, breakdown } = books;

const recognitionRate = financials.gross_inflow_usd > 0
? Math.round((financials.revenue_usd / financials.gross_inflow_usd) * 100)
: 0;

const excludedPct = 100 - recognitionRate;

auth.finish(200, Date.now() - start, endpoint);
return NextResponse.json({
skill: "revenue-analysis",
agent: books.agent,
period,
attributed: true,
wallets: books.wallets,
revenue: {
gross_inflow_usd: financials.gross_inflow_usd,
operating_revenue_usd: financials.revenue_usd,
quarantined_inflows_usd: classification.quarantined_inflows_usd,
dex_excluded_usd: classification.dex_excluded_usd,
revenue_recognition_rate_pct: recognitionRate,
gross_inflow_is_not_revenue: true,
note: excludedPct > 0
? `${excludedPct}% of gross inflows excluded (capital injections, DEX activity, or internal transfers)`
: "All inflows recognized as operating revenue",
},
expenses: {
total_usd: financials.expenses_usd,
bridge_excluded_usd: classification.bridge_excluded_expense_usd,
dex_excluded_usd: classification.dex_excluded_usd,
net_income_usd: financials.net_income_usd,
margin_pct: financials.margin_pct,
},
confidence,
breakdown: {
revenue_by_source: breakdown.revenue_by_source,
expenses_by_category: breakdown.expenses_by_category,
quarantined_events: classification.quarantined_events,
},
generated_at: books.generated_at,
});
} catch (error) {
auth.finish(500, Date.now() - start, endpoint);
return ledgerErrorResponse(error);
}
}
Loading
Loading