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 (15 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.2" (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.

Supporting two testnets

M2 is deployed on both Ethereum Sepolia (L1, chainId 11155111) and Arbitrum Sepolia (L2, chainId 421614). Both networks run identical contracts with identical parameters; the protocol surface is the same. The two-testnet choice is additive coverage, not a switch:
  • The L1 case. Sepolia mirrors Ethereum mainnet’s gas market and ~12 s confirmation cadence. Validating gas-cap behavior, fee-driven publisher pauses, and L1-scale settlement timing requires a live L1 testnet — without it, operators cannot rehearse mainnet conditions. Sepolia is also the canonical L1 testnet for ecosystem dependencies (Pyth, Graph Studio, third-party tooling).
  • The L2 case. Arbitrum Sepolia provides a deployment target with the cost / latency profile that agent integrations (MCP / x402 / CLI) tend to want. A typical 5-tx agent task (approve USDC → deposit margin → open position → poll → close) settles at L1 cadence on Sepolia and at L2 cadence on Arb Sep; both behaviors are correct, but agent iteration loops that want sub-second confirmation need the L2 option.
Neither network is “better”. They cover different validation regimes. Mainnet (M3) decisions for L1 vs L2 deployment are scoped separately and informed by data collected from both testnets.

Choosing a network for agent flows

Use caseRecommended network
Validate behavior under mainnet-shape gas varianceEthereum Sepolia
Validate L1 confirmation cadence (12 s blocks) and ecosystem dependenciesEthereum Sepolia
Benchmark agent throughput / fast iteration loopsArbitrum Sepolia
Long-running scenarios where fee predictability mattersArbitrum Sepolia
End-to-end coverage of protocol on both fee regimesBoth — run integrations in parallel
The protocol surface (function names, parameter types, revert conditions, response envelopes) is identical on both chains. Switching is a one-flag change: --network arbitrumSepolia on the CLI, ?network=arbitrumSepolia on x402, network parameter on MCP, or wallet chain switch in the frontend.