tradex
by benz16107
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
```
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues