Stellar Hackathon 2026 · Agents on Stellar Zero mock data. Real x402 payments. Real Serper.dev Search. Real Groq AI. Real Freighter wallet.
StellarSearch is a pay-per-query web search API for autonomous AI agents. Every search costs 0.001 USDC, settled on Stellar in ~5 seconds using the x402 protocol. No subscriptions, no API keys for the end user — agents pay per request and get real web search results back.
| Layer | Real package / service |
|---|---|
| Payment protocol | @x402/express + @x402/stellar + @x402/core |
| Blockchain | Stellar Testnet (via Horizon API) |
| Facilitator | OpenZeppelin x402 (channels.openzeppelin.com) |
| Wallet connect | @stellar/freighter-api (real Freighter extension) |
| Balances / tx | Stellar Horizon REST API (live, not mocked) |
| Search results | Serper.dev API (real Google search results) |
| AI assistant | groq-sdk · Llama 3.3 70B (real Groq API) |
| Frontend | React 18, TypeScript, Tailwind CSS, Framer Motion |
git clone <this-repo>
cd stellar-search
npm install| Key | Where to get it |
|---|---|
STELLAR_RECEIVING_ADDRESS |
Stellar Lab — generate + fund testnet keypair |
SERPER_API_KEY |
serper.dev — free tier: 2.5k queries/month |
GROQ_API_KEY |
console.groq.com/keys — free |
cp .env.example .env
# Fill in the 3 keys above (plus optional variables — see Environment Variables table below)Install the Freighter browser extension, create a testnet wallet, and fund it with USDC at Stellar Lab.
# Terminal 1 — backend
npm run server
# Terminal 2 — frontend
npm run dev
# → http://localhost:5173npm run test:search "Stellar blockchain"All environment variables are read from .env (see .env.example for a template). Variables prefixed with VITE_ are exposed to the browser by Vite; all others are server-side only.
| Variable | Required | Default | Description | Example |
|---|---|---|---|---|
SERPER_API_KEY |
Yes | — | API key for Serper.dev web search. Without this, all search, image, and news endpoints return 500. |
your_serper_api_key_here |
GROQ_API_KEY |
Yes | — | API key for Groq AI (Llama 3). Without this, the AI assistant and search suggestions fail with an auth error. Server prints GROQ: ✗ MISSING on startup. |
gsk_xxxxxxxxxxxxxxxxxxxxxxxx |
STELLAR_RECEIVING_ADDRESS |
Yes | — | Stellar public key that receives 0.001 USDC per query. Without this, the x402 payment middleware has no payTo address and payments fail. Server prints Receiving: ✗ MISSING on startup. |
GDXA3V2LI3VN3GBH5BMOF25QSFJV7S7ZOWMHHQMJRPP4BVORDDRTIIMU |
STELLAR_NETWORK |
No | stellar:testnet |
Stellar network for the server-side x402 middleware. Accepts stellar:testnet or stellar:mainnet. Falls back to testnet if missing. |
stellar:testnet |
VITE_STELLAR_NETWORK |
No | stellar:testnet |
Frontend copy of STELLAR_NETWORK (must be prefixed VITE_ for browser access). Falls back to testnet if missing. |
stellar:testnet |
FACILITATOR_URL |
No | https://www.x402.org/facilitator |
x402 facilitator endpoint for payment settlement. Falls back to the public OpenZeppelin facilitator if missing. | https://www.x402.org/facilitator |
PORT |
No | 3001 |
Express server listen port. Falls back to 3001 if missing. |
3001 |
VITE_SERVER_URL |
No | http://localhost:3001 |
Frontend URL for AI chat backend calls. On Vercel deployments auto-detects ${origin}/api; locally falls back to http://localhost:3001. |
http://localhost:3001 |
MCP_ENABLE_RECEIPTS |
No | 0 |
Set 1 to opt-in MCP local receipt storage for stellar-search://receipts/recent (in-memory capped at 50) |
1 |
Browser (Freighter) → GET /search?q=...
← HTTP 402 + payment requirements
→ Sign Soroban auth entry (Freighter prompt)
→ GET /search + X-Payment: <signature>
← OpenZeppelin facilitator verifies + settles 0.001 USDC
← 200 OK + Search results
- Agent hits
/search— the@x402/expressmiddleware intercepts - Returns
HTTP 402 Payment Requiredwith price + network + payTo address - The x402 client signs a Soroban authorization entry via Freighter wallet
- Retries with
X-Paymentheader containing the signed entry - OpenZeppelin facilitator at
channels.openzeppelin.com/x402/testnetverifies the signature and settles 0.001 USDC on Stellar testnet - Server enforces payment integrity (
src/lib/paymentIntegrity.ts), rejecting replayed or duplicate payloads within the 300-second validity window, and returns search results
To guarantee that each payment identifier authorizes exactly one provider call, StellarSearch tracks consumed payment identifiers across Express (server/index.ts) and Vercel (api/search.ts) runtimes:
- Payload Invalidation: Extracts transaction hashes (or SHA-256 fallback hashes of payment headers) and invalidates consumed payloads for a 300-second window.
- Concurrency Throttling: Rapid parallel requests using identical payment payloads are throttled so only one search query proceeds; concurrent duplicates immediately receive HTTP 402 (
Payment payload already consumed).
sequenceDiagram
autonumber
actor User as User / Agent
participant Browser as Browser
participant Freighter as Freighter Wallet
participant Server as StellarSearch Server
participant Facilitator as x402 Facilitator
participant Horizon as Stellar Horizon
participant Serper as Serper.dev
User->>Browser: Enter search query
Browser->>Server: GET /search?q=...
Server-->>Browser: 402 Payment Required<br/>(price, network, payTo)
Browser->>Freighter: Request signature of<br/>Soroban auth entry
Freighter-->>Browser: Signed payment payload
Browser->>Server: GET /search?q=...<br/>+ X-Payment header
Server->>Facilitator: Verify X-Payment (HTTPFacilitatorClient)
Facilitator->>Horizon: Submit & settle 0.001 USDC tx
Horizon-->>Facilitator: Transaction confirmed (tx hash)
Facilitator-->>Server: Verification + X-Payment-Response
Server->>Serper: POST https://google.serper.dev/search
Serper-->>Server: Real Google search results
Server-->>Browser: 200 OK + results + txHash
Browser-->>User: Display paid search results
stellar-search/
├── src/ # React frontend
│ ├── hooks/
│ │ ├── useFreighterWallet.ts # Real Freighter + Horizon integration
│ │ └── useSearch.ts # Calls real server endpoint
│ ├── components/
│ │ ├── AnimatedBackground.tsx # Canvas animation
│ │ ├── WalletPanel.tsx # Real Freighter connect + live balances
│ │ ├── PaymentFlowVisualizer.tsx
│ │ ├── SearchResults.tsx
│ │ ├── StatsGrid.tsx # Polls real /health endpoint
│ │ └── GroqAssistant.tsx # Real Groq AI chat
│ ├── pages/
│ │ ├── SearchPage.tsx
│ │ ├── DocsPage.tsx
│ │ └── DashboardPage.tsx # Live Horizon tx history
│ └── lib/stellar.ts # Horizon helpers
├── server/
│ └── index.ts # Express + @x402/express + Serper.dev + Groq + batch/jsonl + jobs/webhooks
├── api/
│ ├── search.ts # Vercel parity for GET /search
│ ├── search/batch.ts # Vercel parity for POST /search/batch (JSONL)
│ ├── jobs.ts # POST /jobs (+ GET /jobs list)
│ ├── jobs/[id].ts # GET /jobs/:id status + verified payment
│ ├── ai/chat.ts # Vercel AI chat (streaming)
│ └── health.ts # Vercel health
├── mcp-server/
│ └── index.ts # MCP tools + resources + prompts + progress
├── scripts/
│ └── test-search.ts # End-to-end test script
├── .env.example
├── claude_mcp.json
└── README.md
// claude_mcp.json
{
"mcpServers": {
"stellar-search": {
"command": "npx",
"args": ["tsx", "./mcp-server/index.ts"],
"env": {
"GROQ_API_KEY": "your_groq_api_key",
"SEARCH_API_URL": "http://localhost:3001"
}
}
}
}Then tell Claude Code: "Search for the latest Stellar x402 examples" — it calls web_search, the server pays via x402, and Claude gets real results.
Paid MCP tools (web_search, image_search, news_search) emit bounded notifications/progress events for actual payment/search phases only when the client sends _meta.progressToken:
| progress | total | phase | message |
|---|---|---|---|
| 1 | 4 | challenge | Requesting payment challenge |
| 2 | 4 | signing | Signing Soroban auth |
| 3 | 4 | settlement | Settling 0.001 USDC on Stellar |
| 4 | 4 | search | Searching Serper |
Cancellation (notifications/cancelled) and errors terminate progress cleanly without false completion — the tool returns isError: true and no additional progress after abort. Progress is never sent without a progressToken; free tools never emit progress.
Resources (no payment required):
stellar-search://capabilities— network, price, x402 scheme, endpoint mapstellar-search://schema/search— JSON schema forSearchResponse, batch JSONL events, and job contractsstellar-search://receipts/recent— recent paid receipts only when opted-in (MCP_ENABLE_RECEIPTS=1, capped at 50, in-memory, no secrets). Otherwise returns guidance to opt-in.
Prompts (no silent payment):
research_brief— proposes 3–5 queries, asks for explicit user approval before calling any paidweb_search, then synthesizes viaai_summarizesummarize_results— free Groq summarization of pasted resultscompare_sources— free comparison with citations
Prompts never call paid tools themselves; the agent must obtain user approval before initiating x402 settlement. This preserves explicit user approval and verified settlement for paid actions.
POST /search/batch (Express: POST /search/batch, Vercel: POST /api/search/batch)
Content-Type: application/json → Response: application/x-ndjson (versioned JSONL)
Bounded to 10 queries and 0.01 USDC aggregate (10 × 0.001). Idempotency via Idempotency-Key header or body.idempotencyKey (24 h). Events are v:1 versioned:
quote— price preview (also returned as 402PAYMENT-REQUIREDwhen noX-Paymentheader)settlement— verifiedpaymentId/txHashafterconsumePaymentPayloadresult— per-query normalized results (one line per query, machine-readable)error— per-item failures without aborting the batch (UPSTREAM_ERROR,SEARCH_FAILED,CLIENT_DISCONNECT,SKIPPED)done— aggregatesucceeded/failed/totalUsdcSpent/aggregateLatencyMs
Disconnect aborts the in-flight Serper fetch via AbortController, emits CLIENT_DISCONNECT/SKIPPED errors for remaining items, and ends without a false done succeeded count. Partial completion is explicit in done. Example:
curl -N -X POST http://localhost:3001/search/batch \
-H "Content-Type: application/json" \
-H "X-Payment: <base64-signed-auth>" \
-H "Idempotency-Key: my-batch-123" \
-d '{"queries":["stellar x402","serper.dev"],"count":5}'
# each line is JSON: {"v":1,"type":"result",...}POST /jobs → 202 { jobId, statusUrl, paymentVerified, paymentId, txHash }
GET /jobs/:id → { job, paymentVerified, statusUrl }
GET /jobs → { jobs, count }
- Idempotent creation via
Idempotency-Key(24 h, returns existingjobId/statusUrlon replay). - Payment verified via
x402header +consumePaymentPayloadreplay protection;GET /jobs/:idexposes verified payment state (paymentVerified,txHash,paidAmount). - Optional webhook:
{ webhookUrl, webhookSecret }—webhookSecret≥16 chars. Delivery is signed (X-Webhook-Signature: HMAC-SHA256(timestamp.payload),X-Webhook-Timestamp,X-Webhook-Attempt,X-Job-Id) and retries with backoff (5 attempts,1s·2^n+ jitter, 5 s timeout, non-retryable 4xx except 429). Protects against replay viatimestamp(5 min window) +nonce, and SSRF by rejectinghttp, private IPs (10/8,192.168/16,172.16/12,169.254/16,fc00::/7,fe80::/10,localhost,127.0.0.1,0.0.0.0,::1) and URLs with credentials.
curl -X POST http://localhost:3001/jobs \
-H "Content-Type: application/json" \
-H "X-Payment: <base64>" \
-H "Idempotency-Key: job-123" \
-d '{"query":"stellar x402","webhookUrl":"https://example.com/hook","webhookSecret":"s3cr3t-16-chars-min"}'
# → {"jobId":"...","statusUrl":"http://localhost:3001/jobs/...","paymentVerified":true}
curl http://localhost:3001/jobs/<jobId>Webhook verification (receiver):
import crypto from 'crypto'
function verify(payload, signature, secret, tsHeader) {
const expected = crypto.createHmac('sha256', secret).update(`${tsHeader}.${payload}`).digest('hex')
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature)) && Date.now() - parseInt(tsHeader) < 5*60*1000
}Express/Vercel/browser/MCP contracts stay aligned: STELLAR_NETWORK, USDC_CONTRACT, AMOUNT_STROOPS=10000 (0.001 USDC), and verified settlement remain the single source of truth (src/lib/constants).
Coverage is enforced via Vitest + @vitest/coverage-v8 with thresholds for statements, branches, functions, and lines (see vite.config.ts:6).
npm run test # unit tests without coverage
npm run test:coverage # run with coverage + thresholds (CI gate)Reports are generated to coverage/ (text, json, html, lcov). CI uploads the coverage/ artifact and fails if thresholds are not met.
Global thresholds are deliberately modest initially and ratchet upward as payment/wallet/API/MCP/UI tests land:
| Scope | Statements | Branches | Functions | Lines |
|---|---|---|---|---|
| Global | 35% | 30% | 28% | 35% |
src/lib/constants.ts |
90% | 60% | 100% | 90% |
src/lib/stellar.ts |
85% | 75% | 85% | 85% |
src/lib/paymentIntegrity.ts |
90% | 85% | 95% | 90% |
server/corsConfig.ts |
90% | 85% | 95% | 90% |
src/components/search/SearchBar.tsx |
80% | 80% | 90% | 80% |
server/index.ts |
65% | 60% | 65% | 65% |
api/search.ts |
90% | 75% | 80% | 90% |
api/health.ts |
80% | 50% | 100% | 80% |
mcp-server/index.ts |
30% | 20% | 20% | 30% |
src/hooks/useFreighterWallet.ts |
85% | 65% | 90% | 85% |
Ratchet policy: When a module's real coverage exceeds its threshold, bump the threshold in
vite.config.tsin the same PR. Global thresholds ratchet15 → 25 → 35as payment, wallet, API, MCP, and UI behavior moves from untested to tested. Keep Express (server/), Vercel (api/), browser (src/), and MCP (mcp-server/) constants aligned (STELLAR_NETWORK,USDC_CONTRACT,AMOUNT_STROOPS=10000→0.001 USDC).
Coverage verifies the x402 settlement semantics for paid routes (/search, /images, /news): scheme=exact, network=stellar:testnet|mainnet, amount=10000 stroops, asset=C... (Soroban USDC contract, not USDC:ISSUER), payTo=G.... See server/index.ts:104, api/search.ts:48, and mcp-server/index.ts:19.
| Requirement | ✓ |
|---|---|
| Open-source repo + README | ✅ |
| 2–3 min video demo | Record showing: connect Freighter → search → see 402 → payment settles → results |
| Real Stellar testnet transactions | ✅ Every search settles 0.001 USDC via OpenZeppelin facilitator |
| x402 protocol | ✅ @x402/express + @x402/stellar |
| Addresses explicit demand signal | ✅ "pay-per-query web search instead of monthly subscriptions" |