The Quickstart tutorial explains how to set up an example AMM Solver,
lets dive into understanding how it works: from connecting to the Message Bus through to settlement.
The project is organized into focused services, each handling one part of the workflow:
Service
File
Responsibility
WebSocket connection
src/services/websocket-connection.service.ts
Connects to the Message Bus, subscribes to events, routes incoming messages
Quoter
src/services/quoter.service.ts
Evaluates quote requests, calculates pricing, builds and signs intents
Cron
src/services/cron.service.ts
Refreshes token balances from the Verifier contract on a 15-second interval
The WebSocket service receives a quote request, hands it to the quoter, and the quoter responds with a signed intent. Meanwhile, the cron service keeps balance data current so the quoter always knows what it can fill.
The WebSocket connection service (src/services/websocket-connection.service.ts) manages the link to the Message Bus. On connect, it subscribes to two event types:
Copy
Ask AI
// Subscribe to incoming quote requeststhis.subscribe(RelayEventKind.QUOTE);// Subscribe to settlement notificationsthis.subscribe(RelayEventKind.QUOTE_STATUS);
Under the hood, each subscription sends a JSON-RPC message over the WebSocket:
Copy
Ask AI
const ws = new WebSocket("wss://solver-relay-v2.chaindefuser.com/ws");ws.onopen = () => { // Subscribe to quote requests ws.send(JSON.stringify({ jsonrpc: "2.0", id: 1, method: "subscribe", params: ["quote"], })); // Subscribe to quote status updates ws.send(JSON.stringify({ jsonrpc: "2.0", id: 2, method: "subscribe", params: ["quote_status"], }));};
The WebSocket endpoint requires an API key. Pass it as a Bearer token in the connection headers. See the Quickstart for how to obtain one.
When a quote event arrives, the WebSocket service checks whether the requested token pair matches the solver’s configured pair. If it does, the request is passed to the quoter service for evaluation.Each quote request contains the following parameters:
Once the quoter has computed a price, it constructs a token_diff intent — a signed statement declaring which tokens the solver is willing to give and receive.
The solver constructs the intent message directly and signs it using near-api-js. The message includes the solver’s account ID, a deadline, and the token_diff intent:
The message is then serialized and signed. The example solver uses the NEP-413 signing standard on NEAR:
Copy
Ask AI
const messageStr = JSON.stringify(message);const nonce = currentState.nonce;const recipient = intentsContract; // "intents.near"// Serialize the intent using Borsh for signingconst quoteHash = serializeIntent(messageStr, recipient, nonce, 'nep413');// Sign with the NEAR keyconst signature = await nearService.signMessage(quoteHash);
Generating a nonce
Every intent requires a unique nonce. The example solver uses a deterministic approach — it hashes the current token reserves with SHA-256, so the nonce only changes when the solver’s balances change (i.e. after a trade settles):
Copy
Ask AI
import { createHash } from 'crypto';const generateDeterministicNonce = (input: string): string => { const hash = createHash('sha256'); hash.update(input); return hash.digest('base64');};// Called with the current reserves:const nonce = generateDeterministicNonce( `reserves:${reserves.join(':')}`);
This avoids fetching the contract salt on every request. The nonce is recomputed whenever the cron service refreshes balances.
Ensure your account has sufficient balance in the Verifier contract for the tokens you’re offering. The intent will fail if you don’t have enough tokens deposited.
After building and signing the intent, the solver sends a quote_response back through the WebSocket. The response includes the quote output (the calculated amount) and the full signed data:
The quote_output field tells the Message Bus which side of the trade the solver is quoting. If the request specified exact_amount_in, the solver responds with amount_out (how much it will give). If the request specified exact_amount_out, it responds with amount_in (how much it wants to receive).
The solver subscribes to quote_status events to learn when its quotes are selected and settled on-chain. The WebSocket service routes incoming messages based on the subscription type:
Copy
Ask AI
switch (subscription.eventKind) { case RelayEventKind.QUOTE: this.processQuote(req.params.data, req.params.metadata); break; case RelayEventKind.QUOTE_STATUS: this.processQuoteStatus(req.params.data); break;}
When a quote_status event arrives, the solver checks whether the settled quote hash matches one of its own cached quotes. If it does, it triggers a balance refresh so that future quotes reflect the updated reserves:
Copy
Ask AI
private async processQuoteStatus(data: IPublishedQuoteData) { // data contains: { quote_hash, intent_hash, tx_hash } const quote = this.cacheService.get(data.quote_hash); if (!quote) { // Not one of our quotes, skip return; } // Our quote was settled — refresh balances await this.quoterService.updateCurrentState();}
A cron service (src/services/cron.service.ts) refreshes token balances from the Verifier contract every 15 seconds. It calls the mt_batch_balance_of method on the intents contract to get the solver’s current reserves. After a successful trade, the settlement handler also triggers an immediate refresh. This ensures the quoter always has accurate reserve data when calculating prices.
The example uses a constant-product AMM formula, but any pricing logic can be used. The quoter service is the place to start — replace the getAmountOut and getAmountIn functions with a custom strategy, whether that is pulling prices from external APIs, using order books, or applying custom spread models.A few additional areas to customize:
Support more token pairs — add additional token IDs in the configuration
Add position limits — cap how much of a token the solver can allocate
Implement risk controls — set minimum trade sizes, maximum exposure, or rate limits
The src/configs/ directory is a good starting point for customization. Each config file maps to a specific concern — tokens, margins, WebSocket URLs, and more.
For production deployments, consider running your solver in TEE (Trusted Execution Environment) mode, which provides additional security guarantees. See the repository README for TEE setup instructions.