Near Learn

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#

MethodParamsReturns
quotedefuse_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_intentquote_hashes, signed_data (a signed payload: nep413, erc191 or raw_ed25519){ status: "OK" | "FAILED", reason?, intent_hash }
publish_intentsquote_hashes, signed_datas, requote?Same; with requote: true failed ones are re-quoted and retried
get_statusintent_hashPENDING → 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#

swap.ts: 5 USDC → wNEAR for a user whose USDC is already in intents.near
TypeScript
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#

SymptomCauseWhat to do
quote returns nothingUnsupported pair, amount too small or too large, solvers offlineTry another amount or route via a common asset; show “no quote” to the user
publish_intent FAILEDQuote expired, intent does not match the quote, bad signatureRe-quote and re-sign; never reuse an old signed payload
NOT_FOUND_OR_NOT_VALIDDeadline passed, nonce used, insufficient balance inside intents.near, key not registered for a named accountRead status_details; check mt_balance_of and has_public_key before quoting
Settled but less arrived than expectedYou read amount_out from a different quote than the one you signedSign from the same object you publish; log quote_hash with the intent hash
HTTP 401Missing or invalid partner keySend X-API-Key; keep the key server-side

Check yourself

3 questions · progress saved in this browser

  1. 1.The best quote offers amount_out 2.31 wNEAR for 5 USDC. What do you sign?
  2. 2.Why can’t the price move against the user after they sign a relay swap?
  3. 3.get_status returns NOT_FOUND_OR_NOT_VALID for a named NEAR account that just deposited. What should you check first?