A synthetic dollar that earns perpetual funding is making a claim about money that moved on an exchange, and the exchange keeps its own record of whether it moved. So there is a question worth putting to any basis-trade protocol: do the venue's records agree with the dashboard? For any account whose address is public, a stranger can answer that from outside, without permission and without asking anyone.
This post is that check, run on ourselves. It is three calls, a forty-line script, and every number printed next to the denominator it was earned on. Where our own published figures could be wrong, they are checked against the venue rather than against us. The method transfers to any protocol that discloses its hedge account, and most do not disclose one.
The three calls
All three are unauthenticated POSTs to api.hyperliquid.xyz/info. No key, no account, nothing to sign.
- userFunding. Every funding payment the account received or paid, signed. This is the carry.
- userFillsByTime. Every fill, with its fee and its realized closed PnL. This is the cost side.
- fundingHistory. The venue's own rate series for the market, with no account in it at all. This is the one nobody makes, and it is the one that turns "our ledger says X" into "our ledger and the exchange agree".
The account is 0x09a2780ac8Be6D5d2d1F85A8D92b09D40C9CA37e, the same address that signs every hourly attestation at /api/por/signed. The window starts at the first protocol mint, 2026-05-14.
First, the trap: a record is not an hour
The raw funding response holds 284 records. Reading that as 284 hourly funding events is the natural thing to do and it is wrong by a factor of nearly seven, because Hyperliquid compresses older funding history into daily buckets. Each record carries an nSamples field: when it is present, that record is that many hours folded into one entry, its szi is the sample-weighted mean position over the bucket, and its fundingRate is the mean hourly rate over it.
On this account, 69 records carry nSamples 24, three carry 14, 4 and 20, and 212 are single hours. That is 1,906 position-hours across 284 records, a compression factor of 6.71. Anyone annualising off the record count lands 6.71 times too high, in the protocol's favour if the protocol is doing the sum and against it if a critic is. The rule is one line: sum delta.nSamples, never len(). The endpoint now publishes positionHours for exactly this reason, and the arithmetic is pinned by unit tests so it cannot quietly regress.
The numerator and the denominator, in the same sentence
Here is the realized ledger, replayed from the raw API, stamped 2026-08-13 19:00 UTC. It matches what /api/funding-attribution publishes to the fourth decimal, on every total and in each of the four monthly buckets.
| measure | value | what it is |
|---|---|---|
| fundingUsd | 1.007301 | funding received, signed, negative hours never clamped |
| feesUsd | 3.087340 | taker fees across 123 fills |
| closedPnlUsd | 52.919313 | short leg only, offset by spot, not profit |
| positionHours | 1,906 | of 2,204 calendar hours, so 86.5 percent uptime |
| mean notional | $73.02 | time-weighted mean short over those hours |
| annualised | 6.340% | funding divided by USD-hours, times 8,760 |
Read the funding figure without its denominator and it says nothing at all. A dollar earned on a hundred million dollar book would be a scandal. The same dollar earned on a time-weighted mean short of 73.02 dollars held for 1,906 hours is 6.34 percent annualised, which is a figure you can hold next to any other funding rate in the market. The denominator carries the whole meaning, which is why it now ships inside the JSON rather than living in a footnote: positionHours, notionalHoursUsd, timeWeightedNotionalUsd and annualisedOnNotional are fields on the endpoint.
The denominator is derived by inverting the venue's own definition of a funding payment, usdc = rate x notional x hours, so it needs no price feed and no second data source. Because that is self-referential, it was cross-checked against a fully independent derivation, the signed position size times the hourly ETH candle close from the same venue: 137,945 against 139,185 USD-hours, agreeing to within 0.9 percent, giving 6.40 percent against 6.34. Both are stated rather than the flattering one.
Did the account actually get the venue's rate? 212 of 212.
An annualised rate computed from an account's own records is still that account's records. The third call settles it. 212 of the 284 records are unbucketed single hours, so each one can be matched against Hyperliquid's published fundingHistory rate for ETH at that same hour.
All 212 match. Byte-identical, zero exceptions, no rounding tolerance applied. The account is a price taker and received exactly the rate the exchange published to everybody, which rules out the whole class of explanations in which a protocol's funding income comes from something other than the market: no venue rebate, no maker incentive folded into carry, no quiet dropping of the bad hours. It confirms the sign convention in the other direction too, because 26 records covering 246 position-hours are negative, totalling minus 3.46 cents, and they subtract.
And is the model's input the venue's number?
Kerne's published yield model takes one funding figure as an input. /api/apy carries it as avgAnnualFunding, with fundingSource: hyperliquid-60d-trailing and fundingEntries: 1440. On 2026-08-13 it read 0.0728.
The last 1,440 hourly ETH funding rates Hyperliquid published, averaged and multiplied by 8,760, come to 7.2811 percent. The record count matches, the window matches, and the figure reproduces. Checking it takes about ten seconds, and a trailing-window input is one of the easier places in any yield model to quietly choose a friendlier window, so it is ten seconds worth spending on anybody's.
Where the difference goes, decomposed
The account realized 6.34 percent over its life against a model input of 7.28 percent, and a fair comparison has to run over the same span, because the input is a 60-day trailing figure while the account is 91 days old. Over the same 60 days:
| measured over the 60 days ending 2026-08-13 | annualised |
|---|---|
| venue mean over all 1,440 hours, the model's input | 7.281% |
| venue mean over only the 1,148 hours the account held a position | 6.349% |
| what the account realized, notional-weighted | 6.635% |
Read those three rows in order and the difference resolves cleanly. Against the model's input the account captured 91.1 percent. Against the venue rate over the hours it was actually in the market it captured 104.5 percent, slightly above, because the book was larger during the richer hours. The shortfall is uptime rather than mechanism. The account held a position for 1,148 of 1,440 hours, 79.7 percent, and the hours it missed were better than average ones. Nothing leaked between the venue's rate and the account's balance; there were simply hours with no position in them.
Uptime is a real limitation and it belongs to the protocol rather than to the market. Unlike the funding rate itself, it is also something an operator can act on.
The fee line, which is three times the funding line
Fees came to 3.087340 dollars against 1.007301 of funding, a ratio of 3.06. That inversion is the first thing a skeptical reader should notice, so here is what the ledger says about it rather than a characterisation of it.
Every one of the 123 fills is a taker fill priced between 4.499 and 4.500 basis points, which is Hyperliquid's standard taker rate. There is no adverse pricing and no hidden cost in there. The bill is turnover. The account traded 6,860.90 dollars of notional against a mean book of 73.02, which is 94.0 times its own size in 91 days. At a flat 4.5 basis points each way a full round trip of the book costs about 9 basis points, while a day of carry at 6.34 percent earns about 1.75, so re-hedging the entire book once a day costs roughly five days of funding.
It also localises, which is the useful part. 95 of the 123 fills fall inside three days, 2026-06-03 to 06-05, during which the position cycled between two target sizes roughly hourly. Those three days traded 5,433.11 dollars of notional and account for 2.4449 dollars, 79 percent of the entire lifetime fee bill. Every other fill across the remaining 88 days cost 64 cents between them. The fee rate is the venue's standard one and the book is tiny; what put the fee line above the funding line is how often the position was rebalanced, and that is the one line item on this ledger an operator directly controls. The account has executed no fills since 2026-07-22, 22.6 days at the time of writing.
Read fundingUsd, not netUsd
The same endpoint reports netUsd of 50.839274, which reads like a fifty dollar profit. It is not one, and quoting it is the most misleading thing anybody could do with this dataset. Of it, 52.919313 is closedPnl: the realized price PnL of the short leg alone. The offsetting spot leg is on chain and is not in this ledger. In a delta-neutral book the price PnL of one leg is by construction cancelled by the other, so a large positive number there means ETH fell while the short was open, and nothing more. The carry is fundingUsd, and the carry is one dollar and seven tenths of a cent.
The flag on this same account, stated here rather than left to be found
Anyone who pulls the hedge account will also pull the attestation, so it should be read here first. /api/por/signed currently reports status: WARNING_DELTA, delta_neutral: false, hedge_base_eth: 0 and offchain_net_delta: 1. In the same signed payload it reports is_solvent: true, psm_solvent: true and aggregate_solvency_ratio: 1.001064.
Both are true, and they measure different things. Net delta is defined in the payload as abs(1 - short / hedge_base_eth), where the base is hedge_base_vault_exposed_eth alone: the ETH-price-exposed collateral that backs kUSD. That leg is exactly zero, because kUSD is backed by USDC in the peg stability module and the attested vault holds no user deposits, so its share supply is zero. A short measured against a base of zero scores as fully directional by construction, which is what returns 1 and raises the flag. It is a hedge-tracking flag rather than a backing shortfall, it has been the expected steady state since schema 8, and it clears when the hedge runs against collateral that actually backs kUSD and not before. The alternative, widening the denominator until the flag turns green, was done once in schema 6 and is recorded in the schema changelog rather than quietly reverted.
What the hedge engine actually sizes against is a disclosed, founder-custodied ETH float that backs no kUSD and enters no solvency ratio. The payload publishes that book too, separately: disclosed_float_eth 0.00515519 against a short of 0.0079, so disclosed_float_delta reads 0.532437 and the account is net short roughly 0.0027 ETH, about five dollars at the payload's own ETH price. Two books, two deltas, both signed hourly, and neither is summed into the other. At this size the residual is five dollars. It is published anyway, because a field like that only means anything if it was already there before the number got large.
What this establishes, and what it does not
It establishes one thing precisely. The funding leg of this strategy transmits the venue's published rate into the account without leakage, and the trailing figure the yield model takes as its input is the venue's own number. Both are checkable by a stranger in about a minute, against a counterparty with no stake in the answer.
It does not establish that the rate persists, that a hedge held 86.5 percent of the time is a hedge held all of the time, or that a 73 dollar book behaves like a large one. Above all it does not establish that anyone has been paid. The realized yield of the staked token has been zero for its entire life, which is published as its own number on the honesty index and in the realized block of /api/apy. A mechanism that works and a holder who has been paid are different claims, and only the first one is being made here.
The generalisable part is the method. If a basis-trade protocol will not name its account, the check cannot be run at all, which is worth knowing on its own. If it names the account, the three calls take about a minute, and the compression trap is the main thing standing between a reader and the real denominator.
Run it yourself
This is the whole thing. No key, no dependencies beyond the standard library, and it printed every headline figure in this post.
import json, urllib.request
A = "0x09a2780ac8Be6D5d2d1F85A8D92b09D40C9CA37e" # Kerne's hedge account
T0 = 1778716800000 # 2026-05-14T00:00:00Z
def info(body):
req = urllib.request.Request("https://api.hyperliquid.xyz/info",
data=json.dumps(body).encode(), headers={"Content-Type": "application/json"})
return json.load(urllib.request.urlopen(req, timeout=30))
def paged(body):
out, t = {}, T0
while True:
page = info({**body, "startTime": t})
if not page: return sorted(out.values(), key=lambda r: r["time"])
for r in page: out[(r["time"], json.dumps(r, sort_keys=True))] = r
nt = max(r["time"] for r in page)
if nt <= t: return sorted(out.values(), key=lambda r: r["time"])
t = nt + 1
fund = paged({"type": "userFunding", "user": A})
fills = paged({"type": "userFillsByTime", "user": A})
hist = paged({"type": "fundingHistory", "coin": "ETH"})
# A RECORD IS NOT AN HOUR. Hyperliquid folds older funding history into daily
# buckets; delta.nSamples is how many hourly samples are in the record.
n = lambda r: (r["delta"].get("nSamples") or 1)
usd = lambda r: float(r["delta"]["usdc"])
rate = lambda r: float(r["delta"]["fundingRate"])
funding = sum(usd(r) for r in fund)
hours = sum(n(r) for r in fund)
notional_h = sum(abs(usd(r) / rate(r)) for r in fund if rate(r)) # USD-hours
fees = sum(float(f["fee"]) for f in fills)
pnl = sum(float(f["closedPnl"]) for f in fills)
print(f"funding ${funding:>12.6f}")
print(f"fees ${fees:>12.6f}")
print(f"closedPnl ${pnl:>12.6f} short leg only, offset by spot, NOT profit")
print(f"records {len(fund)} -> position-hours {hours}")
print(f"mean notional ${notional_h / hours:>12.2f}")
print(f"annualised on notional {funding / notional_h * 8760 * 100:.3f}%")
# The third call has no account in it: the venue's own rate series.
series = [float(r["fundingRate"]) for r in hist]
last60 = series[-1440:]
print(f"venue mean, last 1440 hours {sum(last60) / len(last60) * 8760 * 100:.4f}%")
byhr = {r["time"] - r["time"] % 3600000: float(r["fundingRate"]) for r in hist}
hourly = [r for r in fund if not r["delta"].get("nSamples")]
same = sum(1 for r in hourly if byhr.get(r["time"] - r["time"] % 3600000) == rate(r))
print(f"hourly records matching the venue series {same}/{len(hourly)}")
Every figure above is a measurement stamped to a single read at 2026-08-13 19:00 UTC, and the script is the claim rather than the numbers. Funding accrues hourly, so a later run returns a larger fundingUsd, a different mean notional and a different annualised rate; the published endpoint also caches for an hour, so a fresh replay can lead it by up to one funding payment. The 7.2811 percent figure is a trailing statistic of the ETH perpetual market measured on that date, not a Kerne rate, not a forecast and not a promise of anything. Nothing here is investment advice. The ledger and its denominator are at /api/funding-attribution, the model input at /api/apy, the signed reserve and hedge state at /api/por/signed, and the live funding regime at /funding. Everything outside Kerne that has checked any part of this is listed on the independent verification page.
Verify it yourself
Run the same check on any reserve, or have it run for you.
Paste any issuer's signed attestation into the free verify tool and recover the signer, rehash the figures, and check freshness in your own browser. If you would rather have it run for you, there is a machine-signed instant read of an address you name, a human-reviewed version of the same read, a commissioned teardown like this one on any target, and an independent read of a counterparty you hold or allocate to. Rates are on each page. Attestation tooling, not an audit, and not a solvency opinion.