
Company News
Socket Joins New OpenJS Program to Fund Node.js Security Work
Socket is joining the OpenJS Security Stewardship Program to fund Node.js vulnerability research, maintainer remediation, and security releases.
shotsforbots-mcp
Advanced tools
MCP server for shotsforbots.com: search the photo catalog and buy full-resolution originals over x402 (USDC on Base) from any MCP-capable agent
A Model Context Protocol server for shotsforbots.com: your agent searches the photo catalog for free and buys full-resolution originals with USDC over HTTP 402 (x402 v2, Base). No account, no cart. Runs on Node 22+, stdio by default.
Three of the four tools — search_photos, get_photo, catalog_info — are
free and need no wallet, no key and no configuration. Add the server, ask it
things, get answers. buy_photo is the only one that spends money, and it
does nothing until you give it a wallet to spend from.
To buy a photograph the server has to sign a USDC transfer, which means it
needs a private key: SHOTSFORBOTS_PRIVATE_KEY, a 0x-prefixed 64-hex
string. Generate a fresh one with openssl rand -hex 32 and fund it with a
few dollars of USDC on Base. The buyer needs no ETH — the seller's
facilitator pays the gas.
That key is a hot wallet: it lives in an environment variable on the machine running the agent, so anything that can read the environment — the MCP client, its config file, its logs — can spend whatever the wallet holds. Three habits keep that boring:
SHOTSFORBOTS_MAX_PRICE_USD (default 0.50) or
SHOTSFORBOTS_DAILY_CAP_USD (default 5.00). The caps bound what a
confused or hostile prompt can spend; the balance bounds what a stolen
key can spend.Every buy_photo that settles is a purchase you made. Payment is acceptance
of the Shots for Bots Photo License
for that file, on behalf of whoever the agent acts for (licence section 2),
and payments are final: one download per payment, no refunds after delivery.
An interrupted download retries automatically with the server's redelivery
token (licence section 1.4: 24 hours, three attempts, never a second
payment); a settled payment that still did not produce the file is
redelivered by hand — email licensing@shotsforbots.com with the tx_hash.
Run this server only for a principal who has agreed to that.
| Tool | Cost | Does |
|---|---|---|
search_photos({query?, keywords?, place?, min_megapixels?, max_price_usd?, limit=20, page?}) | free | Case-insensitive; every word of query must match. Returns {total, page, pages, limit, source, items[]}; each item is {id, title, description, keywords, place, taken, width, height, price_usd, preview_url, page_url}. |
get_photo({id}) | free | The full catalog record plus original_url and terms read from the live 402 without paying: amount_usdc, amount_atomic, network, payTo, asset, scheme, and the license summary from the 402 body. |
buy_photo({id, out_dir?, max_price_usd?}) | pays | Checks the caps, signs one EIP-3009 authorization, downloads the JPEG to out_dir/<id>.jpg (default ~/Downloads/shotsforbots/), verifies it against the catalog's sha256 (and Content-Length), and returns {path, bytes, sha256, price_paid, tx_hash, network, payer, redelivered, redelivery_remaining}. A truncated or short transfer is retried with the X-Redelivery-Token the paid 200 came with -- no new signature, no new spend -- up to three times with backoff; if the file still never verifies the tool fails with status: "partial", tx_hash, token_expires, redelivery_remaining, a partial_path holding what arrived, and the licensing contact line. A file already on disk with the right hash is returned without paying. If the server answers 402 again after payment, the server's reason is returned verbatim and nothing is counted. |
catalog_info() | free | Item count, prices in use, license URL, contact, x402 endpoint/protocol from the catalog head, plus this client's wallet address, caps and today's spend. |
search_photos fetches the site's static search index,
/search/index.json — one file
(~480 KB, ~120 KB gzipped) holding every photograph as a slim row plus an
inverted term index — and runs the query locally. Walking the paged catalog
instead is roughly 13 MB over eight pages, which is the reason the index
exists. The result's source says which path answered:
"search-index" — one request. description, keywords, place and
taken come back null, because the index does not carry them; call
get_photo for the full record of an id you care about."catalog" — the paged walk. It happens when you pass place (the one
filter the index cannot serve exactly) and when the origin publishes no
index. Every field is populated.Both the search index and the catalog are cached in memory for ten minutes and
refreshed with If-None-Match, so an unchanged catalog costs one 304.
Once published to npm, every client below can run it with npx -y shotsforbots-mcp (no install step). Until then, or to run from a checkout:
cd mcp && npm ci # dependencies only; there is no build step
node src/index.js # MCP over stdio
node src/index.js --http # MCP Streamable HTTP at http://127.0.0.1:8402/mcp
Use the absolute path to mcp/src/index.js in the snippets below in place
of npx -y shotsforbots-mcp ("command": "node", "args": ["/path/to/repo/mcp/src/index.js"]).
claude_desktop_config.json (Settings > Developer > Edit Config):
{
"mcpServers": {
"shotsforbots": {
"command": "npx",
"args": ["-y", "shotsforbots-mcp"],
"env": {
"SHOTSFORBOTS_PRIVATE_KEY": "0x...",
"SHOTSFORBOTS_MAX_PRICE_USD": "0.50",
"SHOTSFORBOTS_DAILY_CAP_USD": "5.00"
}
}
}
}
Leave env out entirely for a read-only (search and price) setup.
claude mcp add shotsforbots -e SHOTSFORBOTS_PRIVATE_KEY=0x... -- npx -y shotsforbots-mcp
or, checked in for a project, .mcp.json:
{
"mcpServers": {
"shotsforbots": {
"command": "npx",
"args": ["-y", "shotsforbots-mcp"],
"env": { "SHOTSFORBOTS_PRIVATE_KEY": "${SHOTSFORBOTS_PRIVATE_KEY}" }
}
}
}
(Claude Code expands ${VAR} from the shell environment, so the key never
lands in the file.)
.cursor/mcp.json in the project, or ~/.cursor/mcp.json globally:
{
"mcpServers": {
"shotsforbots": {
"command": "npx",
"args": ["-y", "shotsforbots-mcp"],
"env": { "SHOTSFORBOTS_PRIVATE_KEY": "0x..." }
}
}
}
node src/index.js --http [port] serves the same server at
http://127.0.0.1:<port>/mcp (default 8402), stateless, loopback only,
with the SDK's DNS-rebinding guard on the Host header. Point an HTTP MCP
client at that URL (Claude Code: claude mcp add --transport http shotsforbots http://127.0.0.1:8402/mcp). The env vars are read by the
server process, not sent by the client, so the key stays on the machine
running the server. There is no authentication on the endpoint: anyone who
can reach the port can spend up to the caps. Do not expose it beyond
loopback.
| Variable | Default | Meaning |
|---|---|---|
SHOTSFORBOTS_PRIVATE_KEY | unset | 0x + 64 hex. The hot wallet. Unset = read-only tools only. |
SHOTSFORBOTS_NETWORK | eip155:8453 | Base mainnet. eip155:84532 for Base Sepolia (test USDC). The server refuses to sign for any other network the 402 names. |
SHOTSFORBOTS_MAX_PRICE_USD | 0.50 | Per-purchase cap. buy_photo.max_price_usd overrides it per call (either way, never above the daily cap). |
SHOTSFORBOTS_DAILY_CAP_USD | 5.00 | Per-UTC-day cap across every purchase from this machine. |
SHOTSFORBOTS_SPEND_FILE | ~/.shotsforbots/spend.json | The spend ledger (0600). |
SHOTSFORBOTS_OUT_DIR | ~/Downloads/shotsforbots | Default download directory. |
SHOTSFORBOTS_ORIGIN | https://shotsforbots.com | Catalog origin (staging, mirrors). |
buy_photo GETs the original with no payment and decodes
PAYMENT-REQUIRED (what get_photo shows). It refuses, unsigned, if the
scheme is not exact, the network is not SHOTSFORBOTS_NETWORK, the
asset is not USDC on that network, or the amount is above the per-call
cap or the daily cap.spend.json, under a lock so
two servers sharing a home directory cannot both squeeze under the cap)
and refuses, unsigned, if today's reserved + settled + unknown total would
pass SHOTSFORBOTS_DAILY_CAP_USD.TransferWithAuthorization (via
@x402/fetch's client over @x402/evm, whose own spend control is also
set to the daily cap as a second guard) and repeat the GET with
PAYMENT-SIGNATURE.200 -> the reservation becomes settled with the tx_hash from
PAYMENT-RESPONSE (the payment is real whatever happens to the bytes),
the body is checked against the catalog's sha256 and the declared
Content-Length, and the file is written. A body that does not verify
is fetched again with the redelivery token from that 200
(X-Redelivery-Token, also PAYMENT-RESPONSE.redelivery.token) -- the
server serves the same file for the same payment up to three times
within 24 hours and answers 402 redelivery_exhausted / _expired /
_invalid otherwise; the client tries at most three times with
backoff and then reports status: "partial", the entry staying
settled with a partial delivery note. 402 -> the facilitator rejected it
(insufficient balance, replay, ...): nothing moved, the reservation is
released and the server's reason is returned verbatim. 400/404/503
-> rejected before settlement, released. 502 or a network failure after
signing -> the transfer may have landed: the entry is kept as unknown
and counted against the cap until you edit the ledger.The ledger is plain JSON; delete an unknown entry once you have checked
the wallet on basescan.org. Entries older than 45 days are pruned.
No local Node is needed; everything runs in Docker from the repo root:
docker run --rm -v "$PWD/mcp":/app -w /app node:22 npm ci
docker run --rm -v "$PWD/mcp":/app -w /app node:22 npm test # unit tests, offline
docker run --rm -v "$PWD/mcp":/app -w /app node:22 npm run check # tsc over the JS (JSDoc types)
docker run --rm -v "$PWD/mcp":/app -w /app node:22 node test/live.mjs 8baed5e850d0 texas # live, read-only
Plain ESM JavaScript with JSDoc types, checked by tsc --checkJs (that is
all npm run build does): no transpile step, no generated dist/ to drift
from the source, same shape as deploy/facilitator/server.mjs. Layout:
src/index.js CLI: stdio (default) or --http; nothing but MCP on stdout
src/server.js the four tools over injected deps (catalog, ledger, payer, fetch)
src/catalog.js index.json paging, 10-minute cache, ETag, search
src/x402.js 402 decode, terms sanity, signer (x402HTTPClient + ExactEvmScheme)
src/buy.js the purchase flow and its cap/ledger bookkeeping
src/spend.js the daily ledger
src/config.js env -> config; src/money.js micro-USD arithmetic
test/*.test.js node --test, offline: fixture catalog + a fake gate that applies
the same PAYMENT-SIGNATURE checks as photos/original.php
test/live.mjs stdio smoke test against the live site (never pays)
Dependency versions are pinned exactly (@modelcontextprotocol/sdk,
@x402/fetch, @x402/evm, viem, zod); package-lock.json is committed.
FAQs
MCP server for shotsforbots.com: search the photo catalog and buy full-resolution originals over x402 (USDC on Base) from any MCP-capable agent
The npm package shotsforbots-mcp receives a total of 488 weekly downloads. As such, shotsforbots-mcp popularity was classified as not popular.
We found that shotsforbots-mcp demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Company News
Socket is joining the OpenJS Security Stewardship Program to fund Node.js vulnerability research, maintainer remediation, and security releases.

Security News
Two compromised GitHub Actions were re-enabled with malicious tags intact, exposing thousands of downstream repositories to Mini Shai-Hulud.

Research
/Security News
A malicious Firefox extension fetches its payload after installation to evade detection, steal Google session cookies, and automate account takeover.