Tristero
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)
| File | Size | Uploaded | |
|---|---|---|---|
| tristero-0.4.2.tar.gz | 54.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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}
|