Developer Hub

SVM Searcher Integration

Learn how to integrate Express Relay as a searcher on Solana Virtual Machine chains to fulfill market orders and limit orders.

SVM Express Relay searchers fulfill market order opportunities as well as limit orders on the Limo program.

Step 1: Subscribe to New Opportunities

Express Relay provides searchers with Typescript and Python SDKs to interact with Express Relay. Searchers can also directly fetch available opportunities via HTTP or subscribe to them via WebSocket.

Pyth provides a Typescript SDK, which allows searchers to subscribe to opportunities:

import { Client, Opportunity } from "@pythnetwork/express-relay-js";

const handleOpportunity = async (opportunity: Opportunity) => {
  console.log("Received opportunity");
  // Implement your opportunity handler here
};

const client = new Client(
  { baseUrl: "https://per-mainnet.dourolabs.app" },
  undefined, // Default WebSocket options
  handleOpportunity,
);

async function main() {
  await client.subscribeChains(["solana"]);
}

main();

The server responds with opportunities in the following format:

{
  "order": "UxMUbQAsjrfQUp5stVwMJ6Mucq7VWTvt4ICe69BJ8lVXqwM+0sysV8OqZTdM0W4p...", // The Limo order to be executed, encoded in base64
  "order_address": "DUcTi3rDyS5QEmZ4BNRBejtArmDCWaPYGfN44vBJXKL5", // Address of the order account
  "program": "limo", // Identifier of the program that the order exists in
  "chain_id": "development-solana",
  "version": "v1" // Opportunity format version
}

Pyth provides a Python SDK, which allows searchers to subscribe to opportunities:

import asyncio
from express_relay.client import (
    ExpressRelayClient,
)
from express_relay.models import Opportunity

async def opportunity_callback(opportunity: Opportunity):
    print("Received opportunity")
    # Implement your opportunity handler here

client = ExpressRelayClient(
    "https://per-mainnet.dourolabs.app",
    None,
    opportunity_callback,
    None,
)

async def main():
    await client.subscribe_chains(["solana"])
    task = await client.get_ws_loop()
    await task

if __name__ == "__main__":
    asyncio.run(main())

The server responds with opportunities in the following format:

{
  "order": "UxMUbQAsjrfQUp5stVwMJ6Mucq7VWTvt4ICe69BJ8lVXqwM+0sysV8OqZTdM0W4p...", // The Limo order to be executed, encoded in base64
  "order_address": "DUcTi3rDyS5QEmZ4BNRBejtArmDCWaPYGfN44vBJXKL5", // Address of the order account
  "program": "limo", // Identifier of the program that the order exists in
  "chain_id": "development-solana",
  "version": "v1" // Opportunity format version
}

Searchers can request opportunities through an HTTP GET call to the /v1/opportunities endpoint.

curl -X 'GET' \
  'https://per-mainnet.dourolabs.app/v1/opportunities?chain_id=solana&mode=live'

Opportunities are short-lived and could be executed in a matter of seconds. So, the above endpoint could return an empty response.

The server responds with opportunities in the following format:

{
  "order": "UxMUbQAsjrfQUp5stVwMJ6Mucq7VWTvt4ICe69BJ8lVXqwM+0sysV8OqZTdM0W4p...", // The Limo order to be executed, encoded in base64
  "order_address": "DUcTi3rDyS5QEmZ4BNRBejtArmDCWaPYGfN44vBJXKL5", // Address of the order account
  "program": "limo", // Identifier of the program that the order exists in
  "chain_id": "development-solana",
  "version": "v1" // Opportunity format version
}

Searchers can connect to the server via WebSocket to reduce latency and subscribe to various events. The WebSocket endpoint lives at /v1/ws (e.g wss://per-mainnet.dourolabs.app/v1/ws). Here is a sample JSON payload to subscribe to opportunities:

{
  "id": "1",
  "method": "subscribe",
  "params": {
    "chain_ids": ["solana"]
  }
}

Consult the Websocket API reference for a complete list of methods and parameters.

The server responds with opportunities in the following format:

{
  "order": "UxMUbQAsjrfQUp5stVwMJ6Mucq7VWTvt4ICe69BJ8lVXqwM+0sysV8OqZTdM0W4p...", // The Limo order to be executed, encoded in base64
  "order_address": "DUcTi3rDyS5QEmZ4BNRBejtArmDCWaPYGfN44vBJXKL5", // Address of the order account
  "program": "limo", // Identifier of the program that the order exists in
  "chain_id": "development-solana",
  "version": "v1" // Opportunity format version
}

Step 2: Construct the Bid

Searchers should construct a bid by evaluating the fetched opportunity.

⚠️ Warning Before constructing the bid, make sure your wallet has the required assets to fulfill the limit order and SOL to pay the bid amount.

See the following examples of how to construct a bid object via the SDKs:

Below is an excerpt of example code. See the full example in the Typescript SDK.

import { OpportunitySvm } from "../index";
import { BidSvm } from "../types";

import * as anchor from "@coral-xyz/anchor";
import * as limo from "@kamino-finance/limo-sdk";

/**
  * Generates a bid for a given opportunity.
  * The transaction in this bid transfers assets from the searcher's wallet to fulfill the limit order.
  * @param opportunity The SVM opportunity to bid on.
  * @returns The generated bid object.
  */
async generateBid(opportunity: OpportunitySvm): Promise<BidSvm> {
  const order = opportunity.order;
  const limoClient = new limo.LimoClient(
    this.connectionSvm,
    order.state.globalConfig
  );

  const ixsTakeOrder = await this.generateTakeOrderIxs(limoClient, order);
  const feeInstruction = ComputeBudgetProgram.setComputeUnitPrice({
    microLamports:
      this.latestChainUpdate[this.chainId].latestPrioritizationFee,
  });
  const txRaw = new anchor.web3.Transaction().add(
    feeInstruction,
    ...ixsTakeOrder
  );

  const bidAmount = await this.getBidAmount(order);

  const config = await this.getExpressRelayConfig();
  const bid = await this.client.constructSvmBid(
    txRaw,
    this.searcher.publicKey,
    getPdaAuthority(limoClient.getProgramID(), order.state.globalConfig),
    order.address,
    bidAmount,
    new anchor.BN(Math.round(Date.now() / 1000 + DAY_IN_SECONDS)),
    this.chainId,
    config.relayerSigner,
    config.feeReceiverRelayer
  );

  bid.transaction.recentBlockhash =
    this.latestChainUpdate[this.chainId].blockhash;
  bid.transaction.sign(this.searcher);
  return bid;
}

The bid you construct will look like

{
  // serialized transaction object, in base-64 encoding
  "transaction": "SGVsbG8sIFdvcmxkIQ==",
  "chain_id": "solana",
  "env": "svm"
}

where the serialized transaction object should contain an Express Relay SubmitBid instruction that specifies the amount you are bidding and the permission details.

Below is an excerpt of example code. See the full example in the Python SDK.

import logging

from solders.transaction import Transaction

from express_relay.models.svm import BidSvm
from express_relay.svm.limo_client import OrderStateAndAddress

DEADLINE = 2**62
logger = logging.getLogger(__name__)

async def generate_bid(self, opp: OpportunitySvm) -> BidSvm:
    """
    Generates a bid for a given opportunity.
    The transaction in this bid transfers assets from the searcher's wallet to fulfill the limit order.

    Args:
        opp: The SVM opportunity to bid on.
    Returns:
        The generated bid object.
    """
    order: OrderStateAndAddress = {"address": opp.order_address, "state": opp.order}

    ixs_take_order = await self.generate_take_order_ixs(order)
    bid_amount = await self.get_bid_amount(order)
    router = self.limo_client.get_pda_authority(
        self.limo_client.get_program_id(), order["state"].global_config
    )

    submit_bid_ix = self.client.get_svm_submit_bid_instruction(
        searcher=self.private_key.pubkey(),
        router=router,
        permission_key=order["address"],
        bid_amount=bid_amount,
        deadline=DEADLINE,
        chain_id=self.chain_id,
        fee_receiver_relayer=(await self.get_metadata()).fee_receiver_relayer,
        relayer_signer=(await self.get_metadata()).relayer_signer,
    )
    latest_chain_update = self.latest_chain_update[self.chain_id]
    fee_instruction = set_compute_unit_price(latest_chain_update.latest_prioritization_fee)
    transaction = Transaction.new_with_payer(
        [fee_instruction, submit_bid_ix] + ixs_take_order, self.private_key.pubkey()
    )
    transaction.partial_sign(
        [self.private_key], recent_blockhash=latest_chain_update.blockhash
    )
    bid = BidSvm(transaction=transaction, chain_id=self.chain_id)
    return bid

The bid you construct will look like

{
  // serialized transaction object, in base-64 encoding
  "transaction": "SGVsbG8sIFdvcmxkIQ==",
  "chain_id": "solana",
  "env": "svm"
}

where the serialized transaction object should contain an Express Relay SubmitBid instruction that specifies the amount you are bidding and the permission details.

Step 3: Submit Bids on Opportunities to Express Relay

Searchers can submit their constructed bids to Express Relay via the SDKs, an HTTP POST request, or a WebSocket connection.

The code snippet below demonstrates how to submit a bid using the Typescript SDK:

const generateBid = async (opportunity: OpportunitySvm, recentBlockhash: Blockhash): BidSvm => {
    ...
}

const handleOpportunity = async (opportunity: Opportunity) => {
    ...
    const bid = await this.generateBid(opportunity as OpportunitySvm);
    await client.submitBid(bid);
}

The code snippet below demonstrates how to submit a bid using the Python SDK:

import typing

async def generate_bid(opp: OpportunitySvm) -> BidSvm:
    ...

def opportunity_callback(opportunity: Opportunity):
    bid = await self.assess_opportunity(typing.cast(OpportunitySvm, opp))
    await client.submit_bid(bid, subscribe_to_updates=True)

Searchers can submit bids through an HTTP POST call to the /v1/bids endpoint. This endpoint accepts a JSON payload containing the details of the bid.

curl -X 'POST' \
  'https://per-mainnet.dourolabs.app/v1/bids' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
    "chain_id": "solana",
    "transaction": "SGVsbG8sIFdvcmxkIQ=="
  }'

Searchers can submit bids via Websocket to avoid additional network round-trips and get notified about changes to the bid status.

{
  "id": "1",
  "method": "post_bid",
  "params": {
    "bid": {
      "chain_id": "solana",
      "transaction": "SGVsbG8sIFdvcmxkIQ=="
    }
  }
}

A successful response to a bid submission has the following schema:

{
  "id": "1", // Websocket request id
  "status": "success",
  "result": {
    "id": "beedbeed-b346-4fa1-8fab-2541a9e1872d", // Bid id
    "status": "OK"
  }
}

Consult Websocket API reference for more details.