Skip to main content
Glama
README.md
# kushbitx-mcp-agent

An AI-agent integration for [`@kushbitx/sdk`](https://www.npmjs.com/package/@kushbitx/sdk), built for the
`$50 USDC` integration bounty in [kushBitxHQ/kushbitx-sdk#1](https://github.com/kushBitxHQ/kushbitx-sdk/issues/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 |

## Quick start

Node.js 22 or later.

```bash
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. |

### MCP host configuration

```json
{
  "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. |
| `curl-fetch.mjs` | A zero-dependency `fetch` implementation used when a proxy is required (the SDK exposes a `fetchFn` seam, so the published package needs no new dependency). |
| `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 and input shaping. |
| `test/mcp.test.mjs` | Live suite: drives the real MCP server over the official stdio transport. |

## 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

ℹ tests 14   ℹ pass 14   ℹ fail 0
```

Live suite — the real MCP server against the hosted endpoints:

```
$ KUSHBITX_LIVE=1 node --test test/mcp.test.mjs
✔ MCP server lists exactly the three free capabilities (373ms)
✔ MCP preview_token returns live market data (5264ms)
✔ MCP evaluate_spend is advisory and never authorizes execution (779ms)
✔ MCP discover_payment_challenge reads the 402 terms without signing or paying (659ms)
✔ MCP rejects a malformed recipient instead of calling the service (212ms)

ℹ tests 5   ℹ pass 5   ℹ fail 0
```

### Sanitized end-to-end run

`npm start` executed the live flow. The model chose the order itself
(`preview_token → evaluate_spend → discover_payment_challenge`), producing:

```
$ node main.mjs
stopped     : model-finished
tools used  : preview_token -> evaluate_spend -> discover_payment_challenge
steps       : 3 | elapsed: 18439ms
   0. preview_token             token=USDC priceUsd=1.000021
   1. evaluate_spend            decision=APPROVE findings=0 executionAuthorized=false
   2. discover_payment_challenge x402 v2 exact eip155:8453 amount=250000 timeout=300s signed=false paid=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). The SDK endpoints themselves are fine — with an explicit egress the
same requests return `200`. Because Node's global `fetch` does not read the system proxy, this repo
ships `curl-fetch.mjs` and hands it to the SDK through the existing `fetchFn` seam:

```bash
KUSHBITX_PROXY=http://127.0.0.1:7897 npm start
```

Without `KUSHBITX_PROXY` the code uses the global `fetch` and needs no proxy at all, so this workaround
costs a normal environment nothing.

## License

Apache-2.0, matching the SDK.