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.
Updated September 2026: this guide now matches the live /api/v1/me schema, including Business limits, per-chain wallet-tracker usage, price-alert capacity, RHC copy-trade usage, and the per-rule copy-trade wallet cap.
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": 300,
"used": 42,
"remaining": 258,
"window_seconds": 60
}
},
"features": {
"webhooks": { "limit": 3, "used": 2 },
"ws_connections": { "limit": 2 },
"dex_connections": { "limit": 0 },
"copytrade_wallets": { "limit": 5, "used": 8 },
"copytrade_wallets_per_rule": { "limit": 5, "used": 4 },
"copytrade_rules": { "limit": 3, "used": 2 },
"coordination_rules": { "limit": 5, "used": 1 },
"first_touch_subscriptions": { "limit": 0, "used": 0 },
"price_alerts": { "limit": 5, "used": 2 },
"wallet_tracker_watchlist": { "limit": 50, "used": 34 },
"rhc_wallet_tracker_watchlist": { "limit": 50, "used": 6 },
"rhc_copytrade_rules": { "limit": 3, "used": 1 }
}
}
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). Current self-serve limits: BASIC 200, PRO 10,000, ULTRA 100,000, BUSINESS 500,000.
- Burst —
burst.limit requests per rolling 60 seconds (burst.window_seconds). Current self-serve limits: BASIC 60/min, PRO 300/min, ULTRA 600/min, BUSINESS 3,000/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 counts against quota. A once-per-minute refresh costs 1,440 requests/day, which is reasonable on PRO and above but would exceed the BASIC daily allowance by itself. On BASIC, read /v1/me at startup and only occasionally afterward (for example every 15–30 minutes), or rely on response headers for each call.
For an ULTRA or BUSINESS integration doing sustained polling, a 60-second refresh is usually enough to keep a shared gate synchronized. For BASIC, the low daily ceiling is exactly why the control loop should be sparse rather than another source of traffic.
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, /v1/me now exposes both limit and used per chain: Solana and Robinhood Chain have separate budgets (50 on Pro, 100 on Ultra, 500 on Business). The Wallet Tracker routes are PRO+; BASIC reports a limit of 0.
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.
- Quota is per subscriber identity, not a loophole per regenerated key. Multiple processes or keys tied to the same subscriber share the effective account budget; do not design a client around key rotation to escape limits.
burst doesn't drop instantly to zero after a burst. It's a rolling 60-second window — if you used 300 requests in 30 seconds, those 300 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."