Skip to content

Commit b55b99d

Browse files
[Dashboard] Add x402 payments section and disallow robots
1 parent e19f7a2 commit b55b99d

File tree

10 files changed

+998
-0
lines changed

10 files changed

+998
-0
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: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,48 @@ 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+
}
103+
104+
export interface X402SettlementsByChainId {
105+
date: string;
106+
chainId: string;
107+
totalRequests: number;
108+
totalValue: number;
109+
}
110+
111+
export interface X402SettlementsByPayer {
112+
date: string;
113+
payer: string;
114+
totalRequests: number;
115+
totalValue: number;
116+
}
117+
118+
export interface X402SettlementsByResource {
119+
date: string;
120+
resource: string;
121+
totalRequests: number;
122+
totalValue: number;
123+
}
124+
125+
export interface X402SettlementsByAsset {
126+
date: string;
127+
asset: string;
128+
totalRequests: number;
129+
totalValue: number;
130+
}
131+
132+
export type X402SettlementStats =
133+
| X402SettlementsOverall
134+
| X402SettlementsByChainId
135+
| X402SettlementsByPayer
136+
| X402SettlementsByResource
137+
| X402SettlementsByAsset;
138+
139+
export interface X402QueryParams extends AnalyticsQueryParams {
140+
groupBy?: "overall" | "chainId" | "payer" | "resource" | "asset";
141+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ export function ProjectSidebarLayout(props: {
8080
icon: PayIcon,
8181
label: "Payments",
8282
},
83+
{
84+
href: `${props.layoutPath}/x402`,
85+
icon: PayIcon,
86+
label: "x402",
87+
},
8388
{
8489
href: `${props.layoutPath}/bridge`,
8590
icon: BridgeIcon,
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { CreditCardIcon, UsersIcon } from "lucide-react";
2+
import { Suspense } from "react";
3+
import { getX402Settlements } from "@/api/analytics";
4+
import type { Range } from "@/components/analytics/date-range-selector";
5+
import { StatCard } from "@/components/analytics/stat";
6+
import type {
7+
X402SettlementsByPayer,
8+
X402SettlementsOverall,
9+
} from "@/types/analytics";
10+
11+
function X402SummaryInner(props: {
12+
totalPayments: number | undefined;
13+
totalBuyers: number | undefined;
14+
isPending: boolean;
15+
}) {
16+
return (
17+
<div className="grid grid-cols-2 gap-4">
18+
<StatCard
19+
icon={CreditCardIcon}
20+
isPending={props.isPending}
21+
label="Total Payments"
22+
value={props.totalPayments || 0}
23+
/>
24+
<StatCard
25+
icon={UsersIcon}
26+
isPending={props.isPending}
27+
label="Total Buyers"
28+
value={props.totalBuyers || 0}
29+
/>
30+
</div>
31+
);
32+
}
33+
34+
async function AsyncX402Summary(props: {
35+
teamId: string;
36+
projectId: string;
37+
authToken: string;
38+
range: Range;
39+
}) {
40+
const { teamId, projectId, authToken, range } = props;
41+
42+
const [overallStats, payerStats] = await Promise.all([
43+
getX402Settlements(
44+
{
45+
from: range.from,
46+
period: "all",
47+
projectId,
48+
teamId,
49+
to: range.to,
50+
groupBy: "overall",
51+
},
52+
authToken,
53+
).catch(() => []),
54+
getX402Settlements(
55+
{
56+
from: range.from,
57+
period: "all",
58+
projectId,
59+
teamId,
60+
to: range.to,
61+
groupBy: "payer",
62+
},
63+
authToken,
64+
).catch(() => []),
65+
]);
66+
67+
const totalPayments = (overallStats as X402SettlementsOverall[]).reduce(
68+
(acc, curr) => acc + curr.totalRequests,
69+
0,
70+
);
71+
72+
// Count unique payers
73+
const uniquePayers = new Set(
74+
(payerStats as X402SettlementsByPayer[]).map((stat) => stat.payer),
75+
);
76+
const totalBuyers = uniquePayers.size;
77+
78+
return (
79+
<X402SummaryInner
80+
totalBuyers={totalBuyers}
81+
totalPayments={totalPayments}
82+
isPending={false}
83+
/>
84+
);
85+
}
86+
87+
export function X402Summary(props: {
88+
teamId: string;
89+
projectId: string;
90+
authToken: string;
91+
range: Range;
92+
}) {
93+
return (
94+
<Suspense
95+
fallback={
96+
<X402SummaryInner
97+
totalBuyers={undefined}
98+
totalPayments={undefined}
99+
isPending={true}
100+
/>
101+
}
102+
>
103+
<AsyncX402Summary
104+
authToken={props.authToken}
105+
projectId={props.projectId}
106+
range={props.range}
107+
teamId={props.teamId}
108+
/>
109+
</Suspense>
110+
);
111+
}

0 commit comments

Comments
 (0)