@subsquid-labs/gcp-x402
v0.1.10
Published
MCP server to query BigQuery public datasets via the gcp-x402 x402 proxy
Readme
gcp-x402 — MCP server & CLI
Query Google BigQuery's public datasets from an agent that has no Google Cloud account. Each query is priced from a BigQuery dry run and paid per query in USDC or USAT over x402 — no billing account, no API key.
This package is the agent side: it holds a small wallet, signs the payment, and hands your agent typed tools. The queries themselves run on a hosted proxy.
Install
claude mcp add gcp-x402 -- npx -y @subsquid-labs/gcp-x402Node 18+. That points at the hosted deployment; --env PROXY_URL=<proxy-url> points it at
another one.
Any MCP client that speaks stdio works — the equivalent config is:
{
"mcpServers": {
"gcp-x402": {
"command": "npx",
"args": ["-y", "@subsquid-labs/gcp-x402"]
}
}
}Fund the wallet, once
On first run the server generates a wallet for the project and writes it to
./.gcp-x402/wallet.json (mode 0600, with a .gitignore beside it so it cannot be
committed by accident). It starts empty — the MCP server also prints the address to
stderr on every startup, so it shows up in your client's server logs.
To see it directly, run:
npx -y @subsquid-labs/gcp-x402 walletaddress: 0xAbC1234...
balances: none of these holds enough for a query yet (see funding)
[ ] 0 USAT | celo | contract: 0xD2ab3C9A02DBBAB236BfEC45D1d755DF4267F771
[ ] 0 USDC | celo | contract: 0xcebA9300f2b948710d2653dD7B07f33A8B32118C
[ ] 0 USDC | base | contract: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
authorized: $0 of $10 per 24 h
remaining: $10
funding:
- Send USAT on Celo mainnet to 0xAbC1234... (token 0xD2ab3C9A02DBBAB236BfEC45D1d755DF4267F771). Verified humans can claim USA₮ once at https://cloud.google.com/application/web3/faucet/celo/mainnet.
- Send USDC on Celo mainnet to 0xAbC1234... (token 0xcebA9300f2b948710d2653dD7B07f33A8B32118C).
- Send USDC on Base mainnet to 0xAbC1234... (token 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913).balances lists the wallet's balance on every rail the proxy accepts, so the list depends
on the deployment. [✓] marks every rail a new payment may use: one the settings leave
open that holds at least the cheapest query. Each query is paid on the first marked rail
that holds enough for it (see How the client chooses a
rail), and the line above the list says whether
REQUIRED_NETWORK/REQUIRED_ASSET limit the choice. [?] marks an open rail whose
balance could not be read, which is tried when no rail is known to hold enough. A fresh
wallet holds nothing, so nothing is marked, as above.
Each funding step says how to fund one rail: send that token on that network to the
printed address. With no rail pinned, sending the token is enough: once the rail holds
enough, the client pays there by itself. A rail outside the
REQUIRED_NETWORK/REQUIRED_ASSET you set also needs those settings changed, and its
step shows the values. You never need the network's native token for gas. Against a
testnet proxy, a funding step includes a faucet link.
authorized counts every payment this client signed in the window, including one the proxy
then refused. The budget limits what the client signs, not what is collected, so a signed
payment stays counted for the whole window, even once it has expired unused.
Tools
bigquery_query authorizes payment. A prepaid query can fail after payment;
see what a query failure costs
for details.
| Tool | What it does |
| ---------------------- | ------------------------------------------------------------------------- |
| bigquery_query | Runs a query, pays the selected token price, returns a link to the result file. |
| bigquery_estimate | Exact price and bytes for a query, without paying or running it. |
| recover_result | Get back a paid result whose download link was lost. Free. |
| wallet_info | Address, token balances, and how to top it up. |
| list_public_datasets | Popular datasets and current pricing. Free. |
| bigquery_schema | Any public dataset's tables, or a table's columns. Free. |
Every result comes back as {"delivery": "durable", "format": "ndjson", "resultUrl": …}:
a signed link to an NDJSON file — one JSON object per line, INT64 values as decimal
strings — with its rowCount, totalRows, truncated and bytes beside it, never rows
in the response. An empty result is a link to an empty file. Branch on delivery, not on
whether rows is present.
CLI
The same binary is a plain CLI when given arguments, so a skill can drive it over Bash with no MCP registration:
npx -y @subsquid-labs/gcp-x402 doctor # proxy, wallet, balance, cap and budget, in one pass
npx -y @subsquid-labs/gcp-x402 wallet
npx -y @subsquid-labs/gcp-x402 estimate "<sql>"
npx -y @subsquid-labs/gcp-x402 query "<sql>"
npx -y @subsquid-labs/gcp-x402 query --file query.sql # the SQL from a file, sent as written
npx -y @subsquid-labs/gcp-x402 fetch "<resultUrl>" # stream the result file to stdout, byte for byte
npx -y @subsquid-labs/gcp-x402 receipts # what this wallet paid for, and where it landed
npx -y @subsquid-labs/gcp-x402 recover # a fresh link to a paid result, free
npx -y @subsquid-labs/gcp-x402 datasets
npx -y @subsquid-labs/gcp-x402 schema "bigquery-public-data.usa_names" # tables, then columnsSend stdout to a file — query "<sql>" > delivery.json, then
fetch "<resultUrl>" > result.ndjson. A result is addressed by a link printed once, and a
caller that loses it has lost the address of something already paid for. fetch writes
the file as served and appends nothing; read it one line at a time.
estimate and query both take --file <path> in place of the SQL. A long query, or one
with quotes and backticks of its own, is easier to write to a file than to quote for a
shell, and the file is sent as written.
Data goes to stdout, the human-readable summary to stderr, so piping stays clean. A failure the proxy named — an unfunded wallet, a query that will not validate — is reported as one line carrying its class, its retry disposition and a correlation id, not a stack trace.
When a result link is lost
A query that outruns a tool timeout, a truncated response, a process killed before it printed — the payment settles regardless, and the signed link goes with the output. It is recoverable.
npx -y @subsquid-labs/gcp-x402 receipts # every paid attempt: its link, expiry, format, counts and size
npx -y @subsquid-labs/gcp-x402 recover # re-ask for the most recent one, or recover <id>recover costs nothing: it re-presents the authorization the original query signed, and
the proxy re-addresses the object it already wrote. That is why the client journals each
authorization to ./.gcp-x402/receipts.jsonl (mode 0600, beside the keystore) before
sending it — recovery is keyed on the authorization, so a client that discards it can only
pay again. Treat that file as a credential store; the .gitignore beside it keeps it out
of commits.
The window is reported as recoverableUntil and outlives the link itself. Past the link's
expiresAt, recover answers the facts with no link: the file is gone, not empty. Every
result is stored this way; only an inline result from an older proxy has to be re-run.
Checking a payment on the chain
Every paid answer names the settlement transaction: query prints a block-explorer link to
it on its paid line, and the MCP tools add a Settlement transaction: line. receipts
keeps it for each attempt as transaction, network and transactionUrl, so a payment can
be checked from this client alone. The explorer's page for the transaction shows how many
blocks have confirmed it since.
Configuration
Every setting is an environment variable; the rail and the spending limits can also be kept per project in a settings file (below).
| Variable | Default | What it does |
| --------------------- | -------------------------------- | ----------------------------------------------------- |
| PROXY_URL | https://gcp-x402.sqd.dev | Which proxy to query and pay. |
| MAX_PAYMENT_USD | 1.00 | Refuse to auto-pay more than this for a single query. |
| WALLET_FILE | ./.gcp-x402/wallet.json | Keystore path; an absolute path shares one wallet across projects. |
| WALLET_PRIVATE_KEY | — | Bring your own key instead of a generated wallet. |
| SPEND_BUDGET_USD | 10.00 | Cumulative ceiling on what all queries may authorize. |
| SPEND_BUDGET_WINDOW_HOURS | 24 | Rolling window that ceiling is measured over. |
| REQUIRED_ASSET | — | Require USDC or USAT on REQUIRED_NETWORK; matches the token contract and never falls back to another asset. Asset names are case-insensitive. |
| REQUIRED_NETWORK | — | Only authorize new payments on this network (base, base-sepolia, celo, celo-sepolia). Fails if no valid offer on it fits the cap. Unset, the client chooses the rail itself (how). |
| HTTPS_PROXY, HTTP_PROXY, NO_PROXY | — | Send every request (the proxy, the network RPC hosts, result links) through this HTTP proxy, except to the hosts NO_PROXY lists. Needed where a proxy is the only way out, such as a sandboxed agent's shell. Lowercase names work too. |
The project settings file
The rail and the spending limits can also live in .gcp-x402/config.json, beside the
wallet they apply to (beside WALLET_FILE, when that is set). It is a JSON object keyed by
the same names as the variables above: REQUIRED_NETWORK, REQUIRED_ASSET,
MAX_PAYMENT_USD, SPEND_BUDGET_USD, SPEND_BUDGET_WINDOW_HOURS. To pay only in USDC on
Celo, at most 10 cents a query:
{
"REQUIRED_NETWORK": "celo",
"REQUIRED_ASSET": "USDC",
"MAX_PAYMENT_USD": "0.10"
}By default, an environment variable wins over the file, including an empty value.
Add "ALLOW_ENV_OVERRIDES": false to make stored values authoritative: a conflicting
environment value then fails startup. Values that mean the same thing do not conflict:
amounts and hours are compared by value (0.1, "0.10" and "00.100" match), and
REQUIRED_ASSET ignores case. Settings absent from the file can still come from the
environment. Store an empty string to protect automatic rail selection. This policy is
file-only; an environment variable cannot change it. Omitting it or setting it to
true preserves the default precedence.
Unknown keys and non-boolean policy values fail startup. doctor lists the settings the
file supplies. The proxy, key and wallet location remain environment-only. This policy
detects environment conflicts; it does not prevent editing the file or using another
wallet path.
Each CLI command reads the file and the environment when it runs, so a change applies to the next command. The MCP server reads them only when it starts: after changing a setting, restart the server (for example, reconnect it from your MCP client) for the change to apply.
How the client chooses a rail
For each paid query the client takes these steps, in this order:
- It keeps the offers it can sign — a network and token it knows, and complete terms —
at a price no higher than
MAX_PAYMENT_USD. - If
REQUIRED_NETWORKis set, it keeps only the offers on that network, and withREQUIRED_ASSET, only the one for that token. If nothing is left, it stops before signing anything; it never falls back to another rail. - It keeps the cheapest. The proxy prices every rail alike, so this usually keeps them all.
- If one offer is left, it pays it without reading any balance. If several are left, it reads the wallet's balance on each and pays on the first that holds at least the price. If no balance can be read, it takes the first the proxy lists. If every one is known to hold too little, it signs nothing and says what the wallet holds.
So REQUIRED_NETWORK=celo with REQUIRED_ASSET=USDC pays only in USDC on Celo, and
REQUIRED_NETWORK=celo alone pays in whichever Celo token the wallet holds enough of.
estimate names the rail by the same steps, reading the balances when several rails cost
the same, so it names the rail a payment would use. wallet and doctor mark the rail by
them too, judged against the cheapest query the proxy quotes. Recovery always uses the
original authorization, whatever the settings are now.
PREFERRED_NETWORK was removed in 0.1.10: step 4 now finds a funded rail without it. A
settings file or environment that still gives it a value fails startup with a message
saying so; an empty value is ignored.
wallet and wallet_info report each advertised token separately, including
its contract address and funding instructions, and mark the one new payments use.
A funded rail outside REQUIRED_NETWORK/REQUIRED_ASSET is never used by itself;
instead, doctor and a refusal for want of funds name it and the settings that would pay
there.
The client never assumes a rail. If the proxy advertises none it can pay on, or cannot
be reached and no REQUIRED_NETWORK is set, wallet marks no rail and its funding
step says why instead of naming a default.
Queries that stay cheap
You pay for bytes scanned, so two things dominate the bill:
SELECT *reads every column. BigQuery is columnar, so naming the columns you need can cost 100× less on a wide table.LIMITcaps rows returned, not bytes scanned — it never makes a query cheaper. Filter on a partition column instead.
bigquery_estimate is free. Use it before anything that might be large.
Only read-only SELECTs against bigquery-public-data are accepted; DML and DDL are
rejected by the proxy.
Use SAFE_CAST and supported SAFE.PARSE_* conversions, and handle NULL results
deliberately. See the query guidance
for the enforced restrictions and examples.
