Analytics
On-chain, verifiable analytics — and the attribution problem it has to solve.
HourGlass aims for a decentralized, verifiable analytics surface: charge count and token volume over time, broken down by agreement, receiver, payer, and token — all derived from public on-chain data, with no private backend required.
The attribution problem
HourGlass has no contract of its own, and it is local-first (agreements live in the
payer's localStorage). A charge is a call to the shared DelegationManager
plus an ERC-20 Transfer. Nothing on-chain says "HourGlass": the enforcer
addresses are shared across every app on the Delegation Framework, parties vary
per agreement, and the salt is unguessable. So every analytics approach reduces to
one question: how do we plant an HourGlass-attributable marker on-chain that
needs no central registry?
The marker: redeploy the audited enforcers
Deploy HourGlass's own instances of MetaMask's audited enforcers (unmodified
bytecode) and route new agreements to them, then index those contracts' events.
The emitter address of a caveat event is the enforcer instance — so attribution
becomes a one-key filter (WHERE contract_address = <our enforcer>) with no
central list of delegation hashes. This works precisely because the app is
local-first.
This now spans both payment shapes, so attribution rests on two self-deployed instances:
- the
ERC20PeriodTransferEnforcerfor subscriptions; - the
ERC20StreamingEnforcerfor streams.
The rest of the enforcer suite is deployed under the same salt but emits nothing today, since no delegation references it yet. Adding a payment shape later means indexing one more emitter address, not a new deployment — the addresses are in Deployments.
The security implications of self-deploying audited bytecode are covered in Security.
The on-chain trace
Two sources carry everything the dashboard needs.
1. DelegationManager.RedeemedDelegation — once per redemption, for both
shapes. Carries the root delegator (payer), the redeemer (receiver), and the full
delegation struct including caveats and the salt (= the pinned-agreement hash).
2. The enforcer's per-charge event — filtered by our enforcer address:
- Subscriptions:
ERC20PeriodTransferEnforcer.TransferredInPeriod. The per-charge amount is the in-period delta of the cumulativetransferredInCurrentPeriod, which resets each period — handle the period-boundary reset when computing it. - Streams: the streaming enforcer's transfer/claim event. A stream is claim-based, so the per-claim amount is read directly rather than via a period delta.
Confirm the exact streaming event name and field layout against
@metamask/delegation-abis before building the indexer — do not assume it mirrors
the period enforcer. Streams accrue and are claimed, so the amount semantics
differ from the period enforcer's cumulative-with-reset counter.
Deriving the dashboard
From these events, filtered to our enforcer instances:
| Metric | Derivation |
|---|---|
| Charge / claim count | count of per-charge events |
| Amount | period delta (subscriptions) or claimed amount (streams) |
| Volume over time | sum of amounts bucketed by timestamp |
| Per agreement | group by delegationHash |
| Per receiver | group by redeemer |
| Per payer | join delegationHash → RedeemedDelegation.rootDelegator |
| Per token | the token field |
| Active agreements | distinct delegationHash charged in window, cross-referenced with DisabledDelegation for revokes |
Indexing
- Shipped now — direct client-side reads. The analytics page
reads the per-charge events straight from the HourGlass enforcer instances via
eth_getLogs, decodes them in the browser, and derives the metrics above. No backend, no subgraph, nolocalStoragedependency — it works because attribution is just the emitter address. - Both chains are swept. Ethereum mainnet and Base are scanned in parallel and the results merged, each charge tagged with its chain. Because the enforcer addresses are identical on both (same CREATE2 salt), one address filter serves both — see Deployments. Amounts are grouped by chain and token, never summed across chains: the same symbol on two chains is two different assets. If one chain's RPC fails, the page shows the other chain's data and names the gap rather than presenting a partial total as complete.
- Scale path — a subgraph. For larger histories, a subgraph keyed on the same
events (plus
RedeemedDelegation/DisabledDelegation) replaces the live scan behind the same UI. A Dune dashboard can decode the same events for sharing.
Status
The analytics page is live and reads on-chain directly. Only agreements routed to the self-deployed enforcer instances are attributable; the in-browser scan covers a bounded recent block window (ample for the current deployment), and a subgraph is the drop-in upgrade for unbounded history.