Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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.

119 changes: 110 additions & 9 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ datasource db {

model PricePoint {
id String @default(uuid())
network String @default("testnet")
assetA String @map("asset_a")
assetB String @map("asset_b")
pairKey String @map("pair_key")
Expand All @@ -24,13 +25,14 @@ model PricePoint {
eventId String? @map("event_id")

@@id([id, timestamp])
@@index([pairKey, timestamp(sort: Desc)])
@@index([pairKey, source, timestamp(sort: Desc)])
@@index([network, pairKey, timestamp(sort: Desc)])
@@index([network, pairKey, source, timestamp(sort: Desc)])
@@map("price_points")
}

model PoolSnapshot {
id String @default(uuid())
network String @default("testnet")
poolId String @map("pool_id")
assetA String @map("asset_a")
assetB String @map("asset_b")
Expand All @@ -43,12 +45,13 @@ model PoolSnapshot {
timestamp DateTime

@@id([id, timestamp])
@@index([poolId, timestamp(sort: Desc)])
@@index([network, poolId, timestamp(sort: Desc)])
@@map("pool_snapshots")
}

model PriceAggregate {
pairKey String @map("pair_key")
network String @default("testnet")
window String
bucket DateTime
vwap Decimal @db.Decimal(36, 18)
Expand All @@ -63,44 +66,142 @@ model PriceAggregate {
highPrice Decimal? @map("high_price") @db.Decimal(36, 18)
lowPrice Decimal? @map("low_price") @db.Decimal(36, 18)

@@id([pairKey, window, bucket])
@@id([network, pairKey, window, bucket])
@@map("price_aggregates")
}

model PriceSnapshot {
pair String
network String @default("testnet")
ts DateTime
price Decimal @db.Decimal(36, 18)
volume Decimal @default(0) @db.Decimal(36, 7)

@@id([pair, ts])
@@index([pair, ts])
@@id([network, pair, ts])
@@index([network, pair, ts])
@@map("price_snapshots")
}

model IndexerState {
id String @id
id String
network String @default("testnet")
lastCursor String? @map("last_cursor")
lastLedger Int? @map("last_ledger")
lastProcessedAt DateTime? @map("last_processed_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")

@@id([network, id])
@@map("indexer_state")
}

model PairConfig {
pairKey String @id @map("pair_key")
pairKey String @map("pair_key")
network String @default("testnet")
assetACode String @map("asset_a_code")
assetAIssuer String? @map("asset_a_issuer")
assetBCode String @map("asset_b_code")
assetBIssuer String? @map("asset_b_issuer")
addedAt DateTime @default(now()) @map("added_at")

@@id([network, pairKey])
@@map("pair_configs")
}

/// A single x402-discoverable resource in the Bazaar catalog — either an
/// HTTP endpoint or an MCP tool, per the x402 `bazaar` extension
/// (specs/extensions/bazaar.md in x402-foundation/x402).
///
/// HTTP and MCP resources share one table (discriminated by `type`) rather
/// than two, because a discovery listing is fundamentally "a resource with
/// payment requirements and a bazaar.info blob" regardless of transport —
/// splitting them would require the discovery query to UNION two tables on
/// every filter combination for no benefit, since the two types are never
/// looked up via different access patterns.
model BazaarResource {
id String @id @default(uuid())

/// "http" | "mcp" — discriminates which of the two input shapes below applies.
type String

/// Which Stellar network this listing settles on ("mainnet" | "testnet").
/// Mirrors config.ts's NetworkName so a listing is never ambiguous about
/// which network's payTo/asset it refers to.
network String

/// The protected resource URL (`resource.url` in the spec). For MCP this is
/// the MCP server endpoint, not the tool itself — the tool is disambiguated
/// by `mcpToolName` below.
url String

/// `resource.description` — human-readable description of the resource.
description String?

/// `resource.mimeType`.
mimeType String? @map("mime_type")

/// Optional service metadata the spec allows on `resource`.
serviceName String? @map("service_name")
tags String[] @default([])
iconUrl String? @map("icon_url")

/// MCP tool identifier (`input.toolName`). Null for HTTP resources.
/// Per the spec, MCP resources are keyed on the TUPLE of (resource.url,
/// input.toolName) since multiple tools multiplex over one server endpoint.
/// We additionally scope that tuple by `network` (see @@unique below) —
/// a deliberate deviation, called out in the PR: since Lens is
/// dual-network, the same (url, toolName) pair can legitimately exist
/// once per network with a different payTo/asset in `accepts`, and the
/// spec's tuple alone can't express that without collapsing them.
mcpToolName String? @map("mcp_tool_name")

/// HTTP method for HTTP resources (GET/POST/...). Null for MCP resources.
httpMethod String? @map("http_method")

/// Full `accepts[]` payment requirements array (scheme/network/amount/asset/
/// payTo/maxTimeoutSeconds/extra), stored verbatim so the discovery response
/// can round-trip the exact PaymentRequirements the resource advertised.
accepts Json

/// The `payTo` address extracted from accepts[0] for indexed filtering.
/// Denormalized on write because Postgres cannot efficiently index into a
/// JSON array element without a functional/GIN index per accepted scheme,
/// and payTo is the one field the spec calls out as a top-level filter.
payTo String @map("pay_to")

/// `extensions.bazaar.info` — discovery metadata (input type, params, output).
bazaarInfo Json @map("bazaar_info")

/// `extensions.bazaar.schema` — JSON Schema validating `bazaarInfo`.
bazaarSchema Json @map("bazaar_schema")

/// `extensions.bazaar.routeTemplate` — canonical `:param` pattern for
/// dynamic HTTP routes, used by the facilitator to consolidate listings.
routeTemplate String? @map("route_template")

/// Any other declared extension keys beyond "bazaar" (spec's `extensions`
/// filter matches on presence of a key here, "bazaar" always included).
extensionKeys String[] @default(["bazaar"]) @map("extension_keys")

createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")

// HTTP resources are keyed on (network, url, httpMethod); MCP resources are
// keyed on (network, url, mcpToolName) per the spec's tuple. Postgres
// treats NULLs as distinct in a unique index, so these two constraints
// don't collide with each other for a row that only populates one side.
@@unique([network, url, httpMethod], name: "bazaarHttpIdentity", map: "bazaar_http_identity")
@@unique([network, url, mcpToolName], name: "bazaarMcpIdentity", map: "bazaar_mcp_identity")
// Covers the six spec filters (type, payTo, network, extensions via
// extensionKeys, plus limit/offset) and keeps pagination stable — see
// routes/discovery.ts, which always orders by (createdAt, id).
@@index([network, type, payTo, createdAt(sort: Desc), id])
@@index([extensionKeys], type: Gin)
@@map("bazaar_resources")
}

model Webhook {
id String @id @default(uuid())
network String @default("testnet")
url String
assetA String @map("asset_a")
assetB String @map("asset_b")
Expand All @@ -109,7 +210,7 @@ model Webhook {
secret String
createdAt DateTime @default(now()) @map("created_at")

@@index([assetA, assetB])
@@index([network, assetA, assetB])
@@map("webhooks")
}

Expand Down
93 changes: 0 additions & 93 deletions sql/schema.sql

This file was deleted.

Loading