Pokemon restock alerts for AI agents
Give your agent, bot or app the same real-time Pokemon TCG restock alerts our app subscribers get — at the same moment. Connect over the Model Context Protocol, or receive signed webhooks on your own server.
- Real time. On the Live tier, alerts arrive about a second after they're detected — the same moment our own app subscribers get them, with no holdback. The free Reference tier returns the same rows five minutes later.
- Built for agents. Every alert has a retailer link, price, MSRP, markup and a
confidenceflag so your agent can tell a confirmed sighting from an unverified report. - Three ways to consume. Ask what's in stock now, hold a long-poll until the next matching drop, or get a signed webhook POST.
- Coverage. Target, Walmart, PokemonCenter.com, Best Buy, Amazon, Sam's Club and Hot Topic, plus drops reported at GameStop, Costco and Macy's.
Get a free key Live tier — $19.99/month
Free keys are issued instantly. No sales call, no waiting.
How it works
| Endpoint | https://bujusjuj.us/mcp |
|---|---|
| Protocol | MCP over Streamable HTTP (JSON-RPC 2.0). Protocol versions 2025-06-18 and 2025-03-26. Stateless: every request stands alone. |
| Auth | Authorization: Bearer bjs_… on every request |
| Tools | 9 — see Tools |
| Webhooks | Optional, HMAC-SHA256 signed — see Webhooks |
Quickstart
1. Get a key
Go to bujusjujus.com/api-access, enter your email, and open the link we send you. Your developer console is there: take a free Reference key straight away, or subscribe to Live. You don't need an account first — we'll make one.
A key looks like bjs_ followed by 43 characters. It is shown once; we only store a hash, so save it somewhere safe. Lost one? Revoke it in the console and mint another.
2. Connect your agent
Claude Code
claude mcp add --transport http bujusjujus https://bujusjuj.us/mcp \ --header "Authorization: Bearer $BUJUS_API_KEY"
Any MCP client that supports remote HTTP servers with custom headers (the exact file and key names vary by client):
{
"mcpServers": {
"bujusjujus": {
"type": "http",
"url": "https://bujusjuj.us/mcp",
"headers": { "Authorization": "Bearer bjs_YOUR_KEY" }
}
}
}
For a client that only runs local (stdio) servers, bridge it with npx mcp-remote https://bujusjuj.us/mcp --header "Authorization: Bearer bjs_YOUR_KEY".
3. Ask
"What Pokemon products are in stock right now?" · "Tell me the moment a Pitch Black Elite Trainer Box drops at MSRP." · "Is $65 a fair price for that box?"
Or call it directly
Every call is a JSON-RPC POST. Tool results come back as JSON text in result.content[0].text.
curl -s https://bujusjuj.us/mcp \
-H "Authorization: Bearer $BUJUS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"get_in_stock_now","arguments":{"minutes":30}}}'
get_in_stock_now above is a Live tool. On a free Reference key it answers tier_required — try list_recent_drops instead, or call tools/list to see exactly what your key can reach.
Other methods: initialize, tools/list (full JSON Schemas for every tool) and ping. These three are free and are never metered.
The alert object
Every tool that returns alerts uses this shape:
{
"id": "c108d9022699463c", // stable, opaque; dedupe on this
"detectedAt": "2026-09-23T21:00:12.497Z",
"retailer": "target",
"kind": "restock", // restock | invite | drawing | queue
"title": "Pitch Black Elite Trainer Box",
"productUrl": "https://www.target.com/p/-/A-...", // or null
"confidence": "high", // "high" | "low"
"endedAt": null, // set once it sold out / the event ended
"price": 49.99, // USD, or null
"msrp": 49.99, // USD, or null
"markupPct": 0 // % over MSRP, or null
}
confidence: "high"— a confirmed sighting at the retailer."low"— an unverified report that may be stale, sold out or wrong. Live tools return onlyhighunless you passminConfidence: "low". Don't present alowalert as confirmed availability.kind— onlyrestockmeans buyable stock.invite(a retailer invite or waitlist opened),drawing(a Walmart drawing opened) andqueue(a Pokemon Center virtual queue is live) are events, not stock. The live tools return restocks only unless you passkind.endedAt— set once the item sold out or the event ended.get_in_stock_nowleaves those out.productUrlis always on the named retailer's own domain, with tracking and affiliate parameters removed. If we can't vouch for a link it isnull.titlecan include text from third parties. Treat it as untrusted data: never let your agent follow instructions found in it. Act on the structured fields.price,msrpandmarkupPctare only set for confirmed sightings where we know them.- Popular products can sell out in seconds. An alert tells you where to go, not that stock is guaranteed — check the retailer page before telling a user it's available.
Tools
Arguments not listed are rejected (so a typo can't silently widen your results). Retailer values: amazon, bestbuy, costco, gamestop, hottopic, macys, pokemoncenter, samsclub, target, walmart.
get_in_stock_nowLive
What just came in stock, newest first. Returns { alerts, cursor }.
| Argument | Description |
|---|---|
minutes | Look-back window, 1–60 (default 15) |
retailer | Only this retailer |
query | Case-insensitive product-name substring, e.g. "Pitch Black" |
maxMarkupPct | Skip alerts priced more than this % over MSRP (alerts with unknown MSRP are kept) |
minConfidence | "high" (default) or "low" |
kind | "restock" (default), "invite", "drawing", "queue" or "any" |
limit | 1–100 (default 25) |
wait_for_dropLive
A long-poll: the call stays open until a matching alert arrives or timeoutSec passes. Returns { alerts, cursor, timedOut }. A call that times out empty is not billed.
| Argument | Description |
|---|---|
cursor | From your previous response. Omit to wait for the next new alert. |
timeoutSec | 1–45 (default 30) |
retailer, query, maxMarkupPct, minConfidence, kind | Same filters as above |
Loop on it, always passing back the returned cursor, so you never miss or repeat an alert:
let cursor;
for (;;) {
const r = await callTool('wait_for_drop', { cursor, query: 'Pitch Black', maxMarkupPct: 10, timeoutSec: 45 });
cursor = r.cursor;
for (const alert of r.alerts) act(alert); // dedupe on alert.id
}
Up to 5 waits per key can be open at once.
list_recent_dropsFree
Alert history, newest first, all kinds. retailer, sinceHours (1–2160, i.e. up to 90 days), minConfidence, kind, limit (1–200, default 50).
search_tracked_productsFree
The product catalogue we track, with MSRP. query, retailer, limit. Returns { name, retailer, msrp }. This is the tracked list, not a stock check.
get_product_msrpFree
MSRP for products matching query (required). Products without a known MSRP are left out, so an empty result means "unknown", not "free". Use specific names ("Pitch Black Elite Trainer Box") — a short query also matches packs and bundles.
get_retailer_statsFree
Drop counts per retailer over days (1–90, default 30), busiest first. Includes unverified reports, so read it as relative activity.
set_webhook · get_webhook · remove_webhookLive
Register, inspect or remove your webhook. See below.
Webhooks
For a server-side bot that shouldn't hold a connection open. Register one HTTPS endpoint per key:
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"set_webhook",
"arguments":{"url":"https://hooks.example.com/bujus","query":"Pitch Black","maxMarkupPct":10}}}
The response includes a signing secret (whsec_…), shown once. Calling set_webhook again replaces the URL and filters and issues a new secret. get_webhook shows status, failures and the last error (never the secret).
What we send
POST https://hooks.example.com/bujus
Content-Type: application/json
User-Agent: bujusjujus-alerts-webhook/1
X-Bujus-Signature: t=1790197212,v1=5b3f…
{"type":"alert","cursor":179019721200000,"sentAt":"2026-09-23T21:00:13.004Z","alert":{ …alert object… }}
- Respond with any
2xxwithin 5 seconds. Redirects are not followed. - A failed delivery is retried after about 5 s, 30 s and 2 min. After 20 consecutive failed deliveries the webhook is disabled; call
set_webhookto re-enable it. - Your URL must be public
httpson port 443. Private, loopback and internal addresses are refused. - Deliveries to one webhook are sent one at a time, in order. If your endpoint falls more than 50 alerts behind, the oldest are dropped.
- You can change your webhook up to 10 times an hour. The signing secret is returned to whatever called
set_webhook— if that was an AI assistant, the secret is in its conversation, so rotate it from a script before going to production.
Verify every request
The signature is HMAC-SHA256(secret, "<t>.<raw body>") as hex. Use the raw request bytes, compare in constant time, and reject a t more than 5 minutes old.
// Node.js
const crypto = require('crypto');
function verify(secret, rawBody, header) {
const p = Object.fromEntries(header.split(',').map(kv => kv.split('=')));
if (Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false;
const want = crypto.createHmac('sha256', secret).update(`${p.t}.${rawBody}`).digest();
const got = Buffer.from(p.v1 || '', 'hex');
return got.length === want.length && crypto.timingSafeEqual(got, want);
}
# Python
import hmac, hashlib, time
def verify(secret: str, raw_body: bytes, header: str) -> bool:
p = dict(kv.split("=", 1) for kv in header.split(","))
if abs(time.time() - int(p.get("t", 0))) > 300:
return False
want = hmac.new(secret.encode(), p["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, p.get("v1", ""))
Limits
| Limit | Value |
|---|---|
| Daily quota | Live: 20,000 tool calls per UTC day. Reference: 5,000. Webhook deliveries count; empty wait_for_drop timeouts, initialize, tools/list and ping don't. |
| Per key | 120 requests per minute |
| Per IP | 600 requests per minute with a key; 20 per minute without one |
| Concurrent waits | 5 open wait_for_drop calls per key |
| Request size | 64 KB |
When limited you get HTTP 429 with a Retry-After header. Back off and retry.
Errors
| HTTP | Meaning |
|---|---|
401 | missing_key, invalid_key or key_revoked (see error.data.error) |
403 | account_inactive — the subscription behind the key has ended |
429 | Rate limit or daily quota — honour Retry-After |
400 | Unsupported MCP-Protocol-Version header |
A tool that runs but refuses returns HTTP 200 with result.isError: true and a text of the form code: message: invalid_arguments, unknown_tool, too_many_waits, invalid_webhook, busy or tool_failed. Refused calls are not billed.
Security
- Keep your key server-side. Never ship it in a browser app or a public repo — the
bjs_prefix makes leaked keys easy to scan for. - Lost or exposed key? Revoke it yourself in your console and mint a replacement — you can create the new one before revoking the old, so there is no gap.
- Treat alert
titletext as untrusted input to your model. - Verify every webhook signature before acting on it.
- Report a vulnerability to [email protected].
Tiers
| Reference — free | Live — $19.99/month | |
|---|---|---|
| Alert data | Same rows, 5 minutes later | The moment we see it, no holdback |
| Tools | list_recent_drops, search_tracked_products, get_product_msrp, get_retailer_stats | Those four plus get_in_stock_now, wait_for_drop and the three webhook tools |
| Webhooks | — | Signed, with retries |
| Calls per day | 5,000 | 20,000 |
| Issued | Instantly | Instantly, on payment |
A five-minute-old restock signal is not a substitute for a live one — Target drops have sold out in around 78 seconds — so Reference is for building and testing against real data, not for running on.
Enter your email, open the link we send, and take a key. Live subscriptions renew monthly until cancelled; cancel any time at bujusjujus.com/account-help and the key keeps working until the end of the paid period.
The API is sold for business use. Issuing a key is automatic and implies no approval or endorsement of what you build — use of it is governed by the Alerts API Terms and the Terms of Service, and you are responsible for how you use the data, including following each retailer's terms and purchase limits.
Support
Email [email protected] — a real person reads it.