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
42 changes: 42 additions & 0 deletions apps/dashboard/src/@/api/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import type {
WalletStats,
WebhookLatencyStats,
WebhookSummaryStats,
X402QueryParams,
X402SettlementStats,
} from "@/types/analytics";
import { getChains } from "./chain";

Expand Down Expand Up @@ -906,3 +908,43 @@ export function getInsightUsage(
) {
return cached_getInsightUsage(normalizedParams(params), authToken);
}

const cached_getX402Settlements = unstable_cache(
async (
params: X402QueryParams,
authToken: string,
): Promise<X402SettlementStats[]> => {
const searchParams = buildSearchParams(params);

if (params.groupBy) {
searchParams.append("groupBy", params.groupBy);
}

const res = await fetchAnalytics({
authToken,
url: `v2/x402/settlements?${searchParams.toString()}`,
init: {
method: "GET",
},
});

if (res?.status !== 200) {
const reason = await res?.text();
console.error(
`Failed to fetch x402 settlements: ${res?.status} - ${res.statusText} - ${reason}`,
);
return [];
}

const json = await res.json();
return json.data as X402SettlementStats[];
},
["getX402Settlements"],
{
revalidate: 60 * 60, // 1 hour
},
);

export function getX402Settlements(params: X402QueryParams, authToken: string) {
return cached_getX402Settlements(normalizedParams(params), authToken);
}
59 changes: 59 additions & 0 deletions apps/dashboard/src/@/types/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,62 @@ export interface AnalyticsQueryParams {
period?: "day" | "week" | "month" | "year" | "all";
limit?: number;
}

export interface X402SettlementsOverall {
date: string;
totalRequests: number;
totalValue: number;
totalValueUSD: number;
}

interface X402SettlementsByChainId {
date: string;
chainId: string;
totalRequests: number;
totalValue: number;
totalValueUSD: number;
}

export interface X402SettlementsByPayer {
date: string;
payer: string;
totalRequests: number;
totalValue: number;
totalValueUSD: number;
}

interface X402SettlementsByReceiver {
date: string;
receiver: string;
totalRequests: number;
totalValue: number;
totalValueUSD: number;
}

export interface X402SettlementsByResource {
date: string;
resource: string;
totalRequests: number;
totalValue: number;
totalValueUSD: number;
}

interface X402SettlementsByAsset {
date: string;
asset: string;
totalRequests: number;
totalValue: number;
totalValueUSD: number;
}

export type X402SettlementStats =
| X402SettlementsOverall
| X402SettlementsByChainId
| X402SettlementsByPayer
| X402SettlementsByReceiver
| X402SettlementsByResource
| X402SettlementsByAsset;

export interface X402QueryParams extends AnalyticsQueryParams {
groupBy?: "overall" | "chainId" | "payer" | "resource" | "asset";
}
2 changes: 1 addition & 1 deletion apps/dashboard/src/@/utils/number.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const usdCurrencyFormatter = new Intl.NumberFormat("en-US", {
currency: "USD",
maximumFractionDigits: 2, // prefix with $
maximumFractionDigits: 6, // prefix with $
minimumFractionDigits: 0, // don't show decimal places if value is a whole number
notation: "compact", // at max 2 decimal places
roundingMode: "halfEven", // round to nearest even number, standard practice for financial calculations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ export default async function Page(props: {
icon: <WebhookIcon className="size-3.5 text-muted-foreground" />,
},
},
settings: {
href: `/team/${params.team_slug}/${params.project_slug}/settings/payments`,
},
links: [
{
type: "docs",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"use client";
import { Badge } from "@workspace/ui/components/badge";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify Badge import path aligns with project conventions.

The coding guidelines specify importing UI primitives from @/components/ui/* in dashboard apps, but this imports from @workspace/ui/components/badge. Confirm this is the correct import path for your monorepo setup.

As per coding guidelines.


🏁 Script executed:

#!/bin/bash
# Description: Check if Badge is available at the guideline-specified path and verify usage patterns across the dashboard

# Check if Badge exists at @/components/ui/badge
fd -e tsx -e ts . apps/dashboard/src/components/ui/ --exec rg -l "badge"

# Check other Badge import patterns in dashboard
rg -n "from.*badge" apps/dashboard/src/app

Length of output: 9832


Change Badge import from @workspace/ui/components/badge to @/components/ui/badge.

The reviewed file is the only one in the dashboard using @workspace/ui/components/badge; 56+ other files consistently import from @/components/ui/badge. This aligns with the coding guideline requiring imports from @/components/ui/* for dashboard UI primitives.

- import { Badge } from "@workspace/ui/components/badge";
+ import { Badge } from "@/components/ui/badge";
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx
around line 2, update the Badge import to follow the dashboard convention:
replace the import from "@workspace/ui/components/badge" with
"@/components/ui/badge". Ensure the rest of the file uses the same named export
(Badge) and adjust the import path only so it matches the 56+ other files and
the project's UI primitives guideline.

import {
BookTextIcon,
BoxIcon,
Expand Down Expand Up @@ -76,9 +77,13 @@ export function ProjectSidebarLayout(props: {
group: "Monetize",
links: [
{
href: `${props.layoutPath}/payments`,
href: `${props.layoutPath}/x402`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Ensure redirect from old /payments route to /x402.

The route change from /payments to /x402 will break existing bookmarks and external links. Verify that a redirect is configured to maintain backward compatibility.


🏁 Script executed:

#!/bin/bash
# Description: Check for redirect configuration or middleware handling the old /payments route

# Look for redirect configurations in Next.js config or middleware
fd -e js -e ts "next.config|middleware" apps/dashboard/ --exec cat {}

# Search for any redirect/rewrite logic mentioning payments or x402
rg -n "payments.*x402|redirect.*payments" apps/dashboard/src

Length of output: 14154



🏁 Script executed:

#!/bin/bash
# Find the redirects configuration file
fd -type f -name "redirects.*" apps/dashboard/ 2>/dev/null

# Look for the actual redirects file content
find apps/dashboard -name "redirects.*" -exec cat {} \;

# Verify x402 route exists
fd -type f "x402" apps/dashboard/src/app 2>/dev/null || echo "No x402 directory found"

# Look for any x402 references in the file system
fd -path "*x402*" apps/dashboard/src/app 2>/dev/null

# Check if the payments route still exists
fd -path "*payments*" apps/dashboard/src/app 2>/dev/null

Length of output: 11780



🏁 Script executed:

#!/bin/bash
# Search for all x402 references in the codebase
rg -n "x402" apps/dashboard/src --type ts --type tsx

# Check the ProjectSidebarLayout file to understand context
fd -type f "ProjectSidebarLayout.tsx" apps/dashboard/src -exec cat {} \;

# Look for monetize or payments route structure
fd -path "*/sidebar/*" -type d apps/dashboard/src/app | head -20

Length of output: 367



🏁 Script executed:

#!/bin/bash
# Search for x402 references - rg syntax for language type
rg "x402" apps/dashboard/src -g "*.ts" -g "*.tsx"

# Get ProjectSidebarLayout file - search and display
find apps/dashboard/src -name "ProjectSidebarLayout.tsx" -exec cat {} \;

# Check sidebar routes structure
find apps/dashboard/src/app -path "*/sidebar/*" -name "page.tsx" | head -20

Length of output: 7085



🏁 Script executed:

#!/bin/bash
# Check if old payments route still exists in the app directory
find apps/dashboard/src/app -path "*/payments/page.tsx" -o -path "*/payments/layout.tsx" 2>/dev/null | grep -E "sidebar.*payments"

# Search for any /payments route references in sidebar components
rg "payments" apps/dashboard/src/app/\(app\)/team/\[team_slug\]/\[project_slug\]/\(sidebar\)/ -g "*.tsx" -g "*.ts" | grep -v node_modules | head -20

# Check the full projectPageRedirects to see if payments redirects exist
rg -A 2 "projectPageRedirects\s*=" apps/dashboard/next.config.js | head -50

Length of output: 3505



🏁 Script executed:

#!/bin/bash
# Get the complete redirects file content and search for payments redirects
find apps/dashboard -name "redirects.ts" -o -name "redirects.js" | xargs cat | grep -A 5 -B 5 "payments"

Length of output: 1764


Add redirect from project-level /payments routes to /x402.

The sidebar now points to /x402, but the old /payments routes still exist without redirects. This breaks existing bookmarks and creates routing inconsistency. Add a redirect in the projectPageRedirects array (apps/dashboard/redirects.ts) for project-level payment routes:

{
  destination: `${projectRoute}/x402/:path*`,
  permanent: false,
  source: `${projectRoute}/payments/:path*`,
}

Additionally, consider removing or consolidating the orphaned /payments/page.tsx, /settings/payments/page.tsx, and /webhooks/payments/page.tsx files to avoid confusion.

🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx
around line 80 the sidebar link was changed to /x402 but there is no redirect
for legacy project-level /payments routes; add a redirect entry to
apps/dashboard/redirects.ts inside the projectPageRedirects array with source
`${projectRoute}/payments/:path*`, destination `${projectRoute}/x402/:path*`,
and permanent set to false to preserve bookmarks and routing consistency, and
optionally remove or consolidate the orphaned files /payments/page.tsx,
/settings/payments/page.tsx, and /webhooks/payments/page.tsx to avoid confusion.

icon: PayIcon,
label: "Payments",
label: (
<span className="flex items-center gap-2">
x402 <Badge>New</Badge>
</span>
),
},
{
href: `${props.layoutPath}/bridge`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"use client";

import { BotIcon, ServerIcon, WalletIcon } from "lucide-react";
import { FeatureCard } from "../payments/components/FeatureCard.client";

export function QuickStartSection() {
return (
<section>
<div className="mb-4">
<h2 className="font-semibold text-xl tracking-tight">Quick Start</h2>
<p className="text-muted-foreground text-sm">
Choose how to integrate x402 payments into your project.
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<FeatureCard
title="Payment gate your API"
description="Make your endpoints payable with a single line of code"
icon={ServerIcon}
id="x402_server"
setupTime={2}
features={[
"Supports 170+ chains",
"Supports 6.7k+ tokens",
"Dynamic pricing logic",
]}
link={{
href: "https://portal.thirdweb.com/x402/server",
label: "Get Started",
}}
/>

<FeatureCard
title="Let your users pay for x402 resources"
description="Handle x402 payments from any user wallet in your apps"
icon={WalletIcon}
id="x402_client"
setupTime={2}
features={[
"Works with any wallet",
"No gas required",
"One line of code",
]}
link={{
href: "https://portal.thirdweb.com/x402/client",
label: "Get Started",
}}
/>

<FeatureCard
title="Equip your agents with x402 tools"
description="Give your AI agents a wallet and the ability to pay for any x402 resource"
icon={BotIcon}
id="x402_agents"
setupTime={2}
features={[
"Remote MCP server",
"Low level APIs",
"Works with any AI framework",
]}
link={{
href: "https://portal.thirdweb.com/x402/agents",
label: "Get Started",
}}
/>
</div>
</section>
Comment on lines +6 to +67
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Let QuickStartSection accept a className

We need a className passthrough on the root <section> so callers can control layout, and we should type the props/return explicitly. Suggested change:

 import { BotIcon, ServerIcon, WalletIcon } from "lucide-react";
 import { FeatureCard } from "../payments/components/FeatureCard.client";
+import { cn } from "@/lib/utils";
 
-export function QuickStartSection() {
+type QuickStartSectionProps = {
+  className?: string;
+};
+
+export function QuickStartSection({
+  className,
+}: QuickStartSectionProps): JSX.Element {
   return (
-    <section>
+    <section className={cn(className)}>

As per coding guidelines

🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/x402/QuickstartSection.client.tsx
around lines 6 to 67, the component lacks a className passthrough and explicit
typing; change the signature to accept props with an optional className and an
explicit return type (e.g. export function QuickStartSection({ className }: {
className?: string }): JSX.Element), then apply the className to the root
<section> element (e.g. <section className={className}>), preserving existing
markup; if you need to combine with internal classes later, merge them (e.g.
template literal or a classname utility).

);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use client";

import { usePathname, useSearchParams } from "next/navigation";
import { useCallback } from "react";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { Metric } from "./MetricSwitcher";
import { MetricSwitcher } from "./MetricSwitcher";

export function ChartMetricSwitcher() {
const router = useDashboardRouter();
const pathname = usePathname();
const searchParams = useSearchParams();

const metric = (searchParams.get("metric") as Metric) || "volume";

const handleMetricChange = useCallback(
(newMetric: Metric) => {
const params = new URLSearchParams(searchParams.toString());
params.set("metric", newMetric);
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
},
[pathname, router, searchParams],
);

return (
<div className="flex justify-end">
<MetricSwitcher value={metric} onChange={handleMetricChange} />
</div>
);
}
Comment on lines +9 to +30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add explicit return type and className prop.

Per coding guidelines, components should have explicit return types and expose a className prop on the root element for external overrides.

As per coding guidelines

Apply this diff:

+import { cn } from "@/lib/utils";
+
-export function ChartMetricSwitcher() {
+export function ChartMetricSwitcher({
+  className,
+}: {
+  className?: string;
+}): JSX.Element {
   const router = useDashboardRouter();
   const pathname = usePathname();
   const searchParams = useSearchParams();

   const metric = (searchParams.get("metric") as Metric) || "volume";

   const handleMetricChange = useCallback(
     (newMetric: Metric) => {
       const params = new URLSearchParams(searchParams.toString());
       params.set("metric", newMetric);
       router.replace(`${pathname}?${params.toString()}`, { scroll: false });
     },
     [pathname, router, searchParams],
   );

   return (
-    <div className="flex justify-end">
+    <div className={cn("flex justify-end", className)}>
       <MetricSwitcher value={metric} onChange={handleMetricChange} />
     </div>
   );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function ChartMetricSwitcher() {
const router = useDashboardRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const metric = (searchParams.get("metric") as Metric) || "volume";
const handleMetricChange = useCallback(
(newMetric: Metric) => {
const params = new URLSearchParams(searchParams.toString());
params.set("metric", newMetric);
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
},
[pathname, router, searchParams],
);
return (
<div className="flex justify-end">
<MetricSwitcher value={metric} onChange={handleMetricChange} />
</div>
);
}
import { cn } from "@/lib/utils";
export function ChartMetricSwitcher({
className,
}: {
className?: string;
}): JSX.Element {
const router = useDashboardRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const metric = (searchParams.get("metric") as Metric) || "volume";
const handleMetricChange = useCallback(
(newMetric: Metric) => {
const params = new URLSearchParams(searchParams.toString());
params.set("metric", newMetric);
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
},
[pathname, router, searchParams],
);
return (
<div className={cn("flex justify-end", className)}>
<MetricSwitcher value={metric} onChange={handleMetricChange} />
</div>
);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use client";

import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";

export type Metric = "payments" | "volume";

export function MetricSwitcher(props: {
value: Metric;
onChange: (value: Metric) => void;
}) {
return (
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-sm font-medium">Show:</span>
<Select value={props.value} onValueChange={props.onChange}>
<SelectTrigger className="w-[180px] rounded-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="payments">Payments</SelectItem>
<SelectItem value="volume">Volume (USD)</SelectItem>
</SelectContent>
</Select>
</div>
);
}
Comment on lines +13 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Expose className and type MetricSwitcher props

This switcher needs a className passthrough on its root container so parent layouts can control spacing, and we should declare an explicit props type/return type per our TS conventions. Here's a concrete patch:

 import {
   Select,
   SelectContent,
   SelectItem,
   SelectTrigger,
   SelectValue,
 } from "@/components/ui/select";
+import { cn } from "@/lib/utils";
 
 export type Metric = "payments" | "volume";
 
-export function MetricSwitcher(props: {
-  value: Metric;
-  onChange: (value: Metric) => void;
-}) {
+type MetricSwitcherProps = {
+  value: Metric;
+  onChange: (value: Metric) => void;
+  className?: string;
+};
+
+export function MetricSwitcher({
+  value,
+  onChange,
+  className,
+}: MetricSwitcherProps): JSX.Element {
   return (
-    <div className="flex items-center gap-2">
+    <div className={cn("flex items-center gap-2", className)}>
       <span className="text-muted-foreground text-sm font-medium">Show:</span>
-      <Select value={props.value} onValueChange={props.onChange}>
+      <Select value={value} onValueChange={onChange}>

As per coding guidelines

Loading
Loading