PARN / Docs
Documentation

One number, and everything around it.

PARN exists to answer a single question: what is a tokenised equity trading at, relative to the stock behind it, right now? This page explains where that number comes from, what the app does with it, and — just as important — when it refuses to show you one.

01

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.

Who it is for

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.

02

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

FileWhat it is
index.htmlThe whole product — marketing page, terminal, chain client, charts. Images are embedded as data URIs.
docs.htmlThis page.
hero-coins.svgSource of the hero backdrop. The copy inside index.html is what the page actually renders.
make-hero.pyGenerates that artwork and, with --install, embeds it.
*.png, *.b64.txtMascot, logo marks and their base64 forms.
03

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:

basis = ( mid − nav ) / nav × 10 000

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.

04

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.

05

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

ViewWhat it answers
SpreadsWhich 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 trackerPoint 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.
MemecoinsThe 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.
AlertsA 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.
SettingsConnection 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.

06

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.

07

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.

08

Configuration

Everything configurable sits near the top of the script in index.html.

ConstantMeaning
CHAINChain id, the RPC endpoint list tried in order, the Uniswap V3 factory, and the explorer.
FEEDSThe 33 Chainlink equity feeds: ticker, name, brand colour and proxy address.
TOKENSStock Token ERC-20 addresses by ticker. A feed without an entry here shows its NAV and no basis.
QUOTE_DEFAULTThe asset the pools quote in — Global Dollar (USDG), 6 decimals. Changeable in Settings.
MIN_DEPTHQuote-side balance a pool must hold before its price is believed. See the depth rule.
FEE_TIERSThe 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.

KeyHolds
plansYour recurring buys in the DCA view.
alertsThreshold rules.
watchStarred assets.
filterThe Spreads toolbar: search text, depth floor, watchlist switch.
calcThe front-page calculator's asset, amount, cadence and day.
sortWhich column the table is sorted by, and which way.
tokens, quoteToken addresses and the quote asset, when changed in Settings.
histBasis samples recorded on each refresh — the only history PARN has.
09

Network & data sources

ItemValue
ChainRobinhood Chain — id 4663, Arbitrum Orbit (Nitro), gas in ETH
RPC4663.rpc.thirdweb.com, then robinhood.drpc.org
Uniswap V3 factory0x1f7d7550B1b028f7571E69A784071F0205FD2EfA
Quote assetUSDG 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168, 6 decimals
OraclesChainlink, 8 decimals, 24-hour heartbeat
Memecoin indexGeckoTerminal public API, network robinhood
Explorerrobinhoodchain.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.

10

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.

11

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.
12

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.