An OpenAPI document can generate the request and response types for a Solana data API. That removes much of the manual interface maintenance, but the result is only as accurate as the specification you downloaded. Generated types do not validate a live JSON response and do not automatically update when the server changes.
This walkthrough uses MadeOnSol's OpenAPI document, openapi-typescript for generation, and openapi-fetch for HTTP calls. Scalar's API reference is the human-readable view; it is not the code generator.
1. Save the contract and install the tools
Run these commands in a server-side TypeScript project:
curl --fail --show-error --location https://madeonsol.com/api/v1/openapi.json -o openapi.json
npm install --save-dev openapi-typescript typescript @types/node
npm install openapi-fetch
npx openapi-typescript openapi.json -o src/api-types.ts
Keep the downloaded contract and generated types together in version control. Lock dependency versions after the example works. The generated file exports a paths type; the fetch client uses it to constrain route names, parameters, and response shapes.
2. Match the server URL to the path keys
MadeOnSol's specification declares https://madeonsol.com/api/v1 as its server. Its path keys include /kol/leaderboard and /me. Do not add another /v1 to a path that is already under that base URL.
// src/client.ts — server-side only
import createClient from "openapi-fetch";
import type { paths } from "./api-types";
const apiKey = process.env.MADEONSOL_API_KEY;
if (!apiKey) throw new Error("Set MADEONSOL_API_KEY");
export const client = createClient<paths>({
baseUrl: "https://madeonsol.com/api/v1",
headers: { Authorization: "Bearer " + apiKey },
});
Keep this module out of a browser bundle. A public dashboard should call your own authenticated backend, which can cache and filter data according to your account's redistribution rights.
3. Call the leaderboard with the correct contract
import { client } from "./client";
async function topKols() {
const { data, error, response } = await client.GET("/kol/leaderboard", {
params: { query: { period: "7d", limit: 20 } },
});
if (!response.ok || error || !data) {
throw new Error("Leaderboard failed: HTTP " + response.status);
}
if (!Array.isArray(data.leaderboard)) {
throw new Error("Leaderboard response has no ranking array");
}
for (const kol of data.leaderboard) {
const rate = typeof kol.win_rate === "number"
? kol.win_rate.toFixed(1) + "%"
: "unknown";
console.log(kol.name, kol.wallet, rate);
}
console.log("Universe:", data.universe);
}
topKols().catch(console.error);
The result array is leaderboard. The supported periods are today, 7d, 30d, 90d, and 180d. This route's win_rate is already a percentage, so multiplying it by 100 would misreport the value.
The endpoint ranks a fixed universe: the period's top 50 KOLs by realized PnL. Pagination and alternate sorts operate within that set. Read universe and pagination before claiming to have enumerated all tracked wallets. Sorting/filtering access also depends on tier; TypeScript accepting a parameter does not mean your key is entitled to use it.
4. Separate compile-time types from runtime checks
The openapi-fetch documentation describes its data, error, and response return values. HTTP errors are represented in the result; a failed network request can still throw. Handle both in the caller.
Code generation cannot repair an underspecified schema. Optional fields remain optional, and an unrestricted object remains broad. Options that alphabetize output or make properties readonly do not infer missing response fields. If you need stronger guarantees at a trust boundary, validate the actual response with a schema and report mismatches.
For a wallet activity monitor, those runtime checks should distinguish an empty successful result from an unavailable source. For a token blacklist guard, distinguish a known false from an absent flag.
5. Inspect account quota and response headers
The account endpoint exposes separate daily and burst budgets:
const account = await client.GET("/me");
if (!account.response.ok || !account.data) {
throw new Error("Account lookup failed: " + account.response.status);
}
console.log(account.data.quota);
Normal authenticated responses can include X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, and X-RateLimit-Reset. Reset is Unix seconds; the successful-response quota headers describe the daily bucket. Do not assume they describe the current minute's burst window, and do not treat a missing header as numeric zero.
function optionalNumber(headers: Headers, name: string): number | null {
const raw = headers.get(name);
if (raw === null || raw.trim() === "") return null;
const value = Number(raw);
return Number.isFinite(value) ? value : null;
}
const result = await client.GET("/kol/feed", {
params: { query: { limit: 10 } },
});
const remaining = optionalNumber(result.response.headers, "x-ratelimit-remaining");
const reset = optionalNumber(result.response.headers, "x-ratelimit-reset");
if (result.response.status === 429) {
console.log("Retry after:", result.response.headers.get("retry-after"));
} else if (!result.response.ok) {
throw new Error("Feed failed: " + result.response.status);
}
console.log({ remaining, reset });
A retry policy should honor Retry-After, cap its attempts, and distinguish a burst limit from an exhausted daily allowance. Retrying a 403 with the same key and parameters does not grant access. A subscription upgrade or a narrower request may be needed.
The free tier delays selected live feeds; inspect delay_seconds, as_of, and X-Data-Delay where present. Successful decoding alone does not prove that a result is current enough for your application.
6. Regenerate reproducibly
Add a script to your project:
{
"scripts": {
"generate:api": "openapi-typescript openapi.json -o src/api-types.ts",
"check:types": "tsc --noEmit"
}
}
A deterministic CI check should generate from the committed contract:
npm run generate:api
git diff --exit-code -- src/api-types.ts
npm run check:types
That checks reproducibility and the code that consumes the types. Separately, download a new upstream specification, review its changes, regenerate, and run your integration tests before accepting it. A job that merely compares two downloaded JSON files does not prove that the generated TypeScript is fresh or that your application still compiles.
Codegen or the MadeOnSol SDK?
Use the published madeonsol-sdk TypeScript package when its convenience methods and streaming helpers fit your application. Use generated types when you need a route directly from the documented REST surface or want a contract snapshot in your own build process.