Docs · Tools
Paying is minting.
The SwapsPro Pass is a lifetime Pro licence with no account and no database behind it: a soulbound ERC-721 (ERC-5192) on Base. Your wallet holding the pass is the membership, so there is no session to expire, no server to be down and nothing to log in to. This page is the machinery — Free and Pro is the capability table.
What you are buying#
| SwapsPro Pass | |
|---|---|
| Contract | 0x568fcbade475c1f20ed9a5155814eeeadf9c69ef |
| Chain | Base (8453). One chain, because the check is a read and reads are cheap where the pass lives. |
| Standard | ERC-721 plus ERC-5192 (soulbound). It cannot be transferred or sold, ever. |
| Price | 0.01 ETH once, or 40 USDC over x402. The page reads priceWei() live and falls back to that constant only when the chain cannot be reached. |
| Supply | 100, enforced by the contract, and printed into the artwork in bytes nobody can edit. |
| Per wallet | One. A second mint reverts. |
| Artwork | A full SVG in the bytecode. There is no server behind the image and no URI to rot. |
| Where the money goes | Forwarded to the payout split inside the mint transaction — 0xAccF0dB4b6B55Ba692467988D0a1188f26428C2b. The contract never holds ETH. |
The contract address is config in code, not an environment variable. It is neither secret nor variable: it cannot move, its payout, voucher signer and owner are all immutable in the bytecode, and there is no upgrade path. An env var still wins where one is set — that is what makes a fork or a testnet deploy possible — but the committed value is the floor, and an empty variable can never blank it. That precise bug had already shipped once: the contract was live, a pass was minted, and the x402 endpoint still answered 503 saying it was not deployed.
Paying is minting#
There is no checkout and no confirmation email. /pro sends one transaction: mint() with the price as its value. It switches your wallet to Base first, waits for the receipt, and treats a revert as a revert rather than as a success — no funds move beyond gas.
- The serial you would get is read, not assumed. The page calls
totalMinted()and shows the next number. When that read fails it says “the next in the series” and draws?into the artwork. It used to answer#1for an unread supply, which is a factual claim about what somebody is buying at the moment they decide to buy — and “we do not know the supply” and “you are the first” are opposite statements, only one of them flattering. - What you see before minting is what the contract emits. The preview renders the same artwork string the bytecode produces, not a mockup and not a screenshot.
- After minting, the badge is read back from
tokenURI. A holder is shown their real token, decoded from the data URI, rather than the preview.
Lost wallet
The first 100 pay no SwapsPro fee#
Not a discount — zero, on every path where a fee can actually be dropped. That is why the check reads tokenOf(holder) rather than balanceOf: the pass number is itself the benefit, and one call answers both questions. Token ids start at 1 and tokenOf returns 0 for a non-holder, so the qualifying range is the closed interval [1, 100] and 0 can never qualify.
The limit equals the supply cap on purpose. A larger number would be harmless arithmetic today — every id in the series is under it — and wrong everywhere it is read aloud, and it would quietly hand a second series free passes by accident, since a new contract’s ids also start at 1.
10 paths are waivable and 4 are not; both lists live in fees.ts, are asserted by its tests, and are rendered in full on Free and Pro. A waiver that silently fails on some routes is worse than a documented exception.
Beyond fees, a pass raises the partner fee ceiling from 100 to 200 bps — held by the payout address, not by whoever is browsing. It does not change SwapsPro’s own share either way.
Fails closed on the money, open on the truth#
The check is a public-RPC read, cached in localStorage for 1 hour, and it has three outcomes rather than two: a pass, no pass, and we could not tell.
| Outcome | What the fee does | What the interface says |
|---|---|---|
| A pass, in range | Waived | Pro, with the pass number |
| No pass | Charged | Not Pro |
| The read failed | Charged — the safe direction, and the server re-derives it from chain anyway | “Could not check”, never “no pass” |
Those two directions are deliberately different. Charging when we cannot verify costs a holder one quote; the alternative would waive a fee for anyone whose check we could not complete, which is exactly the population that would arrange for it not to complete. But the badge must not quietly downgrade a holder to non-Pro — a read failure wearing the shape of an answer is the bug this codebase spent a night removing everywhere else, so verifyFailed exists as its own state.
- Nothing trusts a client-supplied boolean. The server-stamped 0x path re-reads
tokenOf(taker)on Base itself before it decides a fee. - A per-call RPC error decodes to 0, not to a pass, and an implausibly large token id is treated as no pass rather than as a number.
- Several wallets, lowest id wins — it is the one most likely inside the waiver range, and you own them all either way.
- The cache is an optimisation, never a requirement. Storage that is full or blocked is swallowed; a stale or unreadable entry is re-read rather than mis-read.
Anyone can run the same check we do:
the whole membership check
# Does an address hold a pass? tokenOf, not balanceOf — the NUMBER is a benefit.
# Returns 0 for a non-holder, so one call answers "is this Pro?" and "which pass?".
cast call 0x568fcbade475c1f20ed9a5155814eeeadf9c69ef "tokenOf(address)(uint256)" $ADDRESS \
--rpc-url https://mainnet.base.orgAgents buy it over x402#
/api/pro/x402 sells a pass for 40 USDC on Base over HTTP, so an autonomous agent can become Pro with no browser, no account and no API key. It exists because every other membership purchase on the internet assumes a human at a form.
pay, then mint it yourself
# 1. Ask, with no payment attached. The answer is HTTP 402 and the terms —
# x402 v1 in the JSON body, x402 v2 base64'd in the PAYMENT-REQUIRED header.
curl -i https://www.swaps.pro/api/pro/x402
# 2. Retry with the signed payment. v2 clients send PAYMENT-SIGNATURE,
# v1 clients send X-PAYMENT; the endpoint speaks both at once.
# The 200 carries an EIP-712 mint voucher for the address that PAID.
# → { "voucher": { "to", "deadline", "signature" }, "contract", "chainId": 8453, … }
# 3. Submit it yourself, paying your own gas.
cast send 0x568fcbade475c1f20ed9a5155814eeeadf9c69ef \
"mintWithVoucher(address,uint256,bytes)" $TO $DEADLINE $SIGNATURE \
--rpc-url https://mainnet.base.org| Detail | |
|---|---|
| Asset | Native USDC on Base — 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, 40 USDC exactly. |
| Wire versions | Both. The 402 carries v1 requirements in the JSON body and v2 in the PAYMENT-REQUIRED header, and either payment header is accepted. |
| Settlement | Verified AND settled through a standard x402 facilitator before anything is signed. The USDC lands in the same split the ETH mint pays. |
| Voucher | An EIP-712 message naming the payer's address, valid for 1 hour. |
| Who submits it | Anyone — the pass always mints to `to`. You pay only the gas. |
| Replay | Harmless. One pass per wallet is enforced on-chain, so a reused voucher simply reverts. |
| Payment window | 300 seconds. |
The voucher is signed for the facilitator-confirmed payer, not for an address in the request — an agent cannot pay for one wallet and mint to another. If settlement succeeds but the payer cannot be determined, the endpoint answers 500 rather than signing anything.
The one thing that can turn this off
The route needs a voucher signing key on the deployment. Without it the endpoint answers 503 with a Retry-After and points at /pro, because a mint voucher cannot be signed — and it says that rather than claiming the pass is undeployed, which is a different fact with a different fix. On production today it answers 402 with terms.
Anti-downgrade#
A v2 payload carries the requirements the client agreed to, and they are compared against ours before a facilitator round-trip is spent. A payment for the right asset in the wrong amount, or to the wrong recipient, is refused with a fresh 402 rather than settled and argued about afterwards.
What the pass is not#
- Not a gate. Every chain, wallet, tool, the SDK, the API and the widget are free for everyone, permanently. Pro changes what things cost, not what exists.
- Not transferable. ERC-5192 locked. There is no secondary market and no rental.
- Not a licence server. Where a pass removes the widget badge, an address is public, so the check proves a pass exists rather than that the embedder owns it. It is an honesty lock, and the widget page says so in the same words.
- Not everything on the sales page. Early access to advanced tooling and priority support are commitments from the team; there is no feature flag behind either.
Related#
- Free and Pro — the capability table, and every path the waiver does and does not reach.
- Partners — where a pass raises a ceiling instead of waiving a fee.
- AI agents — the rest of what an agent can do here without a key.
- Fees — one table, every path.
