generated from mintlify/starter
-
Notifications
You must be signed in to change notification settings - Fork 3
feat: add market maker tutorial #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
gagdiez
wants to merge
3
commits into
main
Choose a base branch
from
market-maker-tutorial
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
238 changes: 238 additions & 0 deletions
238
integration/market-makers/become-market-maker-guide.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,238 @@ | ||
| --- | ||
| title: "Become a Market Maker" | ||
| description: "Step-by-step guide to running a market maker solver on NEAR Intents" | ||
| icon: "rocket" | ||
| --- | ||
|
|
||
| Market makers on NEAR Intents are solvers -- programs that listen for swap requests, decide whether to fill them, and respond with signed quotes. In this guide, you will set up and run an example solver that connects to the [Message Bus](/integration/market-makers/message-bus/introduction), receives live quote requests, and automatically responds to the ones it can fill. | ||
|
|
||
| By the end, you will have a working solver running locally and a clear understanding of how to customize it. | ||
|
|
||
| <Info> | ||
| **Prerequisites** | ||
|
|
||
| - Node.js v20.18+ with npm | ||
| - A NEAR mainnet account | ||
| - Liquidity in the Verifier contract (`intents.near`) | ||
| - Basic familiarity with WebSockets and the [NEAR account model](https://docs.near.org/concepts/protocol/account-model) | ||
| </Info> | ||
|
|
||
| --- | ||
|
|
||
| ## How it works | ||
|
|
||
| When a user wants to swap tokens, they will send a quote request to the Message Bus -- a WebSocket-based relay that broadcasts the request to all connected solvers. | ||
|
|
||
| The solver evaluates whether the request matches the configured token pair and whether sufficient liquidity is available. If both conditions are met, it calculates a price, signs an intent with the proposed amounts, and sends a quote response back to the Message Bus. | ||
|
|
||
| Multiple solvers compete for the same request by offering prices. The Message Bus collects the responses and returns top quotes to the user application. If a quote is selected, it is transmitted to the [Verifier contract](/integration/verifier-contract/introduction) where the swap settles on-chain. | ||
|
|
||
| For a deeper look at this architecture, see the [Market Makers introduction](/integration/market-makers/introduction) and the [Message Bus overview](/integration/market-makers/message-bus/introduction). | ||
|
|
||
| The following steps show this flow in action. | ||
|
|
||
| ## Run the example solver | ||
|
|
||
| The [AMM Solver example](https://github.com/defuse-protocol/near-intents-amm-solver) is a Node.js application that implements everything described above. It uses a simple constant-product AMM formula to price quotes -- the same model used by Uniswap-style DEXs. | ||
|
|
||
| <Steps> | ||
| <Step title="Clone the repository"> | ||
|
|
||
| ```bash | ||
| git clone https://github.com/defuse-protocol/near-intents-amm-solver.git | ||
| cd near-intents-amm-solver | ||
| npm install | ||
| ``` | ||
|
|
||
| </Step> | ||
|
|
||
| <Step title="Configure your environment"> | ||
|
|
||
| Create your environment file from the provided example: | ||
|
|
||
| ```bash | ||
| cp env/.env.example env/.env.local | ||
| ``` | ||
|
|
||
| Open `env/.env.local` and fill in these values: | ||
|
|
||
| ```bash | ||
| # Your NEAR account credentials | ||
| NEAR_ACCOUNT_ID=your-solver.near | ||
| NEAR_PRIVATE_KEY=ed25519:your_private_key_here | ||
|
|
||
| # The token pair you want to market-make | ||
| AMM_TOKEN1_ID=usdt.tether-token.near | ||
| AMM_TOKEN2_ID=wrap.near | ||
|
|
||
| # Network and mode | ||
| NEAR_NETWORK_ID=mainnet | ||
| TEE_ENABLED=false | ||
|
|
||
| # Fee margin as a percentage (0.3 = 0.3%) | ||
| MARGIN_PERCENT=0.3 | ||
| ``` | ||
|
|
||
| The `MARGIN_PERCENT` controls your spread -- the difference between what you receive and what you give. A higher value means more profit per trade but fewer quotes accepted. | ||
|
|
||
| Other settings like `RELAY_WS_URL` and `INTENTS_CONTRACT` have sensible defaults and do not need changing for most setups. | ||
|
|
||
| **Get a Message Bus API key:** | ||
|
|
||
| The default Message Bus WebSocket endpoint (`wss://solver-relay-v2.chaindefuser.com/ws`) requires an API key. To get one: | ||
|
|
||
| 1. Sign up at [partners.near-intents.org](https://partners.near-intents.org) | ||
| 2. Request an API key through the partner portal | ||
|
|
||
| Once issued, open `src/services/websocket-connection.service.ts` and add your API key as a Bearer token in the WebSocket connection headers at line 35: | ||
|
|
||
| ```typescript | ||
| headers: { | ||
| Authorization: `Bearer ${YOUR_API_KEY}`, | ||
| } | ||
| ``` | ||
|
|
||
| <Warning> | ||
| Never commit your private key or API key to version control. The `.env.local` file is already in `.gitignore`, but double-check before pushing any changes. | ||
| </Warning> | ||
|
|
||
| </Step> | ||
|
|
||
| <Step title="Deposit liquidity"> | ||
|
|
||
| Your solver can only fill swaps if it has token balances inside the Verifier contract (`intents.near`). You need to do two things: register your solver's public key on the contract, and deposit tokens. | ||
|
|
||
| **Register your public key:** | ||
|
|
||
| ```bash | ||
| npx near-cli-rs contract call-function as-transaction intents.near add_public_key \ | ||
| json-args '{"public_key":"ed25519:YOUR_PUBLIC_KEY"}' \ | ||
| prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' \ | ||
| sign-as SOLVER_ACCOUNT_ID network-config mainnet sign-with-keychain send | ||
| ``` | ||
|
|
||
| Replace `YOUR_PUBLIC_KEY` with the public key that corresponds to the private key in your `.env.local` file, and `SOLVER_ACCOUNT_ID` with your actual NEAR account ID. | ||
|
|
||
| **Deposit tokens** using one of these methods: | ||
|
|
||
| - The [Passive Deposit/Withdrawal Service](/integration/market-makers/deposit-withdrawal-service) -- deposit from any supported chain via API | ||
| - [near-intents.org](https://near-intents.org/) -- a web interface for swapping and depositing tokens | ||
|
|
||
| The solver needs balances for both tokens in the configured pair. For example, if you are market-making USDT/wNEAR, it needs both `usdt.tether-token.near` and `wrap.near` deposited into the contract. | ||
|
|
||
| </Step> | ||
|
|
||
| <Step title="Start the solver"> | ||
|
|
||
| Since the environment file is named `.env.local`, set `NODE_ENV=local` so the app picks it up: | ||
|
|
||
| ```bash | ||
| NODE_ENV=local npm start | ||
| ``` | ||
|
|
||
| On startup, the solver connects to the Message Bus WebSocket, subscribes to quote events, and begins polling the Verifier contract for current token balances every 15 seconds. Log output confirms the connection and initial reserves. | ||
|
|
||
| </Step> | ||
|
|
||
| <Step title="Verify it is working"> | ||
|
|
||
| Check the health endpoint to confirm the solver is running: | ||
|
|
||
| ```bash | ||
| curl http://localhost:3000 | ||
| ``` | ||
|
|
||
| ```json | ||
| {"ready": true} | ||
| ``` | ||
|
|
||
| In the logs, look for: | ||
|
|
||
| - Connection confirmed -- successful WebSocket connection to the Message Bus | ||
| - Quote requests -- incoming swap requests being evaluated | ||
| - Quote responses -- signed quotes being sent back for pairs your solver supports | ||
|
|
||
| The solver only responds to requests for the configured token pair. If a request comes in for a different pair, or if reserves are too low to fill it, the solver skips it. | ||
|
|
||
| <Tip> | ||
| If quote requests are not appearing, that is normal during low-activity periods. The solver sends responses when matching requests arrive. | ||
| </Tip> | ||
|
|
||
| </Step> | ||
| </Steps> | ||
|
|
||
| ## Understanding the code | ||
|
|
||
| Now that the solver is running, the sections below describe what happens under the hood. The project is organized into focused services, each handling one part of the workflow. | ||
|
|
||
| ### Connecting to the Message Bus | ||
|
|
||
| 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: | ||
|
|
||
| ```typescript | ||
| // Subscribe to incoming quote requests | ||
| this.subscribe(RelayEventKind.QUOTE); | ||
|
|
||
| // Subscribe to settlement notifications | ||
| this.subscribe(RelayEventKind.QUOTE_STATUS); | ||
| ``` | ||
|
|
||
| When a quote event arrives, the service checks whether the requested token pair matches the configured pair. If it does, it passes the request to the quoter service for evaluation. | ||
|
|
||
| ### Evaluating and responding to quotes | ||
|
|
||
| The quoter service (`src/services/quoter.service.ts`) is where the core decision-making happens. For each incoming request, it: | ||
|
|
||
| 1. Validates the deadline -- rejects requests with unreasonable timeframes | ||
| 2. Checks reserves -- looks up current balances for both tokens | ||
| 3. Calculates the price -- uses a constant-product AMM formula with the configured margin | ||
| 4. Signs the response -- creates a `token_diff` intent and signs it with the configured NEAR key | ||
|
|
||
| The AMM formula follows the classic `x * y = k` model: | ||
|
|
||
| ```typescript | ||
| // Calculate how many tokens the user receives for their input | ||
| getAmountOut(amountIn, reserveIn, reserveOut, marginBips) { | ||
| const amountInWithFee = amountIn * (10000 - marginBips); | ||
| return (amountInWithFee * reserveOut) / (reserveIn * 10000 + amountInWithFee); | ||
| } | ||
| ``` | ||
|
|
||
| If the calculated output exceeds available reserves, the solver skips the request -- it does not quote what it cannot fill. | ||
|
|
||
| ### Keeping state fresh | ||
|
|
||
| The solver does not only check reserves once. A cron service (`src/services/cron.service.ts`) refreshes token balances from the Verifier contract every 15 seconds. After a successful trade, the solver updates its position and adjusts future quotes accordingly. | ||
|
|
||
| ## Making it your own | ||
|
|
||
| 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 | ||
|
|
||
| <Tip> | ||
| 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. | ||
| </Tip> | ||
|
|
||
| For production deployments, consider running your solver in TEE (Trusted Execution Environment) mode, which provides additional security guarantees. See the [repository README](https://github.com/defuse-protocol/near-intents-amm-solver) for TEE setup instructions. | ||
|
|
||
| ## Next steps | ||
|
|
||
| <CardGroup cols={2}> | ||
| <Card title="Message Bus API" icon="code" href="/integration/market-makers/message-bus/api"> | ||
| Full reference for all WebSocket and JSON-RPC methods | ||
| </Card> | ||
| <Card title="Usage Examples" icon="book-open" href="/integration/market-makers/message-bus/usage-examples"> | ||
| TypeScript patterns for signing intents and responding to quotes | ||
| </Card> | ||
| <Card title="Deposit & Withdrawal" icon="arrow-right-arrow-left" href="/integration/market-makers/deposit-withdrawal-service"> | ||
| Move liquidity in and out of the Verifier contract via API | ||
| </Card> | ||
| <Card title="Example Repository" icon="github" href="https://github.com/defuse-protocol/near-intents-amm-solver"> | ||
| Browse the full source code and contribute | ||
| </Card> | ||
| </CardGroup> | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: defuse-protocol/docs
Length of output: 119
🏁 Script executed:
Repository: defuse-protocol/docs
Length of output: 762
🏁 Script executed:
Repository: defuse-protocol/docs
Length of output: 212
🏁 Script executed:
Repository: defuse-protocol/docs
Length of output: 294
🏁 Script executed:
Repository: defuse-protocol/docs
Length of output: 46
🏁 Script executed:
Repository: defuse-protocol/docs
Length of output: 1165
🏁 Script executed:
Repository: defuse-protocol/docs
Length of output: 46
🏁 Script executed:
Repository: defuse-protocol/docs
Length of output: 91
🏁 Script executed:
Repository: defuse-protocol/docs
Length of output: 234
🏁 Script executed:
Repository: defuse-protocol/docs
Length of output: 3491
🏁 Script executed:
Repository: defuse-protocol/docs
Length of output: 1136
🏁 Script executed:
Repository: defuse-protocol/docs
Length of output: 6837
🏁 Script executed:
Repository: defuse-protocol/docs
Length of output: 2169
Avoid hardcoding the API key in source files; use environment variables instead.
The documentation contradicts itself. It earlier instructs users to configure
env/.env.localand warns against committing API keys to version control, but then instructs them to hardcode the API key directly insrc/services/websocket-connection.service.tsat line 35. Additionally, referencing a specific line number is fragile and breaks when code changes.Instead, add the API key to the
.env.localfile that users are already instructed to create:+The WebSocket connection service will automatically read this value and include it in the Bearer token.
Verify each finding against the current code and only fix it if needed.
In
@integration/market-makers/become-market-maker-guide.mdxaround lines 87 -93, The docs currently instruct hardcoding the API key into
src/services/websocket-connection.service.ts (and reference a fragile line
number); instead instruct users to add RELAY_API_KEY=your_api_key_here to their
.env.local and update the guide text to remove the line-numbered edit. Change
the guide to say the WebSocket connection service
(websocket-connection.service.ts) will read process.env.RELAY_API_KEY and
include it as the Bearer token in the Authorization header, and update the
snippet to show adding the RELAY_API_KEY to .env.local (do not show hardcoded
keys in source).