There are two ways an API client learns about rate limits: it parses X-RateLimit-Remaining headers off every response, or it learns the hard way by getting a 429 and backing off. Both work, neither is great. Header parsing is fiddle-work in every SDK; learning by 429 is rude to the API and leaves your bot off-feed during the back-off.
GET /v1/me is a third option that's strictly better than either: a structured response showing your current quota state — daily used and remaining, burst-window used and remaining, plus per-feature usage counts (webhooks created, copy-trade rules active, watchlist size, etc.). It's the same numbers the rate-limiter is computing internally, exposed as a regular endpoint so your client can self-police instead of probe.
This post walks through what's in the response, when to read it, and a polite-client pattern that uses it to throttle automatically.
The response shape
curl -H "Authorization: Bearer msk_..." https://madeonsol.com/api/v1/me
{
"subscriber": "8b3f...",
"tier": "PRO",
"tier_label": "Pro",
"subscription": {
"status": "active",
"billing_cycle": "monthly",
"current_period_end": "2026-06-14T00:00:00Z",
"started_at": "2026-03-14T11:42:00Z"
},
"quota": {
"daily": {
"limit": 10000,
"used": 4218,
"remaining": 5782,
"resets_at": "2026-05-15T00:00:00Z"
},
"burst": {
"limit": 120,
"used": 42,
"remaining": 78,
"window_seconds": 60
}
},
"features": {
"webhooks": { "limit": 3, "used": 2 },
"ws_connections": { "limit": 1 },
"dex_connections": { "limit": 0 },
"copytrade_wallets": { "limit": 15, "used": 8 },
"copytrade_rules": { "limit": 3, "used": 2 },
"coordination_rules": { "limit": 5, "used": 1 },
"first_touch_subscriptions": { "limit": 0, "used": 0 },
"wallet_tracker_watchlist": { "used": 34 }
}
}
Three blocks. The subscription block tells you when your billing period ends — useful for renewal nags. The quota block tells you headroom right now. The features block tells you how much of your tier's structured-feature budget you've used (webhooks, copy-trade rules, etc.) — separate from request quota.
What daily.remaining and burst.remaining actually mean
Two windows are enforced server-side, both inside verifyAPIProxyAsync (the middleware every authenticated request passes through):
- Daily —
daily.limit requests per UTC day. Resets at UTC midnight (resets_at in the response). Tier defaults: BASIC 200, PRO 10,000, ULTRA 100,000.
- Burst —
burst.limit requests per rolling 60 seconds (burst.window_seconds). Tier defaults: BASIC 10/min, PRO 120/min, ULTRA 600/min.
Both are tracked per subscriber, not per IP. If you have multiple servers calling with the same key, they share one bucket. Splitting traffic across multiple keys is fine — each key has its own bucket — but most users don't need to.
When daily.remaining hits 0, every subsequent request returns 429 until UTC midnight. When burst.remaining hits 0, every subsequent request returns 429 until the rolling 60-second window decays. Both states are visible from /v1/me before they trip.
The self-throttling pattern
The simplest polite-client pattern: poll /v1/me once per minute and update a shared remaining state that the rest of your code reads before firing a request.
import { MadeOnSol } from "madeonsol";
const client = new MadeOnSol({ apiKey: process.env.MADEONSOL_API_KEY! });
class QuotaGate {
private daily = Infinity;
private burst = Infinity;
private resetsAt = 0;
async refresh() {
const me = await client.me();
this.daily = me.quota.daily.remaining;
this.burst = me.quota.burst.remaining;
this.resetsAt = new Date(me.quota.daily.resets_at).getTime();
}
// Reserve a request slot. Returns false if we should skip.
reserve(): boolean {
if (this.burst <= 1 || this.daily <= 1) return false;
this.burst--;
this.daily--;
return true;
}
}
const gate = new QuotaGate();
setInterval(() => gate.refresh().catch(() => {}), 60_000);
await gate.refresh();
// Inside your hot loop:
async function safePoll() {
if (!gate.reserve()) {
console.warn("Quota gate closed — skipping this tick");
return;
}
return client.tokens.list({ ... });
}
The local gate.reserve() decrement is optimistic — it tracks your own usage between refresh ticks so you don't blow past the burst window in a fast loop. The 60-second refresh corrects any drift. The endpoint call itself costs you one request per minute — so the gate's overhead is 1440 requests/day, well under any tier.
For an ULTRA tier doing 6 calls/second of sustained polling, a 60-second refresh is plenty. For BASIC tier doing 200 requests/day total, you don't need self-throttling — you have so much headroom that 429s only happen if you're making a mistake (and /v1/me will show you the mistake).
When to skip /v1/me and just parse headers
If you're writing a one-shot script (a backfill job, a CSV export, a manual cap-table dump), parsing the X-RateLimit-Remaining header off the response you just got is fine. The pattern is:
const res = await fetch("https://madeonsol.com/api/v1/kol/leaderboard", {
headers: { Authorization: `Bearer ${KEY}` },
});
const remaining = Number(res.headers.get("x-ratelimit-remaining") ?? "0");
if (remaining < 10) await sleep(60_000);
The header is always present on authenticated responses. For interactive code that issues a small number of requests, header-based throttling is simpler than maintaining a quota gate.
/v1/me is worth it when your client is long-running (a bot, a dashboard backend, a webhook server) and you want a clean picture of remaining quota without serializing it through every response.
Watching features.*.used for capacity planning
The features block is useful even when you're nowhere near your quota — it shows you how much of your tier's structured-feature budget you've consumed:
{
"webhooks": { "limit": 3, "used": 3 },
"copytrade_rules": { "limit": 3, "used": 3 },
"coordination_rules":{ "limit": 5, "used": 5 }
}
When a used count matches the limit, new rule/webhook creations fail with a 409 until you delete or upgrade. Your management UI should read /v1/me and disable the "create new rule" button when the limit is hit, rather than letting the user submit and getting a 409.
For wallet-tracker watchlists, there's no tier limit exposed today, but wallet_tracker_watchlist.used lets you see how close you are to your effective watchlist size (10 on BASIC, 50 on PRO, 100 on ULTRA per the docs).
Common gotchas
/v1/me itself counts against quota. Polling once a minute is fine; polling once a second is wasteful. The decrement-then-refresh pattern above is the right shape.
- The endpoint is per-key, not per-user. If you regenerate your API key,
used resets to 0 from the new key's perspective even though the old key's traffic doesn't "transfer." The new bucket starts fresh.
burst doesn't drop instantly to zero after a burst. It's a rolling 60-second window — if you used 120 requests in 30 seconds, those 120 will fall out of the window 30 seconds from each request's individual timestamp. The remaining will drift up smoothly as the oldest requests age out.
daily.resets_at is UTC midnight. If you're in UTC+9 and your daily reset feels weird, that's why. Your local-time reset is "9 hours after midnight UTC."