diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/FaucetButton.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/FaucetButton.tsx
index 48264ebd066..5645b994244 100644
--- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/FaucetButton.tsx
+++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/FaucetButton.tsx
@@ -237,6 +237,13 @@ export function FaucetButton({
{canClaimFaucetQuery.data.type === "unsupported-chain" &&
"Faucet is empty right now"}
+
+ {canClaimFaucetQuery.data.type === "paid-plan-required" && (
+ <>
+ This faucet is temporarily only available to Growth and{" "}
+ Pro customers.
+ >
+ )}
);
}
diff --git a/apps/dashboard/src/app/api/testnet-faucet/can-claim/CanClaimResponseType.ts b/apps/dashboard/src/app/api/testnet-faucet/can-claim/CanClaimResponseType.ts
index 28616e54f33..882cb0031e0 100644
--- a/apps/dashboard/src/app/api/testnet-faucet/can-claim/CanClaimResponseType.ts
+++ b/apps/dashboard/src/app/api/testnet-faucet/can-claim/CanClaimResponseType.ts
@@ -7,4 +7,8 @@ export type CanClaimResponseType =
| {
canClaim: false;
type: "unsupported-chain";
+ }
+ | {
+ canClaim: false;
+ type: "paid-plan-required";
};
diff --git a/apps/dashboard/src/app/api/testnet-faucet/can-claim/route.ts b/apps/dashboard/src/app/api/testnet-faucet/can-claim/route.ts
index 503779977b0..937f7b1de13 100644
--- a/apps/dashboard/src/app/api/testnet-faucet/can-claim/route.ts
+++ b/apps/dashboard/src/app/api/testnet-faucet/can-claim/route.ts
@@ -1,3 +1,4 @@
+import { getTeams } from "@/api/team";
import {
DISABLE_FAUCET_CHAIN_IDS,
THIRDWEB_ACCESS_TOKEN,
@@ -8,6 +9,7 @@ import { ipAddress } from "@vercel/functions";
import { cacheTtl } from "lib/redis";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
+import { FAUCET_REQUIRES_PAID_PLAN_CHAIN_ID } from "../config";
import type { CanClaimResponseType } from "./CanClaimResponseType";
// Note: This handler cannot use "edge" runtime because of Redis usage.
@@ -46,6 +48,30 @@ export const GET = async (req: NextRequest) => {
} catch {}
}
+ // IF the faucet requires a paid plan, check if the user has a paid plan
+ if (FAUCET_REQUIRES_PAID_PLAN_CHAIN_ID.has(chainId)) {
+ // get the teams for the account
+ const teams = await getTeams();
+ if (!teams) {
+ const res: CanClaimResponseType = {
+ canClaim: false,
+ type: "paid-plan-required",
+ };
+ return NextResponse.json(res);
+ }
+ // check if ANY of the customer's teams has "growth" or "pro" plan
+ const hasPaidPlan = teams.some((team) =>
+ ["growth", "pro"].includes(team.billingPlan),
+ );
+ if (!hasPaidPlan) {
+ const res: CanClaimResponseType = {
+ canClaim: false,
+ type: "paid-plan-required",
+ };
+ return NextResponse.json(res);
+ }
+ }
+
if (
!THIRDWEB_ENGINE_URL ||
!THIRDWEB_ENGINE_FAUCET_WALLET ||
diff --git a/apps/dashboard/src/app/api/testnet-faucet/claim/route.ts b/apps/dashboard/src/app/api/testnet-faucet/claim/route.ts
index f2d7d0d6e87..5497fc025ac 100644
--- a/apps/dashboard/src/app/api/testnet-faucet/claim/route.ts
+++ b/apps/dashboard/src/app/api/testnet-faucet/claim/route.ts
@@ -1,3 +1,4 @@
+import { getTeams } from "@/api/team";
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import {
API_SERVER_URL,
@@ -11,6 +12,7 @@ import { startOfToday } from "date-fns";
import { cacheGet, cacheSet } from "lib/redis";
import { type NextRequest, NextResponse } from "next/server";
import { ZERO_ADDRESS, getAddress } from "thirdweb";
+import { FAUCET_REQUIRES_PAID_PLAN_CHAIN_ID } from "../config";
import { getFaucetClaimAmount } from "./claim-amount";
interface RequestTestnetFundsPayload {
@@ -53,36 +55,6 @@ export const POST = async (req: NextRequest) => {
);
}
- // Make sure the connected wallet has a thirdweb account
- const accountRes = await fetch(`${API_SERVER_URL}/v1/account/me`, {
- method: "GET",
- headers: {
- Authorization: `Bearer ${authCookie.value}`,
- },
- });
-
- if (accountRes.status !== 200) {
- // Account not found on this connected address
- return NextResponse.json(
- {
- error: "thirdweb account not found",
- },
- { status: 400 },
- );
- }
-
- const account: { data: Account } = await accountRes.json();
-
- // Make sure the logged-in account has verified its email
- if (!account.data.email) {
- return NextResponse.json(
- {
- error: "Account owner hasn't verified email",
- },
- { status: 400 },
- );
- }
-
const requestBody = (await req.json()) as RequestTestnetFundsPayload;
const { chainId, toAddress, turnstileToken } = requestBody;
if (Number.isNaN(chainId)) {
@@ -150,6 +122,66 @@ export const POST = async (req: NextRequest) => {
);
}
+ // Make sure the connected wallet has a thirdweb account
+ const accountRes = await fetch(`${API_SERVER_URL}/v1/account/me`, {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${authCookie.value}`,
+ },
+ });
+
+ if (accountRes.status !== 200) {
+ // Account not found on this connected address
+ return NextResponse.json(
+ {
+ error: "thirdweb account not found",
+ },
+ { status: 400 },
+ );
+ }
+
+ const account: { data: Account } = await accountRes.json();
+
+ // Make sure the logged-in account has verified its email
+ if (!account.data.email) {
+ return NextResponse.json(
+ {
+ error: "Account owner hasn't verified email",
+ },
+ { status: 400 },
+ );
+ }
+
+ // IF the faucet requires a paid plan, check if the user has a paid plan
+ if (FAUCET_REQUIRES_PAID_PLAN_CHAIN_ID.has(chainId)) {
+ // get the teams for the account
+ const teams = await getTeams();
+ if (!teams) {
+ return NextResponse.json(
+ {
+ error: "No teams found for this account.",
+ },
+ {
+ status: 500,
+ },
+ );
+ }
+ // check if ANY of the customer's teams has "growth" or "pro" plan
+ const hasPaidPlan = teams.some((team) =>
+ ["growth", "pro"].includes(team.billingPlan),
+ );
+ if (!hasPaidPlan) {
+ return NextResponse.json(
+ {
+ error: "Paid plan required to claim on this chain.",
+ },
+ {
+ status: 402,
+ },
+ );
+ }
+ }
+
const ipCacheKey = `testnet-faucet:${chainId}:${ip}`;
const addressCacheKey = `testnet-faucet:${chainId}:${toAddress}`;
const accountCacheKey = `testnet-faucet:${chainId}:${account.data.id}`;
diff --git a/apps/dashboard/src/app/api/testnet-faucet/config.ts b/apps/dashboard/src/app/api/testnet-faucet/config.ts
new file mode 100644
index 00000000000..abdceb3b3d4
--- /dev/null
+++ b/apps/dashboard/src/app/api/testnet-faucet/config.ts
@@ -0,0 +1,3 @@
+export const FAUCET_REQUIRES_PAID_PLAN_CHAIN_ID = new Set([
+ 10143, // monad testnet
+]);