Proxy Requests to Figment APIs
This guide will show you how to deploy a proxy using a Cloudflare worker to remove API Keys from your clientside codebase and provide a foundational middleware for further security controls. This guide assumes working knowledge of JavaScript and a Cloudflare account.
Motivation
To make use of Figment APIs, calls must be authenticated using your project's API key. In certain instances you may want to make calls from client side applications. Doing so directly would mean having the API Key in the client codebase and visible during network requests. Using a proxy means we can take API Key out of the client side. In addition having a middleware means we can take further steps such as validating CORS Origins, implementing an allowlist for IPs, throttling requests and more.
Disclaimer
It's important to maintain awareness of the security considerations for the environment in which you're operating. The code example we've provided in the guide below is an example to get you started and not meant for production out of the box.
Always use Transport Layer Security (TLS), meaning WSS or HTTPS protocols. Traffic sent over WS or HTTP protocols is not secure, regardless of the method used.
Create a Cloudflare Service Worker
- Cloudflare Dashboard
- Wrangler CLI
Create a Service
- From the Workers Overview tab, click the "Create a Service" button.

- Give the service a name, then click the "Create service" button.

Add Environment Variables
- From the Settings tab the worker, add the necessary environment variables.

You'll need:
API_KEY
- Your Figment API Key, click the "Encrypt" button once you've set the value.ALLOWED_ORIGINS
- A JSON array of all origins that the service should accept calls from e.g.["docs.figment.io", null]
. Note that providingnull
will allow the service to accept calls without Origin headers e.g. terminal curl request. To disable this feature removenull
from the array.PROXY_HOST
- The the URL where the worker is deployed e.g.https://proxy.yourusername.workers.dev
. If you have set up a custom domain in Cloudflare, use that instead.


Add Proxy Code to Cloudflare Worker
- Click on the "Quick edit" button, then copy and paste the code from the Usage section below into the Cloudflare code editor on the left side of the page.

- Save and Deploy the code explained below to the worker, making any adjustments required by your specific use case.

Check out the Cloudflare guide Get Started with Wrangler for more information about the Wrangler CLI.
You can install Wrangler with the command:
npm install -g wrangler
Create a new Worker with Wrangler
First, create a new directory to contain your project:
mkdir api-proxy && cd api-proxy
This command will generate a wrangler.toml
file and the code for a Fetch or Scheduled handler in the current directory:
wrangler init
Follow the prompts, and then you will be able to edit the Worker code in the src
directory.
When you are ready, replace the code in src/index.js
with the sample code we've provided in the Usage section.
The Worker code will be in src/index.ts
if you chose to use TypeScript when creating the Worker.
Usage
For any Figment API and network to which you want to send a proxied request, you would need to add the hostname to that value in env.HOSTS
in the Worker code.
The values for protocols, networks and services in HOSTS
can be constructed as follows:
- The protocol will be either
wss
orhttps
. Not all networks or API endpoints offer WebSocket support. - Check the API Reference table, which shows the APIs supported by each network.
- The APIs are referred to in the code by the variable
services
.
- The APIs are referred to in the code by the variable
- The keys to use can be taken directly from the Figment docs URL:
https://docs.figment.io/api-reference/node-api/ethereum
- The service is
node-api
. - The network (in this example) is
ethereum
, so the value would be the hostname of the endpoint to which you are proxying the request.
- The service is
The highlighted lines indicate where these values go in env.HOSTS
:
HOSTS: {
wss: {
"node-api": {
...
ethereum: "ethereum-mainnet-websocket-endpoint.figment.io",
...
},
},
https: {
"node-api": {
...
ethereum: "ethereum-mainnet-jsonrpc-endpoint.figment.io",
...
},
...
}
}
The hostnames for Figment's API endpoints are available via the user dashboard when viewing a protocol. Simply copy and paste the appropriate value from the user dashboard.
Remember that the hostname does not include the protocol, wss://
or https://
.

Code examples provided as samples only and should not be used in production out of the box. Use at your own risk.
Click here to view the complete example proxy, ≈200 lines of code.
const env = {
API_KEY: API_KEY,
ALLOWED_ORIGINS: JSON.parse(ALLOWED_ORIGINS),
PROXY_HOST: PROXY_HOST,
ALLOWED_METHODS: ["POST", "GET", "OPTIONS"],
HOSTS: {
wss: {
"node-api": {
arbitrum: "",
bnb: "",
celo: "",
"cosmos-tendermint-rpc": "",
ethereum: "",
fantom: "",
kusama: "",
optimism: "",
"osmosis-tendermint-rpc": "",
polkadot: "",
polygon: "",
solana: "",
},
},
https: {
"node-api": {
arbitrum: "",
"avalanche-c-chain": "",
"avalanche-p-chain": "",
"avalanche-x-chain": "",
bnb: "",
celo: "",
"cosmos-lcd": "",
"cosmos-tendermint-rpc": "",
ethereum: "",
fantom: "",
"kusama-sidecar": "",
"mina-graphql": "",
near: "",
optimism: "",
"osmosis-lcd": "",
"osmosis-tendermint-rpc": "",
"polkadot-sidecar": "",
polygon: "",
solana: "",
},
"rewards-api": {
ethereum: "",
polkadot: "",
solana: "",
near: "",
},
"rewards-rates-api": {
ethereum: "",
polkadot: "",
solana: "",
},
"transaction-search-api": {
avalanche: "",
"near-protocol": "",
},
"staking-api": {
avalanche: "",
ethereum: "",
near: "",
polkadot: "",
solana: "",
},
"staking-api-webhooks": {
avalanche: "",
ethereum: "",
near: "",
polkadot: "",
solana: "",
},
},
},
};
/**
* Builds and returns the URL for the node request
* @param {Request} request - inbount request
* @throws {Response} 500 error if network isn't found
* @returns {URL} of the node endpoint
*/
function buildURL(request) {
const [base, service, network, ...routes] =
request.url.split(/(?<!\/)\/(?!\/)/g);
const protocol =
request.headers.get("Upgrade") === "websocket" ? "wss" : "https";
const services = env.HOSTS[protocol][service];
const url = services ? services[network] : null;
if (!url) {
throw new Response(
`service '${service}' for network '${network}' could not be found`,
{
status: 500,
statusText: `Invalid network`,
}
);
}
return new URL(
`${protocol}://${url}/apikey/${env.API_KEY}/${routes.join("/")}`
);
}
/**
* Ensures the request is authorized to proceed
* @param {Request} request - inbound request
* @throws {Response} 403 or 405 if unauthorized
*/
function ensureAuthorized(request) {
let error;
if (!env.ALLOWED_ORIGINS.includes(request.headers.get("Origin"))) {
error = {
message: `Origin ${request.headers.get("Origin")} not allowed`,
status: 405,
statusText: "Not Allowed",
};
} else if (!env.ALLOWED_METHODS.includes(request.method)) {
error = {
message: `Method ${request.method} not allowed`,
status: 405,
statusText: "Not Allowed",
};
} else if (
request.method !== "OPTIONS" &&
request.headers.get("Authorization") !== env.API_KEY
) {
error = {
message: `Not Authorized ${request.method}!`,
status: 403,
statusText: "Not Allowed",
};
}
if (error) {
throw new Response(error.message, {
status: 403,
statusText: `Not Allowed`,
});
}
}
/**
* Builds and returns preflight response based on request
* @param {Request} request - inbound request
* @returns {Response} with required CORS params
*/
function getPreflightResponse(request) {
return new Response(null, {
status: 200,
headers: {
"Access-Control-Allow-Headers":
request.headers.get("Access-Control-Request-Headers") || "",
"Access-Control-Allow-Origin": request.headers.get("Origin") || "",
"Access-Control-Allow-Methods": env.ALLOWED_METHODS.join(","),
"Access-Control-Max-Age": "86400",
},
});
}
/**
* Establishes a websocket connection and returns response containing ws client
* @param {string} url - the figment api url
* @throws {Response} relays status and statusText if connection fails
* @returns {Response} with required websocket client
*/
async function getWebsocketConnection(url) {
const result = await fetch(url, {
headers: {
Upgrade: "websocket",
Authorization: env.API_KEY,
},
});
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/101
if (result.status !== 101) {
throw new Response(null, {
status: result.status,
statusText: result.statusText,
});
}
// Accept the websocket connection
const websocket = result.webSocket;
websocket.accept();
// Create a client/server to act as the proxy layer
const clientServerPair = new WebSocketPair();
const [client, server] = Object.values(clientServerPair);
// Tell the Workers runtime to listen for WebSocket data & keep the connection open
server.accept();
// Any messages from the client are forwarded to the DataHub socket
server.addEventListener("message", (event) => {
websocket.send(event.data);
});
// Any messages coming from DataHub are forwarded back to the client
websocket.addEventListener("message", (event) => {
server.send(event.data);
});
return new Response(null, {
status: 101,
webSocket: client,
});
}
/**
* Fetches results from Figment APIs and returns result
* @param {string} url - the figment api url
* @param {Request} request - inbound request
* @returns {Response} relays Figment API node response
*/
async function getRpcResponse(url, request) {
const apiRequest = new Request(url, request);
apiRequest.headers.set("origin", `https://${env.PROXY_HOST}`);
// Create then rebuild a new response from the API result so it's mutable
let response = await fetch(apiRequest);
response = new Response(response.body, response);
response.headers.set(
"Access-Control-Allow-Headers",
request.headers.get("Access-Control-Request-Headers") || ""
);
response.headers.set(
"Access-Control-Allow-Origin",
request.headers.get("Origin") || ""
);
response.headers.set(
"Access-Control-Allow-Methods",
env.ALLOWED_METHODS.join(",")
);
return response;
}
// Main function to process the incoming fetch request
addEventListener("fetch", function handleFetch(event) {
try {
const { request } = event;
const url = buildURL(request);
ensureAuthorized(request);
if (request.method === "OPTIONS") {
event.respondWith(getPreflightResponse(request));
} else if (request.headers.get("Upgrade") === "websocket") {
event.respondWith(getWebsocketConnection(url));
} else {
event.respondWith(getRpcResponse(url, request));
}
} catch (error) {
event.respondWith(error);
}
});
You're all set to make requests to Figment APIs and pass responses back to the client without exposing your API keys!
To make calls with the proxy, formulate your URL
using <proxy-url>/<service>/<network>
followed by any query parameters
just as with the Figment API calls.
Here are some examples using the JavaScript fetch API, given the proxy http://api-proxy.username.workers.dev
:
Fetch Ethereum gas price from Node API
fetch("http://api-proxy.username.workers.dev/node-api/ethereum", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
method: "eth_gasPrice",
params: [],
id: 1,
}),
});
Fetch Ethereum rewards for a Validator from Rewards API
fetch("http://api-proxy.username.workers.dev/rewards-api/ethereum", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
network: "ethereum",
accounts: [
"0x8914b656ad92ffd6ad2920f8f2ad186b0502e4848ad5c5451fea01e42898a2ea07009afb5ca0fd20119da155d745a299",
],
start: 151759,
end: 151760,
}),
});
Fetch Avalanche transactions from Transaction Search API
GET
requests supply the query parameters in the request URL. There is no request body.
fetch(
"http://api-proxy.username.workers.dev/transaction-search-api/avalanche/transactions?network=avalanche&chain_ids=mainnet&start_height=1921984&address=avax1pvkhyf0y9674p2ps40vmp0a8w427384lpr8zan,avax1rlu93pnalclt7h0dte3evua97lv4fp4qzax5wq",
{
method: "GET",
}
);
Continue with the explanation below, or proceed to the Testing and Troubleshooting section.
Code Explanation
Environment Variables
The env
constant contains the values of the Cloudflare environment variables API_KEY
, ALLOWED_ORIGINS
, PROXY_ORIGIN
as well as a list of ALLOWED_METHODS
and HOSTS
.
It is also useful to define hostnames for the Figment API endpoints to which we would be sending requests, separated by their protocol — either WebSocket Secure (wss
) or SSL (https
).
HOSTS
separates wss
and https
hostnames, and places them into each applicable service. Figment provides access to infrastructure as well as APIs. One such API is the Node API, allowing clients to communicate directly with Proof-of-Stake networks.
Thus, env.HOSTS['wss']['node-api'][0]
would refer to the Arbitrum hostname, which is where clients send their requests to the Arbitrum Node API.
Click here to view the code.
const env = {
API_KEY: API_KEY,
ALLOWED_ORIGINS: JSON.parse(ALLOWED_ORIGINS),
PROXY_HOST: PROXY_HOST,
ALLOWED_METHODS: ["POST", "GET", "OPTIONS"],
HOSTS: {
"wss": {
"node-api": {
"arbitrum": "arbitrum-mainnet-ws-archive-dbgtxn.datahub.figment.io",
...
}
...
},
"https": {
"node-api": {
"arbitrum": "arbitrum-mainnet-rpc-archive-dbgtxn.datahub.figment.io",
...
}
...
},
},
};
Building a Request URL
buildURL
processes an inbound request, returning the completed URL to be queried. This will be the Figment API endpoint, including the route.
protocol
in this scope will bewss
orhttps
. Based on the presence of anUpgrade
header, we know if the incoming connection is requesting to be upgraded to a WebSocket. Each protocol is handled separately.services
in this scope will be one of the defined keys inHOSTS
, assigned to either protocol — ex.node-api
routes
are passed via the request URL, they indicate which Figment API endpoint to query. For example,/node-api/solana
would direct the request to theHOSTS
value for that service and network.
Click here to view the code.
/**
* Builds and returns the URL for the node request
* @param {Request} request - inbount request
* @throws {Response} 500 error if network isn't found
* @returns {URL} of the node endpoint
*/
function buildURL(request) {
const [base, service, network, ...routes] =
request.url.split(/(?<!\/)\/(?!\/)/g);
const protocol =
request.headers.get("Upgrade") === "websocket" ? "wss" : "https";
const services = env.HOSTS[protocol][service];
const url = services ? services[network] : null;
if (!url) {
throw new Response(
`service '${service}' for network '${network}' could not be found`,
{
status: 500,
statusText: `Invalid network`,
}
);
}
return new URL(
`${protocol}://${url}/apikey/${env.API_KEY}/${routes.join("/")}`
);
}
Restricted Access
We want to restrict access to the proxy to Origins
and methods which are explicitly allowed.
If the request does not meet these criteria, or if there is an error, we will respond to the request with the appropriate 4xx
status codes.
Click here to view the code.
/**
* Ensures the request is authorized to proceed
* @param {Request} request - inbound request
* @throws {Response} 403 or 405 if unauthorized
*/
function ensureAuthorized(request) {
let error;
if (!env.ALLOWED_ORIGINS.includes(request.headers.get("Origin"))) {
error = {
message: `Origin ${request.headers.get("Origin")} not allowed`,
status: 405,
statusText: "Not Allowed",
};
} else if (!env.ALLOWED_METHODS.includes(request.method)) {
error = {
message: `Method ${request.method} not allowed`,
status: 405,
statusText: "Not Allowed",
};
} else if (
request.method !== "OPTIONS" &&
request.headers.get("Authorization") !== env.API_KEY
) {
error = {
message: `Not Authorized ${request.method}!`,
status: 403,
statusText: "Not Allowed",
};
}
if (error) {
throw new Response(error.message, {
status: 403,
statusText: `Not Allowed`,
});
}
}
CORS Requests
Handle Cross Origin Resource Sharing (CORS) requests by responding with the appropriate headers.
All CORS preflight requests use the OPTIONS
method but not all OPTIONS
requests are CORS preflight requests.
What this means in practice is that we must treat all OPTIONS
requests as CORS preflight requests.
CORS Resources:
Click here to view the code.
/**
* Builds and returns preflight response based on request
* @param {Request} request - inbound request
* @returns {Response} with required CORS params
*/
function getPreflightResponse(request) {
return new Response(null, {
status: 200,
headers: {
"Access-Control-Allow-Headers":
request.headers.get("Access-Control-Request-Headers") || "",
"Access-Control-Allow-Origin": request.headers.get("Origin") || "",
"Access-Control-Allow-Methods": env.ALLOWED_METHODS.join(","),
"Access-Control-Max-Age": "86400",
},
});
}
Handle WebSocket Connections
getWebsocketConnection
handles WebSocket traffic by upgrading the connection, accepting the request, creating a client/server pair, and then addEventListener
can pass the messages between the client and server.
A successful response must always return status code 101 Switching Protocols
. Read more about the status code, WebSockets and WebSocket security in the Reference section at the end of the guide.
Click here to view the code.
/**
* Establishes a websocket connection and returns response containing ws client
* @param {string} url - the figment api url
* @throws {Response} relays status and statusText if connection fails
* @returns {Response} with required websocket client
*/
async function getWebsocketConnection(url) {
const result = await fetch(url, {
headers: {
Upgrade: "websocket",
Authorization: env.API_KEY,
},
});
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/101
if (result.status !== 101) {
throw new Response(null, {
status: result.status,
statusText: result.statusText,
});
}
// Accept the websocket connection
const websocket = result.webSocket;
websocket.accept();
// Create a client/server to act as the proxy layer
const clientServerPair = new WebSocketPair();
const [client, server] = Object.values(clientServerPair);
// Tell the Workers runtime to listen for WebSocket data & keep the connection open
server.accept();
// Any messages from the client are forwarded to the DataHub socket
server.addEventListener("message", (event) => {
websocket.send(event.data);
});
// Any messages coming from DataHub are forwarded back to the client
websocket.addEventListener("message", (event) => {
server.send(event.data);
});
return new Response(null, {
status: 101,
webSocket: client,
});
}
Handle HTTP Requests
This function uses the fetch
API after setting the Authorization
and Origin
headers.
This is a naive implementation, though it is effective when all you want to do is keep your API key secure.
From the client point of view, the API key is never visible.
Click here to view the code.
/**
* Fetches results from Figment APIs and returns result
* @param {string} url - the figment api url
* @param {Request} request - inbound request
* @returns {Response} relays Figment API node response
*/
async function getRpcResponse(url, request) {
const apiRequest = new Request(url, request);
apiRequest.headers.set("origin", `https://${env.PROXY_HOST}`);
// Create then rebuild a new response from the API result so it's mutable
let response = await fetch(apiRequest);
response = new Response(response.body, response);
response.headers.set(
"Access-Control-Allow-Headers",
request.headers.get("Access-Control-Request-Headers") || ""
);
response.headers.set(
"Access-Control-Allow-Origin",
request.headers.get("Origin") || ""
);
response.headers.set(
"Access-Control-Allow-Methods",
env.ALLOWED_METHODS.join(",")
);
return response;
}
addEventListener
The addEventListener
function defines triggers for a Worker script to execute.
Read more about the fetch API and addEventListener
in the Cloudflare documentation.
In this case we want to tie all of the functionality together, ensuring requests are handled with the appropriate function. The try
/catch
block here is necessary to pass along any errors that occur.
Click here to view the code.
/**
* addEventListener is the Service Worker entrypoint.
* @param {string} - fetch
* @param {callback} - A named function to handle the event
*/
addEventListener("fetch", function handleFetch(event) {
try {
const { request } = event;
const url = buildURL(request);
ensureAuthorized(request);
if (request.method === "OPTIONS") {
event.respondWith(getPreflightResponse(request));
} else if (request.headers.get("Upgrade") === "websocket") {
event.respondWith(getWebsocketConnection(url));
} else {
event.respondWith(getRpcResponse(url, request));
}
} catch (error) {
event.respondWith(error);
}
});
Testing and Troubleshooting
In the Cloudflare worker's quick edit view, you can send various HTTP requests directly to the proxy URL. To verify that the proxy is working with your application:
- Ensure that you have set your deployed app's hostname in the
ALLOWED_ORIGINS
environment variable. - Also ensure the deployed proxy worker's URL is set in the
PROXY_ORIGIN
environment variable. - Send a request to the proxy in Cloudflare:
- If you change the HTTP method in the Cloudflare Service Worker to
POST
, remember to also add aContent-Type
header with a value ofapplication/json
. - To get the current block height of Solana for example, append the service and network (
/node-api/solana
) to the request URL.
- If you change the HTTP method in the Cloudflare Service Worker to
- Check the response. Status
200
and the expected response from the requested method indicate that all is working as intended.


References
- * Cloudflare Blog: Introducing Cloudflare Workers
- Tim Kleyersburg: Create an API proxy with Cloudflare Workers
- MDN Docs: HTTP Status 101
- PortSwigger Web Security Academy: What are WebSockets?
- Bright AppSec Blog: WebSocket Security
You should now have everything you need in order to successfully implement a proxy as a serverless function. Happy building!