Skip to content

Services API ​

The services package runs the relayer, the event indexer, and the HTTP API in one process. Contracts make all trading and risk decisions. The service does not match orders. The relayer pays transaction gas from its own account.

Source: services/src/ (server.ts, api.ts, relayer.ts, indexer.ts, shock.ts, config.ts).

Start the service ​

Run from the repository root:

sh
pnpm dev:services

This command loads the root .env file if it exists, then runs services/src/server.ts. The server binds to 127.0.0.1 on SERVICE_PORT (default 3001). See Configuration for the variables.

Package commands are also available:

sh
pnpm --filter @richter/services start
pnpm --filter @richter/services test
pnpm --filter @richter/services typecheck

At startup, the service reads the deployment file and checks that the chain ID of the RPC matches it. Without RELAYER_PRIVATE_KEY, the API is read-only.

Conventions ​

  • Responses are JSON with Cache-Control: no-store.
  • All large integers are decimal strings. Contract values have 18 decimals.
  • marketId accepts a configured symbol (MON, ETH) or a market address. Without marketId, the first configured market is the default.
  • An error returns HTTP 400 with { "error": "..." }. An unknown route returns HTTP 404.
  • CORS allows one browser origin: WEB_ORIGIN (default http://localhost:3000).
  • A request body is at most 64 KiB.

The shared package @richter/shared exports a Zod schema for each response (packages/shared/src/api.ts).

Routes ​

RouteResult
GET /healthService state: indexed block, relayer, test oracle flag, and shock flag
GET /deploymentThe validated deployment configuration
GET /marketsLive state of each market
GET /book?marketId=MONLive bids and asks
GET /trades?marketId=MONThe last 200 confirmed Trade events, newest first
GET /account?marketId=MON&address=0x...Live account state and the resting orders of the account
GET /historyIndexed events, newest first, with optional filters
GET /eventsSSE stream of update notices
POST /actionsSubmit a signed order or a signed cancellation. /intents is an alias.
POST /demo/shockRecord an oracle price shock for the operator loop. Local chain, or testnet with SHOCK_ENABLED=true.

GET /health ​

json
{
  "ok": true,
  "indexedBlock": "1520",
  "relayerEnabled": true,
  "isTestOracle": true,
  "shockEnabled": true
}
FieldMeaning
indexedBlockThe last block in the event index
relayerEnabledTrue when the service has a relayer key
isTestOracleThe flag of the deployment file
shockEnabledTrue when the deployment has the test oracle and either chain ID 31337 or SHOCK_ENABLED=true. Then POST /demo/shock is available.

GET /markets ​

Returns { markets, isTestOracle, chainId }. Each market has these fields:

FieldMeaning
symbol, address, feedIdFrom the deployment file
markPriceThe current variance mark
chargeMarkThe charge of the current funding interval
lastPriceThe last accepted oracle price
fundingIndexThe cumulative funding payment for one contract
nextObservationTimeThe next observation boundary, in Unix seconds
observationSecondsThe observation interval of the market
markFrozen, halted, observationDueThe market states
queueTrue for a QueuedMarket

GET /book ​

Returns { market, bids, asks }. The service reads getBook(128), so each side has up to 128 entries of the merged book. Each entry has id, owner, isBuy, price, remaining, createdAt, and expiry.

GET /account ​

Returns { market, address, state, orders }. The state object has the fields of accountState: cash, position, entry, fundedPosition, fundingEpoch, funding, realized, fees, equity, initialMargin, maintenanceMargin, freeCollateral, and liquidating. See Margin and liquidation.

GET /history ​

ParameterUse
marketIdOptional. Selects one market.
addressOptional. Selects the events of one account.
eventsOptional. A comma list of event names, for example MarkUpdated,Observation.
limitOptional. From 1 to 20000. The default is 1000.

Returns { events }. Each event has id, market, event, transactionHash, blockNumber, logIndex, timestamp, and args.

sh
curl 'http://127.0.0.1:3001/history?marketId=MON&events=Observation&limit=5'

GET /events ​

An SSE stream. The service sends a state event with the indexed block at connection and after each index update. The event tells the client to read the snapshots again. The service sends a keepalive comment each 15 seconds.

SSE supplies notices. REST supplies the snapshots. This design replaces the WebSocket transport of the target architecture for this build.

POST /actions ​

The body has one of two forms:

json
{ "kind": "order", "market": "0x...", "action": { "...": "OrderAction fields" }, "signature": "0x..." }
json
{ "kind": "cancel", "market": "0x...", "action": { "...": "CancelAction fields" }, "signature": "0x..." }

Use orderActionSchema, cancelActionSchema, orderTypedData, and cancelTypedData from the shared package. The client keeps the session private key. The service receives only the signed action.

The response has one of two states:

statusMeaningFields
confirmedA successful receipt existstransactionHash, blockNumber, and the decoded events
submittedA hash exists, but the service did not get the receipttransactionHash, message

A confirmed order can have zero fills. Read the OrderResult event for the filled, resting, and canceled sizes. For submitted, keep the state pending and check the hash before you send another action. A revert or a simulation error returns HTTP 400.

POST /demo/shock ​

sh
curl -X POST http://127.0.0.1:3001/demo/shock \
  -H 'content-type: application/json' \
  -d '{"symbol":"MON","percent":-4}'
FieldRule
symbolMON or ETH
percentA number above -100 and at most 100. Negative is down.

The service writes the request to a file that the operator loop reads. On the local chain, the file is .local/demo-shock.json and the loop is pnpm demo serve. On testnet, the file is .local/testnet-shock.json and the loop is pnpm testnet serve or pnpm testnet serve-fast. The loop applies the shock at the next observation of that market. The API and the loop must use the same repository directory. The response is { "symbol": "MON", "percent": -4 }.

Test oracle only

The route returns an error unless the deployment has the test oracle and either chain ID 31337 or SHOCK_ENABLED=true. The route records a request. It has an effect only while the operator loop of that deployment runs.

Relayer behavior ​

Before submission, the relayer simulates the exact signed call and estimates gas. It adds 20% to the estimate. It rejects the action if that gas limit is above the cap RELAYER_MAX_GAS (default 5,000,000). Requests use one submission queue, which prevents nonce races of the gas account.

The relayer cannot change a signed order. The contract checks the session scope, the expiry, the revocation, the nonce, the chain, the account, and the market.

This is a demo sponsorship policy. There is no user relayer fee. A public deployment needs an operator budget and an access policy. Direct market calls stay available if the service stops.

Indexer behavior ​

The indexer writes an atomic JSON file (STATE_FILE) after each block batch. It stores the chain and deployment identity, a checkpoint hash, and the decoded events.

  • A deployment change starts a separate history.
  • A fork or a local chain reset causes a replay from the deployment block.
  • An event decode failure stops the batch. The cursor cannot move past the failed event.
  • History uses the configured confirmation depth (INDEX_CONFIRMATIONS, default 1).

JSON persistence is for a single process. Production needs a database, monitoring, and a selected finality policy.

Deployed on Monad testnet.