Skip to main content
Glama

kushbitx-mcp-agent

An AI-agent integration for @kushbitx/sdk, built for the $50 USDC integration bounty in kushBitxHQ/kushbitx-sdk#1.

The model decides what to call. There is no fixed sequence: the agent loop appends the model's tool calls, executes them, feeds the results back, and repeats until the model stops asking for tools or a SpendGuard decision stops the run. The same three capabilities are also exposed as an MCP stdio server.

Nothing here can sign a payment or move funds. The paid path stops at reading an x402 challenge out of a 402 response: no signature is created, no transaction is sent, and no private key is ever read.

What it demonstrates

Capability

Endpoint

Cost

Used for

preview_token

/api/token-preview

free

Live Base token market data (no wallet)

evaluate_spend

/api/spendguard/evaluate

free

Policy evaluation before money moves (advisory)

discover_payment_challenge

/api/token-risk, /api/transaction-preflight, /api/verify-payment

free to inspect

Reads the 402 PAYMENT-REQUIRED terms; never pays

Related MCP server: Base Intel MCP

Quick start

Node.js 22 or later.

npm install
cp .env.example .env          # then set your LLM key, or export DEEPSEEK_API_KEY
npm run start                 # run the agent end to end
npm test                      # offline suite (no network, no credentials)
npm run mcp                   # run the MCP stdio server

The LLM is any OpenAI-compatible chat-completions endpoint with tool calling. The default is deepseek-chat; set LLM_BASE_URL / LLM_MODEL to point elsewhere.

Environment

Variable

Required

Meaning

DEEPSEEK_API_KEY

for npm start

LLM key. Alternatively DSH_CREDENTIALS may point at a credentials file whose refs: block contains it.

LLM_BASE_URL

no

Default https://api.deepseek.com

LLM_MODEL

no

Default deepseek-chat

KUSHBITX_PROXY

no

Route SDK traffic through a proxy, e.g. http://127.0.0.1:7897. See “Network note” below.

KUSHBITX_LIVE

no

1 enables the live MCP integration tests.

KUSHBITX_PROXY

required by npm run test:live

The live suites dial kushbitx.com; on a network where the Cloudflare edge rejects this host's egress, the suite fails with 403 until this is set. The one proxy-only check skips itself (with a message) when the variable is absent rather than assuming a port.

MCP host configuration

{
  "mcpServers": {
    "kushbitx": { "command": "node", "args": ["mcp-server.mjs"] }
  }
}

Files

Path

Purpose

agent.mjs

The tool-calling loop, the dispatch table and the chat-completions caller.

tools.mjs

Tool schemas plus a thin mapping layer over the published SDK.

mcp-server.mjs

The same three capabilities over MCP stdio.

proxy-fetch.mjs

Proxy transports: curl -K - (every request value, including headers, travels on the child's stdin, never in its argv) and an in-process CONNECT tunnel as a fallback for hosts without curl. Selected by KUSHBITX_TRANSPORT=auto|curl|native.

credentials.mjs

Resolves the LLM key from the environment or a credentials file; never logs it.

run.mjs / main.mjs

End-to-end run and CLI entry point; writes sanitized evidence.

test/agent.test.mjs

Offline suite: the agent loop, the toolkit mapping, input shaping and the x402 decode.

test/proxy-fetch.test.mjs

The argv-leak regression: a stub child records its own argv and stdin.

test/credentials.test.mjs

The credentials parser: quoting, block boundaries, env precedence.

test/mcp.test.mjs

Live suite: drives the real MCP server over the official stdio transport.

scripts/run-live-tests.mjs

Cross-platform launcher for the live suites (npm run test:live).

audit.mjs

Adversarial review probe: injection, edge cases, contract drift.

test/fixtures/verify-argv-leak.mjs

Manual helper that prints what the child process actually saw.

Test report

Offline suite — no network, no credentials, the SDK is driven through its fetchFn seam:

$ npm test
✔ toolkit maps a real SDK preview response into the reported shape
✔ toolkit rejects a malformed token address before any request
✔ toolkit reports the x402 challenge without signing or paying
✔ toolkit surfaces an unknown paid service as an error
✔ agent follows the model's tool order — forward
✔ agent follows the model's tool order — reversed, proving order is not hardcoded
✔ agent ends immediately when the model needs no tools
✔ a non-APPROVE spend decision stops the run without signing or paying
✔ a tool error is reported to the model, not thrown
✔ an unknown tool name is reported back to the model
✔ maxSteps bounds a model that never stops calling tools
✔ normalizeSpendInput applies policy defaults and keeps the caller allowlist
✔ buildPaidInput shapes per-service payloads
✔ every tool schema declares a name and an object schema
✔ the child process argv carries no secret (the leak this fixes)
✔ the stub proves the secret survived the round trip
✔ the transport source spawns a child but passes it no arguments carrying data
✔ every other transport in the repo avoids child processes entirely
✔ parseRawResponse keeps the last HTTP block and its headers
✔ transport selection: explicit modes win, auto is the fallback wrapper
✔ auto falls back to the native tunnel when curl is missing entirely
✔ auto falls back per-request when the curl exec fails without a response
✔ the native fallback tunnel works end to end (no setup needed without curl)

ℹ tests 36   ℹ pass 35   ℹ fail 0   ℹ skipped 1

Live suite — the real MCP server against the hosted endpoints, through the proxy transport. Requires KUSHBITX_PROXY (see the environment table): without it these checks get 403 from Cloudflare in this environment.

$ npm run test:live
ℹ tests 16   ℹ pass 16   ℹ fail 0

Sanitized end-to-end run

npm start executed the live flow. The model chose the order itself, producing:

$ node main.mjs
stopped     : model-finished
tools used  : preview_token -> discover_payment_challenge -> evaluate_spend
steps       : 3 | elapsed: 11693ms
   0. preview_token             token=USDC priceUsd=null   (source.stale=true)
   1. discover_payment_challenge x402 v2 exact eip155:8453 amount=250000 timeout=300s signed=false paid=false
   2. evaluate_spend            decision=APPROVE findings=0 executionAuthorized=false

Final answer (abridged — this is verbatim from the run above; npm start reproduces it and writes the full sanitized trajectory to evidence/agent-run.json, which is git-ignored like any run artifact):

**Token checked:** USDC on Base (0x8335…2913) — Aerodrome USDC/USDbC, price $1.000021,
liquidity $146,020.43; source DexScreener, flagged stale at 182s.

**Spend decision:** APPROVE (advisory only; executionAuthorized: false)
• 1.00 USDC, policy max/tx 5.00, daily 20.00, human approval above 2.00, unknown recipients blocked
• Findings: none.

**x402 terms (token-risk, /api/token-risk):** version 2, scheme exact, network eip155:8453 (Base),
amount 250000 base units = 0.25 USDC, timeout 300s, extra {name: "USD Coin", version: "2"}.

**Confirmation:** nothing was signed and nothing was paid (signed: false, paid: false).

Security model

  • No private key is read, stored, or requested anywhere in this repository. There is no code path that signs a payment or sends a transaction.

  • The paid capability is limited to reading the 402 challenge; signed and paid are returned as false by construction and asserted in both suites.

  • Addresses are redacted (0x8335…2913) in the committed evidence; the full values are the public USDC-on-Base and x402 recipient constants already published in the SDK README and agent.mjs.

  • The LLM key is read from the environment (or a credentials file) at runtime and is never logged.

Network note

On some networks the Cloudflare edge in front of kushbitx.com answers 403 to the machine's default egress (cf-ray …-HKG in my case), and the hosted endpoints answer 403 to a request sent from Node's own TLS stack through a proxy that curl succeeds with on the same host — the block page, not the API, is what rejects it. So when a proxy is configured, requests are sent through curl -K -:

KUSHBITX_PROXY=http://127.0.0.1:7897 npm start

The whole configuration — including any Authorization header — is written to curl's stdin, never to its argument list. An earlier revision passed the header as curl -H "authorization: …", which put the model API key in the child's argv where /proc/<pid>/cmdline or ps can read it; that code is gone from this repo. The property is now pinned by tests that run a stub child process and assert what it actually saw:

child argv      : ["-sS","-K","-"]
secret in argv  : no
secret in stdin : yes (expected)
url in argv     : no
proxy in argv   : no

Reproduce with node test/fixtures/verify-argv-leak.mjs. With KUSHBITX_PROXY unset the code uses the global fetch and spawns no child process at all, so a normal environment pays nothing for this.

License

Apache-2.0, matching the SDK.

Why auto prefers curl

The proxy transport is selectable:

KUSHBITX_TRANSPORT=auto           (default) curl -K -, with a real fallback: the native
                                  tunnel is used when the curl probe fails, and
                                  per-request when a curl exec yields no response
KUSHBITX_TRANSPORT=curl           the stdin-only curl transport, no fallback
KUSHBITX_TRANSPORT=native         in-process CONNECT tunnel, no child process
KUSHBITX_TRANSPORT=auto-nocurl    native, without probing for curl

Three cases reach the fallback and are covered by tests: curl missing from PATH, a curl exec that fails without producing a response, and the forced modes above. Once a fallback has happened the transport stays on native, so a host without curl pays the failed exec at most once.

Measured on this host through the same proxy, same endpoint:

curl (OpenSSL)          -> HTTP 200
node (native tunnel)    -> HTTP 403  (Cloudflare block page)

So the native tunnel is a portability fallback, not an equivalent transport: it is what runs on a host without curl, and on a network where the origin accepts Node's TLS fingerprint it behaves identically. KUSHBITX_TRANSPORT=native forces it for testing; KUSHBITX_TRANSPORT=auto-nocurl selects it without probing for curl.

Related MCP Connectors

Related MCP Servers