Why Three Surfaces?
Each surface serves a different runtime environment, transport model, and authentication strategy. No single API format satisfies all three use cases.| Surface | Target User | Transport | Auth Model |
|---|---|---|---|
| MCP Server | AI agents (Claude, ChatGPT, Cursor, VS Code) | Streamable HTTP (JSON-RPC) | None |
| x402 API | Programmatic clients, pay-per-request agents | REST (HTTP GET/POST) | Free reads; $0.01 USDC per write via x402 |
| CLI | Shell-based agents, CI pipelines, terminal users | stdin/stdout (subprocess) | None (local binary) |
The Shared API Layer
Rather than duplicating data-access code across surfaces, a shared TypeScript module (@nile-markets/api) sits between the surfaces and the data sources. Each surface imports shared functions and adds only its transport-specific layer.
@nile-markets/api. The Rust CLI implements the same logical queries independently via graphql_client derive macros, maintaining semantic parity.
What Is Shared
Config Resolution
Network configuration resolves through a consistent fallback chain across all surfaces:withNetwork() function sets the active network at each request’s entry point using AsyncLocalStorage. All downstream calls (getConfig(), getPublicClient(), getGraphQLClient()) read from this context automatically — no parameter threading needed.
RPC Clients
A cachedviem PublicClient per network handles all onchain reads: contract state, forward prices, margin balances, oracle validity, and position simulation via eth_call. Clients are created once and reused across requests.
GraphQL Queries
Seven shared subgraph query functions fetch historical and aggregate data:| Query | Data |
|---|---|
fetchPositions | Account positions with optional status filter |
searchPositions | Cross-account position search with side/tenor/notional filters |
fetchPoolState | Pool singleton (TVL, shares, fees, exposure) |
fetchDailyStats | Daily trading volume, PnL, and fee aggregates |
fetchPoolTransactions | Vault deposit/withdrawal history |
fetchFeeEvents | Fee collection events by type |
fetchAccount | Account aggregate stats (position count, realized PnL, LP shares) |
fetchPoolStateRpc() reads real-time state (total assets, share price, utilization) from RPC and historical fee totals from the subgraph. If the subgraph is unavailable, it returns RPC data with zeroed aggregate fields rather than failing.
Transaction Encoding
All write operations return unsigned calldata rather than executing transactions:Response Metadata
Every response includes domain-layer metadata with the same structure:| Field | Description |
|---|---|
protocol.name | "nile-markets" |
protocol.version | "0.3.0" (single source of truth in @nile-markets/api) |
network | Active network ("sepolia", "local") |
blockNumber | Subgraph indexing head (when subgraph data is included) |
source | "rpc", "subgraph", or "rpc+subgraph" |
Enum and Label Maps
Solidity enum ordinals (stored in contract state and subgraph) are mapped to human-readable labels by shared functions:sideLabel(), tenorLabel(), modeLabel(), mapCloseReason(), feeTypeLabel(). All surfaces use the same mappings, so a position shows "LONG" and "1W" regardless of whether you query via MCP, REST, or CLI.
What Is Surface-Specific
Each surface adds only its transport and formatting layer on top of the shared API.- MCP Server
- x402 Gateway
- CLI
- Transport: Streamable HTTP with JSON-RPC framing via
mcp-handler - Tool registration: MCP tool schemas with
name,description,inputSchema - Network context:
withNetwork()called per tool invocation - Auth: None
- Response format:
content[0].textJSON string inside MCP tool result - Rate limiting: Shared
createRateLimiter()from@nile-markets/api
Adding a New Surface
To add a fourth API surface (WebSocket, gRPC, or another transport):Import shared functions
Add
@nile-markets/api as a workspace dependency. Import config, clients, queries, and encoding functions.Set network context at entry point
Call
withNetwork(network, handler) at each request boundary. All downstream calls resolve network automatically.Add transport layer
Implement request parsing, response formatting, and auth for the new transport. The shared module handles all data access.