TideFi PropAMM

Integrate with Ulmo Markets instant posted liquidity.

What is TideFi?

TideFi is Ulmo Markets' PropAMM. It brings CEX-like pricing on-chain, enabling token swaps with competitive pricing backed by Ulmo Markets' proprietary quoting engine.

Integrators can get quotes and execute swaps directly against the contract. TideFi exposes a quote function for previewing swap amounts and a swap function for execution.

Market makers can also provide liquidity through TideFi and be part of Ulmo Markets. See Providing liquidity in TideFi.

Deployments

ChainTideFi
Base0x71Def100007A540305DD65d1034D10e809679FD5
Robinhood0xBAd60cDb3FC16Ef516939639847d552f7ddA45Ae

Contract interface

quote

Returns the amounts that would result from a swap given the caller's limits. This function is read-only (view).

function quote(address tokenIn, address tokenOut, uint256 maxAmountIn, UFixed18 limitPrice)
    external
    view
    returns (uint256 amountIn, uint256 amountOut);
ParameterTypeDescription
tokenInaddressInput token
tokenOutaddressOutput token
maxAmountInuint256Cap on the amount of input token spent
limitPriceUFixed18Max tokenIn paid per tokenOut, 18-decimal fixed point over raw token amounts (no decimals adjustment). Pass type(uint256).max for no limit

swap

Executes a swap against TideFi. Two overloads are available: with and without a callback.

function swap(
    address tokenIn,
    address tokenOut,
    uint256 maxAmountIn,
    uint256 minAmountOut,
    UFixed18 limitPrice,
    address to,
    bytes calldata callbackData
) external returns (uint256 amountIn, uint256 amountOut);
function swap(
    address tokenIn,
    address tokenOut,
    uint256 maxAmountIn,
    uint256 minAmountOut,
    UFixed18 limitPrice,
    address to
) external returns (uint256 amountIn, uint256 amountOut);
ParameterTypeDescription
tokenInaddressInput token
tokenOutaddressOutput token
maxAmountInuint256Cap on the amount of input token spent
minAmountOutuint256Minimum output: reverts with TideFiInsufficientAmountOut if not met
limitPriceUFixed18Max tokenIn paid per tokenOut, 18-decimal fixed point over raw token amounts (no decimals adjustment). Pass type(uint256).max for no limit
toaddressRecipient of the output tokens
callbackDatabytesForwarded to the caller's callback

Returns amountIn and amountOut: the actual amounts swapped. Filling stops once the marginal price exceeds limitPrice or the amountOut exceeds the contract's balance, so amountIn can be below maxAmountIn. The swap reverts if amountOut ends up below minAmountOut.

Swap callback

Callers of the callback overload must implement:

interface ITideFiCallback {
    function tidefiSwap(
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        uint256 amountOut,
        bytes calldata data
    ) external;
}

Computing limitPrice

limitPrice is the maximum tokenIn paid per tokenOut over raw token amounts, encoded as the value times 1e18:

limitPrice = humanPrice * 10**decimalsIn / 10**decimalsOut * 1e18

For example, buying WETH with USDC at a max price of 2000 USDC per WETH:

2000 * 10**6 / 10**18 * 1e18 = 2000e6

Selling WETH for USDC at the same price (max 1/2000 WETH per USDC):

(1/2000) * 10**18 / 10**6 * 1e18 = 5e26

It is always a cap on tokenIn per tokenOut, so a worse price is always a larger number, in either direction.

Integration overview

  1. Call quote to preview amounts for your limits and limit price.
  2. Approve tokenIn on the TideFi contract.
  3. Call swap with the same limits, a minAmountOut derived from the quote, a recipient address, and optional callbackData.

Taker API

Websocket API for takers (aggregators, solvers). Quotes against the freshest signed orderbooks before they land on-chain, so prices are ahead of what the contract currently shows. Read-only: swaps are permissionless calls to the TideFi contract itself.

A standalone Rust example is at the end of this section.

Connect

Connect to the Taker API endpoint (provided at onboarding, together with the bearer token) with header Authorization: Bearer <token>.

All messages both ways are JSON-RPC 2.0 shaped: {"jsonrpc": "2.0", "method": ..., "params": ..., "id": ...}.

On connect the server immediately sends one tidefi_markets message:

{
  "server_time_ms": 1788700122000,
  "chain_id": 8453,
  "tidefi": "0x71def100007a540305dd65d1034d10e809679fd5",
  "engine": "<tidefi engine>",
  "quote_asset_symbol": "USD",
  "quote_asset_decimals": 18,
  "assets": [
    {"symbol": "WETH", "token_address": "0x4200000000000000000000000000000000000006", "token_decimals": 18},
    {"symbol": "USDC", "token_address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "token_decimals": 6}
  ]
}
FieldTypeDescription
server_time_msintServer clock, unix ms.
chain_idintChain the deployment lives on (8453 is Base).
tidefiaddressTideFi contract to call quote and swap on.
engineaddressTrading engine behind it.
quote_asset_symbolstringAsset all books are quoted in.
quote_asset_decimalsintDecimals of the quote asset.
assetsarrayQuotable tokens, see below.

Each entry of assets:

FieldTypeDescription
symbolstringToken symbol.
token_addressaddressToken contract.
token_decimalsintToken decimals.

A swap crosses a pair of orderbooks, one per token.

Conventions

tidefi_quote

Request and response, the reply echoes your id:

{"jsonrpc": "2.0", "method": "tidefi_quote", "id": 1, "params": {
  "token_in": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
  "token_out": "0x4200000000000000000000000000000000000006",
  "amount_in": 50.0,
  "caller": "0x6ff5693b99212da76ad316178a184ab56d299b43",
  "priority_fee_ratio": 0.1
}}
FieldTypeDescription
token_inaddressToken you pay.
token_outaddressToken you receive.
amount_infloatAmount paid, whole tokens.
calleraddressRequired. The address that will call TideFi.swap: your router, not the end user.
priority_fee_ratiofloatRequired. Priority fee as a fraction of the base fee your swap will use. Pass your worst case to see the worst price.
{"jsonrpc": "2.0", "result": {
  "token_in": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
  "token_out": "0x4200000000000000000000000000000000000006",
  "amount_in": 50.0,
  "amount_out": 0.019984301571884605,
  "amount_in_subunits": "50000000",
  "amount_out_subunits": "19984301571884605",
  "price_out_per_in": 0.0003996860314376921,
  "book_timestamp_ms": 1788700122765,
  "landed": false
}, "id": 1}
FieldTypeDescription
token_inaddressEchoed.
token_outaddressEchoed.
amount_infloatAmount paid, whole tokens.
amount_outfloatAmount received, whole tokens.
amount_in_subunitsstringExact raw amount paid.
amount_out_subunitsstringExact raw amount received. Derive minAmountOut from it.
price_out_per_infloatamount_out / amount_in.
book_timestamp_msintTimestamp of the book the quote was computed on, unix ms.
landedboolWhether that book is already on-chain.

The quote is the contract's own math (TideFi.quote simulated with the freshest books). It is indicative, not firm: the book may be replaced before your swap executes. Protect the swap with minAmountOut, derived from amount_out_subunits minus your slippage.

Errors come back as {"jsonrpc": "2.0", "error": {"message": "..."}, "id": <your id>}: unknown tokens, or the simulation reverting (e.g. a stale book).

Subscribe

{"jsonrpc": "2.0", "method": "tidefi_subscribe", "params": ["orderbook_updates"], "id": null}

Replies with the current book per asset, then pushes one tidefi_orderbook per accepted maker update: faster than the chain (makers stream several updates per landed one).

tidefi_orderbook

{
  "symbol": "WETH",
  "base_token": "0x4200000000000000000000000000000000000006",
  "timestamp_ms": 1788700122581,
  "landed": false,
  "mid_price": 2500.3857,
  "bids": [{"price": 2499.848876953125, "quantity": 0.06342601776123047}],
  "asks": [{"price": 2500.936279296875, "quantity": 0.06865961104631424}],
  "stale_timestamp_ms": 1788700182000
}
FieldTypeDescription
symbolstringToken symbol, as in assets.
base_tokenaddressToken of the book.
timestamp_msintTimestamp of this book version, unix ms.
landedboolWhether this book version is already on-chain.
mid_pricefloatReference mid price, quote asset per whole token.
bidsarrayBid levels best to worst, each {price, quantity}: price in quote asset per whole token, quantity in whole tokens.
asksarrayAsk levels best to worst, same shape.
stale_timestamp_msintExpiry of the book, unix ms.

The book as the makers quoted it. It is against the quote asset (e.g. USD): to simulate a swap you need to cross two books. Use tidefi_quote for the actual fillable amounts.

Swapping

No swap endpoint: call TideFi.swap(tokenIn, tokenOut, maxAmountIn, minAmountOut, limitPrice, to) directly with limitPrice = type(uint256).max and your minAmountOut. The caller must have approved TideFi for tokenIn.

Rust example

Connects, subscribes to orderbook updates, prints every message and reconnects on disconnect. With the quote flags it requests one quote instead, prints the reply and exits.

//! Minimal taker API client: connects, subscribes to orderbook updates, prints every
//! message, reconnects on disconnect. With the quote flags: requests one quote instead,
//! prints the reply, exits.

use std::time::Duration;

use clap::Parser;
use futures::{SinkExt, StreamExt};
use serde_json::{Value, json};
use tokio_tungstenite::{
    connect_async,
    tungstenite::{Message, client::IntoClientRequest},
};

#[derive(Debug, Parser)]
struct Cli {
    /// The taker API websocket URL.
    #[arg(long)]
    url: String,

    /// The bearer token.
    #[arg(long)]
    auth_token: String,

    /// Request a quote on connect: the token in address.
    #[arg(long, requires_all = ["quote_token_out", "quote_amount_in", "quote_caller", "quote_priority_fee_ratio"])]
    quote_token_in: Option<String>,

    /// The token out address.
    #[arg(long)]
    quote_token_out: Option<String>,

    /// The amount in, whole token units.
    #[arg(long)]
    quote_amount_in: Option<f64>,

    /// The address that will call TideFi.swap.
    #[arg(long)]
    quote_caller: Option<String>,

    /// Priority fee as a fraction of the base fee.
    #[arg(long)]
    quote_priority_fee_ratio: Option<f64>,
}

#[tokio::main]
async fn main() -> eyre::Result<()> {
    let cli = Cli::parse();
    let subscribe = json!({
        "jsonrpc": "2.0",
        "method": "tidefi_subscribe",
        "params": ["orderbook_updates"],
        "id": null,
    })
    .to_string();

    let quote = cli.quote_token_in.as_ref().map(|quote_token_in| {
        json!({
            "jsonrpc": "2.0",
            "method": "tidefi_quote",
            "params": {
                "token_in": quote_token_in,
                "token_out": cli.quote_token_out,
                "amount_in": cli.quote_amount_in,
                "caller": cli.quote_caller,
                "priority_fee_ratio": cli.quote_priority_fee_ratio,
            },
            "id": 1,
        })
        .to_string()
    });

    loop {
        let mut request = cli.url.as_str().into_client_request()?;
        request.headers_mut().insert(
            "authorization",
            format!("Bearer {}", cli.auth_token).parse()?,
        );
        let mut ws = match connect_async(request).await {
            Ok((ws, _)) => ws,
            Err(err) => {
                eprintln!("connection failed, retrying: {err}");
                tokio::time::sleep(Duration::from_secs(1)).await;
                continue;
            }
        };

        loop {
            let Some(message) = ws.next().await else {
                eprintln!("connection closed, reconnecting");
                break;
            };
            let message = match message {
                Ok(message) => message,
                Err(err) => {
                    eprintln!("connection error, reconnecting: {err}");
                    break;
                }
            };
            if !message.is_text() {
                continue;
            }
            let message: Value = serde_json::from_str(message.to_text()?)?;
            match message["method"].as_str() {
                Some("tidefi_error") => eyre::bail!("{}", message["params"]["error"]),
                Some("tidefi_markets") => match &quote {
                    Some(quote) => ws.send(Message::text(quote.clone())).await?,
                    None => {
                        println!("tidefi_markets {}", message["params"]);
                        ws.send(Message::text(subscribe.clone())).await?;
                    }
                },
                Some(method) => println!("{method} {}", message["params"]),
                // The quote reply carries `result` or `error` instead of a method.
                None => {
                    println!("{message}");
                    return Ok(());
                }
            }
        }
    }
}

Stream the books:

cargo run --example client -- --url <Taker API endpoint> --auth-token <token>

Request one quote:

cargo run --example client -- \
    --url <Taker API endpoint> \
    --auth-token <token> \
    --quote-token-in 0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 \
    --quote-token-out 0x4200000000000000000000000000000000000006 \
    --quote-amount-in 50 \
    --quote-caller <your router address> \
    --quote-priority-fee-ratio 0.1

Get access

Taker API access is by arrangement. Contact us on Telegram to discuss.