Skip to main content

Library for trading on Tristero

Project description

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())

Project details


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.

Supported by

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