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

1await 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