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.

This now spans both payment shapes, so attribution rests on two self-deployed instances:

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

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 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, 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 covers a bounded recent block window (ample for the current deployment), and a subgraph is the drop-in upgrade for unbounded history.

On this page