Back to SOOON

Development Detail 21 August 2026 ● Esmeralda Testnet — Live

SOOON

What shipped across the last several build sessions: a working token lifecycle on Tari Ootle, a self-custodial browser wallet, and a privacy layer that hides who launched a token and who bought it.

Signed actions
5work anywhere
Wallet backup
24words · Tari format
Graduation at
8,500tTARI
Stranded per buy
1.49tTARI · was 8.99
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

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.

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

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
ParameterBeforeAfter
Sponsor bootstrap6,000,0001,000,000
Buy overhead9,500,0002,000,000
Launch fee9,500,0003,000,000
Wallet gas reserve2,000,0001,000,000
Total per private buy29.63 tTARI22.37 tTARI
Left stranded8.99 tTARI1.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.

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.

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.

CheckResult
Random phrases cross-checked8 · 32 field comparisons · 0 mismatches
Wallet creates a phrase → daemon derives the same addressidentical
Wipe device → type the words backsame 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.

WhatAddress / value
Curve template (v2.2)template_4cde8dea8ddf1211d2f26ae585fbc9d3f0f9cbbd319e7fac025a9f0ffd7873e8
Factory templatetemplate_d7ed12c34d64b930924cc3ad0987e558257ceeed9bfef964947b0ec7e6f577f3
Liquidity poolTari system template 0000…0002
Platform accountcomponent_3a3285697b920fee07bdabf72127c3f9f9e015f576849a319456e4c16b96ec0c
Graduation threshold8,500,000,000 µtTARI (8,500 tTARI)
Virtual reserveV = 3,000 · T₀ = 1.073B
Price samplerevery 30s
Trade scannerevery 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.

SOOON · sooon.fun Tari Ootle — Esmeralda Development detail · 15 Aug 2026