Add Alchemy RPC To Any Project using Cursor
Learn how to add a server-safe Alchemy JSON RPC endpoint to any project using Cursor
This guide helps you add a server-safe Alchemy JSON RPC endpoint into almost any repo using a single Cursor prompt, or what we are calling: the “one shot prompt”. Your Alchemy URL stays in .env.local
on the server. The client never sees it; this makes sure your Alchemy API key is protected from client-side attacks.
Prerequisites
- Cursor is open at your project root
- You have an Alchemy API Key ready to paste into
.env.local
- Optional for smoother automation in Cursor:
- Enable Auto run for the terminal
- Add a small command allowlist like
npm run dev
,node
,mkdir
,touch
One shot Cursor prompt
Paste everything below into Cursor chat from your project root:
You are scaffolding a flexible Alchemy JSON-RPC proxy. Apply changes directly in the repo. Do not echo full files unless I ask later. =============================================================================== Step 0 — Detect environment and set "@/..." alias if Next.js =============================================================================== 1) If an /app directory exists → Next.js App Router mode. 2) Else if a /pages directory exists → Next.js Pages Router mode. 3) Else → Fallback Node mode. If in a Next.js mode, open tsconfig.json and ensure: "baseUrl": "." "paths": { "@/*": ["./*"] } Add them if missing while preserving all other options. =============================================================================== Step 1 — Env templates and .gitignore (CREATE BOTH FILES) =============================================================================== 1) Create a file named .env.example with exactly these lines: # Generic default upstream (used if no chain-specific env is set) ALCHEMY_API_URL=https://eth-mainnet.g.alchemy.com/v2/<KEY> # Optional chain-specific overrides ALCHEMY_API_URL_ETH_MAINNET=https://eth-mainnet.g.alchemy.com/v2/<KEY> ALCHEMY_API_URL_BASE_MAINNET=https://base-mainnet.g.alchemy.com/v2/<KEY> ALCHEMY_API_URL_OPTIMISM_MAINNET=https://opt-mainnet.g.alchemy.com/v2/<KEY> ALCHEMY_API_URL_ARBITRUM_MAINNET=https://arb-mainnet.g.alchemy.com/v2/<KEY> ALCHEMY_API_URL_POLYGON_MAINNET=https://polygon-mainnet.g.alchemy.com/v2/<KEY> 2) In the TERMINAL, create .env.local if it does not already exist by copying from .env.example (do not overwrite existing): node -e "const fs=require('fs');if(!fs.existsSync('.env.local')){fs.copyFileSync('.env.example','.env.local');console.log('Created .env.local from .env.example');}else{console.log('.env.local already exists, not modified');}" 3) Open .gitignore. If it does not already include a line for .env.local, append: .env.local =============================================================================== Step 2 — RPC proxy (create ONE implementation based on the detected mode) =============================================================================== --- A) Next.js App Router (app/api/rpc/route.ts, Edge) --- If /app exists, create app/api/rpc/route.ts with this content: ------------------------------------------------------------ import { NextRequest, NextResponse } from "next/server"; export const runtime = "edge"; // Optional per-chain envs, fallback to generic ALCHEMY_API_URL const URLS: Record<string, string | undefined> = { "eth-mainnet": process.env.ALCHEMY_API_URL_ETH_MAINNET, "base-mainnet": process.env.ALCHEMY_API_URL_BASE_MAINNET, "optimism-mainnet": process.env.ALCHEMY_API_URL_OPTIMISM_MAINNET, "arbitrum-mainnet": process.env.ALCHEMY_API_URL_ARBITRUM_MAINNET, "polygon-mainnet": process.env.ALCHEMY_API_URL_POLYGON_MAINNET, }; export async function POST(request: NextRequest) { const chain = request.headers.get("x-chain") || "eth-mainnet"; const upstream = URLS[chain] || process.env.ALCHEMY_API_URL; if (!upstream) { return NextResponse.json( { error: \`Missing upstream for chain "\${chain}". Set ALCHEMY_API_URL or chain-specific env.\` }, { status: 500 } ); } let payload: unknown; try { payload = await request.json(); } catch { return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); } const res = await fetch(upstream, { method: "POST", headers: { "content-type": "application/json" }, // do not forward cookies or browser headers body: JSON.stringify(payload), cache: "no-store", }); let json: any; try { json = await res.json(); } catch { return NextResponse.json( { error: "Upstream returned non-JSON", status: res.status }, { status: 502 } ); } return NextResponse.json(json, { status: res.ok ? 200 : res.status || 502, headers: { "cache-control": "no-store" }, }); } ------------------------------------------------------------ --- B) Next.js Pages Router (pages/api/rpc.ts) --- Else if /pages exists, create pages/api/rpc.ts with this content: ------------------------------------------------------------ import type { NextApiRequest, NextApiResponse } from "next"; const URLS: Record<string, string | undefined> = { "eth-mainnet": process.env.ALCHEMY_API_URL_ETH_MAINNET, "base-mainnet": process.env.ALCHEMY_API_URL_BASE_MAINNET, "optimism-mainnet": process.env.ALCHEMY_API_URL_OPTIMISM_MAINNET, "arbitrum-mainnet": process.env.ALCHEMY_API_URL_ARBITRUM_MAINNET, "polygon-mainnet": process.env.ALCHEMY_API_URL_POLYGON_MAINNET, }; export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== "POST") { res.setHeader("Allow", "POST"); return res.status(405).json({ error: "Method not allowed" }); } const chain = (req.headers["x-chain"] as string) || "eth-mainnet"; const upstream = URLS[chain] || process.env.ALCHEMY_API_URL; if (!upstream) { return res .status(500) .json({ error: \`Missing upstream for chain "\${chain}". Set ALCHEMY_API_URL or chain-specific env.\` }); } const payload = req.body ?? {}; const upstreamRes = await fetch(upstream, { method: "POST", headers: { "content-type": "application/json" }, body: typeof payload === "string" ? payload : JSON.stringify(payload), cache: "no-store", }); let data: any = null; try { data = await upstreamRes.json(); } catch { return res.status(502).json({ error: "Upstream returned non-JSON", status: upstreamRes.status }); } res.setHeader("cache-control", "no-store"); return res.status(upstreamRes.ok ? 200 : upstreamRes.status || 502).json(data); } ------------------------------------------------------------ --- C) Fallback Node mode (server/rpc-proxy.mjs + npm script) --- Else (no /app and no /pages), create server/rpc-proxy.mjs with this content: ------------------------------------------------------------ import http from "node:http"; import { readFileSync, existsSync } from "node:fs"; import { resolve } from "node:path"; // Minimal dotenv loader for .env.local or .env function loadDotEnv() { const files = [".env.local", ".env"]; for (const f of files) { const p = resolve(process.cwd(), f); if (existsSync(p)) { const text = readFileSync(p, "utf8"); for (const line of text.split(/\\r?\\n/)) { const m = line.match(/^\\s*([A-Z0-9_]+)\\s*=\\s*(.*)\\s*$/); if (!m) continue; const [, k, raw] = m; if (process.env[k]) continue; const v = raw.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1"); process.env[k] = v; } } } } loadDotEnv(); // Chain map with optional overrides, fallback to generic ALCHEMY_API_URL const URLS = { "eth-mainnet": process.env.ALCHEMY_API_URL_ETH_MAINNET, "base-mainnet": process.env.ALCHEMY_API_URL_BASE_MAINNET, "optimism-mainnet": process.env.ALCHEMY_API_URL_OPTIMISM_MAINNET, "arbitrum-mainnet": process.env.ALCHEMY_API_URL_ARBITRUM_MAINNET, "polygon-mainnet": process.env.ALCHEMY_API_URL_POLYGON_MAINNET, }; const PORT = process.env.PORT ? Number(process.env.PORT) : 8787; const server = http.createServer(async (req, res) => { // CORS for local dev res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type, x-chain"); if (req.method === "OPTIONS") { res.statusCode = 204; res.end(); return; } if (req.url !== "/rpc" || req.method !== "POST") { res.statusCode = 404; res.setHeader("content-type", "application/json"); res.end(JSON.stringify({ error: "Not found" })); return; } const chain = req.headers["x-chain"]?.toString() || "eth-mainnet"; const upstream = URLS[chain] || process.env.ALCHEMY_API_URL; if (!upstream) { res.statusCode = 500; res.setHeader("content-type", "application/json"); res.end(JSON.stringify({ error: \`Missing upstream for chain "\${chain}". Set ALCHEMY_API_URL or chain-specific env.\` })); return; } let body = ""; for await (const chunk of req) body += chunk; const payload = body || "{}"; try { const upstreamRes = await fetch(upstream, { method: "POST", headers: { "content-type": "application/json" }, body: payload, cache: "no-store", }); const text = await upstreamRes.text(); res.statusCode = upstreamRes.ok ? 200 : upstreamRes.status || 502; res.setHeader("content-type", "application/json"); res.setHeader("cache-control", "no-store"); res.end(text); } catch (e) { res.statusCode = 502; res.setHeader("content-type", "application/json"); res.end(JSON.stringify({ error: "Upstream fetch failed", detail: String(e) })); } }); server.listen(PORT, () => { console.log(\`[rpc-proxy] listening on http://localhost:\${PORT}/rpc\`); }); ------------------------------------------------------------ Also, if in Fallback Node mode, update package.json to include this script (add or merge without removing existing scripts): "rpc-proxy": "node server/rpc-proxy.mjs" =============================================================================== Step 3 — Optional tiny helper and self-test (Next.js modes only) =============================================================================== If in a Next.js mode, create lib/rpc.ts with this content: ------------------------------------------------------------ type JsonRpcError = { code: number; message: string; data?: unknown }; type JsonRpcResponse<T = unknown> = { jsonrpc: "2.0"; id: number | string; result?: T; error?: JsonRpcError }; export async function rpc<T = unknown>( method: string, params: unknown[] = [], chain: "eth-mainnet" | "base-mainnet" | "optimism-mainnet" | "arbitrum-mainnet" | "polygon-mainnet" = "eth-mainnet" ): Promise<T> { const res = await fetch("/api/rpc", { method: "POST", headers: { "content-type": "application/json", "x-chain": chain }, body: JSON.stringify({ jsonrpc: "2.0", id: Date.now(), method, params }), cache: "no-store", }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error(\`RPC HTTP \${res.status}\${text ? \`: \${text}\` : ""}\`); } const data = (await res.json()) as JsonRpcResponse<T>; if (data.error) throw new Error(data.error.message || "RPC error"); return data.result as T; } ------------------------------------------------------------ If in App Router mode, create a dev-only page at app/rpc-selftest/page.tsx with this content: ------------------------------------------------------------ "use client"; import { useEffect, useState } from "react"; import { rpc } from "@/lib/rpc"; function hexToInt(h: string) { try { return parseInt(h, 16); } catch { return NaN; } } export default function SelfTest() { const [ethBlock, setEthBlock] = useState<number | null>(null); const [baseBlock, setBaseBlock] = useState<number | null>(null); const [err, setErr] = useState<string | null>(null); useEffect(() => { (async () => { try { const ethHex = await rpc<string>("eth_blockNumber", [], "eth-mainnet"); const baseHex = await rpc<string>("eth_blockNumber", [], "base-mainnet"); setEthBlock(hexToInt(ethHex)); setBaseBlock(hexToInt(baseHex)); } catch (e: any) { setErr(e?.message ?? "Unknown error"); } })(); }, []); return ( <div className="min-h-screen p-6"> <h1 className="text-2xl font-semibold">RPC proxy self test</h1> <p className="text-sm text-gray-600 mt-1">Calls /api/rpc with x-chain header.</p> {err ? ( <p className="mt-4 text-red-600">Error: {err}</p> ) : ( <div className="mt-4 space-y-2"> <div>eth-mainnet block: <b>{ethBlock ?? "…"}</b></div> <div>base-mainnet block: <b>{baseBlock ?? "…"}</b></div> </div> )} </div> ); } ------------------------------------------------------------ If in Pages Router mode, create docs/RPC-TEST.md with these contents (no code fences needed): # RPC proxy test With the dev server running: curl -s -X POST http://localhost:3000/api/rpc \ -H 'content-type: application/json' \ -H 'x-chain: eth-mainnet' \ -d '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' curl -s -X POST http://localhost:3000/api/rpc \ -H 'content-type: application/json' \ -H 'x-chain: base-mainnet' \ -d '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' ------------------------------------------------------------ =============================================================================== Step 4 — Final instructions for the developer (print in chat) =============================================================================== Tell me: - Which mode was detected (App Router, Pages Router, or Fallback Node). - Which files you created or updated. - Next steps: - Open .env.local and replace <KEY> with your real Alchemy keys (or set per-chain URLs). - If Next.js: run npm run dev. - App Router: visit http://localhost:3000/rpc-selftest - Pages Router: use the curl examples in docs/RPC-TEST.md - If Fallback Node: run npm run rpc-proxy and POST JSON-RPC to http://localhost:8787/rpc with optional header x-chain.
How to call the proxy
Send JSON RPC 2.0 to /api/rpc
in Next.js, or to http://localhost:8787/rpc
in the fallback Node server. Choose the network with x-chain
. If you omit it, the proxy uses the generic ALCHEMY_API_URL
.
curl example
$ curl -s -X POST http://localhost:3000/api/rpc \ > -H 'content-type: application/json' \ > -H 'x-chain: base-mainnet' \ > -d '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}'
fetch example
1 await fetch("/api/rpc", { 2 method: "POST", 3 headers: { 4 "content-type": "application/json", 5 "x-chain": "eth-mainnet", 6 }, 7 body: JSON.stringify({ 8 jsonrpc: "2.0", 9 id: Date.now(), 10 method: "eth_getBalance", 11 params: ["0xYourAddress", "latest"], 12 }), 13 });
Quick verification checklist
.env.example
and.env.local
both exist- One of
ALCHEMY_API_URL
or a chain specific URL is set in.env.local
- App Router projects open
http://localhost:3000/rpc-selftest
and show block numbers for two chains - Pages Router projects can run the curl commands in
docs/RPC-TEST.md
without errors