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
| Chain | TideFi |
|---|---|
| Base | 0x71Def100007A540305DD65d1034D10e809679FD5 |
| Robinhood | 0xBAd60cDb3FC16Ef516939639847d552f7ddA45Ae |
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);
| Parameter | Type | Description |
|---|---|---|
tokenIn | address | Input token |
tokenOut | address | Output token |
maxAmountIn | uint256 | Cap on the amount of input token spent |
limitPrice | UFixed18 | Max 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);
| Parameter | Type | Description |
|---|---|---|
tokenIn | address | Input token |
tokenOut | address | Output token |
maxAmountIn | uint256 | Cap on the amount of input token spent |
minAmountOut | uint256 | Minimum output: reverts with TideFiInsufficientAmountOut if not met |
limitPrice | UFixed18 | Max tokenIn paid per tokenOut, 18-decimal fixed point over raw token amounts (no decimals adjustment). Pass type(uint256).max for no limit |
to | address | Recipient of the output tokens |
callbackData | bytes | Forwarded 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
- Call
quoteto preview amounts for your limits and limit price. - Approve
tokenInon the TideFi contract. - Call
swapwith the same limits, aminAmountOutderived from the quote, a recipient address, and optionalcallbackData.
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}
]
}
| Field | Type | Description |
|---|---|---|
server_time_ms | int | Server clock, unix ms. |
chain_id | int | Chain the deployment lives on (8453 is Base). |
tidefi | address | TideFi contract to call quote and swap on. |
engine | address | Trading engine behind it. |
quote_asset_symbol | string | Asset all books are quoted in. |
quote_asset_decimals | int | Decimals of the quote asset. |
assets | array | Quotable tokens, see below. |
Each entry of assets:
| Field | Type | Description |
|---|---|---|
symbol | string | Token symbol. |
token_address | address | Token contract. |
token_decimals | int | Token decimals. |
A swap crosses a pair of orderbooks, one per token.
Conventions
- Amounts and quantities: whole token units (
float).*_subunitsfields are exact decimal strings of the raw token amounts. - Prices: quote asset per whole token (e.g. USD per ETH).
- Timestamps: unix milliseconds (
*_ms). landed: whether the book is already on-chain.falsemeans the quote is against a book still in flight (expected while makers stream updates).
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
}}
| Field | Type | Description |
|---|---|---|
token_in | address | Token you pay. |
token_out | address | Token you receive. |
amount_in | float | Amount paid, whole tokens. |
caller | address | Required. The address that will call TideFi.swap: your router, not the end user. |
priority_fee_ratio | float | Required. 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}
| Field | Type | Description |
|---|---|---|
token_in | address | Echoed. |
token_out | address | Echoed. |
amount_in | float | Amount paid, whole tokens. |
amount_out | float | Amount received, whole tokens. |
amount_in_subunits | string | Exact raw amount paid. |
amount_out_subunits | string | Exact raw amount received. Derive minAmountOut from it. |
price_out_per_in | float | amount_out / amount_in. |
book_timestamp_ms | int | Timestamp of the book the quote was computed on, unix ms. |
landed | bool | Whether 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
}
| Field | Type | Description |
|---|---|---|
symbol | string | Token symbol, as in assets. |
base_token | address | Token of the book. |
timestamp_ms | int | Timestamp of this book version, unix ms. |
landed | bool | Whether this book version is already on-chain. |
mid_price | float | Reference mid price, quote asset per whole token. |
bids | array | Bid levels best to worst, each {price, quantity}: price in quote asset per whole token, quantity in whole tokens. |
asks | array | Ask levels best to worst, same shape. |
stale_timestamp_ms | int | Expiry 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 "e {
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.