Skip to main content
Glama
README.md
# tradex — an open marketplace for the agent economy

Developers list AI agents, scrapers and APIs. Buyers pay **per call, in USDC on Solana**, settled
over **[x402](https://x402.org)**. No accounts, no API keys, no invoices. A **Gemini buyer agent**
autonomously discovers a listing, pays for it, calls it, and composes an answer — while the services
it buys are themselves Gemini-backed.

Reputation is not a number we store. It is the provider wallet's **on-chain settlement history**,
plus a rating tally in a **Solana program we deploy**. Public, verifiable, portable.

```
Buyer agent (Gemini) ──HTTP──▶ Provider endpoint
   │  1. POST /api/agents/…      │  2. 402 + PaymentRequirements
   │  3. signs an SPL transfer (x402 `exact` scheme)
   │  4. retry with X-PAYMENT ──▶ 5. facilitator verifies + settles on Solana
   ▼                                6. 200 + result
Solana: the USDC transfer IS the reputation signal
```

## Quick start

Four terminals' worth of commands, but only one of them blocks:

```bash
npm install
npm run validator     # terminal 1 — a real Solana validator on 127.0.0.1:8899
npm run setup         # keypairs, SOL, an SPL mint, token accounts, funds the buyer
npm run program:deploy # the on-chain reputation program
npm run db:init       # Postgres registry + seed listings
npm run dev           # http://localhost:3777
npm run e2e           # proves a paid call settles on-chain
```

`npm run setup` writes `.env.local` for you. It is idempotent — re-run it any time.

**Optional but recommended:** drop a free key from
[aistudio.google.com/apikey](https://aistudio.google.com/apikey) into `.env.local`:

```
GEMINI_API_KEY=…
```

Without it everything still runs end-to-end — the buyer agent falls back to a deterministic planner
and the paid services to local implementations. With it, Gemini does the planning, the structured
extraction, the semantic search and the live-grounded research. Every response reports which engine
served it, so a fallback result is never passed off as a Gemini one.

## What's actually on-chain

| | |
|---|---|
| **Settlement** | Every paid call is a real SPL `transferChecked` from the buyer's token account to the provider's. That transaction is the receipt. |
| **Reputation (history)** | `getSignaturesForAddress` on the provider's token account → call count + revenue. We store nothing; anyone can recompute it from any RPC. |
| **Reputation (ratings)** | A native Rust program (`programs/reputation`). PDA seeds `["rep", provider]`, holding `call_count`, `total_rating_points`, `rating_count`, `total_volume`. One instruction, `RecordRating`. |
| **Facilitator** | The x402 facilitator runs in-process using the official `@x402/svm` facilitator scheme, and is exposed over the standard REST surface at `/api/facilitator/{supported,verify,settle}`. It is the fee payer, so **the buyer agent never needs SOL** — only tokens. |
| **Blinks** | `/actions.json` + `/api/actions/buy/[id]` — any listing unfurls into a Solana Action that pays from the user's own wallet. |

## Which network

The default is a **local `solana-test-validator`**: the same runtime, the same programs, real
signatures, real explorer links — but with a faucet that works. The public devnet faucet is rate-limited
to the point of being unusable for a hands-off setup (we tried; it 429s).

To move to public devnet, fund the treasury address that `npm run setup` prints, then:

```bash
SOLANA_CLUSTER=devnet npm run setup
```

To settle **real Circle devnet USDC through the public x402.org facilitator** instead of our own,
fund the buyer at [faucet.circle.com](https://faucet.circle.com) and set `X402_MODE=hosted`. Nothing
else changes — same protocol, same code path.

## The paid services

All three are x402-gated and listed in the marketplace.

| Service | Price | Gemini feature |
|---|---|---|
| `POST /api/agents/scrape-summarize` | $0.01 | structured output (`responseJsonSchema`) over fetched page text |
| `POST /api/agents/sentiment` | $0.005 | structured output, constrained enum |
| `POST /api/agents/research` | $0.02 | **Google Search grounding** with citations |

Plus `POST /api/agents/echo` ($0.001) — a dependency-free resource used by `npm run e2e` to prove the
payment rail in isolation.

The **buyer agent** (`POST /api/buyer`) runs a bounded Gemini function-calling loop — hard-capped at 5
tool turns — with three tools: `search_marketplace`, `get_listing`, and `call_paid_agent`. The last one
is where x402 fires. Steps stream to the UI over SSE, so you watch it discover → pay → call → compose.

Marketplace discovery uses **Gemini embeddings** (`gemini-embedding-2`) for semantic ranking, falling
back to keyword overlap.

## Use it as an MCP server

The buyer agent's tools are just an MCP interface with one unusual property: **one of the tools
spends money**. Mount tradex in any MCP-capable agent (Claude Desktop, Claude Code, Cursor) and it
gains a Solana wallet and a marketplace it can buy from.

```
search_marketplace(query, maxPriceUsdc?)   free    find a service
get_listing(id)                            free    exact input schema + on-chain reputation
get_reputation(providerWallet)             free    read straight from Solana
get_wallet()                               free    address, balance, spend so far
buy(id, input)                             SPENDS  x402 pay -> call -> result + tx signature
```

`buy` is annotated `readOnlyHint: false, idempotentHint: false`, so hosts prompt the human before it
runs. Two hard caps sit in front of it — `MCP_MAX_PRICE_USDC` per call (default `0.10`) and
`MCP_MAX_SPEND_USDC` for the process (default `1.00`) — and it refuses rather than overspending. If
a service fails, x402 never settles, and the tool says so explicitly so the agent doesn't think it
wasted money.

Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (or `.mcp.json` for Claude
Code):

```json
{
  "mcpServers": {
    "tradex": {
      "command": "npx",
      "args": ["tsx", "/path/to/tradex/mcp/server.ts"],
      "env": {
        "TRADEX_URL": "http://localhost:3777",
        "MCP_MAX_PRICE_USDC": "0.10",
        "MCP_MAX_SPEND_USDC": "1.00"
      }
    }
  }
}
```

The agent brings **its own wallet** (`BUYER_WALLET_SECRET`) and points at whichever marketplace it
likes, so this works against a local checkout or a remote deployment. Verify it end to end with
`npm run test:mcp`, which spawns the server over stdio, buys a call, and asserts the wallet was
actually debited on-chain.

### Remote MCP (`POST /api/mcp`)

For clients that want a URL rather than a subprocess (claude.ai connectors, Gemini CLI over HTTP).
Two things make this path different from stdio, and both are load-bearing:

- **It spends the app's wallet, not the caller's, and it is on the public internet.** So it is
  bearer-token gated: send `Authorization: Bearer $MCP_AUTH_TOKEN`. Without that, anyone who found
  the URL could drain the buyer wallet one $0.02 call at a time.
- **Serverless rebuilds the handler on every request**, so an in-memory spend counter resets each
  call and enforces nothing. The remote budget is persisted in Postgres (`mcp_spend`) so the cap is
  real across cold starts.

```bash
curl -X POST https://<your-app>/api/mcp \
  -H "Authorization: Bearer $MCP_AUTH_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

An agent that wants to bring its **own** wallet should run `mcp/server.ts` over stdio instead.

### If you're a stranger (BYO wallet — the real path)

The remote endpoint above spends the *marketplace operator's* wallet, which is fine for a demo and
wrong for everything else. A real user brings their own money, and needs no config to start:

```json
{
  "mcpServers": {
    "tradex": { "command": "npx", "args": ["tsx", "/path/to/tradex/mcp/server.ts"] }
  }
}
```

On first run the server **generates a wallet for you**, saves it to `~/.tradex/wallet.json` (mode
`0600`), and defaults to the public marketplace on devnet. Then:

- `search_marketplace` / `get_listing` / `get_reputation` work immediately — browsing is free.
- `get_wallet` shows your new address and tells you it's empty.
- `buy` **refuses** until you fund it, and says exactly how:

```
Your wallet has 0 USDC — not enough for "Sentiment Classifier" ($0.005).
Fund this address with devnet USDC at https://faucet.circle.com (Solana → Devnet):
  CchngwiPkKz2Q5v5jDdgjp4k2LoXpKSX6Mj4fA5D8ZLA
Nothing was spent.
```

Fund it, and you're trading. Default caps ($0.10/call, $1.00 lifetime) apply until you raise them.
The marketplace never holds your funds and never sees your key.

## Notes on the stack

- **x402 v2** (`@x402/core`, `@x402/svm`, `@x402/next`, `@x402/fetch` @ 2.18.0). Note the v2 protocol
  carries `PaymentRequirements` in a base64 **`PAYMENT-REQUIRED` header**, not the response body — the
  402 body is legitimately `{}`.
- `withX402` settles **only after** the handler returns <400. A service that fails never charges the caller.
- **Never mainnet.** The keys in `.keys/` and `.env.local` are throwaway test keys.

## Layout

```
app/api/agents/*     the paid, x402-gated services
app/api/buyer        the Gemini function-calling buyer agent (SSE)
app/api/facilitator  our x402 facilitator, over REST
app/api/actions      Solana Actions / Blinks
lib/x402.ts          resource server, facilitator, buyer client, payAndCall
lib/gemini.ts        tool loop, structured output, embeddings, search grounding
lib/reputation.ts    merges on-chain history + the program's PDA
programs/reputation  the native Rust Solana program
scripts/e2e.ts       end-to-end proof that a paid call settles
```