Overview
Where it stands
Three surfaces behind one front door at sooon.fun. The rail
colour throughout this document is not decoration — purple marks work in
the client and UX layer, green marks work that touches the chain.
/app/#markets
Predict
Prediction markets — the original product, now one of three doors rather than the whole site.
/wallet/
SOOON Wallet
Self-custodial Tari wallet in the browser. Holds the keys, signs every transaction, and doubles as the account system.
/app/#tokens
Pump
Token launcher. Real bonding curves on-chain, automatic graduation into an AMM pool, and an optional stealth mode.
A token can now be created, bought, sold, bonded, graduated into a
liquidity pool, and traded against that pool — every step as a real
transaction on Esmeralda, with the price chart, trade feed and holder
list all reading back from chain state rather than a database guess.
Newest · August 21
The terminal is the token page
Every token now opens into a full trading terminal at
/terminal/ — banner identity, live chart, trade panel,
comments, holders and the trade feed on one page. The whole surface is
true OLED black; the brand purple survives only as hairline borders.
The candle chart became a real trading chart. Drag to pan through
history, pinch or scroll to zoom around your fingers, and the price and
volume axes rescale to the range you are looking at. Panning away from
now breaks follow mode and a Latest pill snaps you back. Candles
poll the chain every 15 seconds, so anyone's buy or sell lands on the
chart without a reload — and the refresh never yanks a view you have
panned somewhere on purpose.
Stealth tokens keep their own chart: the SOOON silhouette that fills as
the curve raises, reading straight from the curve component on-chain —
the database is never trusted for a number the chain can answer.
Trading happens on the page when it is open inside the wallet. Buys and
sells sign through the wallet's approval sheet; a stealth buy walks a
live checklist through every leg — the anonymous buyer, the routing, the
purchase, the cleanup — so a two-minute private flow never looks hung.
Your real balances sit above the trade panel (public tTARI, the token,
and anything held in stealth), and the percentage chips fill from what
you actually hold.
Two structural fixes under all of this. A stealth buy that is already
covered by your shielded balance now skips the public→private conversion
entirely — one fewer transaction, one fewer settle wait, and no revealed
shield event on-chain right before the hop. And holdings aggregate per
token: buying the same coin twice used to show two positions, because
each private buy deliberately uses a fresh throwaway account; the wallet
now shows one bag and a sell drains across the accounts underneath it.
Client
01
The wallet is the account
No signup form, no second login, no pasted public key.
Creating a wallet now creates the SOOON account in the same motion. The
account name is the public address. Authentication is a proof
derived from the wallet's private key — the key never leaves the browser,
and there is deliberately no path where pasting someone's public address
logs you in as them.
Restoring a wallet on a fresh browser re-authenticates against the same
account and adopts back the account component the chain knows about, so a
restored wallet shows its real balance instead of an empty one.
Also in this pass
- Every browser
confirm() and prompt() replaced with in-product UI — signing, backup, and forget-wallet all now use SOOON's own dialogs.
- A real signing step. Transactions used to auto-sign silently; there is now an approval sheet showing what is being signed.
- A settling state, because Tari confirmations lag the click and users assumed nothing had happened.
- Refresh alongside Send and Receive; both tTARI and token balances visible on mobile without clipping.
On-chain
02
Confidential balances
Shield, unshield, and receive private transfers.
Tari's native asset is stealth-capable by design: every vault carries a
public revealed_amount alongside confidential commitment
UTXOs. Shielding moves value from the revealed side into Pedersen
commitments with Bulletproof range proofs; unshielding reverses it.
Both directions are wired, plus a scanner that walks recent transactions
with the view key so an incoming shielded transfer from an
outside wallet actually lands in the private balance — previously it
arrived on-chain and stayed invisible.
On-chain
03
Bonding curves, and keeping them
A constant-product curve that graduates at a fixed raise.
Each token launches against a curve template holding k = V · T₀
with a virtual reserve of 3,000 tTARI against 1.073B supply. Buys walk the
curve up; sells walk it back. At 8,500 tTARI raised the
curve bonds and stops trading.
Testnet reality
Curve progress used to appear, then silently revert seconds later. The
cause was Esmeralda itself — sharded substate can be rolled back or wiped
between sessions, so the chain genuinely forgets. Curve state is now
version-gated in the client so a stale read can never overwrite a newer
one, and the progress bar holds with its confirmation glow.
On-chain
04
Graduation into a pool
Bonded curve → two-resource liquidity pool, automatically.
A watcher on the backend polls curve state and, on crossing the threshold,
collects the graduated reserve and seeds a real AMM pool using Tari's
system liquidity-pool template. From that point buys and sells route
through swap against the pool instead of the curve.
The frontend follows: bonded tokens get their own presentation, and the
price chart is fed from pool price rather than curve price — pool trades
used to leave the graph flat.
Why it took two attempts
Calling graduation from inside the template failed with substate not in
scope — a cross-template child frame does not inherit the parent's
resource scope. It now runs as an instruction-level two-transaction flow
instead.
Client
05
Charts, trades, holders
The token page rebuilt around real market data.
- Candlesticks via self-hosted TradingView Lightweight Charts (Apache-2.0 — no licence application, no cost), fed by a price sampler running every 30 seconds with selectable timeframes.
- Trade feed and holder list from an on-chain scanner that reads actual curve and pool transactions every 20 seconds.
- Page restructured — price, market cap, raised and holders moved up into the chart header; contract-address copy, X link and share moved into the space they left.
- Grid / rows toggle on the token browser, remembered between visits.
- Optional X.com link at creation, surfaced as an icon beside contract and share.
Privacy
06
Stealth launches
Two launch modes at creation: public, or stealth.
A stealth launch funds a throwaway identity from shielded tTARI and
creates the curve through it, so the creator's main address never appears
as the origin. The token trades publicly and normally — what is hidden is
who is behind it.
Buyer-side privacy
Buyers get the same treatment. A private buy spends from the shielded
balance through a fresh per-buy identity, so an on-chain observer cannot
link a position back to a wallet or link two positions to each other.
The buy panel carries an info control explaining the parameters in plain
language rather than leaving them unlabelled.
Against forensic sweeping
- Per-position key derivation, so positions do not share an identity.
- The platform sponsors the gas an anonymous identity needs — the classic linkability leak, where the throwaway has to be funded from somewhere traceable.
- An owner-rule fingerprint that distinguished stealth curves from public ones was found and removed in the v2.2 template.
Privacy
07
Making it affordable on mainnet
The throwaway-gas problem, measured rather than estimated.
Every stealth identity needs gas, and over-funding it strands real value in
an address the user cannot easily reach again. The original figures were
guesses and they were too high. Measuring actual burn showed roughly
0.6 tTARI consumed against ~9 parked.
Gas parameters before and after tuning
| Parameter | Before | After |
| Sponsor bootstrap | 6,000,000 | 1,000,000 |
| Buy overhead | 9,500,000 | 2,000,000 |
| Launch fee | 9,500,000 | 3,000,000 |
| Wallet gas reserve | 2,000,000 | 1,000,000 |
| Total per private buy | 29.63 tTARI | 22.37 tTARI |
| Left stranded | 8.99 tTARI | 1.49 tTARI |
Sponsorship is net-zero to the platform and the pattern is the same one
Tornado and RAILGUN use — the anonymity set is what makes it safe, not
secrecy about the mechanism.
Privacy
08
Getting back out, and getting back in
A sell path for private positions, and recovery that restores them.
Stealth positions could be opened but not closed. There is now a sell path
that works against both venues — curve for pre-graduation tokens, pool for
bonded ones — routing back through the position's own identity.
Found the hard way
Stealth keys were random and lived only in localStorage.
A cleared browser meant permanently unreachable positions — which
happened for real during testing. Keys are now derived deterministically
from the recovery phrase and mirrored to an encrypted, opaque vault, so
the recovery key alone restores every position.
One subtlety worth recording: a raw SHA-256 digest is not a valid Ristretto
scalar roughly fifteen times out of sixteen. Masking the high nibble
(h[31] &= 0x0f) took validity from 0/12 to 12/12 in testing.
Client
09
A front door
Three products needed somewhere to be chosen between.
sooon.fun now opens on a three-column OLED page: Predict on
the left, the wallet in the centre, Pump on the right, split by two
glowing dividers — SOOON purple on the left edge, SOOON green on the
right. It stacks to three rows under 760px. Routing was restructured
underneath it so /, /wallet/ and /app/
are each their own entry point, with a deploy script that preserves the
arrangement on future builds.
Test data was cleared at the same time — 11 tokens, 3 trades and 52,005
price samples removed, with the database backed up beforehand.
Client
10
One wallet, everywhere
Signing stopped depending on where the page is open.
Every signed action — buying, selling, paying a launch fee, a stealth
launch, a private buy — used to send its request to the page's parent
window. That is the wallet only when the site is running inside the
wallet's own browser. Anywhere else there was simply nothing listening, so
the launcher was effectively wallet-only without ever saying so.
The site now asks a wallet channel instead: the parent
window inside the wallet, or a wallet the site hosts in a frame on the open
web. The wallet already replied to whoever asked, so it needed no change.
All five signed actions work from an ordinary phone browser.
The distinction that caused it
Connecting and signing are not the same thing. Connecting hands the site
a public key and a login proof; signing needs the private key, which only
ever exists inside the wallet. A connected wallet on the open web was
signed in but had nothing able to sign — which is exactly how it behaved.
Client
11
Signing in without leaving the page
The wallet's own unlock screen, inline.
Tapping the network chip while signed out used to send people to another
page to make a wallet. Now, if a wallet already exists on the device, the
chip renders the wallet's own unlock screen in the modal. The PIN is
typed into the wallet's frame and the keys are derived there; only the
public key and the auth proof cross back. Nobody leaves the page they were
reading, and there is no second copy of the vault crypto to drift.
Made to survive hostile browsers
Testing across browsers showed the same flow failing two different ways: in
one, the wallet's reply never arrived at all; in another it arrived, but the
follow-up link call failed because unlocking had already registered
the account server-side. Both left the modal sitting open on a wallet that
had actually connected.
- The outcome is now confirmed against the server rather than inferred from the mechanism.
- An "already connected" error advances the UI instead of reporting failure.
- A poll runs while the frame is open, so a dropped message still completes — measured recovery, ~4s.
- A visible fallback to the full wallet page, for browsers that partition storage so the frame comes up blank.
On-chain
12
STari out of the launcher
The launcher is tTARI, end to end.
STari — the internal points balance — is being retired, and the launcher no
longer touches it. Percentages and Max come off the tTARI balance only;
trades are signed against the token's own contract; the launch fee is paid
in tTARI from the user's wallet. STari remains only in prediction markets
until those move too.
The part that mattered
The UI had already stopped offering STari trades on on-chain tokens, but
the API endpoint behind it had no such guard. A direct call
would have spent STari and written reserve, supply and holder counts
straight into the database for a token whose real state lives in its
contract — desyncing the two, and with a large enough amount tripping a
false graduation. The endpoint now refuses curve-backed tokens outright.
Client
13
Details that decide whether it feels finished
Small things, each of which read as broken.
- Every prompt is SOOON's own. The last browser-native
confirm() dialogs are gone — destructive actions get a red confirm, Escape cancels, and nothing renders in OS chrome any more.
- Refresh means the page, not the wallet. The in-wallet browser has its own refresh; a hard reload now returns to the page you were reading rather than the wallet home, since the wallet holds keys in memory and must re-lock.
- Buy is green, sell is red — tabs, action button and input outline, so direction reads before the numbers do.
- The network chip is a control again. On mobile it had collapsed to an unlabelled dot; it now shows a short state label while there is something to do, and shrinks back to the light once connected so the balances keep their room.
- Addresses stopped spilling. Wallet-created accounts are named by their public address — 118 characters — which the leaderboard printed in full. Long identifiers are shortened everywhere they surface; real handles are left alone.
- Token names stopped being unreadable on phones. In list view a fixed-width sparkline held its 80px whatever the screen size, squeezing the name column to 130px so "tari moon inu" rendered as "ta…". Below 430px the chart gives way to the name — the market cap beside it already carried the same signal.
- Back goes where you came from. Predict and Pump return to the start screen; inside the wallet, where there is no start screen, they fall back rather than stranding you.
On-chain
14
A backup you can write down
Wallets are 24 words now, and the words are Tari's.
A wallet used to be a random keypair whose only backup was 128 hexadecimal
characters — unwritable on paper without transcription errors, and useless
anywhere but here. It is now a 24-word Tari CipherSeed, the
same format Tari's own wallets use.
That turned out to be possible because Ootle account keys are not random at
all: the wallet daemon derives them from a seed, owner key from branch
account and view key from view_only_key at the same
index, through a domain-separated Blake2b-512. We were the outlier. Matching
the scheme means the same phrase opens the same wallet in Tari's own
software — and, if someone chooses, the same phrase can cover the
base layer too.
Proving it, without any test vectors
Getting this subtly wrong does not throw. It produces a perfectly valid
keypair for the wrong account — so someone writes down 24 words,
wipes their phone, restores, and finds an empty wallet. Tari publishes no
known-answer vectors for the scheme, so there was nothing to check against.
So we made the reference implementation the test: generate a phrase, hand it
to a real tari_ootle_walletd, and require its address to equal
the one our JavaScript derives, byte for byte.
| Check | Result |
| Random phrases cross-checked | 8 · 32 field comparisons · 0 mismatches |
| Wallet creates a phrase → daemon derives the same address | identical |
| Wipe device → type the words back | same wallet |
The derivation code is vendored, not depended on. It is 439
reviewed lines with no install hooks and no network access, pinned to the
exact commit that was read — because a live dependency could ship a new
version straight into key derivation. tools/xcheck.sh re-runs the
comparison and refuses to pass if the two ever disagree.
The cost, taken deliberately
The old hex format is gone entirely — no import path. Keys created before
this change cannot be retrofitted, because there is no seed to derive them
from, so roughly two dozen testnet wallets are abandoned rather than
carried forward. On a testnet where the faucet is free that is the cheaper
trade; it would not be on mainnet.
Reference
15
On-chain reference
Live addresses and the constants that govern the system.
| What | Address / value |
| Curve template (v2.2) | template_4cde8dea8ddf1211d2f26ae585fbc9d3f0f9cbbd319e7fac025a9f0ffd7873e8 |
| Factory template | template_d7ed12c34d64b930924cc3ad0987e558257ceeed9bfef964947b0ec7e6f577f3 |
| Liquidity pool | Tari system template 0000…0002 |
| Platform account | component_3a3285697b920fee07bdabf72127c3f9f9e015f576849a319456e4c16b96ec0c |
| Graduation threshold | 8,500,000,000 µtTARI (8,500 tTARI) |
| Virtual reserve | V = 3,000 · T₀ = 1.073B |
| Price sampler | every 30s |
| Trade scanner | every 20s |
Post-mortem
16
Bugs worth remembering
The ones that cost real time, kept here so they cost it only once.
Launching opened the wallet, then hung forever
Moving the launch flow onto the wallet channel updated where the request was sent but not where the reply was listened for — that filter still expected the wallet to be the parent window. On the open web the reply arrived from the frame and was silently discarded, so the approval window sat open and the token was never created.
Both listeners retargeted. Verified by letting a genuinely locked wallet answer: every signed path now replies in under a second instead of hanging to its timeout.
The wallet served a blank white page
Its build emits relative asset paths, which worked while it was served from the root and broke when it moved to /wallet/. It failed silently rather than loudly: the SPA fallback answered the missing script with 200 and an HTML body, so the browser refused to execute it and rendered bare unstyled markup.
Assets moved next to the page that references them, and the deploy script now fetches every asset the built page asks for and fails the deploy if any comes back as HTML.
"Bridge offline" shown to every signed-out visitor
The wallet modal checked a bridge-enabled flag first — but that flag is only ever fetched for a signed-in user and is hard-set false otherwise. So it never meant "the bridge is down", it meant "we don't know who you are", and it hid the entire connect flow behind a false server error.
The check moved below the sign-in and link paths, where the flag actually carries meaning.
The connect prompt keyed on the wrong question
Inline unlock was gated on "is this visitor signed out". But creating a wallet registers the account immediately, so the common case is signed in and not linked — which fell through to an older form offering no way to connect.
Gated on "is a wallet linked" instead, and one unlock now signs in and links in a single step.
Max offered to spend a balance the user didn't have
The percentage buttons were gated on whether the browser could sign a trade, not on which currency the panel was priced in. Outside the wallet that gate is false, so Max filled in the STari balance while every label on the panel said tTARI.
Percentages come off the tTARI balance whenever it is known, and never fall back to a different asset.
Stealth tokens could never graduate
The curve owner defaulted to the throwaway identity, so the platform's collection method was denied forever. Every stealth token would have bonded and then stuck permanently. Found only by selling a bonded position to exercise the pool path.
Explicit owner rule pinned to the platform key in template v2.2 — which also removed the fingerprint distinguishing stealth curves from public ones.
Invalid transaction signature on every stealth transaction
The published npm package inlines its own WASM build a full five versions behind what the network runs.
Dependency overrides plus a signer wrapper that routes through the current transaction signer instead of the bundled one.
WASM publish rejected twice
Disallowed custom sections — first name, then target_features. The obvious fix, stripping the module, destroyed the template definition section instead.
Strip precisely: debug, producers and target-features only, leaving the template definition intact.
Trade scanner recorded zero trades
Three faults at once — JSON serialisation inserting whitespace that broke matching, the trader read from the wrong side of the transaction, and a missing constant.
Compact separators, trader taken from the transaction rather than the result, constant restored.
Restored wallets showed zero and offered the faucet
The main account component is not derivable from the key, so a restored wallet had no idea which on-chain account was its own.
Authentication now returns the stored component and the wallet adopts it on restore.
Template drift between backend and wallet
The backend was pinned to v2.0 while the wallet had moved to v2.1 — the same token behaved differently depending on which side initiated.
Both pinned to v2.2 and the address recorded in the reference table above.
Standing
17
Known limits
Stated plainly, because they affect what can be promised.
- Esmeralda is a testnet and can wipe state. Curves have been observed reverting. The client defends against stale reads, but it cannot recreate substate the network has discarded.
- Privacy is bounded by what Tari exposes. Stealth mode hides the creator and the buyers; it does not hide that a token exists or that trades occurred. Timing and amount correlation remain open surfaces, and the anonymity set is currently small.
- Gas sponsorship needs a real funding model on mainnet. It is net-zero as measured, but it depends on the platform account staying funded.
- Roughly 1.49 tTARI is still stranded per private buy — down from 8.99, not eliminated.
- The stealth vault is opaque but unauthenticated. That is deliberate — requiring auth would defeat the unlinkability — and it means the design leans on the lookup value being unguessable.
- Signing needs a wallet on the device. The channel reaches a wallet wherever it runs, but it cannot conjure one: a browser with no wallet can read everything and sign nothing, and says so rather than failing at the tap.
- Browsers disagree about cross-frame messaging. The connect flow is built to survive a dropped message, but that resilience is a fallback, not a guarantee — privacy-hardened browsers remain the most likely place for this to misbehave.
- The legacy STari trade path still exists in the client for tokens with no curve. There are none, so it is unreachable rather than removed.
- Wallets created before recovery phrases are stranded by choice. Their keys were random, so no phrase can be derived for them, and the old import path was removed rather than carried indefinitely.
- Seed correctness rests on agreeing with the reference implementation, since upstream publishes no test vectors. Re-run the cross-check after touching the derivation or its vendored source — a wrong result looks exactly like a right one.