01Introduction#
Passavant is a cross-chain OTC desk. You post one fixed price for a whole bag of tokens on one chain and pick the chain where you want to be paid in USDC. Another wallet takes the whole pass, and the Passavant Officer wallet settles both sides.
A passavant is an old customs paper: it lets goods cross a border in transit without stopping at every toll. Here the paper is a short block of plain text your wallet signs, and the toll you skip is the pool. A big bag sold through a pool walks the price down with every swap. A pass is sold at the price written on it.
02Quick start#
- Open Make offer and search the token. Search reads DexScreener on Solana, Ethereum, BNB Chain and Base.
- Set the amount and one price per token. The page shows the live market price and how far your price sits from it.
- Pick the chain where the money lands and how long the pass stays open.
- Sign the plain text. The pass shows up in the book right away.
To take a pass instead, open Markets, press Buy or Sell on a row, read the terms and sign. You watch the Officer run all four legs and stamp the result.
Everything the desk does is also in a small module you can import from this site:
import * as P from "https://passavant.vercel.app/lib/passavant.js"; const pass = P.createPass({ side: "sell", tokenSymbol: "BONK", tokenChain: "solana", tokenAddress: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", amount: 50_000_000, price: 0.00002, payChain: "base", expiry: "3d" }); console.log(pass.id); // PV-SOL-BASE-… console.log(P.terms(pass)); // the exact text your wallet signs console.log(P.legs(pass)); // the four transfers the Officer runs
# The market price the desk compares every pass against curl -s https://api.dexscreener.com/tokens/v1/solana/DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263 \ | jq 'max_by(.liquidity.usd) | {symbol: .baseToken.symbol, priceUsd, liquidity: .liquidity.usd}' # The library itself, plain JavaScript, no build curl -s https://passavant.vercel.app/lib/passavant.js | head -20
<script type="module"> import { quote, fmtUsd } from "https://passavant.vercel.app/lib/passavant.js"; const q = quote(10000, 1.25); document.body.textContent = "Seller gets " + fmtUsd(q.sellerGets); // Seller gets $12,438 </script>
03Dry run#
The desk is open in dry run. Searching, live market prices, the book, plain-text terms and wallet signatures are all real. Settlement runs every check and every leg in your browser, but the Officer sends nothing on chain. Every deal in the ledger says so.
| Part | In dry run |
|---|---|
| Token search and market price | Live, DexScreener |
| Wallet connect | Live, reads your address only |
| Signing terms | Live, Phantom signMessage or EIP-191 personal_sign |
| The book | Sample passes repriced against the live market every hour, plus the passes you file from this browser |
| Settlement legs | Simulated by settle(), each leg gets a dry-… id instead of a transaction hash |
04How a deal works#
File MAKER
- 1Connect a walletYour address is your account
- 2Set the termsToken, amount, one price, USDC chain, expiry
- 3SignPlain text, nothing moves yet
Take TAKER
- 4Find the passEvery open pass sits in one book
- 5Take it wholeSign the other side, the pass is Accepted
Stamp OFFICER
- 6Collect both sidesToken and USDC move to the Officer
- 7Pay outToken to the buyer, USDC minus 0.5% to the seller
On an Ask the maker sells the token and the taker pays USDC. On a Bid the maker pays USDC and the taker brings the token. A pass is all or nothing: there are no partial fills.
05Settlement#
When a pass is taken, the Officer runs four transfers and pays nobody until both sides are in.
| # | Leg | Chain |
|---|---|---|
| 1 | Token seller → Officer, the whole bag | token chain |
| 2 | USDC payer → Officer, the deal total | pay chain |
| 3 | Officer → USDC payer, the whole bag | token chain |
| 4 | Officer → token seller, total minus 0.5% | pay chain |
If leg 2 does not arrive, leg 1 is refunded and the deal ends as failed. In the book you can see this happen: tick “Dry run a bounce” before you sign a take.
06What you sign#
Filing, taking and cancelling each start with your wallet signing a short block of plain text. A signature cannot move funds. It proves the terms came from your wallet, and a random nonce ties it to one pass only.
Solana wallets sign the bytes with signMessage (ed25519). EVM wallets sign with personal_sign (EIP-191). Without a wallet you can still file and take as a guest, which only works in dry run.
08Chains and routes#
A route is the token's chain plus the chain where USDC is paid. Every pair of the four chains is open, same chain included.
Dollar contracts
The only dollar a deal accepts on each chain:
09Fees#
0.5% of the deal total, taken from the USDC paid to the seller, and only when a deal clears.
| Deal total | 10,000 USDC |
| Buyer pays | 10,000 USDC |
| Fee, 0.5% | 50 USDC |
| Seller gets | 9,950 USDC |
| Filing, cancelling, expiring | Free |
| Smallest deal | $25 |
10Order states#
| State | Meaning |
|---|---|
open | Filed, anyone can take it |
expired | Nobody took it before the deadline |
cancelled | The maker withdrew it |
accepted | Taken, settlement is about to start |
pending | The Officer is collecting and paying |
failed | A side did not arrive, anything collected went back |
completed | Both sides paid out |
canMove(from, to) in the library holds this exact graph, and move() throws BAD_STATE on anything else.
11Markets#
One book for every route. Filter by token or address, by chain (a pass matches if either side is on it), by minimum size, and by Asks or Bids. Vs market compares the pass price to the live DexScreener price of the deepest pool: green means a good take for you, red means you pay over the odds.
Recent clearances and the route list sit under the book. A row marked YOURS is a pass you filed from this browser, and the desk will not let you take it.
12Make offer#
Three steps: token, size and price, where you get paid. The pass on the right fills in as you type: what you give, what you get after the fee, its number and expiry. The address field fills from your connected wallet for that chain and checks the format before it lets you sign.
13Deals#
The Officer's ledger: every taken pass with its state, total and fee. Open a row to see the legs and their dry run ids. Sample rows come from the desk's own dry run book.
14My passes#
Your connected addresses, the passes you filed with Cancel (one more signature), and the deals you took.
15passavant.js#
The rules of the desk live in one file with no dependencies: /lib/passavant.js. Every page of this site imports it, so what the docs say is what the desk does. It runs in browsers and in Node 18+.
| Constant | Value |
|---|---|
FEE_BPS | 50 |
MIN_TOTAL | 25 |
EXPIRY | { "1h": 3600, "1d": 86400, "3d": 259200, "7d": 604800 } |
CHAIN_IDS | ["solana", "ethereum", "bnb", "base"] |
STATES | the seven states above |
16API reference#
| Function | Returns |
|---|---|
createPass(o) | A pass with id, total, fee, expiresAt, nonce, state: "open". Throws PassError. |
quote(amount, price) | { total, fee, sellerGets, buyerPays } |
terms(pass, action) | The plain text to sign, action is create, take or cancel |
parseTerms(text) | The fields back from that text |
legs(pass) | The four transfers, in order |
settle(pass, { onStep, failAt, delay }) | Promise of the final pass, dry run |
route(tokenChain, payChain) | { open, label, dollar, dollarAddress } |
vsMarket(price, market) | Percent above or below market |
marketPrice(chain, address) | Live price from the deepest DexScreener pool |
searchTokens(q, chain?) | Up to 12 tokens on the four chains, deepest first |
isAddress(chain, a) | Format check, base58 or 0x |
canMove(from, to), move(pass, to) | The state machine |
import { createPass, settle } from "https://passavant.vercel.app/lib/passavant.js"; const p = createPass({ side: "sell", tokenSymbol: "WIF", tokenChain: "solana", tokenAddress: "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm", amount: 2000, price: 1.1, payChain: "ethereum" }); const done = await settle(p, { failAt: 2, // the USDC never shows up onStep: e => console.log(e.state, e.leg?.kind ?? "") }); console.log(done.state); // "failed", leg 1 refunded
17Try it#
This block imports the same file and runs it here. Change a field and the pass, its text and its legs update. Press settle to watch a dry run.
18Pass format#
A pass number is PV-<token chain>-<pay chain>-<7 chars>. The tail is an FNV-1a hash of side, token, amount, price, pay chain and nonce in base 36, so the same terms with a new nonce get a new number.
| Field | Type | Notes |
|---|---|---|
side | "sell" | "buy" | From the maker's side |
tokenAddress | string | Mint on Solana, 0x contract elsewhere |
amount, price | number | Whole tokens, USD per token |
payChain | chain id | Where the dollars move |
makerReceive | string | Where the maker is paid, defaults to the maker |
expiresAt | ms | createdAt + EXPIRY[expiry] × 1000 |
nonce | 16 hex | Random, from crypto.getRandomValues |
19Errors#
| Code | When |
|---|---|
MISSING_FIELD | A required field is empty |
BAD_SIDE | side is not sell or buy |
ROUTE_CLOSED | A chain is not one of the four |
TOO_SMALL | Total under $25 |
BAD_EXPIRY | Expiry is not 1h, 1d, 3d or 7d |
BAD_STATE | A state change the graph does not allow |
BAD_TERMS | Text that does not start with PASSAVANT: … pass |
20Risks and trust#
- The Officer is trusted. It holds both sides for a moment and pays them out. That is the whole design, and it is why the desk can cross chains without a bridge.
- A pass does not lock funds. Balances and approvals are checked again at settlement, which is why a side can fail to arrive.
- The maker sets the price. Every row shows how far it sits from the market. Check it before you take one.
- Look-alike tokens exist. Match the address, not the symbol. Search shows both.
21Limits#
Whole bags only, four chains, USDC as the only dollar, $25 minimum, seven days maximum. Market prices come from the deepest DexScreener pool and can be missing for fresh tokens, in which case the book shows n/a.
22$PASSAVANT#
The desk's own token. Its contract is published here and on the main page the moment it exists.
| Ticker | $PASSAVANT |
| Chain | Solana |
| Contract | soon |
| Links | Buy · Chart · X |
23FAQ#
Do I need an account?
No. Your wallet address is your account. Without a wallet you get a guest id on this browser, which works in dry run.
Can someone take only part of my pass?
No. A pass is taken whole or not at all.
Can I cancel?
Any time while it is open, in My passes. One signature, no fee.
What if the other side does not pay?
The Officer never pays anyone before both sides are in. If the second side fails, the first goes back and the deal is marked failed.
Can I take my own pass?
No. The desk refuses a take from the wallet that filed it.
Where does the market price come from?
DexScreener, the pool with the most liquidity for that token on its chain.
Is there a minimum size?
$25 total. Below that the Officer does not get out of the chair.