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

Metadata

Release files for tristero 0.4.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tristero 0.4.2
File Size Uploaded
tristero-0.4.2.tar.gz 54.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for tristero 0.4.2
File Interpreter ABI Platform
tristero-0.4.2-py3-none-any.whl Python 3 none any Details

Total release size: 110.7 kB

Release files / tristero-0.4.2.tar.gz

Download URL tristero-0.4.2.tar.gz
Size 54.6 kB
Tags Source
SHA-256 checksum
How to use checksums
bc917e7330ecfe6eaabbc1bf07d07c2f5c516e2be111b05cac274dfb77048209
BLAKE2b-256 checksum
How to use checksums
94128e0c0a48435e9496e5b5b99c11fec5b630d48c5ea877187766a5f4036e51
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release files / tristero-0.4.2-py3-none-any.whl

Download URL tristero-0.4.2-py3-none-any.whl
Size 56.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
01da98d6a158e1c804aec2cfd99c67dfa315d56c8b13ee9b2b391052df51951a
BLAKE2b-256 checksum
How to use checksums
736b26cb776e7d1078785b54386afea686e5e81312a224450b4c5b734a957862
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release history Release notifications | RSS feed

This release

0.4.2 This release

2 release files

0.3.8

2 release files

0.3.7

2 release files

0.3.5

2 release files

0.3.4

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page