@paydprotocol/mcp 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,351 @@
1
+ # HOW_IT_WORKS.md — the whole system, once through
2
+
3
+ `README.md` says what a holder gets. This says how it is built, in one pass, for
4
+ someone who intends to read the code afterwards. `ARCHITECTURE.md` holds the
5
+ reasoning behind every choice mentioned here and is not a prerequisite.
6
+
7
+ ---
8
+
9
+ ## The mechanism in one paragraph
10
+
11
+ A token is launched on **Pons v2** with a contract — not a wallet — as its
12
+ creator-fee recipient. Every trade pays the curve fee plus **the tax that
13
+ launch's creator set**, and most of it reaches that contract — the tax in full,
14
+ plus the share of the curve fee Pons does not keep. **There is no single rate**:
15
+ it is chosen per launch, `FeeVault.economics()` derives it rather than storing
16
+ it, and any figure written here would be true of one token and false of the
17
+ rest. The contract converts what it collects into **tokenised stocks** on
18
+ Uniswap v3, the whole basket in one purchase, and hands them to a second
19
+ contract that distributes them to the token's holders **pro rata to what they
20
+ held**, with no staking and no action on their part. Who held what is computed
21
+ off-chain, committed on-chain as a Merkle root, and re-computable by anyone from
22
+ the chain alone.
23
+
24
+ That pair of contracts is no longer made once, for one token: a **`Payd`**
25
+ registry mints a fresh pair per launch through its **`DistributionFactory`**, and the platform's own token, `$PAYD`, is one of
26
+ its tenants rather than its landlord.
27
+
28
+ ## Why any of it is off-chain
29
+
30
+ The token is minted by the Pons factory. It has **no transfer hook**, so no
31
+ contract can know a past balance. Without staking — and staking is a click, a
32
+ lock-up, and a contract holding your tokens — the split has to be computed off
33
+ the chain and committed to it.
34
+
35
+ That is the whole trust surface, and it is worth stating plainly rather than
36
+ burying: **one key publishes the roots.** What it cannot do is invent money, pay
37
+ someone twice, take back what was paid, or move funds anywhere — those are
38
+ contract-level guarantees, not promises. What it could do is misdirect what has
39
+ not been delivered yet, a figure the contract publishes as
40
+ `Distributor.quoteAtRisk()` and which continuous delivery keeps at roughly one
41
+ epoch. [§S29](ARCHITECTURE.md#s29--the-keeper-publishes-and-the-root-takes-effect-immediately)
42
+ argues the alternative — a bond and a challenge window — and says why it was
43
+ dropped.
44
+
45
+ ## The eight contracts
46
+
47
+ **Per launch**, one of each:
48
+
49
+ | | |
50
+ | :--- | :--- |
51
+ | **`FeeVault`** | Receives the creator fees in the launch's currency. Splits them three ways, buys the whole basket, sends the stocks straight to the Distributor. Holds no stock, ever. |
52
+ | **`Distributor`** | Holds the stocks and pays them out against a Merkle proof. Remembers what each holder has already been paid. |
53
+
54
+ **Once, for the platform:**
55
+
56
+ | | |
57
+ | :--- | :--- |
58
+ | **`Payd`** | The registry, and **an interface over the factories** rather than a pointer to one. Holds the parameters a creator may not choose alone: which stocks a basket may contain, which currencies a launch may be quoted in, what the platform takes — and which factories may build here at all. **It launches nothing** — the creator launches their own token on Pons, so the registry is the Pons `deployer` of nothing and holds no standing power over any launch. |
59
+ | **`DistributionFactory`** | Makes the pairs, and only that. Split out of `Payd` because a contract that does `new FeeVault()` carries that creation code: `Payd` weighed **54,635 bytes of initcode**, above the EIP-3860 ceiling, and 18,704 once the machinery left. It is also what makes a new vault implementation ordinary — a new factory, admitted by `Payd` under two keys and 48 h, and the vaults that follow are born in **this** registry. **One factory declares one payout mode** (`MODE`, `"distribution"` here): several may be admitted at once, and a vault is stamped with its own at birth. ([§S46](ARCHITECTURE.md#s46--one-payout-mode-is-one-factory-and-the-stamp-that-keeps-them-apart)) |
60
+ | **`Treasury`** | Receives the platform's share of every launch and splits it four ways: dev ⅓, `$PAYD` rewards ⅓, buy-and-burn ⅙, LP ⅙. No gas refund anywhere in it — the platform has every reason to call its own pockets. |
61
+ | **`Collector`** | Settles N launches in one transaction. A router that holds nothing: the stocks go straight to the holder, the refunded gas straight to the caller. ([§S39](ARCHITECTURE.md#s39--collector-settling-n-launches-in-one-transaction-and-the-door-it-uses)) |
62
+ | **`Timelock`** | 48 h, OpenZeppelin, self-administered. Its powers are listed below, and none of them moves value. |
63
+ | **`Bootstrap`** | Ties the knot: `FeeVault` and `Distributor` each need the other's address, so neither can be deployed first. The factory deploys one per `create` and abandons it in the same transaction — no `Bootstrap` is ever a standing contract. |
64
+
65
+ There is **no owner** anywhere. No `withdraw` exists for any privileged address;
66
+ the only `withdraw()` on either contract pays the caller a payment that had
67
+ already failed to reach that same caller, takes no address argument, and can move
68
+ nobody else's balance.
69
+
70
+ ## One turn of the cycle
71
+
72
+ Every step is callable **by anyone**. On an ETH-quoted vault each one refunds its
73
+ own gas at the real cost, priced at `block.basefee` — which the caller does not
74
+ choose — and capped. `publishRoot` is the single exception.
75
+
76
+ ```
77
+ harvest() pull the creator fees out of Pons, split them
78
+ │ platform (fixed at birth) · rewards (rewardsBps,
79
+ │ raisable only) · creator (the residue)
80
+ ▼
81
+ buyBasket(minOuts[]) buy the WHOLE basket for every epoch since the last
82
+ │ purchase — one shared hop into the pivot, then one
83
+ │ leg per stock, each above its own price floor,
84
+ │ delivered straight to the Distributor
85
+ ▼
86
+ publishRoot(...) KEEPER ONLY. Commit the cumulative root, immediately
87
+ │ in force. Preceded by five preflight checks.
88
+ ▼
89
+ distribute(holder, ...) push a holder's shares to them. Or the holder calls
90
+ claim(...) and pays their own gas — or collect(...)
91
+ and settles every launch they hold at once.
92
+ ```
93
+
94
+ An epoch nobody buys for simply carries over: the money stays in the vault and
95
+ the next purchase covers every epoch that went by. Nothing is lost and nothing is
96
+ stuck.
97
+
98
+ ### One purchase, the whole basket
99
+
100
+ Payd bought **one stock per epoch** and honoured the weights by rotation. The
101
+ value was fair; the composition was noise — at a 24-hour cadence a holder needed
102
+ ten days to see a whole basket go by. A purchase now covers a **window**: every
103
+ epoch the Distributor has not been funded for, up to the last one that has
104
+ finished. ([§S41](ARCHITECTURE.md#s41--one-purchase-takes-the-whole-basket-and-a-skipped-leg-brings-nothing-down),
105
+ which supersedes [§S16](ARCHITECTURE.md#s16--one-epoch-one-stock-weighted-rotation))
106
+
107
+ What the window shares, instead of paying per leg: the hop into the pivot
108
+ currency (139,625 gas), the TWAP read of that same pool (69,389 gas), the
109
+ ETH/USD feed, the base transaction, the refund, and one `fundWindow` instead of
110
+ one `fund` per epoch.
111
+
112
+ **A leg that cannot be bought is skipped, not fatal.** A paused stock, a dry
113
+ pool, a floor the market will not meet: that leg's pivot currency stays in
114
+ `pivotReserve`, the contract emits `LegSkipped`, and the next purchase spends it.
115
+ A stock Robinhood pauses costs a delay, never a loss — and never the other legs.
116
+ This is what the `try/catch` buys back, and why `_fund` truncates the arrays to
117
+ the legs that actually bought: `fundWindow` refuses a zero amount, so passing the
118
+ full-width arrays made one skipped leg revert the entire purchase.
119
+
120
+ Two legs never touch a pool at all:
121
+
122
+ - a **pivot line** (USDG here) is already in the basket's currency. There is no
123
+ pool of a token against itself, so there is no floor to compute and no price to
124
+ protect. It is the one line `Payd` lists at tier 0;
125
+ - a line that **is the vault's own quote** is held back *before* the hop rather
126
+ than bought back after it. The round trip costs two pool fees and two
127
+ slippages to end up where it started — 0.10 % measured on NVDA/USDG at tier
128
+ 500.
129
+
130
+ ### One currency per vault, and the pivot it routes through
131
+
132
+ Pons takes `pairToken` as an argument of `launchToken`, and its escrow keeps
133
+ **one ledger per currency**. Measured over seven days of `V2FeeEscrow` credits
134
+ (2026-09-08): **40.9 %** of Pons volume is quoted in ETH, **22.0 %** in USDG,
135
+ **37.2 %** in stock tokens. A vault that reads only the ETH ledger leaves three
136
+ fifths of the market unreachable, which is what v1 did.
137
+
138
+ So a vault declares **one `QUOTE` at birth and never changes it** — every number
139
+ it holds is denominated in that currency, and `bind` refuses any launch quoted
140
+ elsewhere. Everything then routes through one **`PIVOT`** currency, USDG on this
141
+ chain, because that is where the stocks' liquidity is (`recon.md` §4.1).
142
+
143
+ **The pivot is a crossroads, not a wall.** A currency can be deeply traded and
144
+ still invisible against the pivot: COIN and cbBTC have no pivot pool at all, and
145
+ $33,144 / $158,774 of WETH depth — between them 198 of the ~220 weekly credits
146
+ among the otherwise-unreachable pairs. Those are reached by `QUOTE → WETH →
147
+ PIVOT`, whose second hop is the very pool an ETH-quoted vault already uses. The
148
+ route is **declared at birth from a measurement, never probed when the money
149
+ moves**: exactly one of `QUOTE_FEE` / `QUOTE_WETH_FEE` is non-zero.
150
+
151
+ Neither the pivot nor the WETH→pivot tier is a constant any more. Both are
152
+ written at `init` from `Payd`'s wiring, so a redeployed registry can point
153
+ somewhere else without a line of contract changing.
154
+ ([§S40](ARCHITECTURE.md#s40--one-currency-per-vault-and-the-pivot-as-a-crossroads))
155
+
156
+ **A non-ETH vault skims no delivery budget, and pays its caller in its own
157
+ currency.** The refund is computed in wei and the Distributor spends wei; a vault
158
+ holding NVDA has neither. So the cycle pays a **bounty** instead — 2 % of what
159
+ the call moved, capped at about $25 — which needs no price feed because it is
160
+ already denominated in the vault's currency. The delivery budget has no such
161
+ escape — it is wei, and the Distributor cannot spend NVDA on gas — so the keeper
162
+ fronts every push there and the bounty is what pays for it. The airdrop floor is
163
+ **40 % of `MIN_BUY_QUOTE`, about $10** — the same value an ether vault uses,
164
+ simply expressed in the currency the vault actually holds.
165
+
166
+ ### The price floor
167
+
168
+ Never `minOut = 0`. Every purchase prices through a **30-minute Uniswap v3
169
+ TWAP**, tightened by a **Chainlink** feed where one exists and never blocked by
170
+ one that is stale — equity feeds go quiet at the weekend, and around a split the
171
+ feed and the token's own multiplier disagree for an hour. The caller may pass a
172
+ tighter `minOut` per leg; the contract takes `max(caller, on-chain floor)`, so a
173
+ hostile caller can only make their own transaction fail.
174
+ ([§S3](ARCHITECTURE.md#s3--minout-an-on-chain-floor-tightenable-off-chain-p3-p4))
175
+
176
+ Upstream of the floor, `Payd` refuses to list a stock or a quote whose
177
+ declared pool **does not exist or carries nothing**. Existence alone would have
178
+ caught nothing here: all four NVDA/USDG tiers exist and `getPool` answers on each
179
+ — it is the liquidity that separates them, 1.4e19 at tier 500 and **zero** at
180
+ tier 10000. ([§S42](ARCHITECTURE.md#s42--a-listed-line-must-have-a-pool-that-carries-something))
181
+
182
+ ## The snapshot
183
+
184
+ An epoch's balances are the **time-weighted average over the whole epoch**: not
185
+ a balance read at some instant, but `∫ balance dt` divided by the epoch's length.
186
+ Hold for the full period and you weigh what you hold; hold for a second and you
187
+ weigh a second.
188
+
189
+ That leaves nothing to snipe, and nothing to draw. The period is
190
+ `[GENESIS + e·L, GENESIS + (e+1)·L)` — two immutables and a subtraction — so the
191
+ publisher chooses no part of it, and no seed has to be committed on-chain to
192
+ prove they did not. The two transactions per epoch that used to do that, and the
193
+ delay between them, are gone. ([§S38](ARCHITECTURE.md#s38--the-time-weighted-average-and-the-machinery-it-deletes))
194
+
195
+ `L` is per vault: `Payd` accepts anything from **30 minutes to 24 hours**.
196
+ Thirty minutes is what a token with real volume wants; a day is what a quiet one
197
+ wants, so its keeper is not publishing roots into the void.
198
+
199
+ Excluded from the snapshot: the Uniswap pool, the Pons bonding curve, the vault
200
+ and its Distributor, address zero, the burn address `0xdead`, and a list the timelock
201
+ maintains. That list is
202
+ **on-chain and dated**: an exclusion applies from a given epoch onward, so two
203
+ people replaying the same epoch on either side of a change still agree.
204
+ ([§S23](ARCHITECTURE.md#s23--an-exclusion-is-dated-not-live))
205
+
206
+ ## Two roots, and why cumulative
207
+
208
+ A leaf is `(holder, stock, cumulative)` — the total owed **since inception**, not
209
+ for one epoch. The contract remembers what it has already paid and settles only
210
+ the difference. Cost therefore scales with the number of **stocks**, never with
211
+ the number of epochs elapsed: settling a holder after a week costs the same as
212
+ after an hour, and replaying an old proof pays nothing.
213
+ ([§S18](ARCHITECTURE.md#s18--cumulative-roots-cost-follows-the-stocks-no-longer-time))
214
+
215
+ Each publication carries **two** roots:
216
+
217
+ - **`claimRoot`** — every eligible holder. This is what `claim` verifies against,
218
+ and it is open to anyone at any time.
219
+ - **`pushRoot`** — only the entries worth delivering: a holder whose whole outstanding
220
+ share is worth ~$10.
221
+ `distribute` verifies against this one, so the gas refund can only ever fund a
222
+ delivery that was worth making.
223
+
224
+ Both are standard OpenZeppelin sorted-pair trees, checked leaf by leaf against
225
+ the reference implementation in `test/MerkleCompat.t.sol` and
226
+ `front/src/merkle.test.ts`. They are **different trees**, and a proof built on
227
+ the wrong one passes every check in the browser and reverts on-chain — the trap
228
+ §S39 documents.
229
+
230
+ ## Verifying without asking anyone
231
+
232
+ ```bash
233
+ pnpm --filter offchain dispute
234
+ ```
235
+
236
+ Recomputes the shares from the chain alone — no key, no input from the project, no
237
+ archive node — and reports whether the published root matches. Everything it
238
+ consumes is public: the exclusions are on-chain and dated, the artifact is
239
+ committed as a sha256 digest in `Root.digest`, and the period each average covers
240
+ falls out of `GENESIS` and `EPOCH_LENGTH`.
241
+
242
+ The keeper runs **five preflight checks** before publishing and refuses to
243
+ publish if any fails — conservation, monotonicity, population, provenance, and a
244
+ full recomputation on a second RPC. Publishing nothing delays rewards;
245
+ publishing wrongly misdirects them.
246
+ ([§S32](ARCHITECTURE.md#s32--the-preflight-refusing-to-publish-rather-than-publishing-wrongly))
247
+
248
+ ## Who can do what
249
+
250
+ | Actor | Can | Cannot |
251
+ | :--- | :--- | :--- |
252
+ | **Anyone** | harvest, buy the basket, pay the creator and the platform their share, distribute, claim, collect across launches, top up the rewards reserve, split the Treasury and run its four pockets | change any parameter |
253
+ | **Keeper** | publish a root | move funds, change anything else |
254
+ | **Creator** (of one launch) | raise their vault's `rewardsBps` — **upwards only**, floor 50 % | lower it, touch the basket, touch funds |
255
+ | **Timelock** (48 h, proposed by the Safe, executed by anyone) | per vault: reweight the basket, set the payout rate, set the gas share, manage exclusions, rotate the keeper, `migrate` the fee stream. Platform-wide: list and delist stocks and quotes, set `platformBps` for FUTURE vaults, set the Treasury split, admit a factory the generation key has approved (`setFactory`, `enableFactory`) and stop one building (`disableFactory`) | withdraw, redirect to itself, or freeze anything |
256
+ | **Safe** (2-of-3) | launch its own token, propose to the timelock | touch funds already in the contracts, or reach a creator's launch |
257
+ | **Generation key** (cold, immutable, holds nothing) | `approve` a candidate factory — and revoke it while the timelock has not executed. Open or shut `crossModeMigration`, the one thing that lets a migration cross payout modes | name anything by itself: `setFactory` and `enableFactory` are still the timelock's, still 48 h, and opening the cross-mode door migrates nothing on its own |
258
+
259
+ `platformBps` is stamped into each vault **at its birth** and is immutable there:
260
+ changing it on `Payd` reaches only vaults that do not exist yet, so a
261
+ creator knows at launch what the platform takes, for good.
262
+
263
+ `migrate` is the only door out of a vault, and it leads to exactly one place: a
264
+ vault of the **same token, same creator, same currency and the same payout
265
+ mode**, that pays holders at least as well and takes no more for the platform. **Not one stock leaves the
266
+ Distributor** — what was already credited stays claimable where it is; the vault's
267
+ own undelivered reserve follows the stream, into the successor's rewards pool and
268
+ pivot reserve, which are one-way pockets there too (`_moveReserve`). It exists
269
+ because only the *current* fee recipient can replace itself, so without it a bug
270
+ would strand a token's fee stream forever.
271
+
272
+ The destination is checked against **the registry, and nothing the destination
273
+ itself says**: `Payd.isVault`, a flag written by `_create` and by nothing else,
274
+ and `Payd.modeOf`, stamped there in the same breath. It used to be `recognised`,
275
+ which walked a chain of
276
+ `setSuccessor` pointers so a vault of another generation could be accepted — and
277
+ that was the largest residual hole in the system: `setSuccessor` took any address,
278
+ and five lines answering `isVault(x) = true` for every `x` opened the migration,
279
+ hence the future flow *and* the reserve, onto anything. No check could close it,
280
+ because everything you read from an unknown contract is written by that contract.
281
+ It existed only because the implementations lived inside the registry as
282
+ `immutable`, so a new vault version forced a new registry. `DistributionFactory` carries
283
+ them now, a new version is born in **this** registry, and the successor chain has
284
+ been deleted rather than guarded.
285
+ [§S24](ARCHITECTURE.md#s24--the-escape-valve-and-why-it-points-at-the-safe)
286
+ records the version that pointed at a maintainer-controlled Safe, and why it no
287
+ longer does.
288
+
289
+ **The mode is the bound that was added rather than removed.** `isVault` says the
290
+ destination was born here; it does not say it pays the same way. While one mode
291
+ exists the two questions have the same answer — the day a second factory builds
292
+ something else they do not, and one timelock operation would move a pro-rata
293
+ stream into another payout shape. What is compared is what the two vaults *pay*,
294
+ not which deployment made them, so a new version of the same mode still migrates:
295
+ that is the entire point of the function. Crossing modes takes a second key that
296
+ is not the timelock's — the generation key opens `crossModeMigration`, shut from
297
+ birth — and even open, it only lets the timelock schedule a migration that must
298
+ still clear 48 h and every other condition above.
299
+ [§S46](ARCHITECTURE.md#s46--one-payout-mode-is-one-factory-and-the-stamp-that-keeps-them-apart)
300
+ has the reasoning and what it deliberately does not do.
301
+
302
+ ## The code
303
+
304
+ ```
305
+ contracts/ FeeVault · Distributor · Payd · DistributionFactory · Treasury
306
+ Collector · Timelock · Bootstrap
307
+ interfaces/ every external ABI, each one read on-chain and dated
308
+ libraries/ TwapFloor + the Uniswap maths it needs, ported to 0.8
309
+ test/ fork tests against live state — no mock on Pons or Uniswap
310
+ Invariants.t.sol what must hold after ANY sequence of calls
311
+ script/ Deploy · DeployPayd · Allowlist · Quotelist · the Measure* recons
312
+ offchain/src/
313
+ snapshot.ts replay Transfer logs, weight each balance by the time it was held
314
+ eligibility.ts who is in the tree — a pure function, replayable
315
+ epoch.ts build the cumulative artifact and both roots
316
+ preflight.ts the five checks that block a publication
317
+ keeper.ts the loop: every step idempotent, restartable with no memory
318
+ dispute.ts recompute a published root from the chain alone
319
+ front/ the claim page: reads the chain, rebuilds the tree, proves
320
+ site/ the shop window
321
+ ```
322
+
323
+ A test that only passes thanks to a mock on Pons or Uniswap is rejected. The
324
+ suite runs against the real chain, with real pools and real stock tokens taken
325
+ from real holders.
326
+
327
+ ```bash
328
+ forge test --fork-url $RPC_URL_FALLBACK --compute-units-per-second 60 -j 1
329
+ pnpm --filter offchain test && pnpm --filter front test
330
+ ```
331
+
332
+ ## What can go wrong
333
+
334
+ Stated in the README for holders, repeated here for readers of the code:
335
+ Robinhood can pause or block a stock token and this protocol is exposed like
336
+ everyone else — a paused stock now costs a skipped leg rather than a failed
337
+ purchase, but it still costs; Pons can redirect the fee stream with three days'
338
+ notice and no veto from the vault; the publishing service stopping means fees pile
339
+ up undistributed until it comes back. And **no external audit has been done** —
340
+ [`SECURITY.md`](../SECURITY.md) says what is in scope and how to report.
341
+
342
+ ## Where to go next
343
+
344
+ | | |
345
+ | :--- | :--- |
346
+ | [`ARCHITECTURE.md`](ARCHITECTURE.md) | 42 decisions with their measurements — including the ones we got wrong |
347
+ | [`recon.md`](recon.md) | Every external address, how it was verified, on what date |
348
+ | [`CONVENTIONS.md`](CONVENTIONS.md) | The rules the code is held to, and the measurement behind each |
349
+ | [`recon-launchpad.md`](recon-launchpad.md) | The same, for what the registry had to learn |
350
+ | [`allowlist.md`](allowlist.md) | The stocks and quotes, with the depth measured behind each |
351
+ | [`../SECURITY.md`](../SECURITY.md) | Scope, the known trade-offs, how to report a finding |
package/docs/SDK.md ADDED
@@ -0,0 +1,322 @@
1
+ # SDK.md — integrating a Payd vault by hand
2
+
3
+ `sdk/README.md` covers the two-line drop-in. This is the other document: every
4
+ call the SDK makes, in order, so you can rebuild exactly the part you want in
5
+ whatever stack you already ship — viem, ethers, wagmi, web3.py, a Go backend, a
6
+ Dune query, a Telegram bot.
7
+
8
+ Nothing here needs a key, a server, or our permission. It is all public state on
9
+ Robinhood Chain.
10
+
11
+ ```
12
+ chain id 4663
13
+ rpc https://rpc.mainnet.chain.robinhood.com
14
+ explorer https://robinhoodchain.blockscout.com
15
+ ```
16
+
17
+ ---
18
+
19
+ ## 1. The one address you need
20
+
21
+ A launch has **one vault**. Everything else hangs off it:
22
+
23
+ ```solidity
24
+ vault.DISTRIBUTOR() → address // where shares are settled
25
+ vault.token() → address // the launched ERC-20
26
+ vault.CREATOR() → address // you
27
+ vault.QUOTE() → address // the currency fees arrive in; 0x0 = native ETH
28
+ ```
29
+
30
+ There is **one Distributor per vault**, cloned at launch. Do not hardcode it,
31
+ and do not reuse another launch's — read it from your vault every time. Same for
32
+ the token: a vault knows its token, so a page that takes both as configuration
33
+ has two chances to be wrong instead of one.
34
+
35
+ `Payd.vaultsOf(creator) → address[]` lists every vault you own, and
36
+ `Payd.vaults()` lists all of them.
37
+
38
+ ---
39
+
40
+ ## 2. What your token pays — `economics()`
41
+
42
+ ```solidity
43
+ vault.economics() → (
44
+ uint256 taxBps, // creator tax on every trade
45
+ uint256 curveFeeBps, // the Pons curve fee
46
+ uint256 ponsShareBps, // the slice of that fee Pons keeps
47
+ uint256 grossOfVolumeBps, // what reaches the vault, as a share of VOLUME
48
+ uint256 rewardsOfVolumeBps, // …of which, to holders as stock
49
+ uint256 creatorOfVolumeBps, // …to you
50
+ uint256 platformOfVolumeBps // …to Payd, fixed at creation, never raisable
51
+ )
52
+ ```
53
+
54
+ **These are shares of traded volume, not of the vault.** That is deliberate: it
55
+ is what a trader actually pays, and the only one of the two figures you can
56
+ compare from one launch to another. `rewardsOfVolumeBps = 250` means *2.5 % of
57
+ every trade comes back to holders as stock* — that is your headline number, and
58
+ it is different for every launch, so it cannot be written into a template.
59
+
60
+ The call reverts to zeros if a Pons getter moves under it. Show nothing rather
61
+ than a stale percentage; that is what the SDK does.
62
+
63
+ ## 3. Is it still working — `hookStatus()`
64
+
65
+ ```solidity
66
+ vault.hookStatus() → (uint8 status, address current, uint64 effectiveAt)
67
+ ```
68
+
69
+ | status | meaning |
70
+ |---|---|
71
+ | 0 | not launched — the vault exists, no token is paying into it yet |
72
+ | 1 | **collecting** — the healthy state |
73
+ | 2 | redirect scheduled — fees will move at `effectiveAt` |
74
+ | 3 | fees lost — the creator wallet on Pons is no longer this vault |
75
+
76
+ Anything other than 1 deserves a visible badge on your page. A card that shows a
77
+ cheerful percentage while status is 3 is lying to your holders.
78
+
79
+ ## 4. The basket — `getAllocations()`
80
+
81
+ ```solidity
82
+ vault.getAllocations() → (address stock, uint24 poolFee, uint16 bps, address feed)[]
83
+ ```
84
+
85
+ Two to eight Robinhood stock tokens (`MIN_BASKET` / `MAX_BASKET`), weights in
86
+ bps summing to 10 000 and no line under `MIN_ALLOC_BPS` (1 000). Read the array's
87
+ length rather than assuming it. One purchase buys
88
+ the *whole* basket, each line by its weight. `poolFee` and `feed` are plumbing —
89
+ the Uniswap tier and the oracle used for the slippage floor. For a UI you want
90
+ `stock` (call `symbol()` on it) and `bps`.
91
+
92
+ `allocationOf(uint256)` and `ROTATION_STRIDE()` **no longer exist** (removed
93
+ 2026-09-09). They survived from a weighted-rotation design — one stock per epoch
94
+ — replaced by buying the whole basket in one transaction. Reading them reverts.
95
+
96
+ They were removed for a measured reason, not for tidiness: `FeeVault` was 26 152
97
+ bytes of runtime, **1 576 above the EIP-170 cap**, and those two dead members
98
+ were 1 322 of the way back under.
99
+
100
+ ## 5. The clock — the Distributor
101
+
102
+ ```solidity
103
+ distributor.currentEpoch() → uint256
104
+ distributor.epochEnd(epoch) → uint256 // unix timestamp
105
+ distributor.EPOCH_LENGTH() → uint256 // 30 min in production
106
+ ```
107
+
108
+ `epochEnd(currentEpoch()) - now` is your countdown to the next buy. Nothing
109
+ about the epoch needs a subscription or a websocket; two reads on page load and
110
+ a local timer are enough, since `epochEnd` only changes when the epoch rolls.
111
+
112
+ ---
113
+
114
+ ## 6. Paying a holder — the settlement path
115
+
116
+ This is the part with a sharp edge. Read it before you write it.
117
+
118
+ ### 6.1 The root
119
+
120
+ Every epoch the keeper publishes one root:
121
+
122
+ ```solidity
123
+ distributor.activeRoot() → uint256 // 0 = none published yet
124
+ distributor.roots(id) → (
125
+ address publisher, uint40 publishedAt,
126
+ bytes32 claimRoot, bytes32 pushRoot,
127
+ uint48 upToEpoch, bytes32 digest
128
+ )
129
+ ```
130
+
131
+ > Decode that tuple **field for field**. Dropping one raises no error in most
132
+ > libraries — the decoder reads a word too early and everything after it shifts
133
+ > by one slot, so `digest` silently returns `upToEpoch` and you never find the
134
+ > data. This has bitten this codebase; it is why the ABI in `front/src/chain.ts`
135
+ > carries a warning comment.
136
+
137
+ ### 6.2 The artifact, and why you must not trust the gateway
138
+
139
+ `digest` is `sha256(the canonical epoch JSON)`. The JSON itself lives on IPFS.
140
+ Two ways to address it, in this order:
141
+
142
+ 1. the `cid` string in the `RootPublished` log for that `rootId` — the only one
143
+ that works once the artifact exceeds one IPFS block, around 160 holders;
144
+ 2. the CIDv1 rebuilt from the digest (`raw`, `sha2-256`) — correct while it fits
145
+ in one block, and your only option if the chain no longer serves old logs.
146
+
147
+ ```
148
+ event RootPublished(uint256 indexed rootId, address indexed publisher,
149
+ bytes32 claimRoot, bytes32 pushRoot,
150
+ uint256 upToEpoch, bytes32 digest, string cid)
151
+ ```
152
+
153
+ **Then hash what you fetched and compare it to `digest` before you use a single
154
+ byte of it.** Gateways are a convenience, not an authority. With the check, a
155
+ hostile or broken gateway can only make your page fail to load. Without it, it
156
+ can invent amounts and addresses in your UI. Try gateways in order and take the
157
+ first whose content matches; `ipfs.io` and `dweb.link` return a 403 Cloudflare
158
+ page to any browser fetch carrying an `Origin` header, so they belong at the end
159
+ of the list, never alone.
160
+
161
+ The JSON:
162
+
163
+ ```jsonc
164
+ {
165
+ "upToEpoch": 412,
166
+ "excluded": ["0x…"], // pool, curve, vault, distributor, CEXs
167
+ "entries": [
168
+ { "holder": "0x…", "stock": "0x…",
169
+ "cumulative": "1234567890", // raw units, since genesis
170
+ "push": true } // large enough to be airdropped
171
+ ]
172
+ }
173
+ ```
174
+
175
+ Shares are **cumulative since genesis**, not per-epoch. One claim settles the
176
+ whole history, one transfer per stock, no matter how long the holder waited.
177
+
178
+ ### 6.3 Two trees, and the mistake that costs a transaction
179
+
180
+ ```solidity
181
+ distributor.claim(address[] stocks, uint256[] cumulative, bytes32[][] proofs)
182
+ ```
183
+
184
+ `claim()` — signed by the holder for themselves — verifies against
185
+ **`claimRoot`**, built over **every** entry.
186
+ `distribute()` — callable by anyone for a third party, and it refunds its own
187
+ gas — verifies against **`pushRoot`**, built over the entries with
188
+ `push: true` only.
189
+
190
+ A proof built on the wrong tree passes every check you can make off-chain and
191
+ reverts on-chain with `InvalidProof`. Build the claim tree from
192
+ `entries`, the push tree from `entries.filter(e => e.push)`, and never mix them.
193
+
194
+ ### 6.4 The tree shape
195
+
196
+ The leaf is a **double keccak** of `abi.encode(address holder, address stock,
197
+ uint256 cumulative)`. Leaves are sorted by hash, laid out backwards in the second
198
+ half of a complete binary tree of `2n-1` nodes, and paired with an ordered hash
199
+ (`a < b ? H(a,b) : H(b,a)`). That is OpenZeppelin's `StandardMerkleTree`, and it
200
+ is what fixes the root.
201
+
202
+ If you are in JS, use `@openzeppelin/merkle-tree` and stop thinking about it.
203
+ If you are anywhere else, port it and test it: **naively pairing sorted leaves
204
+ two by two gives the same root at 2, 3, 4, 6 and 8 leaves and diverges at 5, 7,
205
+ 9** — so every quick trial passes and every real epoch reverts.
206
+ `front/src/merkle.ts` is a dependency-free implementation, checked leaf by leaf
207
+ against the real library from 1 to 200 entries in `front/src/merkle.test.ts`.
208
+
209
+ ### 6.5 Before you send
210
+
211
+ ```solidity
212
+ distributor.owedTo(holder, stock, cumulative) → uint256 // still to be paid
213
+ distributor.claimedSoFar(holder, stock) → uint256 // already received
214
+ ```
215
+
216
+ - **Drop any stock where `owedTo` is 0.** It passes the Merkle check and
217
+ delivers nothing, and the caller pays for its branch anyway.
218
+ - **Re-read `activeRoot` and rebuild the proofs immediately before sending.** A
219
+ tab left open across a publication holds proofs the contract will reject, and
220
+ `_settle` reverts the *whole* batch on the first `InvalidProof` — the wallet
221
+ shows a bare "execution reverted" that explains nothing to your user. One
222
+ extra read costs less than one failed transaction.
223
+ - `owed` is in the stock token's own units. Read its `decimals()`. It is not ETH.
224
+
225
+ ### 6.6 You usually do not have to do any of this
226
+
227
+ Shares arrive on their own: the automatic airdrop runs at most every 24 h, as
228
+ soon as a holder's whole outstanding share is worth about $10. A claim button is a *shortcut* for
229
+ someone who does not want to wait — never the only door. If building section 6
230
+ is more than you want to carry, ship sections 1–5 and link to
231
+ [paydprotocol.eth/app](https://paydprotocol.eth.limo/app/) for the collection.
232
+
233
+ `Collector.collect(account, distributors, stocks, cumulative, proofs)` settles
234
+ several launches in one transaction for holders of more than one Payd token. It
235
+ holds nothing and has no owner; it is a shortcut too, and a broken or replaced
236
+ one never stands between anyone and their share.
237
+
238
+ ---
239
+
240
+ ## 7. Who is not in the tree
241
+
242
+ Excluded from the snapshot, and therefore paid nothing: the Uniswap pool, the
243
+ Pons bonding curve, the vault, the distributor, address 0, and a timelocked
244
+ `excluded[]` list (CEXs, contracts). Read it with
245
+ `distributor.excludedList()`, or `isExcludedAt(account, epoch)` for a past
246
+ epoch.
247
+
248
+ Holders below the value threshold are not in the tree either, and their weight
249
+ is redistributed to the others. The threshold is expressed as the **value of the
250
+ share**, not a percentage of supply — a fixed percentage is too permissive at
251
+ launch and too restrictive later.
252
+
253
+ An epoch's balance is the **time-weighted average over the whole epoch**,
254
+ `∫ balance dt / L`, not a sample. Someone who holds for an instant is paid for
255
+ an instant. If a holder asks why their number looks lower than their balance,
256
+ that is the answer.
257
+
258
+ ---
259
+
260
+ ## 8. Things that will look like bugs and are not
261
+
262
+ | What you see | What it is |
263
+ |---|---|
264
+ | `shares()` returns nothing for a real holder | they are not in the *current* root — bought after the last publication, or below the threshold. Next epoch fixes it. |
265
+ | `activeRoot() == 0` | no root published yet. A brand-new launch. Show "preparing the first payout". |
266
+ | `economics()` returns zeros | a Pons getter moved. Hide the block; do not show 0 %. |
267
+ | the artifact fetch fails on every gateway | the content is fine, the gateways are not. Retry, add gateways. Never fall back to unverified content. |
268
+ | a claim reverts with `InvalidProof` | a new root was published between building and sending, or you used the push tree. See 6.3 and 6.5. |
269
+ | a vault whose `QUOTE` is not ETH refunds no gas | by design — it pays a **bounty** instead: 2 % of what the call moved, in its own currency, capped at `MIN_BUY_QUOTE`. `_refundAmount` is in wei and that vault holds none. |
270
+ | `allowQuotes` reverts `QuoteNotSweepable` | the platform's till has no route to convert that currency back into ether. `Treasury.allowSweeps` has to land first — both are timelock votes, so it is an ordering, not a blocker. |
271
+ | a vault whose `QUOTE` is not ETH uses a floor in its own currency | its delivery floor is 40 % of `MIN_BUY_QUOTE` (~$10) — the same value an ether vault uses, but `quoteSpent` is denominated in the vault's QUOTE there, so a wei constant never matched. Same cadence, different unit. |
272
+
273
+ ---
274
+
275
+ ## 9. Running the cycle yourself — keepers, bots and agents
276
+
277
+ Every step that moves a launch's money forward is **open to anyone** and pays
278
+ whoever calls it. A keeper runs them today; nothing stops a second one, and more
279
+ callers only make the cycle livelier — none of these calls gives its caller any
280
+ say over where the money goes.
281
+
282
+ | Call | On | What it does | What the caller gets |
283
+ |---|---|---|---|
284
+ | `harvest()` | vault | pulls creator fees out of the Pons escrow and splits them | ETH vault: its gas back, out of the rewards share. Other quotes: a bounty, `keeperBountyBps` of what it moved, in the vault's own currency |
285
+ | `buyBasket(uint256[] minOuts)` | vault | buys the whole basket for every closed epoch not yet bought | the same refund or bounty, capped |
286
+ | `distribute(account, stocks, cumulative, proofs)` | distributor | pushes one holder's share to **that holder** | its gas back, bounded by the value delivered — on an ETH vault only; on any other quote the delivery budget is zero and the call pays nothing |
287
+ | `payCreator()` / `payPlatform()` | vault | sends an accrued share to its fixed address | nothing — but it costs nobody anything either |
288
+
289
+ Four things that save a wasted transaction:
290
+
291
+ - **`minOuts` can only tighten.** The vault applies `max(minOuts[i], its own
292
+ oracle floor)` per leg, so an array of zeros of the basket's length is a valid
293
+ call and the slippage bound still holds. Never derive a floor from a model or
294
+ a quote API and pass it as a loosening — it cannot loosen, it can only make
295
+ your own call fail.
296
+ - **Simulate first.** `NothingToDo` (nothing to harvest, no closed epoch to buy)
297
+ and `NothingDelivered` (the share already left) are the normal answers when
298
+ another caller got there first. A reverted call is paid nothing.
299
+ - **`distribute` takes the push tree.** Entries with `push: true` only, proofs
300
+ built from `entries.filter(e => e.push)` — section 6.3.
301
+ - **A wallet that refuses ETH within 30 000 gas is not lost money.** The refund
302
+ is set aside instead of sent, and the caller collects it with the contract's
303
+ own `withdraw()`. Smart-contract and 7702-delegated agent wallets hit this.
304
+
305
+ What stays closed: `publishRoot`. A root names who is owed what, so it takes the
306
+ distributor's keeper or an address `Payd.isKeeper` names — both behind the
307
+ timelock.
308
+
309
+ ---
310
+
311
+ ## 10. Source of truth
312
+
313
+ Everything above is read from the contracts in `contracts/`. When this document
314
+ and the code disagree, the code wins — and the disagreement is a bug worth
315
+ reporting.
316
+
317
+ - `contracts/FeeVault.sol` — fees in, basket out
318
+ - `contracts/Distributor.sol` — roots, claims, exclusions
319
+ - `contracts/Payd.sol` — the vault index
320
+ - `sdk/src/payd.ts` — a working implementation of this whole document, ~300 lines
321
+ - `front/src/merkle.ts` + `merkle.test.ts` — the tree, and its proof that it is right
322
+ - `docs/ARCHITECTURE.md` — why each of these choices, at length