Swaps through the solver network
Swap through NEAR Intents solvers: request quotes from the solver relay, sign a matching token_diff, publish_intent, poll get_status, and handle failures.
Advanced9 min read3-question check
A swap in NEAR Intents is two token_diff intents that cancel out: the user gives A and gets B, a solver gives B and gets A. The solver relay (also called the Message Bus) finds the solver: it broadcasts your quote request to connected market makers, returns their offers, and when you publish your signed intent with the chosen quote hash, it bundles both sides into one execute_intents call.
Most apps should use 1Click (next lesson), which does all of this plus cross-chain deposits. Use the relay directly when your users already hold balances inside intents.near and you want full control over quotes. Docs: Message Bus API.
The relay API#
| Method | Params | Returns |
|---|---|---|
quote | defuse_asset_identifier_in, defuse_asset_identifier_out, exactly one of exact_amount_in / exact_amount_out, min_deadline_ms (default 60000) | Array of offers: quote_hash, amount_in, amount_out, expiration_time. The relay waits up to ~3 s for solvers. |
publish_intent | quote_hashes, signed_data (a signed payload: nep413, erc191 or raw_ed25519) | { status: "OK" | "FAILED", reason?, intent_hash } |
publish_intents | quote_hashes, signed_datas, requote? | Same; with requote: true failed ones are re-quoted and retried |
get_status | intent_hash | PENDING → TX_BROADCASTED → SETTLED, or NOT_FOUND_OR_NOT_VALID; plus data.hash (NEAR tx) and filled_amounts |
Endpoint: POST https://solver-relay-v2.chaindefuser.com/rpc, JSON-RPC 2.0 with params as a one-element array. It requires a partner JWT in the X-API-Key header, issued through the Partner Portal.
Quote, check, sign, publish, poll#
import { IntentsSDK, createIntentSignerViem } from '@defuse-protocol/intents-sdk'
import { privateKeyToAccount } from 'viem/accounts'
const RELAY = 'https://solver-relay-v2.chaindefuser.com/rpc'
const USDC = 'nep141:17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1'
const WNEAR = 'nep141:wrap.near'
async function relay<T>(method: string, params: object): Promise<T> {
const res = await fetch(RELAY, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.INTENTS_API_KEY! },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params: [params] }),
})
const { result, error } = await res.json()
if (error) throw new Error(`${method}: ${JSON.stringify(error)}`)
return result as T
}
type Quote = { quote_hash: string; amount_in: string; amount_out: string; expiration_time: string }
// 1. Ask solvers
const quotes = await relay<Quote[] | null>('quote', {
defuse_asset_identifier_in: USDC,
defuse_asset_identifier_out: WNEAR,
exact_amount_in: '5000000',
min_deadline_ms: 60_000,
})
if (!quotes?.length) throw new Error('no solver quoted this pair/amount')
const best = quotes.reduce((a, b) => (BigInt(b.amount_out) > BigInt(a.amount_out) ? b : a))
// 2. Your own slippage guard: compare with a reference price you trust
const expectedOut = await referenceAmountOut(USDC, WNEAR, 5_000_000n) // your oracle / a dry 1Click quote
const minOut = (expectedOut * 9_950n) / 10_000n // 0.5%
if (BigInt(best.amount_out) < minOut) throw new Error('quote worse than 0.5% from reference')
// 3. Sign a token_diff that mirrors the quote exactly
const signer = createIntentSignerViem({ signer: privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`) })
const sdk = new IntentsSDK({ referral: 'my-app', intentSigner: signer })
const { signed } = await sdk
.intentBuilder()
.setDeadline(new Date(Math.min(Date.parse(best.expiration_time), Date.now() + 60_000)))
.addIntent({
intent: 'token_diff',
diff: { [USDC]: `-${best.amount_in}`, [WNEAR]: best.amount_out },
})
.buildAndSign(signer)
// 4. Publish with the quote hash; the relay pairs it with the solver's side
const pub = await relay<{ status: string; reason?: string; intent_hash: string }>('publish_intent', {
quote_hashes: [best.quote_hash],
signed_data: signed,
})
if (pub.status !== 'OK') throw new Error(`publish failed: ${pub.reason}`)
// 5. Poll until settled
for (;;) {
const s = await relay<{ status: string; status_details?: string; data?: { hash: string } }>('get_status', {
intent_hash: pub.intent_hash,
})
if (s.status === 'SETTLED') { console.log('NEAR tx', s.data?.hash); break }
if (s.status === 'NOT_FOUND_OR_NOT_VALID') throw new Error(s.status_details ?? 'intent rejected or expired')
await new Promise((r) => setTimeout(r, 2_000))
}
declare function referenceAmountOut(tokenIn: string, tokenOut: string, amountIn: bigint): Promise<bigint>What can fail#
| Symptom | Cause | What to do |
|---|---|---|
quote returns nothing | Unsupported pair, amount too small or too large, solvers offline | Try another amount or route via a common asset; show “no quote” to the user |
publish_intent FAILED | Quote expired, intent does not match the quote, bad signature | Re-quote and re-sign; never reuse an old signed payload |
NOT_FOUND_OR_NOT_VALID | Deadline passed, nonce used, insufficient balance inside intents.near, key not registered for a named account | Read status_details; check mt_balance_of and has_public_key before quoting |
| Settled but less arrived than expected | You read amount_out from a different quote than the one you signed | Sign from the same object you publish; log quote_hash with the intent hash |
| HTTP 401 | Missing or invalid partner key | Send X-API-Key; keep the key server-side |
Check yourself
3 questions · progress saved in this browser