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.
The two payment shapes are attributable through one self-deployed instance each:
- the
ERC20PeriodTransferEnforcerfor subscriptions; - the
ERC20StreamingEnforcerfor streams.
Ten instances in total are now routed through, since the
Agent DeFi mandates carry their own caveats — balance-change,
exact-execution, redeemer, limited-calls and the functionCall scope enforcers.
Routing every caveat a mandate uses through a HourGlass instance is deliberate:
it makes the whole mandate attributable rather than one caveat of it, and the
discovery side matches on those same addresses, so a mandate signed against the
canonical enforcer would never be rediscovered.
The dashboard shipped today still derives its metrics from the two payment enforcers; the agent rails are attributable by the same mechanism but are not yet broken out in the UI. The rest of the enforcer suite is deployed under the same salt and emits nothing, since no delegation references it. Adding a 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:
ERC20StreamingEnforcer.IncreasedSpentMap. A stream is claim-based and the event carries a cumulativespent, so the per-claim amount is the delta between consecutive events for a delegation — it never resets, unlike the period enforcer's 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 ever charged, 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 runs from the enforcers' deployment block to head, so the totals are lifetime figures rather than a recent-window sample. The scan cost grows with history — a subgraph is the drop-in upgrade once that becomes the bottleneck.