From 806745d90f78ce4d6d6e7da9c5df04789990243c Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Thu, 26 Feb 2026 15:05:11 -0800 Subject: [PATCH 1/5] feat: add faucet API route at /api/faucet --- src/pages/_api/api/faucet.ts | 88 ++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/pages/_api/api/faucet.ts diff --git a/src/pages/_api/api/faucet.ts b/src/pages/_api/api/faucet.ts new file mode 100644 index 00000000..5190bb7c --- /dev/null +++ b/src/pages/_api/api/faucet.ts @@ -0,0 +1,88 @@ +import { createClient, http } from 'viem' +import { tempoModerato } from 'viem/chains' +import { Actions } from 'viem/tempo' + +function getClient() { + return createClient({ + chain: tempoModerato, + transport: http(import.meta.env.VITE_TEMPO_RPC_URL ?? 'https://rpc.moderato.tempo.xyz'), + }) +} + +export async function GET(request: Request): Promise { + const origin = request.headers.get('origin') + const corsHeaders = cors(origin) + + const url = new URL(request.url) + const address = url.searchParams.get('address') + + if (!address || !/^0x[a-fA-F0-9]{40}$/.test(address)) { + return Response.json( + { data: null, error: 'Invalid or missing address parameter' }, + { status: 400, headers: corsHeaders }, + ) + } + + return fund(address.toLowerCase() as `0x${string}`, corsHeaders) +} + +export async function POST(request: Request): Promise { + const origin = request.headers.get('origin') + const corsHeaders = cors(origin) + + let body: { address?: string } + try { + body = await request.json() + } catch { + return Response.json( + { data: null, error: 'Invalid request: could not parse JSON' }, + { status: 400, headers: corsHeaders }, + ) + } + + const address = body?.address + if (!address || !/^0x[a-fA-F0-9]{40}$/.test(address)) { + return Response.json( + { data: null, error: 'Invalid or missing address' }, + { status: 400, headers: corsHeaders }, + ) + } + + return fund(address.toLowerCase() as `0x${string}`, corsHeaders) +} + +export async function OPTIONS(request: Request): Promise { + const origin = request.headers.get('origin') + return new Response(null, { status: 200, headers: cors(origin) }) +} + +async function fund(address: `0x${string}`, headers: Record): Promise { + try { + const client = getClient() + const hashes = await Actions.faucet.fund(client, { account: address }) + + const data = hashes.map((hash) => ({ hash })) + return Response.json({ data, error: null }, { headers }) + } catch (error) { + console.error('Faucet error:', error) + const message = error instanceof Error ? error.message : 'Unknown error' + return Response.json({ data: null, error: message }, { status: 500, headers }) + } +} + +function cors(origin: string | null): Record { + const allowedOrigins = ['https://docs.tempo.xyz', 'https://mainnet.docs.tempo.xyz'] + + if (origin?.includes('vercel.app')) allowedOrigins.push(origin) + if (process.env.NODE_ENV === 'development') allowedOrigins.push('http://localhost:5173') + + const headers: Record = { + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + } + + if (origin && allowedOrigins.some((allowed) => origin.startsWith(allowed))) + headers['Access-Control-Allow-Origin'] = origin + + return headers +} From 96ff9f652f41164918eb68b174a1e6601dbe305c Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Thu, 26 Feb 2026 15:06:48 -0800 Subject: [PATCH 2/5] docs: update faucet curl examples to use docs.tempo.xyz/api/faucet --- src/pages/guide/use-accounts/add-funds.mdx | 6 ++---- src/pages/quickstart/faucet.mdx | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/pages/guide/use-accounts/add-funds.mdx b/src/pages/guide/use-accounts/add-funds.mdx index bac91f30..10c649de 100644 --- a/src/pages/guide/use-accounts/add-funds.mdx +++ b/src/pages/guide/use-accounts/add-funds.mdx @@ -43,14 +43,12 @@ Send test stablecoins to any address.
-Request tokens programmatically via the [Tempo API](https://api.tempo.xyz/docs). +Request tokens programmatically via the faucet API.
```bash -curl "https://api.tempo.xyz/actions/faucet\ -?chainId=42431\ -&address=" +curl "https://docs.tempo.xyz/api/faucet?address=" ```
diff --git a/src/pages/quickstart/faucet.mdx b/src/pages/quickstart/faucet.mdx index 7b06e0ea..ba2b4be6 100644 --- a/src/pages/quickstart/faucet.mdx +++ b/src/pages/quickstart/faucet.mdx @@ -44,14 +44,12 @@ Send test stablecoins to any address.
-Request tokens programmatically via the [Tempo API](https://api.tempo.xyz/docs). +Request tokens programmatically via the faucet API.
```bash -curl "https://api.tempo.xyz/actions/faucet\ -?chainId=42431\ -&address=" +curl "https://docs.tempo.xyz/api/faucet?address=" ```
From 2d890e3277803274df804f0c4591d95cb48645e3 Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Thu, 26 Feb 2026 15:08:01 -0800 Subject: [PATCH 3/5] fix: align faucet CORS with index-supply pattern --- src/pages/_api/api/faucet.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/_api/api/faucet.ts b/src/pages/_api/api/faucet.ts index 5190bb7c..7b3667d1 100644 --- a/src/pages/_api/api/faucet.ts +++ b/src/pages/_api/api/faucet.ts @@ -71,14 +71,14 @@ async function fund(address: `0x${string}`, headers: Record): Pr } function cors(origin: string | null): Record { - const allowedOrigins = ['https://docs.tempo.xyz', 'https://mainnet.docs.tempo.xyz'] + const allowedOrigins = ['https://docs.tempo.xyz'] if (origin?.includes('vercel.app')) allowedOrigins.push(origin) if (process.env.NODE_ENV === 'development') allowedOrigins.push('http://localhost:5173') const headers: Record = { 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Allow-Headers': 'Content-Type, x-api-token', } if (origin && allowedOrigins.some((allowed) => origin.startsWith(allowed))) From 9af3f7e9110b505af9206491e0c33fca6175ca20 Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Thu, 26 Feb 2026 15:09:25 -0800 Subject: [PATCH 4/5] fix: use chain default RPC URL, align error message with index-supply --- src/pages/_api/api/faucet.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/_api/api/faucet.ts b/src/pages/_api/api/faucet.ts index 7b3667d1..423bd2b6 100644 --- a/src/pages/_api/api/faucet.ts +++ b/src/pages/_api/api/faucet.ts @@ -5,7 +5,7 @@ import { Actions } from 'viem/tempo' function getClient() { return createClient({ chain: tempoModerato, - transport: http(import.meta.env.VITE_TEMPO_RPC_URL ?? 'https://rpc.moderato.tempo.xyz'), + transport: http(), }) } @@ -65,7 +65,7 @@ async function fund(address: `0x${string}`, headers: Record): Pr return Response.json({ data, error: null }, { headers }) } catch (error) { console.error('Faucet error:', error) - const message = error instanceof Error ? error.message : 'Unknown error' + const message = error instanceof Error ? error.message : 'Unknown error occurred' return Response.json({ data: null, error: message }, { status: 500, headers }) } } From 95915ce0309934af51507a7d00a3e948d0421b72 Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Thu, 26 Feb 2026 15:12:26 -0800 Subject: [PATCH 5/5] fix: remove GET handler, POST-only to prevent prefetcher/crawler abuse --- src/pages/_api/api/faucet.ts | 19 +------------------ src/pages/guide/use-accounts/add-funds.mdx | 4 +++- src/pages/quickstart/faucet.mdx | 4 +++- 3 files changed, 7 insertions(+), 20 deletions(-) diff --git a/src/pages/_api/api/faucet.ts b/src/pages/_api/api/faucet.ts index 423bd2b6..6051c133 100644 --- a/src/pages/_api/api/faucet.ts +++ b/src/pages/_api/api/faucet.ts @@ -9,23 +9,6 @@ function getClient() { }) } -export async function GET(request: Request): Promise { - const origin = request.headers.get('origin') - const corsHeaders = cors(origin) - - const url = new URL(request.url) - const address = url.searchParams.get('address') - - if (!address || !/^0x[a-fA-F0-9]{40}$/.test(address)) { - return Response.json( - { data: null, error: 'Invalid or missing address parameter' }, - { status: 400, headers: corsHeaders }, - ) - } - - return fund(address.toLowerCase() as `0x${string}`, corsHeaders) -} - export async function POST(request: Request): Promise { const origin = request.headers.get('origin') const corsHeaders = cors(origin) @@ -77,7 +60,7 @@ function cors(origin: string | null): Record { if (process.env.NODE_ENV === 'development') allowedOrigins.push('http://localhost:5173') const headers: Record = { - 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, x-api-token', } diff --git a/src/pages/guide/use-accounts/add-funds.mdx b/src/pages/guide/use-accounts/add-funds.mdx index 10c649de..fcced800 100644 --- a/src/pages/guide/use-accounts/add-funds.mdx +++ b/src/pages/guide/use-accounts/add-funds.mdx @@ -48,7 +48,9 @@ Request tokens programmatically via the faucet API.
```bash -curl "https://docs.tempo.xyz/api/faucet?address=" +curl -X POST https://docs.tempo.xyz/api/faucet \ + -H "Content-Type: application/json" \ + -d '{"address": ""}' ```
diff --git a/src/pages/quickstart/faucet.mdx b/src/pages/quickstart/faucet.mdx index ba2b4be6..26e6b5a6 100644 --- a/src/pages/quickstart/faucet.mdx +++ b/src/pages/quickstart/faucet.mdx @@ -49,7 +49,9 @@ Request tokens programmatically via the faucet API.
```bash -curl "https://docs.tempo.xyz/api/faucet?address=" +curl -X POST https://docs.tempo.xyz/api/faucet \ + -H "Content-Type: application/json" \ + -d '{"address": ""}' ```