Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
570513a
Add Markee featured message widget to footer
pglavin2 Apr 7, 2026
b20849b
Replace simple widget with full in-site Markee sign + buy modal
pglavin2 Apr 7, 2026
0641595
Move Markee sign to bottom of desktop sidebar
pglavin2 Apr 7, 2026
4cc28ce
Fix wagmi version: pin to v2 to match RainbowKit peer dep
pglavin2 Apr 7, 2026
de8b4ec
Address code review: res.ok checks, writeContractAsync, connect flow,…
pglavin2 Apr 8, 2026
b8c1a13
fix: bump tsconfig target to ES2020 for BigInt literal support
pglavin2 Apr 8, 2026
0a649a4
fix: pass undefined instead of placeholder for missing WalletConnect …
pglavin2 Apr 8, 2026
234d75e
fix: revert projectId to placeholder string -- getDefaultConfig requi…
pglavin2 Apr 8, 2026
6d51529
fix: lazy-init OpenAI client to prevent build-time crash when OPENAI_…
pglavin2 Apr 8, 2026
874b110
fix: fall back to default data on API error instead of showing unavai…
pglavin2 Apr 8, 2026
aa5ccb7
feat: proxy markee leaderboard API to avoid CORS dependency
pglavin2 Apr 8, 2026
c41abd8
feat(MarkeeModal): UX improvements and leaderboard fix
pglavin2 Apr 8, 2026
3f1c20a
feat(MarkeeModal): tab UI, boost flow, success modal, and UX fixes
pglavin2 Apr 8, 2026
1a52dee
fix(MarkeeModal): network detection, balance floor, boost UX
pglavin2 Apr 8, 2026
ce40e10
fix(MarkeeModal): success state shows alone, no auto-dismiss
pglavin2 Apr 8, 2026
fce8f16
fix(MarkeeModal): cap ETH input to 8 total digit characters
pglavin2 Apr 8, 2026
5673c2e
fix: replace markee-light.png with correct light variant from markee …
pglavin2 Apr 8, 2026
3037d39
fix(MarkeeModal): balance click fills max 8-digit amount, floored
pglavin2 Apr 8, 2026
df5e8a8
feat(markee): view tracking, moderation system, view count display
pglavin2 Apr 8, 2026
6fc510b
feat(markee): add /api/markee/health integration health check endpoint
pglavin2 Apr 8, 2026
fefe5a6
fix(MarkeeSign): eye icon view count in bottom right of card
pglavin2 Apr 8, 2026
596f1a7
feat(moderation): add 0x00De4B13 as moderator address
pglavin2 Apr 8, 2026
5d3e3fa
Move MarkeeSign from sidebar to homepage hero center
pglavin2 Apr 8, 2026
415364c
Adjust hero spacing around MarkeeSign
pglavin2 Apr 8, 2026
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
9,638 changes: 7,940 additions & 1,698 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,17 @@
"@next/third-parties": "^16.1.6",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@rainbow-me/rainbowkit": "^2.2.10",
"@streamdown/cjk": "^1.0.2",
"@streamdown/code": "^1.0.2",
"@streamdown/math": "^1.0.2",
"@streamdown/mermaid": "^1.0.2",
"@tailwindcss/postcss": "^4.1.18",
"@tanstack/react-query": "^5.96.2",
"@types/node": "^25.0.3",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vercel/kv": "^3.0.0",
"ai": "^6.0.79",
"autoprefixer": "^10.4.23",
"class-variance-authority": "^0.7.1",
Expand All @@ -54,6 +57,8 @@
"tailwindcss": "^4.1.18",
"three": "^0.182.0",
"typescript": "^5.9.3",
"viem": "^2.47.10",
"wagmi": "^2.19.5",
"zod": "^4.3.6"
},
"devDependencies": {
Expand Down
Binary file added public/markee-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 7 additions & 3 deletions src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import path from "node:path";

export const maxDuration = 30;

const openaiClient = new OpenAI();
let _openaiClient: OpenAI | null = null;
function getOpenAIClient() {
if (!_openaiClient) _openaiClient = new OpenAI();
return _openaiClient;
}

// Loaded once at startup from src/data/chatbot-context.md.
const GITCOIN_CONTEXT = fs.readFileSync(
Expand All @@ -16,7 +20,7 @@ const GITCOIN_CONTEXT = fs.readFileSync(

async function searchKnowledgeBase(query: string): Promise<string> {
try {
const results = await openaiClient.vectorStores.search(
const results = await getOpenAIClient().vectorStores.search(
process.env.OPENAI_VECTOR_STORE_ID!,
{ query, max_num_results: 8 },
);
Expand Down Expand Up @@ -69,7 +73,7 @@ async function buildSearchQuery(messages: UIMessage[]): Promise<string> {
.join("\n");

try {
const response = await openaiClient.chat.completions.create({
const response = await getOpenAIClient().chat.completions.create({
model: "gpt-4o-mini",
messages: [
{
Expand Down
178 changes: 178 additions & 0 deletions src/app/api/markee/health/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/**
* GET /api/markee/health
*
* Returns the health status of all Markee integration components:
* - leaderboard : can the API find this leaderboard, and does it have an active message?
* - viewTracking : is the view tracking API reachable and returning counts?
* - moderation : is the Vercel KV store connected?
*
* Overall status: "ok" | "warn" | "error"
* ok — all checks passed
* warn — partial degradation (e.g. no messages yet, view count is 0)
* error — a component is unreachable or misconfigured
*/

import { NextResponse } from "next/server";
import { kv } from "@vercel/kv";
import { LEADERBOARD_ADDRESS_LOWER } from "@/lib/markee";

type CheckStatus = "ok" | "warn" | "error";

interface CheckResult {
status: CheckStatus;
message: string;
detail?: Record<string, unknown>;
}

// ─── Leaderboard ──────────────────────────────────────────────────────────────

interface LeaderboardResult extends CheckResult {
topMarkeeAddress?: string;
topMessage?: string;
}

async function checkLeaderboard(): Promise<LeaderboardResult> {
try {
const res = await fetch(
"https://markee.xyz/api/openinternet/leaderboards",
{ next: { revalidate: 0 } },
);
if (!res.ok) {
return { status: "error", message: `Leaderboard API returned HTTP ${res.status}` };
}

const json = await res.json();
const lb = (json.leaderboards ?? []).find(
(l: { address: string }) => l.address.toLowerCase() === LEADERBOARD_ADDRESS_LOWER,
);

if (!lb) {
return {
status: "error",
message: `Leaderboard ${LEADERBOARD_ADDRESS_LOWER} not found in API — confirm the address is registered`,
};
}

if (!lb.topMessage || lb.topFundsAddedRaw === "0") {
return {
status: "warn",
message: "Leaderboard found but has no messages yet",
detail: { markeeCount: lb.markeeCount },
};
}

return {
status: "ok",
message: "Leaderboard found with active top message",
topMarkeeAddress: lb.topMarkeeAddress,
topMessage: lb.topMessage,
detail: {
topMessage: lb.topMessage,
topMarkeeAddress: lb.topMarkeeAddress,
topMessageOwner: lb.topMessageOwner,
markeeCount: lb.markeeCount,
topFundsAdded: lb.topFundsAddedRaw,
},
};
} catch (err) {
return { status: "error", message: `Leaderboard check threw: ${err}` };
}
}

// ─── View Tracking ────────────────────────────────────────────────────────────

async function checkViewTracking(
topMarkeeAddress?: string,
topMessage?: string,
): Promise<CheckResult> {
if (!topMarkeeAddress || !topMessage) {
return { status: "warn", message: "Skipped — no active markee to check views against" };
}

try {
const params = new URLSearchParams({
address: topMarkeeAddress,
messages: topMessage,
});
const res = await fetch(
`https://markee.xyz/api/views?${params.toString()}`,
{ next: { revalidate: 0 } },
);
if (!res.ok) {
return { status: "error", message: `Views API returned HTTP ${res.status}` };
}

const data = await res.json();
const viewCount = data[topMessage];

if (typeof viewCount !== "number") {
return {
status: "error",
message: "Views API responded but returned an unexpected format",
detail: { raw: data },
};
}

return {
status: viewCount > 0 ? "ok" : "warn",
message: viewCount > 0
? "View tracking is active and returning counts"
: "View tracking reachable but count is 0 — page may not have been visited yet",
detail: { messageViews: viewCount },
};
} catch (err) {
return { status: "error", message: `View tracking check threw: ${err}` };
}
}

// ─── Moderation ───────────────────────────────────────────────────────────────

async function checkModeration(): Promise<CheckResult> {
try {
const flagged = await kv.smembers("moderation:flagged");
return {
status: "ok",
message: "Moderation KV store is connected",
detail: { flaggedCount: Array.isArray(flagged) ? flagged.length : 0 },
};
} catch {
return {
status: "error",
message:
"Moderation KV unavailable — set KV_REST_API_URL and KV_REST_API_TOKEN in Vercel project settings",
};
}
}

// ─── Handler ─────────────────────────────────────────────────────────────────

export async function GET() {
// Leaderboard and moderation checks are independent — run in parallel
const [lbResult, modResult] = await Promise.all([
checkLeaderboard(),
checkModeration(),
]);

// View tracking depends on the top markee address from the leaderboard check
const vtResult = await checkViewTracking(lbResult.topMarkeeAddress, lbResult.topMessage);

const statuses: CheckStatus[] = [lbResult.status, vtResult.status, modResult.status];
const overall: CheckStatus = statuses.includes("error")
? "error"
: statuses.includes("warn")
? "warn"
: "ok";

// Strip internal fields before returning
const { topMarkeeAddress: _a, topMessage: _m, ...lbPublic } = lbResult;

return NextResponse.json({
timestamp: Date.now(),
overall,
checks: {
leaderboard: lbPublic,
viewTracking: vtResult,
moderation: modResult,
},
});
}
16 changes: 16 additions & 0 deletions src/app/api/markee/leaderboards/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const revalidate = 60; // cache for 60s at the CDN edge

export async function GET() {
const res = await fetch(
"https://markee.xyz/api/openinternet/leaderboards",
{ next: { revalidate: 60 } },
);
if (!res.ok) {
return new Response(JSON.stringify({ leaderboards: [] }), {
status: res.status,
headers: { "Content-Type": "application/json" },
});
}
const json = await res.json();
return Response.json(json);
}
31 changes: 31 additions & 0 deletions src/app/api/markee/views/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { NextRequest } from "next/server";

const MARKEE_VIEWS_API = "https://markee.xyz/api/views";

export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const upstream = new URL(MARKEE_VIEWS_API);
searchParams.forEach((v, k) => upstream.searchParams.set(k, v));
const res = await fetch(upstream.toString(), { next: { revalidate: 0 } });
const data = await res.json();
return Response.json(data, { status: res.status });
} catch {
return Response.json({}, { status: 200 });
}
}

export async function POST(req: NextRequest) {
try {
const body = await req.json();
const res = await fetch(MARKEE_VIEWS_API, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await res.json();
return Response.json(data, { status: res.status });
} catch {
return Response.json({ error: "view tracking unavailable" }, { status: 200 });
}
}
84 changes: 84 additions & 0 deletions src/app/api/moderation/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* GET /api/moderation -- returns all flagged markee keys
* POST /api/moderation -- flag or unflag a markee (admin only, signed)
*
* Keys: "{chainId}:{markeeAddress}" stored in a Vercel KV Set.
* Requires KV_REST_API_URL + KV_REST_API_TOKEN in environment.
*/

import { NextRequest, NextResponse } from "next/server";
import { kv } from "@vercel/kv";
import { verifyMessage } from "viem";
import { ADMIN_ADDRESSES } from "@/lib/moderation/config";

const KV_KEY = "moderation:flagged";

function isAdmin(address: string | null): boolean {
if (!address) return false;
return ADMIN_ADDRESSES.some(
(a) => a.toLowerCase() === address.toLowerCase(),
);
}

export async function GET() {
try {
const flagged = await kv.smembers(KV_KEY);
return NextResponse.json({ flagged: flagged ?? [] });
} catch (err) {
console.error("[moderation] GET error:", err);
return NextResponse.json({ flagged: [] });
}
}

export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { markeeId, chainId, action, adminAddress, signature, timestamp } =
body as {
markeeId: string;
chainId: number | string;
action: "flag" | "unflag";
adminAddress: string;
signature: `0x${string}`;
timestamp: number;
};

if (!markeeId || !chainId || !action || !adminAddress || !signature || !timestamp) {
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
}

const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > 300) {
return NextResponse.json({ error: "Signature expired" }, { status: 401 });
}

const message = `markee-moderation:${action}:${chainId}:${markeeId}:${timestamp}`;
const valid = await verifyMessage({
address: adminAddress as `0x${string}`,
message,
signature,
});
if (!valid) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}

if (!isAdmin(adminAddress)) {
return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
}

const key = `${chainId}:${markeeId}`;
if (action === "flag") {
await kv.sadd(KV_KEY, key);
} else if (action === "unflag") {
await kv.srem(KV_KEY, key);
} else {
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
}

const flagged = await kv.smembers(KV_KEY);
return NextResponse.json({ success: true, action, key, flagged: flagged ?? [] });
} catch (err) {
console.error("[moderation] POST error:", err);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
6 changes: 6 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import SearchModal from "@/components/search/SearchModal";
import AIChatSidebar from "@/components/search/AIChatSidebar";
import { ScrollToTop } from "@/components/layout/ScrollToTop";
import { SidebarProvider } from "@/context/SidebarContext";
import { Web3Provider } from "@/providers/Web3Provider";
import { ModerationProvider } from "@/components/moderation";

const inter = Inter({
subsets: ["latin"],
Expand Down Expand Up @@ -112,6 +114,8 @@ export default function RootLayout({
className={`${inter.variable} ${bdoGrotesk.variable} ${source_serif.variable} ${ibm_plex_mono.variable}`}
>
<body className="min-h-screen flex flex-col">
<Web3Provider>
<ModerationProvider>
<SidebarProvider>
<SearchProvider>
<ScrollToTop />
Expand All @@ -122,6 +126,8 @@ export default function RootLayout({
<AIChatSidebar />
</SearchProvider>
</SidebarProvider>
</ModerationProvider>
</Web3Provider>
</body>
<GoogleAnalytics gaId="G-MYMQNTYY27" />
<Script
Expand Down
Loading
Loading