New:Microsoft Teams Notifications Are Now Available in Socket.Learn more →
Get Started

shotsforbots-mcp

Package Overview
Dependencies
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

shotsforbots-mcp

MCP server for shotsforbots.com: search the photo catalog and buy full-resolution originals over x402 (USDC on Base) from any MCP-capable agent

latest
npmnpm
Version
0.1.2
Version published
Weekly downloads
488
Maintainers
1
Weekly downloads
 
Created
Source

shotsforbots-mcp

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.

What it costs to run

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.

Buying: the wallet key

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:

  • Use a fresh key. Never point this at a wallet you care about.
  • Fund it small. A wallet holding five dollars can lose five dollars.
  • Keep the caps. The server refuses, without signing, when a purchase would exceed 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.

What a purchase commits you to

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.

Tools

ToolCostDoes
search_photos({query?, keywords?, place?, min_megapixels?, max_price_usd?, limit=20, page?})freeCase-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})freeThe 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?})paysChecks 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()freeItem 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.

How a search is answered

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.

Install

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

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 Code

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

.cursor/mcp.json in the project, or ~/.cursor/mcp.json globally:

{
  "mcpServers": {
    "shotsforbots": {
      "command": "npx",
      "args": ["-y", "shotsforbots-mcp"],
      "env": { "SHOTSFORBOTS_PRIVATE_KEY": "0x..." }
    }
  }
}

Streamable HTTP

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.

Environment

VariableDefaultMeaning
SHOTSFORBOTS_PRIVATE_KEYunset0x + 64 hex. The hot wallet. Unset = read-only tools only.
SHOTSFORBOTS_NETWORKeip155:8453Base mainnet. eip155:84532 for Base Sepolia (test USDC). The server refuses to sign for any other network the 402 names.
SHOTSFORBOTS_MAX_PRICE_USD0.50Per-purchase cap. buy_photo.max_price_usd overrides it per call (either way, never above the daily cap).
SHOTSFORBOTS_DAILY_CAP_USD5.00Per-UTC-day cap across every purchase from this machine.
SHOTSFORBOTS_SPEND_FILE~/.shotsforbots/spend.jsonThe spend ledger (0600).
SHOTSFORBOTS_OUT_DIR~/Downloads/shotsforbotsDefault download directory.
SHOTSFORBOTS_ORIGINhttps://shotsforbots.comCatalog origin (staging, mirrors).

How the caps work

  • 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.
  • It reserves the amount in the ledger (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.
  • Only then does it sign the EIP-712 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.

Development

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.

Keywords

mcp

FAQs

Package last updated on 23 Sep 2026

Related posts