{"id":51929943,"url":"https://github.com/rickstaa/livepeer-circular-demand-check","last_synced_at":"2026-07-28T06:01:08.417Z","repository":{"id":359374298,"uuid":"1245763665","full_name":"rickstaa/livepeer-circular-demand-check","owner":"rickstaa","description":"Small script to check circular livepeer demand for a given time period.","archived":false,"fork":false,"pushed_at":"2026-05-21T15:08:35.000Z","size":7,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-22T00:11:13.169Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rickstaa.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-21T14:30:42.000Z","updated_at":"2026-05-21T15:17:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/rickstaa/livepeer-circular-demand-check","commit_stats":null,"previous_names":["rickstaa/livepeer-circular-demand-check"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/rickstaa/livepeer-circular-demand-check","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rickstaa%2Flivepeer-circular-demand-check","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rickstaa%2Flivepeer-circular-demand-check/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rickstaa%2Flivepeer-circular-demand-check/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rickstaa%2Flivepeer-circular-demand-check/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rickstaa","download_url":"https://codeload.github.com/rickstaa/livepeer-circular-demand-check/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rickstaa%2Flivepeer-circular-demand-check/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35979044,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-07-28T02:00:06.341Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-28T06:01:07.722Z","updated_at":"2026-07-28T06:01:08.404Z","avatar_url":"https://github.com/rickstaa.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# circular-tickets\n\nFind \"circular\" winning-ticket redemptions on the Livepeer Arbitrum subgraph:\nevents where the **sender** (gateway/broadcaster) address equals the\n**recipient** (orchestrator/transcoder) address — i.e. an operator paying\nfees to themselves.\n\nThe script aggregates totals across all gateways and breaks them down per\nsender address.\n\n## Prerequisites\n\n- Python 3.9+\n- A free API key from [The Graph Studio](https://thegraph.com/studio/apikeys/)\n\n## Install\n\n```bash\npip install -r requirements.txt\nexport THEGRAPH_API_KEY=\u003cyour-key\u003e\n```\n\n## Run\n\n```bash\npython circular_tickets.py                   # all-time\npython circular_tickets.py --since 30d       # last 30 days\npython circular_tickets.py --since 2025-01-01\n```\n\nOptional flags:\n\n| Flag             | Default                   | Purpose                                                           |\n| ---------------- | ------------------------- | ----------------------------------------------------------------- |\n| `--since`        | _all-time_                | `Nd` for a relative window, `YYYY-MM-DD` for an absolute UTC date |\n| `--tickets-csv`  | `circular_tickets.csv`    | Per-event output path                                             |\n| `--gateways-csv` | `circular_by_gateway.csv` | Per-gateway aggregate output path                                 |\n\n## Output\n\nConsole:\n\n- **Total protocol fees** in the selected window (ETH + USD)\n- **Circular fees** (self-payments) in the same window — count, ETH, USD\n- **Circular share** — circular fees as a percentage of total protocol fees\n- **Per-gateway breakdown** sorted USD-descending\n\nTwo CSVs:\n\n- `circular_tickets.csv` — one row per matched event (`timestamp_utc`, `tx_hash`, `sender_recipient`, `face_value_eth`, `face_value_usd`)\n- `circular_by_gateway.csv` — per-sender aggregates sorted USD-descending\n\n### How USD values are computed\n\nUSD figures are the **spot value at ticket-redemption time**, as recorded by\nthe subgraph when each `WinningTicketRedeemed` event was indexed. Nothing is\nmarked-to-current — a ticket redeemed at $3000/ETH still counts as $3000/ETH\nin this report, even if ETH is $4000 today. This applies to both the total\nprotocol fees and the circular total, so the percentage comparison is\napples-to-apples within the window.\n\n## How it works\n\n1. Fetch all `Transcoder` entities with non-zero fee volume (the only addresses that could possibly receive a self-payment).\n2. For each, query `winningTicketRedeemedEvents` where `sender == recipient == \u003caddr\u003e` — the subgraph indexes both fields, so this filter runs server-side.\n3. Parallelize the per-orchestrator queries across 16 worker threads with `tqdm` progress.\n4. Aggregate and write CSVs.\n\nThe subgraph schema doesn't support comparing two fields in a `where` clause\n(`sender == recipient` directly), which is why we iterate per-orchestrator\ninstead of paginating every ticket and filtering client-side. The per-orch\napproach transfers far less data over the wire.\n\n## Limitations\n\nThis only catches **literal** self-payments (`sender == recipient` on-chain).\nOperators running a gateway and an orchestrator under **different** addresses\nwill not show up — detecting that requires an external operator-to-address\nmapping or clustering heuristics.\n\n---\n\n## Mapped operators: `mapped_tickets.py`\n\nTo catch the broader case where an operator runs gateways and orchestrators\nunder **different** addresses, supply an external mapping and run\n`mapped_tickets.py`. It queries `WinningTicketRedeemed` events where the\nsender is one of the operator's gateways and the recipient is one of the\nsame operator's orchestrators, and reports the per-operator and combined\nshare of total protocol fees.\n\n### Mapping format\n\nCopy `operators.example.json` to `operators.json` and edit:\n\n```json\n[\n  {\n    \"name\": \"operator-a\",\n    \"gateways\":      [\"0x...\", \"0x...\"],\n    \"orchestrators\": [\"0x...\"]\n  }\n]\n```\n\nYou can list as many operators as you like. The address sets can be derived\nhowever you want — explorer.livepeer.org broadcasting/orchestrator pages,\nlivepeer.tools, ServiceRegistry / AIServiceRegistry ServiceURI hostnames,\nor off-chain disclosure.\n\n`operators.json` is gitignored; `operators.example.json` is checked in as a\ntemplate.\n\n### Helper: discovering orchs by ServiceURI\n\n`fetch_registry_orchs.py` scans the on-chain `ServiceRegistry` and\n`AIServiceRegistry` for every address that has ever set a `ServiceURI`\nmatching a caller-supplied substring. Useful when you suspect an operator\nruns many orchestrators under one hostname.\n\n```bash\npython fetch_registry_orchs.py --match example.com\n# writes registry_orchs.json — paste matched addresses into operators.json\n```\n\nAn archive RPC (Alchemy/Infura/Ankr) is strongly recommended over the\npublic Arbitrum RPC for full-history scans.\n\n### Run\n\n```bash\npython mapped_tickets.py --operators operators.json\npython mapped_tickets.py --operators operators.json --since 30d\npython mapped_tickets.py --operators operators.json --since 2025-01-01\n```\n\nOutput:\n\n- Console: per-operator fee totals, per-(sender, recipient) breakdown,\n  and each operator's share of total protocol fees in the window.\n- `mapped_tickets.csv` — one row per matched event with an `operator`\n  column.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frickstaa%2Flivepeer-circular-demand-check","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frickstaa%2Flivepeer-circular-demand-check","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frickstaa%2Flivepeer-circular-demand-check/lists"}