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:
pnpm dev:servicesThis 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:
pnpm --filter @richter/services start
pnpm --filter @richter/services test
pnpm --filter @richter/services typecheckAt 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.
marketIdaccepts a configured symbol (MON,ETH) or a market address. WithoutmarketId, 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(defaulthttp://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
| Route | Result |
|---|---|
GET /health | Service state: indexed block, relayer, test oracle flag, and shock flag |
GET /deployment | The validated deployment configuration |
GET /markets | Live state of each market |
GET /book?marketId=MON | Live bids and asks |
GET /trades?marketId=MON | The last 200 confirmed Trade events, newest first |
GET /account?marketId=MON&address=0x... | Live account state and the resting orders of the account |
GET /history | Indexed events, newest first, with optional filters |
GET /events | SSE stream of update notices |
POST /actions | Submit a signed order or a signed cancellation. /intents is an alias. |
POST /demo/shock | Record an oracle price shock for the operator loop. Local chain, or testnet with SHOCK_ENABLED=true. |
GET /health
{
"ok": true,
"indexedBlock": "1520",
"relayerEnabled": true,
"isTestOracle": true,
"shockEnabled": true
}| Field | Meaning |
|---|---|
indexedBlock | The last block in the event index |
relayerEnabled | True when the service has a relayer key |
isTestOracle | The flag of the deployment file |
shockEnabled | True 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:
| Field | Meaning |
|---|---|
symbol, address, feedId | From the deployment file |
markPrice | The current variance mark |
chargeMark | The charge of the current funding interval |
lastPrice | The last accepted oracle price |
fundingIndex | The cumulative funding payment for one contract |
nextObservationTime | The next observation boundary, in Unix seconds |
observationSeconds | The observation interval of the market |
markFrozen, halted, observationDue | The market states |
queue | True 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
| Parameter | Use |
|---|---|
marketId | Optional. Selects one market. |
address | Optional. Selects the events of one account. |
events | Optional. A comma list of event names, for example MarkUpdated,Observation. |
limit | Optional. From 1 to 20000. The default is 1000. |
Returns { events }. Each event has id, market, event, transactionHash, blockNumber, logIndex, timestamp, and args.
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:
{ "kind": "order", "market": "0x...", "action": { "...": "OrderAction fields" }, "signature": "0x..." }{ "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:
status | Meaning | Fields |
|---|---|---|
confirmed | A successful receipt exists | transactionHash, blockNumber, and the decoded events |
submitted | A hash exists, but the service did not get the receipt | transactionHash, 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
curl -X POST http://127.0.0.1:3001/demo/shock \
-H 'content-type: application/json' \
-d '{"symbol":"MON","percent":-4}'| Field | Rule |
|---|---|
symbol | MON or ETH |
percent | A 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.