Notus API

Limit Orders Guide

Create, sign, execute, and cancel limit orders with the Notus API.

Overview

Limit orders use the same swap endpoint as market swaps. Add minAmountOut to the /crypto/swap request to ask Notus for a limit-order quote, then sign both the returned userOperationHash and the limit-order typedData.

Steps

Install dependencies and initialize Wallet Account

Install viem to sign the UserOperation hash and the EIP-712 typed data returned for the limit order.

npm install viem
import { privateKeyToAccount } from 'viem/accounts'

const BASE_URL = 'https://api.notus.team/api/v1'
const API_KEY = '<api-key>'

const BRZ_POLYGON = '0x4ed141110f6eeeaba9a1df36d8c26f684d2475dc'
const USDC_POLYGON = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'

const privateKey = '0x<private-key>'
const account = privateKeyToAccount(privateKey)

Register a Smart Wallet Address

Register the EOA that signs operations and retrieve the smart wallet address that will own the order.

async function setupSmartWallet() {
  const FACTORY_ADDRESS = '0x0000000000400CdFef5E2714E63d8040b700BC24'
  const externallyOwnedAccount = account.address

  const res = await fetch(`${BASE_URL}/wallets/register`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': API_KEY,
    },
    body: JSON.stringify({
      externallyOwnedAccount,
      factory: FACTORY_ADDRESS,
      salt: '0',
    }),
  })

  if (!res.ok) {
    throw new Error(await res.text())
  }

  const data = await res.json()
  return {
    smartWalletAddress: data.wallet.accountAbstraction,
    externallyOwnedAccount,
  }
}

Create a Limit Order Quote

Call /crypto/swap with minAmountOut. This tells Notus to prepare a limit order instead of a market swap.

const { smartWalletAddress, externallyOwnedAccount } = await setupSmartWallet()

const { quotes } = await fetch(`${BASE_URL}/crypto/swap`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': API_KEY,
  },
  body: JSON.stringify({
    amountIn: '5',
    chainIdIn: 137,
    chainIdOut: 137,
    routeProfile: 'QUICKEST_QUOTE',
    gasFeePaymentMethod: 'DEDUCT_FROM_AMOUNT',
    payGasFeeToken: BRZ_POLYGON,
    tokenIn: BRZ_POLYGON,
    tokenOut: USDC_POLYGON,
    walletAddress: smartWalletAddress,
    toAddress: smartWalletAddress,
    signerAddress: externallyOwnedAccount,
    minAmountOut: '0.19069',
    expiration: 2592000,
    partialFillable: true,
    metadata: {
      orderType: 'limit',
      requestId: 'limit_order_123',
    },
  }),
}).then((res) => res.json())

const quote = quotes[0]

if (!quote?.userOperationHash) {
  throw new Error('No executable limit-order quote returned')
}

minAmountOut is a decimal string in the output token. expiration is optional and is expressed in seconds from quote creation. partialFillable defaults to true.

Sign the UserOperation and Limit Order

The UserOperation signature authorizes the smart wallet operation. The typedData signature authorizes the limit order itself.

const userOperationSignature = await account.signMessage({
  message: {
    raw: quote.userOperationHash,
  },
})

let limitOrderSignature

if (quote.typedData) {
  limitOrderSignature = await account.signTypedData({
    domain: quote.typedData.domain,
    types: quote.typedData.types,
    primaryType: quote.typedData.primaryType,
    message: quote.typedData.message,
  })
}

For limit orders, limitOrderSignature is required whenever the quote includes typedData. It is separate from the UserOperation signature.

Execute the Limit Order

Submit both signatures to /crypto/execute-user-op. Use userOperationHash; quoteId is deprecated.

const executionResponse = await fetch(`${BASE_URL}/crypto/execute-user-op`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': API_KEY,
  },
  body: JSON.stringify({
    userOperationHash: quote.userOperationHash,
    signature: userOperationSignature,
    limitOrderSignature,
  }),
}).then((res) => res.json())

console.log(executionResponse)

If the quote includes authorization.hash, sign that hash and include the resulting authorization field in the execute request. This is required for the first transaction of an EIP-7702 wallet.

Cancel a Limit Order

To cancel an active order, prepare the cancellation payload, sign the returned typed data, and execute the cancellation.

const transactionId = '<limit-order-transaction-id>'

const cancelPreparation = await fetch(
  `${BASE_URL}/crypto/limit-orders/${transactionId}/cancel`,
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': API_KEY,
    },
    body: JSON.stringify({
      walletAddress: smartWalletAddress,
      chainId: 137,
      payGasFeeToken: BRZ_POLYGON,
    }),
  },
).then((res) => res.json())

const cancelTypedData = cancelPreparation.signData[0].typedData

const cancelSignature = await account.signTypedData({
  domain: cancelTypedData.domain,
  types: cancelTypedData.types,
  primaryType: cancelTypedData.primaryType,
  message: cancelTypedData.message,
})

const cancelExecution = await fetch(
  `${BASE_URL}/crypto/limit-orders/${transactionId}/cancel/execute`,
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': API_KEY,
    },
    body: JSON.stringify({
      signature: cancelSignature,
    }),
  },
).then((res) => res.json())

console.log(cancelExecution)