From 57b077abea06129cbbb00b4d5e27a060038958d4 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Wed, 19 Nov 2025 13:43:36 +1300 Subject: [PATCH] [Dashboard] Add x402 fee configuration and disable robots.txt indexing --- .changeset/tidy-paws-feel.md | 5 + apps/dashboard/src/@/api/x402/config.ts | 27 +++ .../x402/configuration/X402FeeConfig.tsx | 181 ++++++++++++++++++ .../(sidebar)/x402/configuration/page.tsx | 39 ++-- packages/service-utils/src/core/api.ts | 2 + 5 files changed, 241 insertions(+), 13 deletions(-) create mode 100644 .changeset/tidy-paws-feel.md create mode 100644 apps/dashboard/src/@/api/x402/config.ts create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/x402/configuration/X402FeeConfig.tsx diff --git a/.changeset/tidy-paws-feel.md b/.changeset/tidy-paws-feel.md new file mode 100644 index 00000000000..752e994b81d --- /dev/null +++ b/.changeset/tidy-paws-feel.md @@ -0,0 +1,5 @@ +--- +"@thirdweb-dev/service-utils": patch +--- + +Update engineCloud service config diff --git a/apps/dashboard/src/@/api/x402/config.ts b/apps/dashboard/src/@/api/x402/config.ts new file mode 100644 index 00000000000..3b60fdf8207 --- /dev/null +++ b/apps/dashboard/src/@/api/x402/config.ts @@ -0,0 +1,27 @@ +import type { Project } from "@/api/project/projects"; + +type X402Fee = { + feeRecipient: string; + feeBps: number; +}; + +/** + * Extract x402 fee configuration from project's engineCloud service + */ +export function getX402Fees(project: Project): X402Fee { + const engineCloudService = project.services.find( + (service) => service.name === "engineCloud", + ); + + if (!engineCloudService) { + return { + feeRecipient: "", + feeBps: 0, + }; + } + + return { + feeRecipient: engineCloudService.x402FeeRecipient || "", + feeBps: engineCloudService.x402FeeBPS || 0, + }; +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/x402/configuration/X402FeeConfig.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/x402/configuration/X402FeeConfig.tsx new file mode 100644 index 00000000000..da9f88f3333 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/x402/configuration/X402FeeConfig.tsx @@ -0,0 +1,181 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import type { Project } from "@/api/project/projects"; +import { SettingsCard } from "@/components/blocks/SettingsCard"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { updateProjectClient } from "@/hooks/useApi"; +import { + type ApiKeyPayConfigValidationSchema, + apiKeyPayConfigValidationSchema, +} from "@/schema/validations"; + +interface X402FeeConfigProps { + project: Project; + fees: { + feeRecipient: string; + feeBps: number; + }; + projectWalletAddress?: string; +} + +export const X402FeeConfig: React.FC = (props) => { + const form = useForm({ + resolver: zodResolver(apiKeyPayConfigValidationSchema), + values: { + developerFeeBPS: props.fees.feeBps ? props.fees.feeBps / 100 : 0, + payoutAddress: props.fees.feeRecipient ?? "", + }, + }); + + const updateFeeMutation = useMutation({ + mutationFn: async (values: { + payoutAddress: string; + developerFeeBPS: number; + }) => { + // Find and update the engineCloud service + const newServices = props.project.services.map((service) => { + if (service.name === "engineCloud") { + return { + ...service, + x402FeeBPS: values.developerFeeBPS, + x402FeeRecipient: values.payoutAddress, + }; + } + return service; + }); + + // Update the project with the new services configuration + await updateProjectClient( + { + projectId: props.project.id, + teamId: props.project.teamId, + }, + { + services: newServices, + }, + ); + }, + }); + + const handleSubmit = form.handleSubmit( + ({ payoutAddress, developerFeeBPS }) => { + updateFeeMutation.mutate( + { + developerFeeBPS: developerFeeBPS ? developerFeeBPS * 100 : 0, + payoutAddress, + }, + { + onError: (err) => { + toast.error("Failed to update fee configuration"); + console.error(err); + }, + onSuccess: () => { + toast.success("Fee configuration updated"); + }, + }, + ); + }, + (errors) => { + console.log(errors); + }, + ); + + return ( +
+ + +
+

+ Fee Sharing +

+

+ thirdweb collects a 0.3% service fee on x402 transactions. You may + set your own developer fee in addition to this fee. +

+ +
+ ( + + Recipient address + +
+ + {props.projectWalletAddress && ( + + )} +
+
+
+ )} + /> + ( + + Fee amount + +
+ + % +
+
+
+ )} + /> +
+
+
+
+ + ); +}; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/x402/configuration/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/x402/configuration/page.tsx index e87e9e5b966..31caede4261 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/x402/configuration/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/x402/configuration/page.tsx @@ -1,33 +1,46 @@ import { redirect } from "next/navigation"; import { getAuthToken } from "@/api/auth-token"; import { getProject } from "@/api/project/projects"; +import { getTeamBySlug } from "@/api/team/get-team"; +import { getX402Fees } from "@/api/x402/config"; +import { getProjectWallet } from "@/lib/server/project-wallet"; import { loginRedirect } from "@/utils/redirects"; +import { X402FeeConfig } from "./X402FeeConfig"; export default async function Page(props: { params: Promise<{ team_slug: string; project_slug: string }>; }) { - const params = await props.params; + const { team_slug, project_slug } = await props.params; + + const [team, project, authToken] = await Promise.all([ + getTeamBySlug(team_slug), + getProject(team_slug, project_slug), + getAuthToken(), + ]); - const authToken = await getAuthToken(); if (!authToken) { - loginRedirect( - `/team/${params.team_slug}/${params.project_slug}/x402/configuration`, - ); + loginRedirect(`/team/${team_slug}/${project_slug}/x402/configuration`); + } + + if (!team) { + redirect("/team"); } - const project = await getProject(params.team_slug, params.project_slug); if (!project) { - redirect(`/team/${params.team_slug}`); + redirect(`/team/${team_slug}`); } + const projectWallet = await getProjectWallet(project); + + const fees = getX402Fees(project); + return (
-
-

Coming Soon

-

- x402 payments configuration will be available soon. -

-
+
); } diff --git a/packages/service-utils/src/core/api.ts b/packages/service-utils/src/core/api.ts index 58b6e5a8ff0..5e66043b957 100644 --- a/packages/service-utils/src/core/api.ts +++ b/packages/service-utils/src/core/api.ts @@ -242,6 +242,8 @@ export type ProjectService = encryptedAdminKey?: string | null; encryptedWalletAccessToken?: string | null; projectWalletAddress?: string | null; + x402FeeBPS?: number | null; + x402FeeRecipient?: string | null; } | ProjectBundlerService | ProjectEmbeddedWalletsService;