Kuest Docs

Clients & SDKs

SDKs for CLOB trading, relayer wallet actions, market discovery, and market maker workflows

Use the SDKs when you want your bot or integration to read markets, sign orders, place and cancel orders, deploy a deposit wallet, or route wallet actions such as approvals, splits, merges, redeems, and transfers.

SDK Families

CLOB client SDKs

Use the CLOB clients for market data, L1/L2 authentication, order signing, limit and market orders, order management, and trade history.

Available in TypeScript, Python, and Rust.

Builder Relayer client SDKs

Use the relayer clients for deposit wallet deployment and signed wallet batches that execute onchain actions without requiring users to hold gas.

Available in TypeScript, Python, and Rust.

Downloads

Download your SDK bundle from Settings / SDKs.

When To Use Each Client

TaskClient
Discover markets, read books, midpoints, spreads, and pricesCLOB client
Create or derive API credentialsCLOB client
Sign and post limit or market ordersCLOB client
Cancel orders and read account tradesCLOB client
Deploy a deposit walletBuilder Relayer client
Approve collateral or outcome tokensBuilder Relayer client
Split collateral into outcome token inventoryBuilder Relayer client or Rust CLOB SDK onchain CTF helpers
Merge complete token sets back to collateralBuilder Relayer client or Rust CLOB SDK onchain CTF helpers
Redeem resolved winning positionsBuilder Relayer client or Rust CLOB SDK onchain CTF helpers

CLOB Quickstart

The CLOB client can be public or authenticated. Public clients can read markets and prices. Authenticated clients need a signer, Deposit Wallet funder address, and L2 API credentials.

TypeScript

import { ClobClient, OrderType, Side, SignatureType } from '@kuestcom/clob-client'
import { createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { polygonAmoy } from 'viem/chains'

const host = 'https://clob.kuest.com'
const chainId = 80002
const tokenID = process.env.CLOB_TOKEN_ID!

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
const signer = createWalletClient({
  account,
  chain: polygonAmoy,
  transport: http(process.env.RPC_URL),
})

const bootstrap = new ClobClient(host, chainId, signer)
const creds = await bootstrap.createOrDeriveApiKey()

const client = new ClobClient(
  host,
  chainId,
  signer,
  creds,
  SignatureType.DEPOSIT_WALLET,
  process.env.DEPOSIT_WALLET!,
)

const book = await client.getOrderBook(tokenID)
const midpoint = await client.getMidpoint(tokenID)
const tickSize = await client.getTickSize(tokenID)
const negRisk = await client.getNegRisk(tokenID)

const order = await client.createAndPostOrder(
  {
    tokenID,
    price: 0.42,
    size: 5,
    side: Side.BUY,
  },
  { tickSize, negRisk },
  OrderType.GTC,
  false,
  true,
)

console.log({ bestBid: book.bids.at(-1), bestAsk: book.asks.at(-1), midpoint, order })

Python

import os

from py_clob_client.client import ClobClient
from py_clob_client.clob_types import OrderArgs, OrderType, PartialCreateOrderOptions
from py_clob_client.order_builder.constants import BUY, SELL

HOST = "https://clob.kuest.com"
CHAIN_ID = 80002
TOKEN_ID = os.environ["CLOB_TOKEN_ID"]

client = ClobClient(
    HOST,
    chain_id=CHAIN_ID,
    key=os.environ["PRIVATE_KEY"],
    signature_type=3,
    funder=os.environ["DEPOSIT_WALLET"],
)
client.set_api_creds(client.create_or_derive_api_creds())

tick_size = client.get_tick_size(TOKEN_ID)
neg_risk = client.get_neg_risk(TOKEN_ID)
book = client.get_order_book(TOKEN_ID)

buy_order = client.create_order(
    OrderArgs(token_id=TOKEN_ID, price=0.42, size=5, side=BUY),
    options=PartialCreateOrderOptions(tick_size=tick_size, neg_risk=neg_risk),
)
sell_order = client.create_order(
    OrderArgs(token_id=TOKEN_ID, price=0.48, size=5, side=SELL),
    options=PartialCreateOrderOptions(tick_size=tick_size, neg_risk=neg_risk),
)

buy_response = client.post_order(buy_order, OrderType.GTC)
sell_response = client.post_order(sell_order, OrderType.GTC)

print({"book": book, "buy": buy_response, "sell": sell_response})

Rust

Enable the features you use:

[dependencies]
kuest-client-sdk = { version = "2", features = ["clob"] }
rust_decimal = "1"
rust_decimal_macros = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
use std::str::FromStr as _;

use alloy::signers::Signer as _;
use alloy::signers::local::LocalSigner;
use kuest_client_sdk::clob::types::{OrderType, Side, SignatureType};
use kuest_client_sdk::clob::{Client, Config};
use kuest_client_sdk::types::{Address, U256};
use kuest_client_sdk::AMOY;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;

#[tokio::main]
async fn main() -> kuest_client_sdk::Result<()> {
    let signer = LocalSigner::from_str(&std::env::var("PRIVATE_KEY")?)?
        .with_chain_id(Some(AMOY));
    let deposit_wallet = Address::from_str(&std::env::var("DEPOSIT_WALLET")?)?;
    let token_id = U256::from_str(&std::env::var("CLOB_TOKEN_ID")?)?;

    let client = Client::new("https://clob.kuest.com", Config::default())?
        .authentication_builder(&signer)
        .signature_type(SignatureType::DepositWallet)
        .funder(deposit_wallet)
        .authenticate()
        .await?;

    let limit = client
        .limit_order()
        .token_id(token_id)
        .side(Side::Buy)
        .price(dec!(0.42))
        .size(Decimal::new(5, 0))
        .order_type(OrderType::GTC)
        .build()
        .await?;

    let signed = client.sign(&signer, limit).await?;
    let posted = client.post_order(signed).await?;

    println!("{posted:?}");
    Ok(())
}

Relayer Quickstart

The relayer client submits signed deposit wallet batches. Use it for onchain setup and inventory operations so the user does not need to hold gas.

Required environment:

RELAYER_URL=https://relayer.kuest.com
KUEST_BUILDER_API_KEY=...
KUEST_BUILDER_SECRET=...
KUEST_BUILDER_PASSPHRASE=...
PRIVATE_KEY=...

Deploy A Deposit Wallet And Split Inventory

This TypeScript example deploys the expected deposit wallet, approves the CTF contract to spend collateral, and splits 100 collateral units into 100 YES and 100 NO tokens for a binary market.

import { BuilderConfig } from '@kuestcom/builder-signing-sdk'
import { RelayClient } from '@kuestcom/builder-relayer-client'
import type { DepositWalletCall } from '@kuestcom/builder-relayer-client'
import { createWalletClient, encodeFunctionData, erc20Abi, http, maxUint256, parseUnits } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { polygonAmoy } from 'viem/chains'

const COLLATERAL_TOKEN = '0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582'
const CONDITIONAL_TOKENS = '0x4682048725865bf17067bd85fF518527A262A9C7'
const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000'

const conditionalTokensAbi = [
  {
    name: 'splitPosition',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [
      { name: 'collateralToken', type: 'address' },
      { name: 'parentCollectionId', type: 'bytes32' },
      { name: 'conditionId', type: 'bytes32' },
      { name: 'partition', type: 'uint256[]' },
      { name: 'amount', type: 'uint256' },
    ],
    outputs: [],
  },
] as const

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
const wallet = createWalletClient({
  account,
  chain: polygonAmoy,
  transport: http(process.env.RPC_URL),
})

const builderConfig = new BuilderConfig({
  localBuilderCreds: {
    key: process.env.KUEST_BUILDER_API_KEY!,
    secret: process.env.KUEST_BUILDER_SECRET!,
    passphrase: process.env.KUEST_BUILDER_PASSPHRASE!,
  },
})

const relayer = new RelayClient(process.env.RELAYER_URL!, 80002, wallet, builderConfig)
const depositWallet = await relayer.deriveDepositWallet()

if (!(await relayer.getDeployed(depositWallet))) {
  await (await relayer.deployDepositWallet()).wait()
}

const conditionId = process.env.CONDITION_ID as `0x${string}`
const amount = parseUnits('100', 6)

const calls: DepositWalletCall[] = [
  {
    target: COLLATERAL_TOKEN,
    value: '0',
    data: encodeFunctionData({
      abi: erc20Abi,
      functionName: 'approve',
      args: [CONDITIONAL_TOKENS, maxUint256],
    }),
  },
  {
    target: CONDITIONAL_TOKENS,
    value: '0',
    data: encodeFunctionData({
      abi: conditionalTokensAbi,
      functionName: 'splitPosition',
      args: [COLLATERAL_TOKEN, ZERO_BYTES32, conditionId, [1n, 2n], amount],
    }),
  },
]

const deadline = Math.floor(Date.now() / 1000 + 300).toString()
const response = await relayer.executeDepositWalletBatch(calls, depositWallet, deadline)
const transaction = await response.wait()

console.log({ depositWallet, transaction })

Python Relayer Batch

import os
import time

from eth_abi import encode
from eth_utils import keccak, to_hex
from py_builder_relayer_client.client import RelayClient
from py_builder_relayer_client.models import DepositWalletCall
from py_builder_signing_sdk.config import BuilderApiKeyCreds, BuilderConfig

COLLATERAL_TOKEN = "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582"
CONDITIONAL_TOKENS = "0x4682048725865bf17067bd85fF518527A262A9C7"
ZERO_BYTES32 = "0x0000000000000000000000000000000000000000000000000000000000000000"
MAX_UINT256 = 2**256 - 1


def bytes32(value: str) -> bytes:
    return bytes.fromhex(value.removeprefix("0x"))


def calldata(signature: str, types: list[str], values: list) -> str:
    selector = keccak(text=signature)[:4]
    return to_hex(selector + encode(types, values))


def usdc(value: int) -> int:
    return value * 10**6


builder_config = BuilderConfig(
    local_builder_creds=BuilderApiKeyCreds(
        key=os.environ["KUEST_BUILDER_API_KEY"],
        secret=os.environ["KUEST_BUILDER_SECRET"],
        passphrase=os.environ["KUEST_BUILDER_PASSPHRASE"],
    )
)

client = RelayClient(
    os.environ["RELAYER_URL"],
    80002,
    os.environ["PRIVATE_KEY"],
    builder_config,
)

deposit_wallet = client.derive_deposit_wallet()
if not client.get_deployed(deposit_wallet):
    client.deploy_deposit_wallet().wait()

condition_id = os.environ["CONDITION_ID"]
calls = [
    DepositWalletCall(
        target=COLLATERAL_TOKEN,
        value="0",
        data=calldata(
            "approve(address,uint256)",
            ["address", "uint256"],
            [CONDITIONAL_TOKENS, MAX_UINT256],
        ),
    ),
    DepositWalletCall(
        target=CONDITIONAL_TOKENS,
        value="0",
        data=calldata(
            "splitPosition(address,bytes32,bytes32,uint256[],uint256)",
            ["address", "bytes32", "bytes32", "uint256[]", "uint256"],
            [COLLATERAL_TOKEN, bytes32(ZERO_BYTES32), bytes32(condition_id), [1, 2], usdc(100)],
        ),
    ),
]

deadline = str(int(time.time()) + 300)
response = client.execute_deposit_wallet_batch(calls, deposit_wallet, deadline)
print(response.wait())

Build A Market Maker

A market maker loop is usually:

Discover candidate markets from Gamma.

Filter for active, open, accepting-orders markets with valid CLOB token IDs.

Split collateral into YES and NO inventory before quoting a fresh binary market.

Read the current order book and choose a fair price from your model.

Post a ladder of post-only buy orders below fair value and sell orders above fair value.

Cancel or replace stale quotes when the book, fair price, risk limits, or inventory changes.

Avoid crossed quotes

Always validate that your best buy price is lower than your best sell price before posting. A crossed ladder can fill immediately at a loss.

Identify New Markets

Use Gamma for market metadata and the CLOB client for live prices. In Rust, enable both gamma and clob features:

[dependencies]
kuest-client-sdk = { version = "2", features = ["gamma", "clob", "ctf"] }
alloy = { version = "2", features = ["providers", "signer-local"] }
rust_decimal = "1"
rust_decimal_macros = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
use kuest_client_sdk::gamma::types::request::MarketsRequest;
use kuest_client_sdk::gamma::Client as GammaClient;

async fn discover() -> kuest_client_sdk::Result<()> {
    let gamma = GammaClient::new("https://gamma-api.kuest.com")?;
    let markets = gamma
        .markets(
            &MarketsRequest::builder()
                .closed(false)
                .limit(100)
                .build(),
        )
        .await?;

    let candidates: Vec<_> = markets
        .into_iter()
        .filter(|market| market.active.unwrap_or(false))
        .filter(|market| market.accepting_orders.unwrap_or(false))
        .filter(|market| market.clob_token_ids.as_ref().is_some_and(|ids| ids.len() >= 2))
        .filter(|market| market.condition_id.is_some())
        .collect();

    for market in candidates {
        println!(
            "{} {:?} {:?}",
            market.slug.unwrap_or_default(),
            market.condition_id,
            market.clob_token_ids,
        );
    }

    Ok(())
}

Split Initial Inventory

For a binary market, splitting 100 collateral units creates 100 YES tokens and 100 NO tokens. You can run the split through the relayer as shown above, or use the Rust CLOB SDK onchain CTF helpers when your service wallet is funded for direct onchain transactions.

use std::str::FromStr as _;

use alloy::primitives::{address, B256, U256};
use alloy::providers::ProviderBuilder;
use alloy::signers::Signer as _;
use alloy::signers::local::LocalSigner;
use kuest_client_sdk::ctf::types::SplitPositionRequest;
use kuest_client_sdk::ctf::Client as CtfClient;
use kuest_client_sdk::AMOY;

const RPC_URL: &str = "https://rpc-amoy.polygon.technology";

async fn split_inventory(condition_id: B256) -> Result<(), Box<dyn std::error::Error>> {
    let collateral_token = address!("41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582");
    let signer = LocalSigner::from_str(&std::env::var("PRIVATE_KEY")?)?
        .with_chain_id(Some(AMOY));

    let provider = ProviderBuilder::new()
        .wallet(signer)
        .connect(RPC_URL)
        .await?;

    let ctf = CtfClient::new(provider, AMOY)?;
    let split = SplitPositionRequest::for_binary_market(
        collateral_token,
        condition_id,
        U256::from(100_000_000u64),
    );

    let receipt = ctf.split_position(&split).await?;
    println!("split tx: {}", receipt.transaction_hash);

    Ok(())
}

Post A Buy/Sell Ladder

This example builds three post-only buys below your fair value and three sells above it. Use the YES token ID for a YES book. Repeat with the NO token ID if your strategy quotes both outcome books directly.

use std::str::FromStr as _;

use alloy::signers::Signer as _;
use alloy::signers::local::LocalSigner;
use kuest_client_sdk::clob::types::{OrderType, Side, SignatureType};
use kuest_client_sdk::clob::{Client as ClobClient, Config};
use kuest_client_sdk::types::{Address, U256};
use kuest_client_sdk::AMOY;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;

#[derive(Clone, Copy)]
struct Quote {
    side: Side,
    price: Decimal,
    size: Decimal,
}

fn clamp_price(price: Decimal, tick: Decimal) -> Decimal {
    price.max(tick).min(Decimal::ONE - tick)
}

fn ladder(fair: Decimal, half_spread: Decimal, step: Decimal, size: Decimal) -> Vec<Quote> {
    let mut quotes = Vec::new();
    for level in 0..3 {
        let offset = half_spread + step * Decimal::from(level);
        quotes.push(Quote {
            side: Side::Buy,
            price: clamp_price(fair - offset, step),
            size,
        });
        quotes.push(Quote {
            side: Side::Sell,
            price: clamp_price(fair + offset, step),
            size,
        });
    }
    quotes
}

async fn quote_ladder() -> kuest_client_sdk::Result<()> {
    let signer = LocalSigner::from_str(&std::env::var("PRIVATE_KEY")?)?
        .with_chain_id(Some(AMOY));
    let deposit_wallet = Address::from_str(&std::env::var("DEPOSIT_WALLET")?)?;
    let token_id = U256::from_str(&std::env::var("YES_TOKEN_ID")?)?;

    let client = ClobClient::new("https://clob.kuest.com", Config::default())?
        .authentication_builder(&signer)
        .signature_type(SignatureType::DepositWallet)
        .funder(deposit_wallet)
        .authenticate()
        .await?;

    let fair = dec!(0.55);
    let quotes = ladder(fair, dec!(0.015), dec!(0.005), Decimal::new(10, 0));

    let best_buy = quotes
        .iter()
        .filter(|quote| quote.side == Side::Buy)
        .map(|quote| quote.price)
        .max()
        .unwrap();
    let best_sell = quotes
        .iter()
        .filter(|quote| quote.side == Side::Sell)
        .map(|quote| quote.price)
        .min()
        .unwrap();
    assert!(best_buy < best_sell, "quote ladder is crossed");

    let mut signed_orders = Vec::new();
    for quote in quotes {
        let order = client
            .limit_order()
            .token_id(token_id)
            .side(quote.side)
            .price(quote.price)
            .size(quote.size)
            .order_type(OrderType::GTC)
            .post_only(true)
            .build()
            .await?;

        signed_orders.push(client.sign(&signer, order).await?);
    }

    let responses = client.post_orders(signed_orders).await?;
    println!("posted {} quotes", responses.len());

    Ok(())
}

For live operation, keep a small state table per market: condition ID, token IDs, current fair price, open order IDs, inventory, max exposure, last quote timestamp, and last observed book hash. Requote only when the fair price moves by at least one tick, the top of book changes enough to matter, or your inventory limits require skewing the ladder.

Authentication Notes

  • CLOB trading uses Deposit Wallet orders with signature type 3.
  • The CLOB client derives L2 API credentials from your wallet signature and uses them for trading routes.
  • Relayer clients use builder-style auth headers named KUEST_BUILDER_*.
  • Downloaded bundles include defaults for the selected environment, so keep examples aligned with your bundle.
  • Do not put private keys, CLOB API secrets, relayer credentials, or builder credentials in browser-exposed code.

Updating SDK Bundles

Downloaded SDK bundles include an updater. To refresh the SDK files in place, run this from the bundle directory:

./.sdk/update --latest