Skip to main content

Tristero

PyPI version Python Support

This repository is home to Tristero's trading library.

How it works

Tristero supports two primary swap mechanisms:

Permit2 Swaps (EVM-to-EVM)

  • Quote & Approve - Request a quote and approve tokens via Permit2 (gasless approval)
  • Sign & Submit - Sign an EIP-712 order and submit for execution
  • Monitor - Track swap progress via WebSocket updates

Feather Swaps (UTXO-based)

  • Quote & Deposit - Request a quote to receive a deposit address
  • Manual Transfer - Send funds to the provided deposit address
  • Monitor - Track swap completion via WebSocket updates

This library provides both high-level convenience functions and lower-level components for precise control.

Installation

pip install tristero

Environment Configuration

Tristero supports three environments: PRODUCTION (default), STAGING, and LOCAL.

Set the environment globally at startup:

from tristero import set_config

set_config("STAGING")  # all subsequent calls use the staging API

Or override per call:

quote = await get_swap_quote(..., env="LOCAL")

Every user-facing function in the SDK accepts an optional env keyword argument.

Quick Start

Spot Swap (quote, sign, submit)

import asyncio
import json
import os

from eth_account import Account

from tristero import get_swap_quote, sign_and_submit, make_async_w3


async def main() -> None:
    private_key = os.getenv("TEST_ACCOUNT_PRIVKEY")
    if not private_key:
        raise RuntimeError("Set TEST_ACCOUNT_PRIVKEY")

    wallet = Account.from_key(private_key).address
    w3 = make_async_w3(os.getenv("ARB_RPC_URL", "https://arbitrum-one-rpc.publicnode.com"))

    # 1. Get a quote (USDC -> WETH on Arbitrum)
    quote = await get_swap_quote(
        wallet=wallet,
        src_chain=42161,
        src_token="0xaf88d065e77c8cC2239327C5EDb3A432268e5831",  # USDC
        dst_chain=42161,
        dst_token="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",  # WETH
        amount=1_000_000,  # 1 USDC (6 decimals)
    )
    print(json.dumps(quote, indent=2))

    # 2. Sign and submit (w3 required for Permit2 approval on source chain)
    result = await sign_and_submit(quote, private_key, w3=w3, wait=True, timeout=300)
    print(result)


asyncio.run(main())

Margin Position (quote, sign, submit)

import asyncio
import json
import os

from eth_account import Account

from tristero import get_swap_quote, sign_and_submit


async def main() -> None:
    private_key = os.getenv("TEST_ACCOUNT_PRIVKEY")
    if not private_key:
        raise RuntimeError("Set TEST_ACCOUNT_PRIVKEY")

    wallet = Account.from_key(private_key).address

    # 1. Get a margin quote (2x leveraged USDC/WETH on Arbitrum)
    quote = await get_swap_quote(
        wallet=wallet,
        src_chain=42161,
        src_token="0xaf88d065e77c8cC2239327C5EDb3A432268e5831",  # USDC (collateral)
        dst_chain=42161,
        dst_token="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",  # WETH (base)
        amount=1_000_000,  # 1 USDC collateral (6 decimals)
        leverage=2,
    )
    print(json.dumps(quote, indent=2))

    # 2. Sign and submit (no w3 needed for margin)
    result = await sign_and_submit(quote, private_key, wait=True, timeout=120)
    print(result)


asyncio.run(main())

More Examples

Spot Swap (direct execution)

import os
import asyncio

from eth_account import Account

from tristero import ChainID, TokenSpec, execute_permit2_swap, make_async_w3


async def main() -> None:
    private_key = os.getenv("TEST_ACCOUNT_PRIVKEY")
    if not private_key:
        raise RuntimeError("Set TEST_ACCOUNT_PRIVKEY")

    account = Account.from_key(private_key)

    arbitrum_rpc = os.getenv("ARB_RPC_URL", "https://arbitrum-one-rpc.publicnode.com")
    w3 = make_async_w3(arbitrum_rpc)

    result = await execute_permit2_swap(
        w3=w3,
        account=account,
        src_t=TokenSpec(chain_id=ChainID(42161), token_address="0xaf88d065e77c8cC2239327C5EDb3A432268e5831"),  # USDC (Arbitrum)
        dst_t=TokenSpec(chain_id=ChainID(8453), token_address="0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2"),  # USDT (Base)
        raw_amount=1_000_000,  # 1 USDC (6 decimals)
        timeout=300,
    )
    print(result)


asyncio.run(main())

Margin: Direct Open

import asyncio
import os

from eth_account import Account
from tristero import open_margin_position


async def main() -> None:
    private_key = os.getenv("TEST_ACCOUNT_PRIVKEY", "")
    if not private_key:
        raise RuntimeError("Set TEST_ACCOUNT_PRIVKEY")

    wallet = Account.from_key(private_key).address

    result = await open_margin_position(
        private_key=private_key,
        chain_id="42161",
        wallet_address=wallet,
        collateral_token="0xaf88d065e77c8cC2239327C5EDb3A432268e5831",  # USDC
        base_token="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",  # WETH
        leverage_ratio=2,
        collateral_amount="1000000",  # 1 USDC (6 decimals)
        wait_for_result=True,
        timeout=120,
    )
    print(result)


asyncio.run(main())

Any token can be used as collateral. For example, open a 2x long WETH position using USDT0:

result = await open_margin_position(
    private_key=private_key,
    chain_id="42161",
    wallet_address=wallet,
    collateral_token="0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",  # USDT0
    base_token="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",        # WETH
    leverage_ratio=2,
    collateral_amount="1000000",  # 1 USDT0 (6 decimals)
)

Or use WETH itself as collateral:

result = await open_margin_position(
    private_key=private_key,
    chain_id="42161",
    wallet_address=wallet,
    collateral_token="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",  # WETH
    base_token="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",        # WETH
    leverage_ratio=2,
    collateral_amount="500000000000000",  # 0.0005 WETH (18 decimals)
)

The quote response includes loan_token, collateral_token, and base_token objects with full metadata, plus interest_rate_bps for the borrowing cost.

Margin: List Positions / Close Position

import asyncio
import os

from eth_account import Account
from tristero import close_margin_position, list_margin_positions


async def main() -> None:
    private_key = os.getenv("TEST_ACCOUNT_PRIVKEY", "")
    if not private_key:
        raise RuntimeError("Set TEST_ACCOUNT_PRIVKEY")

    wallet = Account.from_key(private_key).address

    positions = await list_margin_positions(wallet)
    open_pos = next((p for p in positions if p.status == "open"), None)
    if not open_pos:
        raise RuntimeError("no open positions")

    result = await close_margin_position(
        private_key=private_key,
        chain_id="42161",
        position_id=open_pos.taker_token_id,
        escrow_contract=open_pos.escrow_address,
        authorized=open_pos.filler_address,
        cash_settle=False,
        fraction_bps=10_000,
        deadline_seconds=3600,
        wait_for_result=True,
        timeout=120,
    )
    print(result)


asyncio.run(main())

WebSocket Quote Streaming

subscribe_quotes opens a persistent WebSocket connection and delivers live quotes via an async callback (~500 ms updates).

If the callback is still running when a newer quote arrives, intermediate updates are dropped and only the latest quote is delivered once the callback finishes (latest-only pattern). This makes it safe to do slow work (e.g. sign + submit) inside the callback without worrying about duplicate executions.

Simple: print every quote

import asyncio
from tristero import subscribe_quotes


async def main() -> None:
    async def on_quote(quote):
        print(f"dst_qty={quote['dst_token_quantity']}  order_id={quote['order_id'][:16]}...")

    async def on_error(exc):
        print(f"Error: {exc}")

    async with await subscribe_quotes(
        wallet="0xYOUR_WALLET",
        src_chain=42161,
        src_token="0xaf88d065e77c8cC2239327C5EDb3A432268e5831",  # USDC
        dst_chain=1,
        dst_token="0xdAC17F958D2ee523a2206206994597C13D831ec7",  # USDT
        amount=1_000_000,
        on_quote=on_quote,
        on_error=on_error,
    ) as sub:
        await asyncio.sleep(10)  # stream for 10 seconds


asyncio.run(main())

Advanced: sign and submit the first quote, then stop

import asyncio
import os

from eth_account import Account
from tristero import subscribe_quotes, sign_and_submit, make_async_w3


async def main() -> None:
    private_key = os.getenv("TEST_ACCOUNT_PRIVKEY", "")
    wallet = Account.from_key(private_key).address
    w3 = make_async_w3(os.getenv("ARB_RPC_URL", "https://arbitrum-one-rpc.publicnode.com"))

    done = asyncio.Event()

    async def on_quote(quote):
        if done.is_set():
            return
        quote["_type"] = "swap"
        result = await sign_and_submit(quote, private_key, w3=w3, wait=False)
        print(result)
        done.set()

    sub = await subscribe_quotes(
        wallet=wallet,
        src_chain=42161,
        src_token="0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
        dst_chain=42161,
        dst_token="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
        amount=1_000_000,
        on_quote=on_quote,
    )
    await done.wait()
    await sub.close()


asyncio.run(main())

Limit order: wait for a target price, then submit

import asyncio
import os

from eth_account import Account
from tristero import subscribe_quotes, sign_and_submit, make_async_w3


async def main() -> None:
    private_key = os.getenv("TEST_ACCOUNT_PRIVKEY", "")
    wallet = Account.from_key(private_key).address
    w3 = make_async_w3(os.getenv("ARB_RPC_URL", "https://arbitrum-one-rpc.publicnode.com"))

    done = asyncio.Event()
    baseline: list[int] = []
    improvement_bps = 10  # submit when price is 10 bps better than first quote

    async def on_quote(quote):
        if done.is_set():
            return
        dst_qty = int(quote.get("dst_token_quantity", 0))

        if not baseline:
            baseline.append(dst_qty)
            print(f"Baseline: {dst_qty}")
            return

        threshold = baseline[0] * (10_000 + improvement_bps) / 10_000
        if dst_qty < threshold:
            print(f"dst_qty={dst_qty} (waiting for >= {threshold:.0f})")
            return

        # Threshold met — sign and submit THIS specific quote
        print(f"Target reached: {dst_qty} >= {threshold:.0f}, submitting!")
        quote["_type"] = "swap"
        result = await sign_and_submit(quote, private_key, w3=w3, wait=False)
        print(result)
        done.set()

    sub = await subscribe_quotes(
        wallet=wallet,
        src_chain=42161,
        src_token="0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
        dst_chain=42161,
        dst_token="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
        amount=1_000_000,
        on_quote=on_quote,
    )
    await done.wait()
    await sub.close()


asyncio.run(main())

The callback receives the exact quote that triggered the condition. Because of the latest-only pattern, even if signing takes longer than 500 ms, that specific quote is what gets signed — newer arrivals simply queue up and don't cause duplicates.

Feather: Start (get deposit address)

Feather swaps are deposit-based: you start an order to receive a deposit_address, send funds to it manually, then optionally wait for completion.

Submit only:

import asyncio

from tristero import ChainID, TokenSpec, start_feather_swap


async def main() -> None:
    # Example: ETH (native) -> XMR (native)
    src_t = TokenSpec(chain_id=ChainID.ethereum, token_address="native")
    dst_t = TokenSpec(chain_id=ChainID.monero, token_address="native")

    # Replace with your own destination address on the destination chain.
    dst_addr = "YOUR_XMR_ADDRESS"

    swap = await start_feather_swap(
        src_t=src_t,
        dst_t=dst_t,
        dst_addr=dst_addr,
        raw_amount=100_000_000_000_000_000,  # 0.1 ETH in wei
    )

    order_id = (
        (swap.data or {}).get("id")
        or (swap.data or {}).get("order_id")
        or (swap.data or {}).get("orderId")
        or ""
    )

    print("order_id:", order_id)
    print("deposit_address:", swap.deposit_address)


asyncio.run(main())

Submit + wait (WebSocket):

import asyncio

from tristero import ChainID, OrderType, TokenSpec, start_feather_swap, wait_for_completion


async def main() -> None:
    src_t = TokenSpec(chain_id=ChainID.ethereum, token_address="native")
    dst_t = TokenSpec(chain_id=ChainID.monero, token_address="native")
    dst_addr = "YOUR_XMR_ADDRESS"

    swap = await start_feather_swap(
        src_t=src_t,
        dst_t=dst_t,
        dst_addr=dst_addr,
        raw_amount=100_000_000_000_000_000,
    )

    order_id = (
        (swap.data or {}).get("id")
        or (swap.data or {}).get("order_id")
        or (swap.data or {}).get("orderId")
        or ""
    )
    if not order_id:
        raise RuntimeError(f"Feather swap response missing order id: {swap.data}")

    print("deposit_address:", swap.deposit_address)
    print("Waiting for completion...")
    completion = await wait_for_completion(order_id, order_type=OrderType.FEATHER)
    print(completion)


asyncio.run(main())

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tristero-0.4.2.tar.gz (54.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tristero-0.4.2-py3-none-any.whl (56.1 kB view details)

Uploaded Python 3

File details

Details for the file tristero-0.4.2.tar.gz.

File metadata

  • Download URL: tristero-0.4.2.tar.gz
  • Upload date:
  • Size: 54.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for tristero-0.4.2.tar.gz
Algorithm Hash digest
SHA256 bc917e7330ecfe6eaabbc1bf07d07c2f5c516e2be111b05cac274dfb77048209
MD5 068ae84cb66431178578bbd2f1fb0abd
BLAKE2b-256 94128e0c0a48435e9496e5b5b99c11fec5b630d48c5ea877187766a5f4036e51

See more details on using hashes here.

File details

Details for the file tristero-0.4.2-py3-none-any.whl.

File metadata

  • Download URL: tristero-0.4.2-py3-none-any.whl
  • Upload date:
  • Size: 56.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for tristero-0.4.2-py3-none-any.whl
Algorithm Hash digest
SHA256 01da98d6a158e1c804aec2cfd99c67dfa315d56c8b13ee9b2b391052df51951a
MD5 c9a19af12e30331120caa16145c0a691
BLAKE2b-256 736b26cb776e7d1078785b54386afea686e5e81312a224450b4c5b734a957862

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.4.2 This release

2 files

0.3.8

2 files

0.3.7

2 files

0.3.5

2 files

0.3.4

2 files

0.3.3

2 files

0.3.2

2 files

0.3.0

2 files

0.2.1

2 files

0.1.7

2 files

0.1.6

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page