Skip to main content

Why Three Surfaces?

Each surface serves a different runtime environment, transport model, and authentication strategy. No single API format satisfies all three use cases.
SurfaceTarget UserTransportAuth Model
MCP ServerAI agents (Claude, ChatGPT, Cursor, VS Code)Streamable HTTP (JSON-RPC)None
x402 APIProgrammatic clients, pay-per-request agentsREST (HTTP GET/POST)Free reads; $0.01 USDC per write via x402
CLIShell-based agents, CI pipelines, terminal usersstdin/stdout (subprocess)None (local binary)
All three expose the same 22 operations (14 read, 8 write) with the same data semantics. The difference is transport, auth, and output formatting.

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.
+-------------------+     +-------------------+     +-------------------+
|    MCP Server     |     |    x402 Gateway    |     |       CLI         |
|  (JSON-RPC tools) |     |  (REST endpoints)  |     |  (Rust commands)  |
+--------+----------+     +--------+----------+     +--------+----------+
         |                         |                          |
         |    TypeScript           |    TypeScript             |    Rust
         v                         v                          v
+--------+-------------------------+----------+     +--------+----------+
|              @nile-markets/api              |     |  crates/cli/      |
|  config | clients | queries | encoding      |     |  (mirrors same    |
|  types  | logger  | rate-limit | context    |     |   query semantics)|
+--------+-------------------------+----------+     +--------+----------+
         |                         |                          |
         v                         v                          v
+--------+-------------------------+-------------------------+----------+
|                  Ethereum RPC (viem / alloy-rs)                       |
+----------------------------------------------------------------------+
|                  The Graph Subgraph (GraphQL)                         |
+----------------------------------------------------------------------+
The two TypeScript surfaces import directly from @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:
RPC_URL_{NETWORK} > RPC_URL (legacy) > per-network default > error
The 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 cached viem 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:
QueryData
fetchPositionsAccount positions with optional status filter
searchPositionsCross-account position search with side/tenor/notional filters
fetchPoolStatePool singleton (TVL, shares, fees, exposure)
fetchDailyStatsDaily trading volume, PnL, and fee aggregates
fetchPoolTransactionsVault deposit/withdrawal history
fetchFeeEventsFee collection events by type
fetchAccountAccount aggregate stats (position count, realized PnL, LP shares)
Some operations combine RPC and subgraph data. For example, 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:
{
  "to": "0x...",
  "data": "0x...",
  "value": "0",
  "estimatedGas": "150000",
  "chainId": 11155111,
  "description": "Open LONG position with 100000000 notional"
}
Signing and broadcasting happen externally via the user’s wallet. This keeps all surfaces zero-custody.

Response Metadata

Every response includes domain-layer metadata with the same structure:
FieldDescription
protocol.name"nile-markets"
protocol.version"0.3.0" (single source of truth in @nile-markets/api)
networkActive network ("sepolia", "local")
blockNumberSubgraph 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.
  • 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].text JSON 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):
1

Import shared functions

Add @nile-markets/api as a workspace dependency. Import config, clients, queries, and encoding functions.
2

Set network context at entry point

Call withNetwork(network, handler) at each request boundary. All downstream calls resolve network automatically.
3

Add transport layer

Implement request parsing, response formatting, and auth for the new transport. The shared module handles all data access.
4

Update downstream sync rules

Add the new surface to the API surface parity checklist. Every MCP tool and x402 endpoint must have a corresponding operation in the new surface.