Two seas, one lock.
Tidewell launches every token on Solana and Base a few seconds apart. The two markets price it on their own, so the same token ends up with two prices. We draw each price as a sea, put a lock between them, and let the higher side spill into the lower one.
What you are looking at
The home page is one scene. On the left is the Solana sea, on the right the Base sea, and the concrete column in the middle is the lock. Every glass buoy is a token. A buoy floating on both surfaces is trading on both chains. A buoy hanging on a tether is still flooding. A buoy lying sealed on the floor sits in the dry dock and has no price yet.
The water height on each side follows the selected token’s price on that chain. When the gap is big enough, the sluice opens and water arcs out of the lock into the lower sea. The giant number at the bottom of the hero is that gap.
Why two seas
Two chains never agree on a price for long. Bridges are slow, liquidity is split, and buyers on one side do not see the other. The gap is real money sitting in plain sight, and Tidewell is built to make it visible in one glance and claimable in one click.
Quick start
tidewell.js is a single ES module with no dependencies. Import it straight from the site, pass two addresses, and you get both prices, the gap and the spill back.
Read a tide
import { readTide } from "https://tidewellpad.vercel.app/lib/tidewell.js"; const tide = await readTide({ solana: "So11111111111111111111111111111111111111112", base: "0x4200000000000000000000000000000000000006" }); console.log(tide.stage, tide.gap.pct.toFixed(2), tide.spill.from, "→", tide.spill.to);
# the same data tidewell.js reads, one sea at a time
curl -s https://api.dexscreener.com/tokens/v1/solana/So11111111111111111111111111111111111111112
curl -s https://api.dexscreener.com/tokens/v1/base/0x4200000000000000000000000000000000000006
<script type="module"> import { watch, formatPrice } from "https://tidewellpad.vercel.app/lib/tidewell.js"; watch({ solana: "<mint>", base: "<0x address>" }, (tide, err) => { if (err || !tide.gap) return; document.querySelector("#gap").textContent = "+" + tide.gap.pct.toFixed(2) + " %"; }); </script> <b id="gap">…</b>
The example pair above is wrapped SOL against wrapped ETH, so the gap is huge on purpose. Use the two addresses of one token on two chains and the number becomes the spread you can actually close.
Only the math
No network needed if you already have the prices.
import { gap, spill } from "https://tidewellpad.vercel.app/lib/tidewell.js"; gap(0.0000131, 0.0000041).pct; // 219.51 spill(0.0000131, 0.0000041, { names: ["solana", "base"] }); // { open: true, from: "solana", to: "base", grossPct: 219.51, netPct: 217.6, rate: 0.65 }
Two seas
One token, two markets, two prices. Everything else on Tidewell is a picture of that one fact.
The launch
A creator registers a token once. Tidewell gives it the same name and supply on both chains and opens trading on each sea when the creator says so, one sea first or both at once. From that moment the two markets drift apart, because the buyers on each side are different people with different wallets.
The lock
The lock is the column between the seas. It never lets the water mix on its own. The only way water crosses is through the sluice, and the sluice only opens when the gap is wider than the fees it costs to close it.
| Piece | On the page | Meaning |
|---|---|---|
| Solana sea | Left, indigo | The token’s best USD price on Solana |
| Base sea | Right, cobalt | The token’s best USD price on Base |
| Lock | Concrete column with two lamps | Keeps the prices apart until someone closes the gap |
| Sluice | Amber outlet with a pour | The direction and size of the gap |
| Buoy | Glass sphere with a ticker | One token, drawn once per sea it trades on |
Water levels
The pricier sea stands taller against the lock. Height follows the ratio between the two prices on a log scale, so a 30 % gap and a 3000 % gap both stay on screen.
The formula
ratio = max(a, b) / min(a, b)
h = min(1, ln(ratio) / ln(maxRise)) // maxRise = 6
high sea → +h, low sea → −h, equal → 0With maxRise = 6 a token whose prices differ by +500 % or more draws at full height. On the home page full height is about six percent of the screen on each side, so the two surfaces never swallow the hero.
| Gap | Ratio | Height h |
|---|---|---|
| +1 % | 1.01 | 0.006 |
| +30 % | 1.30 | 0.146 |
| +100 % | 2.00 | 0.387 |
| +219.5 % | 3.20 | 0.649 |
| +500 % and up | 6.00+ | 1.000 |
The spill
The spill is the gap, measured against the lower sea, minus what it costs to cross. If it is worth crossing, the sluice opens and pours from high to low.
Gross and net
gross = ratio − 1
net = ratio × (1 − fee)² − 1 // fee = 0.003 per leg
open = gross ≥ 0.01 and net > 0The fee is paid twice: once to buy on the low sea, once to sell on the high sea. Below a 1 % gap the sluice stays shut and the scene shows two calm surfaces.
Direction
Water always pours from the high sea into the low one. The label above the sluice says where it goes, for example SLUICE → SOLANA, and the line under the big number repeats it as spills → Solana.
Claiming it
On the page, a click on the amber sluice claims the spill for a second: the pour bursts and the toast shows the gap you closed. On chain the same move is a buy on the low sea and a sell on the high sea, which pulls both prices toward each other.
Stages
Depth is state. The deeper a buoy sits, the earlier it is in its life. The gauge on the right edge of the home page reads the same three stages top to bottom.
| Stage | Where | Rule |
|---|---|---|
open | Floating on both surfaces | Trading on both seas, has a gap |
flooding | Mid-water, on a tether | One or both seas armed, not trading on both yet |
sealed | Dry dock on the sea floor | Registered, nothing armed, no price |
The dry dock
Every token starts in the dry dock as a sealed buoy with a line across it. The dock is the dashed chamber at the bottom of the Solana sea, next to the lock. The counter above it shows how many are waiting.
Rising
When the creator floods the chamber, the buoy lifts off the floor and climbs on a tether. If only one sea is armed it rises on that side only. When it breaks the surface on both sides, two ripples spread and the token turns open.
import { stage } from "https://tidewellpad.vercel.app/lib/tidewell.js"; stage({}); // "sealed" stage({ armed: { solana: true } }); // "flooding" stage({ live: { solana: true, base: true } }); // "open"
Reading the scene
Everything on the home page is clickable water. Here is what each click does.
| You click | What happens |
|---|---|
| A buoy | It becomes the selected token. Both seas move to its prices, the big number shows its gap, and a card pops up with both prices and the direction. |
| The amber sluice | Claims the spill. The pour bursts and the toast shows the gap. |
| Open water | Drops a new sealed buoy into the dry dock with a fresh ticker. It floods on its own a little later. |
| A gauge label | Dives to that depth: Open water, Flooding or Dry dock. |
The ticker
The strip at the top lists every token that is open or flooding with both prices and the gap. It updates every second with the scene.
The gauge
The right edge shows how deep you are. Scrolling is diving: the camera moves down through the water column from the surface to the floor, and the amber reading shows the depth in metres.
tidewell.js
A small ES module that reads the tide between two chains. It is the same math the home page draws with, plus a reader for live prices from DexScreener.
Get it
import tide from "https://tidewellpad.vercel.app/lib/tidewell.js"; console.log(tide.VERSION); // "0.3.1"
curl -sO https://tidewellpad.vercel.app/lib/tidewell.js
<script type="module" src="https://tidewellpad.vercel.app/lib/tidewell.js"></script>
What is inside
- Math:
gap,levels,spill,stage,formatPrice. Pure functions, no network. - Reader:
price,readTide,watch. Fetch from the public DexScreener API, which allows browser requests. - Constants:
SEAS,LOCK,VERSION, and theTideErrorclass.
About 5 KB, no build step, works in any modern browser, Deno, Bun and Node 18+ (save it as .mjs for Node).
API reference
Every export, what it takes and what it returns.
gap(a, b)
Returns { pct, ratio, high, level }. pct is measured against the lower price. high is "a", "b" or null. level is true when the gap is under 1 %.
levels(a, b, { maxRise })
Returns { a, b, gap } where a and b are heights in −1…1. See Water levels.
spill(a, b, { fee, minSpill, names })
Returns { open, from, to, grossPct, netPct, rate }. names labels the two sides, for example ["solana","base"]. rate is 0…1 and drives how hard the sluice pours.
stage({ armed, live })
Returns "sealed", "flooding" or "open".
price(chain, address, { fetch, api })
Async. Returns { chain, address, symbol, usd, liquidity, pair, url } for the most liquid pair where the token is the base side. Throws TideError.
readTide({ solana, base }, opts)
Async. Reads both seas in parallel and returns a tide object. A sea with no pair yet comes back as null, and the stage reads flooding.
watch(addrs, onTide, ms)
Polls readTide every ms (never faster than 5 s, default 15 s) and calls onTide(tide, err). Returns a stop() function.
const stop = watch({ solana: mint, base: addr }, (tide, err) => { if (err) return console.warn(err.code, err.message); if (tide.spill?.open) console.log("spill", tide.spill.netPct.toFixed(2), "% →", tide.spill.to); }); // later stop();
Try it
This block imports tidewell.js from this site and runs it in your browser. Type two prices and watch the lock, or read a live tide from two addresses.
From two prices
From two addresses
Spec & formats
Constants and the shape of a tide object.
Constants
| Name | Value | What it does |
|---|---|---|
LOCK.minSpill | 0.01 | Gaps under 1 % keep the sluice shut |
LOCK.fee | 0.003 | Swap fee per leg, paid twice |
LOCK.maxRise | 6 | Ratio that draws at full height |
LOCK.pollMs | 15000 | Default interval for watch |
LOCK.api | …/tokens/v1 | DexScreener endpoint base |
SEAS | solana, base | Known chains, keyed by DexScreener chainId |
Tide object
{
"seas": {
"solana": { "symbol": "TIDE", "usd": 0.0000131, "liquidity": 41200, "pair": "…", "url": "…" },
"base": { "symbol": "TIDE", "usd": 0.0000041, "liquidity": 18800, "pair": "…", "url": "…" }
},
"stage": "open",
"gap": { "pct": 219.51, "ratio": 3.195, "high": "a", "level": false },
"spill": { "open": true, "from": "solana", "to": "base", "grossPct": 219.51, "netPct": 217.6, "rate": 0.648 },
"levels": { "solana": 0.649, "base": -0.649 },
"at": "2026-10-29T18:22:45.594Z"
}Errors
Everything tidewell.js throws is a TideError with a short code.
| Code | When | What to do |
|---|---|---|
E_PRICE | A price is zero, negative or not a number | Check the input, prices must be positive |
E_SEA | Unknown chain name | Use solana or base |
E_ADDR | Missing address | Pass at least one of { solana, base } |
E_DRY | No priced pair on that chain yet | readTide turns this into null and stage flooding |
E_RATE | DexScreener answered 429 | Poll slower, 15 s is plenty |
E_HTTP | Any other non-OK answer | Retry later |
Limits
A gap on screen is not money in your wallet yet. Here is what stands between them.
Time
The two legs land on two chains with different block times. If you move funds across a bridge between legs, the gap can be gone when you arrive. Holding inventory on both seas is the only way to close a spill in one move.
Depth
A thin pool moves the moment you trade in it. The liquidity field in each sea tells you how deep it is; a 300 % gap in a pool with a few hundred dollars is a puddle, not a sea.
Look-alikes
Anyone can deploy a token with the same name on another chain. Always compare addresses, never tickers. tidewell.js matches pairs by the exact token address and ignores everything else.
Data
Prices come from DexScreener and can lag a few seconds behind the chain. The home page shows a simulated tide until the $TIDE contract is set.
$TIDE
The token of the lock itself. It is the big amber buoy on the home page, the one selected when you arrive.
| Ticker | $TIDE |
| Contract | soon |
| Buy | Buy $TIDE |
| Chart | Open the chart |
| X | Tidewell on X |
The buttons go live the moment the contract is published. Until then they stay where they are and wait, like everything else in the dry dock.
FAQ
Why is the number so big?
Because fresh tokens on two chains really do sit that far apart for the first minutes. The gap shrinks as people close it. Small gaps mean the lock is doing its job.
Which way does the water go?
From the pricier sea into the cheaper one. The label on the sluice and the line under the big number both say it.
Can I use tidewell.js for any token?
Yes, if it trades on Solana and Base and DexScreener sees the pairs. Pass its two addresses to readTide.
Does clicking the water launch a real token?
No. On the home page a click drops a sealed buoy into the dry dock so you can watch the life cycle. Real launches go through the creator flow.
Where is the contract?
On the $TIDE page and in the chip at the top of the home page, as soon as it is published.