HourGlass

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:

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 cumulative transferredInCurrentPeriod, 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 cumulative spent, 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:

MetricDerivation
Charge / claim countcount of per-charge events
Amountperiod delta (subscriptions) or claimed amount (streams)
Volume over timesum of amounts bucketed by timestamp
Per agreementgroup by delegationHash
Per receivergroup by redeemer
Per payerjoin delegationHashRedeemedDelegation.rootDelegator
Per tokenthe token field
Active agreementsdistinct 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, no localStorage dependency — 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.

On this page