What PARN is
A tokenised stock trades every hour of the weekend. The exchange behind it does not. When the New York market is shut, the token keeps changing hands on-chain while the price it is supposed to track sits frozen at Friday's last print — and the two drift apart.
That drift is the basis: the gap between what the token costs on-chain and what the underlying share is worth, measured in basis points. One basis point is a hundredth of a percent, so a token trading 0.5% above fair value carries a basis of +50 bp.
PARN reads both sides of that gap directly from Robinhood Chain and prices it. It holds no funds, asks for no wallet, and signs nothing. It is a reading instrument.
Anyone buying tokenised equities on a schedule. A recurring buy fires whether the exchange is open or closed, and on a closed-market fill you may be paying a premium that never appears in your average cost. PARN separates that premium from your actual gain or loss.
Running it
The whole product is one HTML file with no build step and no dependencies. Serve the folder over HTTP and open it:
cd "D:/Claude/Project 1/parn"
python -m http.server 4663
Then open http://localhost:4663/. The port matches the chain id, which is a convenience, not a requirement.
Opening index.html straight off the disk as a file:// URL will not work: the browser blocks the cross-origin requests the page makes to the RPC endpoints. It has to be served over HTTP.
After editing the file, hard-reload. A plain reload can serve the previous copy from cache and leave you looking at stale markup. Use Ctrl+Shift+R.
Where things live
| File | What it is |
|---|---|
index.html | The whole product — marketing page, terminal, chain client, charts. Images are embedded as data URIs. |
docs.html | This page. |
hero-coins.svg | Source of the hero backdrop. The copy inside index.html is what the page actually renders. |
make-hero.py | Generates that artwork and, with --install, embeds it. |
*.png, *.b64.txt | Mascot, logo marks and their base64 forms. |
How the basis is computed
Three reads, every refresh. No indexer, no backend, no price API — every figure on screen was returned by chain 4663 while you were looking at it.
Step 1 — the reference NAV
Chainlink publishes a price feed per tokenised equity on this chain. PARN calls latestRoundData() on each proxy, takes the 8-decimal answer, and notes when the round was last updated. A feed whose last update is older than its 24-hour heartbeat is flagged stale rather than quietly used.
Step 2 — the on-chain mid
The Uniswap V3 factory is asked for the pool holding that token against the quote asset, trying each fee tier in turn — 0.01%, 0.05%, 0.30%, 1.00%. The first pool that exists wins and is remembered for the session. slot0() then returns sqrtPriceX96, which becomes a mid once the square is taken and token order and decimals are applied.
Step 3 — the basis
The gap, in basis points:
Positive is a premium: the token costs more than the share it tracks. Negative is a discount. Every refresh records a sample, so the weekday and session averages elsewhere in the app are built from observations the app actually made, not from an estimate.
Nothing here is a library call. Each read is a fixed four-byte selector and a hand-decoded return value, which is less code than an ABI coder would be. The selectors are latestRoundData 0xfeaf968c, slot0 0x3850c7bd, getPool 0x1698ee82 and balanceOf 0x70a08231.
The depth rule
A Uniswap pool can exist and hold nothing at all. Such a pool still answers slot0() — with whatever price it happened to be initialised at, which nobody can trade against.
Several equities on this chain are in exactly that state, and taking their word for it produced basis figures in the thousands of basis points. One empty pool quoted a mid of $3.4×1038 against a NAV of $12.25.
So PARN reads the pool's quote-side balance before it reads the price, and a pool holding less than MIN_DEPTH (250 USDG) is treated as having no price at all. The asset keeps its NAV and is marked:
no pool
The factory has no pool for this token against the quote asset, on any fee tier.
no depth
A pool exists but is empty. There is a price in it; it just does not mean anything.
This follows the rule the front page already states: where there is no market, PARN shows the NAV alone and says so, rather than inventing a spread.
The terminal
The terminal lives at #/app, with each view on its own route — #/app/spreads, #/app/dca and so on, so a particular view can be linked or bookmarked.
The four views
| View | What it answers |
|---|---|
| Spreads | Which tokens have drifted furthest from parity right now, ranked, with pool depth beside each one so a real dislocation can be told from a thin book. Selecting a row charts its basis over the samples collected, shaded by whether the exchange was open. |
| DCA tracker | Point it at a recurring buy and it reconstructs every fill: the price paid, the NAV at that moment, and the premium handed over. See premium drag. |
| Memecoins | The chain's memecoins have no NAV to measure against, so they get the honest treatment instead — liquidity, 24-hour volume, turnover, and what averaging in costs in slippage. No invented fair value. |
| Alerts | A line in basis points per asset, above or below. Checked on every refresh; triggered rules are listed in the view. In-app only — nothing is emailed or pushed. |
| Settings | Connection status, the quote asset, and adding token addresses. Anything added here is verified with symbol() on chain before the app will use it. |
Working the table
Thirty-three feeds is more than anyone reads at once, so the Spreads table carries a toolbar. Every setting on it is kept in this browser and survives a reload.
Search
Matches the ticker or the company name, so co finds Coinbase, CoreWeave and Invesco QQQ alike.
Depth floor
Hide anything whose pool holds less than $10k, $100k, $500k or $1M. The fastest way to drop the names whose basis is technically true and practically untradeable.
Watchlist
Star a row to keep it, then switch the toggle to show only what you starred. The star is its own column, so it never shifts the asset name.
Export
Writes exactly what the table is showing, in the order it is showing it, to a CSV.
The count beside the toolbar always reads N of M shown, so a filtered table can never be mistaken for the whole set. The tiles above it keep describing the whole market rather than the filtered rows — a median across three hand-picked names would not be a median of anything.
What the CSV contains
symbol, name, on_chain_mid, chainlink_nav, basis_bp,
pool_depth, pool, nav_updated, stale
Values are quoted the RFC way, so a company name with a comma in it survives the round trip, and the file opens with a UTF-8 byte-order mark so Excel reads it as UTF-8 instead of guessing a codepage. An asset with no tradeable pool exports empty mid and basis cells rather than zeros — a zero would be a measurement, and there was none.
Reading the numbers
Basis, in bp
100 bp = 1%. Amber is a premium, cyan a discount. These two colours are reserved for market direction and are never used decoratively anywhere in the product.
Pool depth
The quote-side balance of the pool the mid came from. A wide basis on a shallow pool is mostly noise; the same basis on a deep one is information.
Age
How long ago the Chainlink round was published. Past the 24-hour heartbeat the feed is marked stale.
Exchange state
Open or closed, from the NYSE session clock. While the exchange is shut the NAV is carried from the last print, which is precisely when the basis has room to run.
The chart's shaded bands mark the hours the exchange was closed. Drift that only appears inside those bands is the weekend effect this whole product is pointed at.
Premium drag
A recurring buy is indifferent to the clock. It fires whether the market is open or not, and on a tokenised stock that indifference has a price — one that never shows up in your average cost, because your average cost only knows what you paid, not what it was worth.
PARN prices each fill against the NAV at that moment and sums the differences. That total is premium drag: the amount paid above fair value, separated from the gain or loss on the position itself.
The DCA view also averages the basis by weekday, which usually makes one answer obvious: the cheapest day to schedule a buy is a day the exchange is open. In the sample ledger, every fill landed while the market was shut, and those fills carry all of the drag.
The plans are samples. They are not your trades. Add your own in the DCA view — amount, cadence and day — and they are kept in this browser only.
The calculator on the front page
The same question, without opening the terminal. Set an asset, an amount, a cadence and the weekday you buy on, and the front page answers four things: what that schedule pays above NAV in a year, the average basis on your day, which weekday is cheapest, and what moving to it would save.
It runs on the same recorded series and the same weekday convention as the DCA view — a weekday is “closed” when it is Saturday or Sunday, not when most of its hours fall outside the session — so the two cannot quietly disagree about the same asset.
When the profile is modelled
A weekday with no observations averages to zero, and a zero in this chart reads as parity on Monday rather than nothing seen on Monday. Recommending a buy day on that basis would be the most confident wrong answer the page could give.
So a profile is used only when every weekday carries at least three samples. Short of that, PARN substitutes a modelled profile anchored to the live NAV and says so on the panel:
N samples
Recorded observations covering the whole week. The figures are measurements.
modelled profile
Live NAV, modelled basis. Leave the page open and observations replace it.
simulated
The chain was unreachable. The arithmetic is real; the basis behind it is not.
The live table
Below the calculator, the front page lists the widest premiums and the deepest discounts on the chain right now, with pool depth beside each, and a line reading N priced · N NAV-only · live — or simulated, when that is what it is.
Configuration
Everything configurable sits near the top of the script in index.html.
| Constant | Meaning |
|---|---|
CHAIN | Chain id, the RPC endpoint list tried in order, the Uniswap V3 factory, and the explorer. |
FEEDS | The 33 Chainlink equity feeds: ticker, name, brand colour and proxy address. |
TOKENS | Stock Token ERC-20 addresses by ticker. A feed without an entry here shows its NAV and no basis. |
QUOTE_DEFAULT | The asset the pools quote in — Global Dollar (USDG), 6 decimals. Changeable in Settings. |
MIN_DEPTH | Quote-side balance a pool must hold before its price is believed. See the depth rule. |
FEE_TIERS | The Uniswap fee tiers searched, in order, when resolving a pool. |
What is stored, and where
PARN has no server and no account. Everything you change is written to localStorage under a parity: prefix — the name the project carried before it was PARN. The prefix is deliberately left alone: renaming it would orphan every watchlist, plan and recorded sample already sitting in someone's browser, which is a high price for tidiness nobody can see. Clearing site data resets the app to its defaults. Nothing leaves the page.
| Key | Holds |
|---|---|
plans | Your recurring buys in the DCA view. |
alerts | Threshold rules. |
watch | Starred assets. |
filter | The Spreads toolbar: search text, depth floor, watchlist switch. |
calc | The front-page calculator's asset, amount, cadence and day. |
sort | Which column the table is sorted by, and which way. |
tokens, quote | Token addresses and the quote asset, when changed in Settings. |
hist | Basis samples recorded on each refresh — the only history PARN has. |
Network & data sources
| Item | Value |
|---|---|
| Chain | Robinhood Chain — id 4663, Arbitrum Orbit (Nitro), gas in ETH |
| RPC | 4663.rpc.thirdweb.com, then robinhood.drpc.org |
| Uniswap V3 factory | 0x1f7d7550B1b028f7571E69A784071F0205FD2EfA |
| Quote asset | USDG 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168, 6 decimals |
| Oracles | Chainlink, 8 decimals, 24-hour heartbeat |
| Memecoin index | GeckoTerminal public API, network robinhood |
| Explorer | robinhoodchain.blockscout.com |
Living with public endpoints
Both RPC endpoints are free tiers, and they disagree about what they will accept. One takes a batch of six calls but rate-limits a burst; the other refuses any batch larger than three. So each endpoint carries its own batch size, halved whenever it complains, plus a cooldown stamped on it when it answers 429 — and the other endpoint covers that stretch.
A rate-limited response sometimes arrives without CORS headers, which makes it unreadable from the browser rather than merely unsuccessful. Both cases fall through to the next endpoint, and the 60-second refresh retries regardless of how the last pass went. That retry is how the page climbs back out of simulated data on its own.
Regenerating the artwork
The hero backdrop is a generated SVG — a field of struck coins carrying the tickers PARN tracks. It is vector so it stays sharp at whatever size cover gives it:
python make-hero.py hero-coins.svg index.html --install
That writes hero-coins.svg, its base64 twin, and swaps the result into the single url(data:...) in the stylesheet. The random seed is fixed, so a run with unchanged constants reproduces the same artwork byte for byte; change random.Random(8213) to deal a different field.
Worth knowing before you tune it: the canvas is cut to the shape the hero band takes on a wide screen. Because the background is sized with cover, a canvas that is too tall for its width gets scaled up on a wide monitor and magnifies every coin. The script's COPY and MASCOT constants keep the readable coins out of the headline column and away from the mascot.
Limits & honesty
- Prices are whatever the chain returns when you load the page. There is no cache, no server and no history beyond the samples this browser has collected.
- When the chain cannot be reached, the app says so and shows simulated figures — clearly labelled, never mixed with live ones.
- Sample DCA plans and alerts ship with the app so the views have something to show. They are not your positions.
- Depth is the pool's quote-side balance, not Uniswap's
liquidity, which is a square-root-scaled quantity rather than an amount. - The token decimals are assumed to be 18. Every Stock Token on this chain checked so far is; one that is not would be mispriced by a power of ten.
- None of this is advice. A basis is an observation about two numbers, not a recommendation to act on it.
Troubleshooting
The banner says the figures are simulated
The first pass could not reach chain 4663 — usually a rate limit on the public endpoints. Wait for the 60-second retry, or press refresh in the top bar. Settings shows the endpoint in use, the block height and the exact error.
An asset shows a NAV but no basis
Expected, and deliberate. Either no pool exists for it, or the pool is empty. See the depth rule.
Edits to the file do not appear
The browser is serving a cached copy. Hard-reload with Ctrl+Shift+R.
My watchlist, filters or plans are gone
They live in this browser's localStorage and nowhere else, so clearing site data, a different browser, or a private window all start from the defaults. There is no account to sign into and no copy on a server.
The table looks short
Check the toolbar: a search term, a depth floor or the watchlist switch may be filtering it. The count reads N of M shown whenever the two differ.
The page looks wrong on a phone
Make sure the <meta name="viewport"> tag is present in the file you are serving. Without it the browser lays the page out at 980 px and crops it.