Docs Vault

Vault

Your object ETFs first, valued at NAV with a Redeem pill each. Below them, every stock held directly, read in one Multicall3 round trip.

AAPLbalanceOf → AAPLTSMbalanceOf → TSMNVDAbalanceOf → NVDASKHYbalanceOf → SKHYAVGObalanceOf → AVGOMSFTbalanceOf → MSFTCOSTbalanceOf → COST
getHoldings: one Multicall3 aggregate per 120 stocks, balanceOf for each, back as one answer. Zero balances are dropped before anything is valued.

Your object ETFs

The first section of the vault lists the object ETFs this wallet holds. For every catalog slug, plus every ETF seen in the factory's ETFCreated logs (that is how scanned objects show up), getEtfHoldings asks the factory for etfOf(slug), then reads balanceOf, totalSupply and constituents of each ETF in one multicall.

ColumnMeaningSource
Sharesbalance / 1e18balanceOf
Of the ETFbalance / totalSupply, as a percentage with a bartotalSupply
Value at NAVshares × Σ value(units_i)constituents + Robinhood mid
RedeemBurns every share and sends the stocks behind them to youetf.redeem(shares, you)

Redeem asks once, lists exactly which stocks come back, simulates the call and then sends it. The stocks arrive as ordinary ERC-20s and appear in the next section on the next refresh. When no factory is deployed the section says so and the rest of the page is unchanged.

Reading balances

Below the ETFs, Stocks held directly lists every stock the wallet holds outright: bought through the direct swap, redeemed from an ETF, or sent from anywhere else.

The vault page asks /api/assets for the live stock list, then calls getHoldings(address, tokens) from lib/holdings.ts. The public RPC cannot take JSON-RPC batches, so every balanceOf goes through Multicall3 at 0xcA11bde05977b3631167028862bE2a173976CA11 in chunks of 120 with allowFailure: true. A stock whose call fails is skipped, not fatal.

const results = await client.multicall({
  contracts: slice.map((t) => ({
    address: t.address,
    abi: ERC20_ABI,
    functionName: "balanceOf",
    args: [address],
  })),
  allowFailure: true,
});

Only non-zero balances become holdings. Each carries the raw balance, the share count, the Robinhood mid and the USD value, sorted by value.

Share maths

Raw units are not shares. rawToShares applies the ERC-8056 multiplier from the registry, and sharesToRaw is its exact inverse for the buy side. The multiplier arrives as an 18-decimal fixed-point string (the registry says 1.000566080061092436, the code stores 1000566080061092436) and parseMultiplier accepts either form.

FieldMeaningSource
rawERC-20 balance, 18 decimalsbalanceOf
sharesraw × multiplier / 1e18 / 1e18rawToShares
usdshares × midTokenQuote.mid
haltedTrading halt flag from the price feedisTradingHalt

The same conversion runs on the way in: describePlan turns each leg's expected raw output into shares so the buy screen and the vault agree on the number. See Shares and the multiplier.

Objects you own

Below the positions, the vault matches your holdings back to the catalog. For each of the fourteen objects it counts how many of its listed companies you hold. Any object where you hold two or more is shown as something you own, at least in part, sorted by the fraction covered.

const held = new Set(positions.map((p) => p.ticker));
CATALOG.map((o) => {
  const have = o.basket.companies.filter((c) => held.has(c.ticker)).length;
  return { object: o, have, of: o.basket.companies.length };
})
  .filter((r) => r.have >= 2)
  .sort((a, b) => b.have / b.of - a.have / a.of || b.have - a.have);

Buy an iPhone basket and a MacBook basket and the vault will tell you that you also own most of an AirPods case. That is the point.

What the vault does not do

  • It does not store anything. Every visit reads the chain again. Disconnect and it forgets you.
  • It does not track cost basis. There is no purchase history to read; only the chain has it.
  • It does not sell. ETF shares redeem into stocks; the stocks are plain ERC-20s in your wallet. Sell them on Uniswap v4 or move them. A sell flow is on the roadmap.

Your keys

Photoswap never has custody. If the site disappears, your balances do not.