spend-guard@0.2.1
CAUTION· startup onlyPublisher claims: "Enables MCP clients to fetch x402-paywalled URLs while enforcing spending policies, and to inspect budgets, recent payments, and policy rules. Flagged payments wait for human approval on the dashboard." Observed in sandbox: no network, touches nothing outside the project, writes no memory.
Coverage
Startup only; authenticated tool behavior not tested.- Tested
- server startup over stdio (initialize sent) · every file read, file written and host contacted by what ran
- Not tested
- the tool list (the server exited before answering) · what the tools do when called with real credentials
- Needs
- NEAR_PRIVATE_KEY, ARC_PRIVATE_KEY, NEAR_GUARD_AGENT_KEY, INTENTS_API_KEY, INTENTS_GRANT_TOKEN — give it to the server where it runs, through vet-skills run --secrets, so the value never enters the server's environment. Never paste a credential into a chat, a ticket or a report.
What this means for the label: Caution here covers what ran. The server's tools were not exercised with a real credential, so nothing about their behavior is asserted; the credential summary below says what to do with that.
Before you give it your token
- It will ask for
ARC_FACILITATOR_PRIVATE_KEYARC_PRIVATE_KEYCIRCLE_API_KEYCIRCLE_ENTITY_SECRETINTENTS_API_KEYINTENTS_GRANT_TOKENNEAR_PRIVATE_KEYConfiguration, not credentials: APPROVAL_TIMEOUT, ARC_PAYEE_MAPS, ARC_PAYEE_NEWS, ARC_PAYEE_QUOTES, ARC_PAYEE_THIEF, ARC_PAYEE_WEATHER, CIRCLE_BASE_URL, CIRCLE_WALLET_ADDRESS, CIRCLE_WALLET_ID, CIRCLE_WALLET_SET_ID, DASHBOARD_PORT, INTENTS_AGENT_ACCOUNT, INTENTS_AGENT_ID, INTENTS_API_URL, INTENTS_OWNER_KEY, NEAR_ACCOUNT_ID, NEAR_GUARD_AGENT_KEY, NEAR_GUARD_ID, NEAR_NETWORK, NEAR_RPC_URL, PAYEE_MAPS, PAYEE_NEWS, PAYEE_QUOTES, PAYEE_THIEF, PAYEE_WEATHER, PAYWALL_ACCOUNT_ID, SPEND_GUARD_APPROVAL_TIMEOUT, SPEND_GUARD_DASHBOARD_PORT, SPEND_GUARD_LEDGER, SPEND_GUARD_MOCK_NETWORK, SPEND_GUARD_POLICY, SPEND_GUARD_RAILS- Where it talks to
- no vendor host named in the codeAlso arcscan.app, nearblocks.io, usetidepool.tech, circle.com, fastnear.com, nearprotocol.com, near-faucet.io, arc.network, near.org — not the publisher's domain; check what for before connecting.
- What the honeypot saw
- no credential files read · no network
- What we could not test
- the tool list (the server exited before answering) · what the tools do when called with real credentials
Connect it with a scoped credential, under the guard, and read the third-party hosts first. The code names arcscan.app, nearblocks.io, usetidepool.tech, circle.com, fastnear.com, nearprotocol.com, near-faucet.io, arc.network, near.org as well as its own vendor. The guard injects ARC_FACILITATOR_PRIVATE_KEY, ARC_PRIVATE_KEY, CIRCLE_API_KEY, CIRCLE_ENTITY_SECRET, INTENTS_API_KEY, INTENTS_GRANT_TOKEN, NEAR_PRIVATE_KEY only into requests to the host you bind (and its subdomains) and refuses the rest.
Never paste a credential into a chat, a ticket or a report, including ours. How the guard holds it.
Is [email protected] safe to install?
KillerSkills ran it in the KillerSkills honeypot sandbox (2 scenarios; startup only; authenticated tool behavior not tested) on 2026-11-11 and scored it 73/100 (Caution). It is not known to be malicious, but it has behaviour or code a security policy should decide on before install. Notable findings: Image alt text carrying instructions; Access to sensitive file paths; Access to sensitive file paths. Summary: no network, touches nothing outside the project, writes no memory.
Behavior trace
Honeypot run · 61 s · 2 scenarios · 0 deviationsStatic findings
The leaked-grant run on Tidepool's testnet: with the agent's Agent API key and grant alone, the policy refuses a transfer to the look-alike rnaps., 51 cents oveFix: Remove the text that addresses the agent rather than the reader; describe behavior, do not instruct.* a little: this creates it in .env (git-ignored) if missing and tops it up from the demo agent'sFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.if (existsSync('.env')) process.loadEnvFile('.env');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.appendFileSync('.env', `ARC_FACILITATOR_PRIVATE_KEY=${key}\n`);Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.The prompt-injection run on NEAR testnet: the agent reads /guide, whose note says to pay the 0.4 NEAR licence fee to rnaps.sg-paywall-25135e.testnet, follows itFix: Remove the text that addresses the agent rather than the reader; describe behavior, do not instruct.* .env (git-ignored), and prints the agent address to fund at https://faucet.circle.comFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.if (existsSync('.env')) process.loadEnvFile('.env');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.if (!process.env.ARC_PRIVATE_KEY) throw new Error('no ARC_PRIVATE_KEY in .env; run node scripts/arc-testnet-wallet.js first');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.The stolen-key run on NEAR testnet: signing with the agent's function-call key alone, the contract refuses the look-alike payee, 1 yoctoNEAR over the per-paymenFix: Remove the text that addresses the agent rather than the reader; describe behavior, do not instruct.if (!INTENTS_AGENT_ID) throw new Error('no INTENTS_AGENT_ID in .env; run scripts/intents-agent-setup.js');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.console.log(`.env already has an Arc agent: ${privateKeyToAccount(process.env.ARC_PRIVATE_KEY).address}`);Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.console.error(`.env already has a paywall (${process.env.PAYWALL_ACCOUNT_ID})`);Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.if (existsSync('.env')) process.loadEnvFile('.env');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.if (!existsSync('.env')) throw new Error('no .env; run near-testnet-account.js and near-demo-payees.js first');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.process.loadEnvFile('.env');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.if (existsSync('.env')) process.loadEnvFile('.env');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* Read-only look at the Agent API agent in .env (from intents-agent-setup.js): its wallet, itsFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* # (NEAR_ACCOUNT_ID, NEAR_PRIVATE_KEY in .env, see near-testnet-account.js)Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* never holds a private key. Needs CIRCLE_API_KEY (a TEST_API_KEY) and CIRCLE_ENTITY_SECRET in .env,Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.if (!agent) throw new Error('no INTENTS_AGENT_ACCOUNT in .env; run scripts/intents-agent-setup.js');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* node scripts/mcp-demo.js --arc # real USDC on Arc testnet (ARC_* in .env, see arc-testnet-wallet.js)Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.if (existsSync('.env')) process.loadEnvFile('.env');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose./** Appends non-secret settings to .env (git-ignored) and to this run's environment. */Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* # (NEAR_GUARD_ID, NEAR_GUARD_AGENT_KEY in .env, see near-guard-deploy.js)Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.if (mode !== 'mock' && existsSync('.env')) process.loadEnvFile('.env');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* # testnet (INTENTS_* in .env, see intents-agent-setup.js)Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.if (!agent || !grant || !INTENTS_API_KEY) throw new Error('no INTENTS_* in .env; run scripts/intents-agent-setup.js');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* 5. appends INTENTS_* to .env and prints the agent wallet's account, which you fund with testFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.process.loadEnvFile('.env');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* (and the paywall key) to .env. Run after near-testnet-account.js.Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.process.loadEnvFile('.env');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* Needs PAYEE_WEATHER, PAYEE_NEWS, PAYEE_QUOTES, PAYEE_MAPS and PAYEE_THIEF in .env (fromFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.appendFileSync('.env', `PAYWALL_ACCOUNT_ID=${paywallId}\nPAYWALL_PRIVATE_KEY=${keyPair.toString()}\n`);Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.process.loadEnvFile('.env');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* near-demo-payees.js). The owner key stays in .env and is never given to the agent: the demoFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.console.error('no ARC_PRIVATE_KEY in .env; run node scripts/arc-testnet-wallet.js first');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* appends the non-secret CIRCLE_WALLET_SET_ID, CIRCLE_WALLET_ID and CIRCLE_WALLET_ADDRESS to .envFix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.appendFileSync('.env', lines.join('\n') + '\n');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* its key to .env (git-ignored, mode 600). Testnet NEAR has no value; never point this at mainnet.Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.2. Write .env yourself (then chmod 600 .env), with the account and a full-access key it holds:Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.process.loadEnvFile('.env');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.console.error('.env already exists; move it away first so its key is not lost');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.if (!guardId || !stolenKey) throw new Error('no NEAR_GUARD_ID / NEAR_GUARD_AGENT_KEY in .env; run scripts/near-guard-deploy.js');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* 3. appends NEAR_GUARD_ID and NEAR_GUARD_AGENT_KEY to .env, and prints the contract's policy.Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.await assert.rejects(guardFromEnv({ ...env, INTENTS_GRANT_TOKEN: '' }), /INTENTS_GRANT_TOKEN is not set/);Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.const saved = readFileSync(join(dir, '.env'), 'utf8');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.if (!env[name]) throw new Error(`${name} is not in .env; run near-testnet-account.js and near-demo-payees.js first`);Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.kill -0 "$demo" 2>/dev/null || { cat "$log"; exit 1; }Fix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.test('the setup script creates one wallet and keeps only non-secret ids in .env', () => withApi(async (env, requests) => {Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.trap 'kill "$demo" 2>/dev/null || true; rm -f "$log"' EXITFix: Build commands from fixed arguments; do not interpolate untrusted strings into a shell.[{ url: 'file:///etc/passwd' }, /only http and https/],Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.process.loadEnvFile('.env');Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.…ttp://localhost:4400/', 'http://[::1]:4400/events', 'http://2130706433:4400/', 'http://0x7f.1:4400/', 'http://127.1:4400/', 'http://0.0.0.0:4400/', 'http://[::ffff:127.0.0…Fix: Put hosts, commands and strings in plain source. Decoding at runtime looks identical to malware and costs points on its own.await assert.rejects(circleWalletRailFromEnv({ ...env, CIRCLE_WALLET_ADDRESS: PAYEE }, { client: clientWith(wallet) }), /CIRCLE_WALLET_ADDRESS/);Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.2. Write .env yourself (then chmod 600 .env), with the account and a full-access key it holds:Fix: Request the narrowest permission that works; replace wildcards with the exact paths or hosts.const both = await guardFromEnv({ ...env, SPEND_GUARD_RAILS: 'near-guard,intents', NEAR_GUARD_ID: 'guard.owner.testnet', NEAR_GUARD_AGENT_KEY: KeyPair.fromRandom('ed25519').toString() });Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.console.error(`.env already has a guard (${env.NEAR_GUARD_ID})`);Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.for (const file of ['test/guard.test.js', 'demo/paid-api.js', 'demo/agent.js', 'scripts/mcp-demo.js', 'docs/img/demo.gif', '.env']) assert.ok(!ships(file), `${file} should not ship`);Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.if (existsSync('.env')) {Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.* Needs .env from near-testnet-account.js and near-demo-payees.js. It:Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.const r = await circleWalletRailFromEnv({ ...env, CIRCLE_WALLET_ADDRESS: ADDRESS.toLowerCase() }, { client: clientWith(wallet), mode: 'authorization' });Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.appendFileSync('.env', `PAYEE_${name === 'rnaps' ? 'THIEF' : name.toUpperCase()}=${id}\n`);Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.const run = () => promisify(execFile)(process.execPath, [SETUP], { cwd: dir, env: { PATH: process.env.PATH, ...env } });Fix: Stop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.test('starts the stdio server the way the README does: npx @vulinh1704/spend-guard mcp', () => {Fix: Pin versions and sources; vendor anything fetched at install time.Cost to the agent
Tokens estimated at four characters each. The tool list is re-read by the agent on every turn, so it is the number to compare between servers that do the same job: ranked across the index.
Known vulnerabilities
Vulnerability advisories (OSV) cover npm and PyPI packages; this registry has no advisory database.
What would make this Verified
7 points to 80 · weights: behavior 45%, static 25%, prompt 15%, publisher 15%- Unclaimed publisher≈ +8 on the totalClaim vulinh1704: a verified publisher gets +50 on publisher trust. It is free and usually instant with GitHub sign-in.
- Hidden instructions≈ +6 on the totalRemove HTML comments, zero-width characters and white-on-white text that carry instructions. Everything the agent is meant to read should be visible to the person reading the file.
- Access to sensitive file paths≈ +4 on the totalStop enumerating or reading credential locations; take secrets from a declared environment variable the user sets on purpose.
- Process kill command≈ +2 on the totalBuild commands from fixed arguments; do not interpolate untrusted strings into a shell.
- Hex-encoded URL≈ +2 on the totalPut hosts, commands and strings in plain source. Decoding at runtime looks identical to malware and costs points on its own.
- File permission modification command≈ +2 on the totalRequest the narrowest permission that works; replace wildcards with the exact paths or hosts.