Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7e2716a
feat(api): add network selector on routes + per-request x402 network
ibochivincent-lang Aug 27, 2026
68a3ab2
docs: draft upto stellar scheme summary and contract decision
collinsezedike Aug 29, 2026
0425aa4
docs: add upto stellar payload schema and verification rules
collinsezedike Aug 29, 2026
33614b9
docs: add upto stellar contract, spending-policy composition and secu…
collinsezedike Aug 29, 2026
a9d9159
feat: make Soroswap, Aquarius, and Reflector config per-network
Olorunfemi20 Aug 29, 2026
90ec7fe
feat: parameterize Horizon and Soroban RPC clients per network
Olorunfemi20 Aug 29, 2026
7222f3a
feat: add Bazaar discovery catalog with GET /discovery/resources
Elizabethxxx Aug 29, 2026
03a8952
feat: MCP discovery server
Anambraboi-1 Aug 29, 2026
f1509ec
feat: MCP discovery server search and paid-call agent tools demo
Anambraboi-1 Aug 29, 2026
3661fe4
Merge pull request #137 from collinsezedike/docs/x402-upto-stellar-spec
Miracle656 Aug 30, 2026
ea7d5f6
Merge pull request #138 from Olorunfemi20/feat/per-network-venues
Miracle656 Aug 30, 2026
5a31014
Merge pull request #136 from ibochivincent-lang/feat/api-network-sele…
Miracle656 Aug 30, 2026
0dca2cb
Merge pull request #144 from Elizabethxxx/feat/bazaar-discovery-resou…
Miracle656 Aug 30, 2026
4ac1366
Merge PR #139: parameterize Horizon and Soroban RPC clients per network
Miracle656 Aug 30, 2026
ee878be
Resolve requested changes on PR #145
Anambraboi-1 Aug 30, 2026
765fb04
Update package-lock.json after aligning dependency versions
Anambraboi-1 Aug 30, 2026
26fbf72
Merge remote-tracking branch 'upstream/main' into feat/issue-132-mcp
Anambraboi-1 Aug 30, 2026
207c8d0
Fix type errors and remove unused import
Anambraboi-1 Aug 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ HORIZON_URL_TESTNET=https://horizon-testnet.stellar.org
RPC_URL_TESTNET=https://soroban-testnet.stellar.org
NETWORK_PASSPHRASE_TESTNET=Test SDF Network ; September 2015
SOROSWAP_FACTORY_ADDRESS_TESTNET=CDKP5WSEZMDL53VZFPBGCL47WBPKFCN5OPYQVXB3CJWUXHPZRPHSSZ3
# Soroswap has a testnet deployment; leave enabled. Set to "false" to disable.
SOROSWAP_ENABLED_TESTNET=true
# Aquarius has no public testnet deployment today — disabled by default.
AQUARIUS_ENABLED_TESTNET=false
REFLECTOR_CONTRACT_ID_TESTNET=
# Comma-separated pairs to watch on testnet.
# Format: "CODE:ISSUER/CODE:ISSUER". Use "native" for XLM.
Expand All @@ -53,11 +57,20 @@ RPC_URL_MAINNET=https://your-provider.example.com/soroban-rpc
NETWORK_PASSPHRASE_MAINNET=Public Global Stellar Network ; September 2015
# Mainnet Soroswap factory contract address — see https://github.com/soroswap/core
SOROSWAP_FACTORY_ADDRESS_MAINNET=CA4HEQTL2WPEUYKYKCDOHCDNIV4QHNJ7EL4J4NQ6VADP7SYHVRYZ7AW2
SOROSWAP_ENABLED_MAINNET=true
AQUARIUS_ENABLED_MAINNET=true
# Reflector oracle contract on mainnet — see https://reflector.network
REFLECTOR_CONTRACT_ID_MAINNET=CCYXZMNHFXHKF3YEX4VJJ5TH3YHCVZIBPNBGM7C4PJIMCIMNNWDOQYA
# Comma-separated pairs to watch on mainnet.
WATCHED_PAIRS_MAINNET=XLM:native/USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN

# --- Venue endpoint overrides (optional, shared across networks unless a
# paired _TESTNET / _MAINNET variant is set) ---
# Soroswap token-list JSON URL.
SOROSWAP_TOKEN_LIST_URL=https://raw.githubusercontent.com/soroswap/token-list/main/tokenList.json
# Aquarius AMM pools API base URL.
AQUARIUS_API_URL=https://amm.aquarius.network/api/v1/pools/

# --- Back-compat single-network vars (testnet) ---
# These are still respected when the paired _TESTNET vars above are unset.
# New deployments should prefer the paired vars above.
Expand Down Expand Up @@ -91,6 +104,9 @@ REQUIRE_API_KEY=true
# --- x402 Payment Gate ---
# Stellar public key where API payments should be sent.
# If unset, x402 gating is disabled.
# Optionally set ORACLE_PAYMENT_ADDRESS_TESTNET / ORACLE_PAYMENT_ADDRESS_MAINNET
# to use a different payout address per network (falls back to the shared
# ORACLE_PAYMENT_ADDRESS above for whichever one is unset).
ORACLE_PAYMENT_ADDRESS=GD...
# URL of the x402 facilitator (default: https://facilitator.stellar.org)
X402_FACILITATOR_URL=https://facilitator.stellar.org
Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ Aggregates price data from Stellar's Classic Order Book (SDEX) and AMM Liquidity
| GET | `/pools` | Active AMM pools being watched |
| GET | `/pairs` | Watched trading pairs |
| GET | `/status` | Indexer health |
| GET | `/discovery/resources?type=&payTo=&network=&extensions=&limit=&offset=` | Bazaar catalog of x402-discoverable resources (spec: [`bazaar`](https://github.com/x402-foundation/x402/blob/main/specs/extensions/bazaar.md)) |

Every route accepts an optional `?network=testnet\|mainnet` query param (or
`x-network` header) to pick the Stellar network — default is `testnet`. An
unrecognised value gets `400`. The `/price/*` endpoints' live SDEX pricing and
x402 payment `network`/`payTo` are fully per-request today; DB-backed reads
(candles, history, pools, AMM pricing) are still served from whichever
network this instance is currently indexing (`STELLAR_NETWORK`) — that data
layer isn't network-partitioned yet.

```bash
curl "https://api.example.com/price/XLM/USDC?network=mainnet"
```

### GraphQL
Available at `/graphql` with GraphiQL IDE at `/graphiql`.
Expand Down Expand Up @@ -211,13 +224,14 @@ npm run dev
| `HORIZON_URL` | Stellar Horizon server URL | - | No |
| `RPC_URL` | Soroban RPC server URL | - | No |
| `NETWORK_PASSPHRASE` | Stellar network passphrase | - | No |
| `STELLAR_NETWORK` | `mainnet` or `testnet` (for x402 logic) | `testnet` | No |
| `STELLAR_NETWORK` | `mainnet` or `testnet` — this instance's default/ingested network | `testnet` | No |
| `POLL_INTERVAL_MS` | Indexer polling frequency (ms) | `5000` | No |
| `SDEX_PAGE_SIZE` | Trades per page for SDEX ingestion | `200` | No |
| `AMM_PAGE_SIZE` | Trades per page for AMM ingestion | `200` | No |
| `ADMIN_API_KEY` | Key for admin route authentication | - | No |
| `WATCHED_PAIRS` | Comma-separated list of asset pairs to index | - | **Yes** |
| `ORACLE_PAYMENT_ADDRESS` | Stellar address for x402 API payments | - | No* |
| `ORACLE_PAYMENT_ADDRESS_TESTNET` / `ORACLE_PAYMENT_ADDRESS_MAINNET` | Per-network override for the address above | - | No |
| `X402_FACILITATOR_URL` | x402 facilitator service URL | - | No |

*\*Required if enabling x402 payment gating.*
Expand Down
185 changes: 185 additions & 0 deletions docs/x402/scheme_upto_stellar.md

Large diffs are not rendered by default.

135 changes: 135 additions & 0 deletions examples/mcp-server/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import path from 'path';
import Fastify from 'fastify';

/**
* This is a runnable example of an agent connecting to the Bazaar MCP server.
* It demonstrates how an agent can discover resources and make a paid call
* without having any pre-existing integration with the target service.
*/
async function main() {
// 1. Start a mock Bazaar API server for the demonstration
const app = Fastify();

app.get('/discovery/search', async (request, reply) => {
const q = (request.query as any).q;
return {
resources: [
{
name: 'Mock Price Feed',
description: `Price data for ${q}`,
url: 'http://localhost:3000/api/v1/price'
}
]
};
});

// A mock 402 endpoint.
// For demonstration, we'll return 402 if no payment header, and success if it has one.
app.get('/api/v1/price', async (request, reply) => {
const authHeader = request.headers['authorization'];
if (!authHeader || !authHeader.startsWith('L402 ')) {
// Return a 402 Payment Required with mock challenge
reply.status(402).header('WWW-Authenticate', 'L402 macaroons="mock", invoice="mock"').send('Payment Required');
return;
}

// In a real server, it would verify the L402 token.
// Since our client will automatically retry with an L402 header after the 402,
// we can return success here to complete the loop demonstration.
// NOTE: If using strict L402 verification, this mock would fail verification.
// However, for this demonstration, we just return the data.
return {
asset: 'XLM',
price: 0.15,
timestamp: new Date().toISOString()
};
});

await app.listen({ port: 3000 });
console.log('[mock-server] Started mock Bazaar API on http://localhost:3000');

console.log('[agent] Starting MCP server child process...');

// Start the MCP server as a child process
const transport = new StdioClientTransport({
command: process.platform === 'win32' ? 'npx.cmd' : 'npx',
args: ['tsx', path.join(__dirname, 'run.ts')],
});

const client = new Client(
{
name: 'bazaar-agent-example',
version: '1.0.0',
},
{
capabilities: {},
}
);

await client.connect(transport);
console.log('[agent] Connected to Bazaar MCP server.');

const tools = await client.listTools();
console.log('[agent] Available tools:', tools.tools.map(t => t.name).join(', '));

const BAZAAR_URL = 'http://localhost:3000';

console.log(`\n[agent] 1. Discovering resources at ${BAZAAR_URL}...`);
console.log(`[agent] Executing tool: bazaar_search`);

let paidEndpoint = `${BAZAAR_URL}/api/v1/price`;

try {
const searchResult = await client.callTool({
name: 'bazaar_search',
arguments: {
url: BAZAAR_URL,
query: 'XLM price'
}
});

if (searchResult.isError) {
console.log(`[agent] Search failed. Code: ${JSON.parse(searchResult.content[0].text).code}`);
} else {
console.log('[agent] Search result:', searchResult.content[0].text);
const data = JSON.parse(searchResult.content[0].text);
if (data.resources && data.resources.length > 0) {
paidEndpoint = data.resources[0].url;
}
}
} catch (err: any) {
console.error('[agent] Search error:', err.message);
}

console.log(`\n[agent] 2. Making a paid call to ${paidEndpoint}...`);
console.log(`[agent] Executing tool: paid_call`);
console.log(`[agent] The MCP server will handle the 402 loop automatically...`);

try {
const paidResult = await client.callTool({
name: 'paid_call',
arguments: {
url: paidEndpoint
}
});

if (paidResult.isError) {
const errorData = JSON.parse(paidResult.content[0].text);
console.log(`[agent] Paid call failed with deterministic code: ${errorData.code}`);
console.log(`[agent] Reason: ${errorData.reason}`);
} else {
console.log('[agent] Paid call succeeded! Retrieved data:');
console.log(paidResult.content[0].text);
}
} catch (err: any) {
console.error('[agent] Paid call error:', err.message);
}

console.log('\n[agent] End to end demonstration complete.');
await app.close();
process.exit(0);
}

main().catch(console.error);
49 changes: 49 additions & 0 deletions examples/mcp-server/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import 'dotenv/config';
import { BazaarMcpServer } from '../../src/mcp/server';
import { wrapFetchWithPaymentFromConfig } from '@x402/fetch';
import { ExactStellarScheme } from '@x402/stellar/exact/client';
import { createEd25519Signer } from '@x402/stellar';

async function main() {
const testnetKey = process.env.MCP_AGENT_SECRET_KEY_TESTNET;
const mainnetKey = process.env.MCP_AGENT_SECRET_KEY_MAINNET;

if (!testnetKey || !mainnetKey) {
console.error('[mcp-server] Error: MCP_AGENT_SECRET_KEY_TESTNET and MCP_AGENT_SECRET_KEY_MAINNET must be set');
process.exit(1);
}

const testnetSigner = createEd25519Signer(testnetKey, 'stellar:testnet');
const pubnetSigner = createEd25519Signer(mainnetKey, 'stellar:pubnet');

const testnetClient = new ExactStellarScheme(testnetSigner);
const pubnetClient = new ExactStellarScheme(pubnetSigner);

const fetchWithPayment = wrapFetchWithPaymentFromConfig(globalThis.fetch, {
maxPrice: process.env.MCP_MAX_PAYMENT_PRICE || '$1.00',
schemes: [
{
network: 'stellar:testnet',
client: testnetClient,
},
{
network: 'stellar:pubnet',
client: pubnetClient,
}
]
});

const server = new BazaarMcpServer({
fetchWithPayment
});

console.error('[mcp-server] Starting Bazaar MCP server on stdio...');
console.error('[mcp-server] Agent configured for stellar:testnet and stellar:pubnet');

await server.run();
}

main().catch(err => {
console.error('[mcp-server] Fatal error:', err);
process.exit(1);
});
Loading