GunSpec
Practice

Stay inside the plan

Most quota is spent re-asking for data that has not changed. What each plan allows, which calls are free, and which shapes replace five requests.

Daily ceilings, per key, from the configuration the middleware enforces. Rate limits covers the per-minute side.

PlanRequests / dayMCP calls / dayPages deep
Explorer50205
Builder2,00050025
Studio10,0002,500No ceiling
Enterprise50,00010,000No ceiling

MCP calls are a sub-cap inside the request cap, never an extra allowance: whichever is reached first refuses the call, which the MCP limits page explains. A burst of more than 10 pages in 90 seconds is also refused, so walk the catalog steadily rather than in parallel.

Two of them, and together they are usually the difference between a plan that fits and one that does not.

  • A 304 Not Modified is not counted against your daily cap. Revalidating what you already hold is free; re-downloading it is not. Caching has the mechanics.
  • A response served from your own cache never reaches us at all. Respect the Cache-Control we send and a busy page collapses to one upstream call per lifetime.
  • A webhook delivery is not a request of yours either, since we are calling you. Taking changes as deltas rather than polling for them therefore costs nothing per record, which is what mirroring the catalog is built on.

The same page, the same data, a quarter of the requests. These endpoints exist because the loop that replaces them is the one everybody writes first.

compare.ts
javascript
// Four calls, one page render.const g17 = await gunspec.firearms.get('glock-g17')const p226 = await gunspec.firearms.get('sig-sauer-p226')const m9 = await gunspec.firearms.get('beretta-92fs')const p320 = await gunspec.firearms.get('sig-sauer-p320') // One call, the same page, and the fields already lined up for a// table. Up to five ids, as one comma-separated string.const { data } = await gunspec.firearms.compare({  ids: 'glock-g17,sig-sauer-p226,beretta-92fs,sig-sauer-p320',})
  • Compare up to five records in one call rather than fetching each. The fields come back already lined up.
  • Resolve up to fifty names in one call instead of one at a time, which the resolve guide covers.
  • Ask for what you render with fields, and raise per_page rather than making more pages: one page of a hundred costs one request, five pages of twenty cost five.
  • Prefer an endpoint that already answers your question (variants, similar, attachments) over reassembling it from a list plus a loop.

Caps apply per key, and the API refuses one key at a time, so an account-wide average hides the key that is about to start failing. The usage endpoint reports both, and the same numbers are on your profile.

usage-alert.ts
javascript
// What the account has actually spent, from the same counters// the caps are checked against.const { data } = await gunspec.usage.get() if (data.currentMonth.percentage > 80) {  notify(`${data.currentMonth.used} of ${data.currentMonth.limit} requests used this month`)} // Watch the busiest key, never the account's sum: the API refuses one// key at a time, so an average hides the key about to start failing.const busiest = data.mcp?.busiestKeyTodayif (busiest && busiest.percentage > 80) {  notify(`${busiest.keyName} is at ${busiest.percentage}% of today's MCP allowance`)}

Both are legitimate. The signal tells you which one you are actually looking at.

What you seeWhat it means
Many requests, few distinct recordsCache. You are re-asking for the same rows within their lifetime.
Bursts of 429 at the top of the hourSpread the schedule and add jitter. Your cron is stampeding, not your traffic.
Steady growth in distinct records readUpgrade. This is real demand and no amount of caching removes it.
PLAN_REQUIRED on one endpointUpgrade, or do without that endpoint. A new key on the same plan gets the same answer.
Depth limit hit while walkingFilter the walk into narrower slices, or move to a plan without a page ceiling.