Skip to content

Commit bea4f01

Browse files
[Dashboard] Add x402 payments section (#8394)
1 parent e19f7a2 commit bea4f01

File tree

15 files changed

+1042
-3
lines changed

15 files changed

+1042
-3
lines changed

apps/dashboard/src/@/api/analytics.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import type {
1616
WalletStats,
1717
WebhookLatencyStats,
1818
WebhookSummaryStats,
19+
X402QueryParams,
20+
X402SettlementStats,
1921
} from "@/types/analytics";
2022
import { getChains } from "./chain";
2123

@@ -906,3 +908,43 @@ export function getInsightUsage(
906908
) {
907909
return cached_getInsightUsage(normalizedParams(params), authToken);
908910
}
911+
912+
const cached_getX402Settlements = unstable_cache(
913+
async (
914+
params: X402QueryParams,
915+
authToken: string,
916+
): Promise<X402SettlementStats[]> => {
917+
const searchParams = buildSearchParams(params);
918+
919+
if (params.groupBy) {
920+
searchParams.append("groupBy", params.groupBy);
921+
}
922+
923+
const res = await fetchAnalytics({
924+
authToken,
925+
url: `v2/x402/settlements?${searchParams.toString()}`,
926+
init: {
927+
method: "GET",
928+
},
929+
});
930+
931+
if (res?.status !== 200) {
932+
const reason = await res?.text();
933+
console.error(
934+
`Failed to fetch x402 settlements: ${res?.status} - ${res.statusText} - ${reason}`,
935+
);
936+
return [];
937+
}
938+
939+
const json = await res.json();
940+
return json.data as X402SettlementStats[];
941+
},
942+
["getX402Settlements"],
943+
{
944+
revalidate: 60 * 60, // 1 hour
945+
},
946+
);
947+
948+
export function getX402Settlements(params: X402QueryParams, authToken: string) {
949+
return cached_getX402Settlements(normalizedParams(params), authToken);
950+
}

apps/dashboard/src/@/types/analytics.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,62 @@ export interface AnalyticsQueryParams {
9494
period?: "day" | "week" | "month" | "year" | "all";
9595
limit?: number;
9696
}
97+
98+
export interface X402SettlementsOverall {
99+
date: string;
100+
totalRequests: number;
101+
totalValue: number;
102+
totalValueUSD: number;
103+
}
104+
105+
interface X402SettlementsByChainId {
106+
date: string;
107+
chainId: string;
108+
totalRequests: number;
109+
totalValue: number;
110+
totalValueUSD: number;
111+
}
112+
113+
export interface X402SettlementsByPayer {
114+
date: string;
115+
payer: string;
116+
totalRequests: number;
117+
totalValue: number;
118+
totalValueUSD: number;
119+
}
120+
121+
interface X402SettlementsByReceiver {
122+
date: string;
123+
receiver: string;
124+
totalRequests: number;
125+
totalValue: number;
126+
totalValueUSD: number;
127+
}
128+
129+
export interface X402SettlementsByResource {
130+
date: string;
131+
resource: string;
132+
totalRequests: number;
133+
totalValue: number;
134+
totalValueUSD: number;
135+
}
136+
137+
interface X402SettlementsByAsset {
138+
date: string;
139+
asset: string;
140+
totalRequests: number;
141+
totalValue: number;
142+
totalValueUSD: number;
143+
}
144+
145+
export type X402SettlementStats =
146+
| X402SettlementsOverall
147+
| X402SettlementsByChainId
148+
| X402SettlementsByPayer
149+
| X402SettlementsByReceiver
150+
| X402SettlementsByResource
151+
| X402SettlementsByAsset;
152+
153+
export interface X402QueryParams extends AnalyticsQueryParams {
154+
groupBy?: "overall" | "chainId" | "payer" | "resource" | "asset";
155+
}

apps/dashboard/src/@/utils/number.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const usdCurrencyFormatter = new Intl.NumberFormat("en-US", {
22
currency: "USD",
3-
maximumFractionDigits: 2, // prefix with $
3+
maximumFractionDigits: 6, // prefix with $
44
minimumFractionDigits: 0, // don't show decimal places if value is a whole number
55
notation: "compact", // at max 2 decimal places
66
roundingMode: "halfEven", // round to nearest even number, standard practice for financial calculations

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/bridge/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ export default async function Page(props: {
6969
icon: <WebhookIcon className="size-3.5 text-muted-foreground" />,
7070
},
7171
},
72+
settings: {
73+
href: `/team/${params.team_slug}/${params.project_slug}/settings/payments`,
74+
},
7275
links: [
7376
{
7477
type: "docs",

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"use client";
2+
import { Badge } from "@workspace/ui/components/badge";
23
import {
34
BookTextIcon,
45
BoxIcon,
@@ -76,9 +77,13 @@ export function ProjectSidebarLayout(props: {
7677
group: "Monetize",
7778
links: [
7879
{
79-
href: `${props.layoutPath}/payments`,
80+
href: `${props.layoutPath}/x402`,
8081
icon: PayIcon,
81-
label: "Payments",
82+
label: (
83+
<span className="flex items-center gap-2">
84+
x402 <Badge>New</Badge>
85+
</span>
86+
),
8287
},
8388
{
8489
href: `${props.layoutPath}/bridge`,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"use client";
2+
3+
import { BotIcon, ServerIcon, WalletIcon } from "lucide-react";
4+
import { FeatureCard } from "../payments/components/FeatureCard.client";
5+
6+
export function QuickStartSection() {
7+
return (
8+
<section>
9+
<div className="mb-4">
10+
<h2 className="font-semibold text-xl tracking-tight">Quick Start</h2>
11+
<p className="text-muted-foreground text-sm">
12+
Choose how to integrate x402 payments into your project.
13+
</p>
14+
</div>
15+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
16+
<FeatureCard
17+
title="Payment gate your API"
18+
description="Make your endpoints payable with a single line of code"
19+
icon={ServerIcon}
20+
id="x402_server"
21+
setupTime={2}
22+
features={[
23+
"Supports 170+ chains",
24+
"Supports 6.7k+ tokens",
25+
"Dynamic pricing logic",
26+
]}
27+
link={{
28+
href: "https://portal.thirdweb.com/x402/server",
29+
label: "Get Started",
30+
}}
31+
/>
32+
33+
<FeatureCard
34+
title="Let your users pay for x402 resources"
35+
description="Handle x402 payments from any user wallet in your apps"
36+
icon={WalletIcon}
37+
id="x402_client"
38+
setupTime={2}
39+
features={[
40+
"Works with any wallet",
41+
"No gas required",
42+
"One line of code",
43+
]}
44+
link={{
45+
href: "https://portal.thirdweb.com/x402/client",
46+
label: "Get Started",
47+
}}
48+
/>
49+
50+
<FeatureCard
51+
title="Equip your agents with x402 tools"
52+
description="Give your AI agents a wallet and the ability to pay for any x402 resource"
53+
icon={BotIcon}
54+
id="x402_agents"
55+
setupTime={2}
56+
features={[
57+
"Remote MCP server",
58+
"Low level APIs",
59+
"Works with any AI framework",
60+
]}
61+
link={{
62+
href: "https://portal.thirdweb.com/x402/agents",
63+
label: "Get Started",
64+
}}
65+
/>
66+
</div>
67+
</section>
68+
);
69+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"use client";
2+
3+
import { usePathname, useSearchParams } from "next/navigation";
4+
import { useCallback } from "react";
5+
import { useDashboardRouter } from "@/lib/DashboardRouter";
6+
import type { Metric } from "./MetricSwitcher";
7+
import { MetricSwitcher } from "./MetricSwitcher";
8+
9+
export function ChartMetricSwitcher() {
10+
const router = useDashboardRouter();
11+
const pathname = usePathname();
12+
const searchParams = useSearchParams();
13+
14+
const metric = (searchParams.get("metric") as Metric) || "volume";
15+
16+
const handleMetricChange = useCallback(
17+
(newMetric: Metric) => {
18+
const params = new URLSearchParams(searchParams.toString());
19+
params.set("metric", newMetric);
20+
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
21+
},
22+
[pathname, router, searchParams],
23+
);
24+
25+
return (
26+
<div className="flex justify-end">
27+
<MetricSwitcher value={metric} onChange={handleMetricChange} />
28+
</div>
29+
);
30+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"use client";
2+
3+
import {
4+
Select,
5+
SelectContent,
6+
SelectItem,
7+
SelectTrigger,
8+
SelectValue,
9+
} from "@/components/ui/select";
10+
11+
export type Metric = "payments" | "volume";
12+
13+
export function MetricSwitcher(props: {
14+
value: Metric;
15+
onChange: (value: Metric) => void;
16+
}) {
17+
return (
18+
<div className="flex items-center gap-2">
19+
<span className="text-muted-foreground text-sm font-medium">Show:</span>
20+
<Select value={props.value} onValueChange={props.onChange}>
21+
<SelectTrigger className="w-[180px] rounded-full">
22+
<SelectValue />
23+
</SelectTrigger>
24+
<SelectContent>
25+
<SelectItem value="payments">Payments</SelectItem>
26+
<SelectItem value="volume">Volume (USD)</SelectItem>
27+
</SelectContent>
28+
</Select>
29+
</div>
30+
);
31+
}

0 commit comments

Comments
 (0)