YouTube OAuth and analytics
hermes · confidence high · status active
tags: youtube oauth analytics cron credentials
Now
OAuth is RESTORED as at 2026-10-02 17:29 AEST, after 24 days on public fallback. Dwayne
completed the browser consent; the loopback listener exchanged the code and wrote a new token
with all three scopes. Verified live, not just from the token file: channels.list(mine=true)
returns UC1MD4A9Ok_xbTOh7Xmb-3mw / @grand_gold_rl / 731 subs / 89,663 views / 68 videos, and
youtube_analytics_fetcher.py prints Source: oauth-mine-true. A second refresh against the
new refresh token succeeded (token: refreshed), so it is not a one-shot token.
Two caveats that survive the fix:
- The report's CTR claim was wrong and I repeated it. The Analytics API has no
impressions or thumbnail-CTR metric.
impressions,impressionCtr,impressionsClickThroughRate,videoThumbnailImpressionsandvideoThumbnailImpressionsClickRateare all rejected by the API, and the official metric reference lists only legacyannotationClickThroughRate/annotationCloseRate(which return 0 here). CTR, retention curves and video-level subscriber conversion are Studio-only and no re-auth can produce them. Restored by OAuth: watch minutes (estimatedMinutesWatched= 6),averageViewPercentage(56.36), engaged views, likes, comments, shares,subscribersGained/Lost, and the video dimension. - The sprint ledger is still stale, and that is now the only blocker to honest reporting.
youtube-growth-sprint.jsonis unchanged since 2026-05-16; its window ended 2026-05-26.sprint_progressclamps elapsed toduration_days, so every job still prints "day 14/14" and the+645/+22508/+7deltas are all-time from the 2026-05-16oauth_baseline, not sprint measures. The ledger also carries abaselineblock for a different, real channel:UC1BDGDirPEwEMbKJcMeTQbw(@grandgoldrl, title "Grandgold", 251 subs / 201,003 views / 207 videos). Two distinct channels, handles differing only by case.
Channel tooling for @grand_gold_rl (channel UC1MD4A9Ok_xbTOh7Xmb-3mw) runs across three
scheduled jobs: youtube-growth-optimizer (10:30 daily), youtube-weekly-analytics (10:10
Sunday), zappo-youtube-growth-autoresearch (03:17 daily), plus youtube-combined
(08:00/20:00, youtube_oauth_status_report.py). All read through
~/.hermes/scripts/youtube_sprint_common.py, which prefers OAuth mine=true and falls back
to scraping the public channel page.
The fallback masks OAuth failure. When OAuth fails, every job prints a short
Data warning: oauth fetch failed ... line and continues on public stats. Public stats give
subscribers, views and video count only.
OAuth state as at 2026-09-13, the three faults that were found then:
invalid_grant: the refresh token is expired or revoked. A direct POST tohttps://oauth2.googleapis.com/tokenreturns HTTP 400 with{"error": "invalid_grant", "error_description": "Token has been expired or revoked."}. The stored credentials are internally consistent (tokenclient_idandclient_secretboth matchclient_secret.json, projectgen-lang-client-[redacted-id]), so this is not a config mismatch. No automated retry can fix it. This needs a human re-auth.- The self-heal script was deleted.
youtube_oauth_status_report.pycalled~/.hermes/scripts/youtube_oauth_self_heal.py, which did not exist, soSELF_HEAL.exists()was False and every refresh attempt silently no-opped. Restored 2026-09-13 from the surviving.pycstrings. It now delegates to~/clawd/scripts/youtube-oauth.py refresh-silentand logs the real outcome to~/Library/Logs/youtube-oauth-self-heal.log. It also reportsself_heal_unavailablerather than failing silently if the refresh tool ever goes missing again. - The live token has never carried the analytics scope.
youtube_oauth_setup.pyrequested onlyyoutube.readonlyandyoutube.force-ssl, and the stored token matches that set.analytics_summary()callsyoutubeanalytics.googleapis.com/v2/reports, which needsyt-analytics.readonly. So weekly analytics could not have worked even with a valid token. Fixed inyoutube_oauth_setup.py(commit72050a7in~/clawd, unpushed).
The token was last written 2026-09-08 03:17 AEST and was rejected by 04:17 the same morning. Failure has recurred daily since 2026-09-08.
How to re-auth
Use ~/clawd/scripts/youtube-oauth.py, not youtube_oauth_setup.py. The latter still writes
its URL to skills/youtube-analytics/oauth-auth-url.txt with a stale localhost:8765
redirect and the old scope set.
youtube-oauth.py urlprints a consent URL requestingyoutube.readonlyandyt-analytics.readonly,access_type=offline,prompt=consent select_account, redirecthttp://localhost:8080/.youtube-oauth.py exchange <URL_OR_CODE>saves the access and refresh token to~/clawd/skills/youtube-analytics/.youtube-tokens.json(mode 600).youtube-oauth.py refresh-silentrefreshes without printing.youtube-oauth.py statusprints redacted state as JSON.
All three redirect forms were probed live on 2026-09-13: http://localhost:8080/,
http://localhost:8765/ and bare http://localhost each return invalid_grant (the probe
code) rather than redirect_uri_mismatch, so the configured http://localhost registration
covers all of them.
Facts worth keeping
- Analytics needs
yt-analytics.readonly.youtube.readonlyalone serves the Data API (channels?mine=true) but not the Analytics API. Two different endpoints and two different scopes; conflating them is what hid fault 3. force-sslis requested but nothing locally uses an upload or write path, so it is not currently load-bearing.- The OAuth client is an installed app with
redirect_uris: ["http://localhost"]and lives at~/clawd/skills/youtube-analytics/client_secret.json. - Refresh tokens issued by this client have failed repeatedly, including one issued 2026-09-08 that died within an hour. If the next re-auth also dies quickly, suspect the Google Cloud project's OAuth consent screen being in Testing status, which expires refresh tokens after 7 days. That is unverified (it needs the Cloud console) and is the first thing to check.
- Never print token values, client secrets, or auth URLs in cron output. The self-heal script and its tests assert this.
Log
-
2026-10-02 17:29 AEST (resolution, live probes): OAuth restored after 24 days. Re-auth path used was
~/clawd/scripts/youtube_oauth_setup.py listen --port 8765(not the olderyoutube-oauth.pyrecommended on 2026-09-13); it bound127.0.0.1:8765, wrote the consent URL toskills/youtube-analytics/oauth-auth-url.txt, and on redirect printedtoken: saved. New token: three scopes (youtube.readonly,youtube.force-ssl,yt-analytics.readonly),updated_at2026-10-02 17:29:02, mode 600. Pre-change token backed up to.youtube-tokens.json.bak-20261002-171810. A follow-up refresh against the new refresh token returnedtoken: refreshed, so the grant is durable, not one-shot.Verified by live API, not by reading the token file:
channels.list(mine=true)returned exactly one item,UC1MD4A9Ok_xbTOh7Xmb-3mw/GrandgoldRL/@grand_gold_rl/ 731 subs / 89,663 views / 68 videos, matching the public fallback numbers.youtube_analytics_fetcher.pyprintedSource: oauth-mine-true. The 28-day channel report returned views 35, estimatedMinutesWatched 6, averageViewPercentage 56.36, subscribersGained 1, subscribersLost 4, likes 5, comments 2, shares 0, plus a workingvideodimension.youtube_oauth_status_report.py(the 08:00/20:00youtube-combinedjob, id04b6378d3971) also printsSource: oauth-mine-true, so tonight's 20:00 run restores private reporting without further action.Corrected an inherited claim: CTR is not recoverable by OAuth.
impressions,impressionCtr,impressionsClickThroughRate,videoThumbnailImpressionsandvideoThumbnailImpressionsClickRateare all rejected (Unknown identifier/The query is not supported), and the official metric reference (developers.google.com/youtube/analytics/metrics) lists only legacyannotationClickThroughRate/annotationCloseRate, both 0 here. Thumbnail impressions and CTR, retention curves and video-level subscriber conversion are Studio-only. The ledger'sblocked_metrics_until_oauth_fixedlist is therefore wrong about the reason as well as the membership:estimatedMinutesWatchedandengagedViewsare now available, while CTR and the retention curve can never come from the API.Sprint ledger remains stale and is now the only blocker to honest framing:
youtube-growth-sprint.jsonunchanged since 2026-05-16, window ended 2026-05-26 (129 days ago),sprint_progressstill clamps to day 14/14 and+645/+22508/+7are all-time from the 2026-05-16oauth_baseline. Itsbaselineblock names a different real channel,UC1BDGDirPEwEMbKJcMeTQbw/@grandgoldrl/ "Grandgold" / 251 subs / 201,003 views / 207 videos, distinct from@grand_gold_rl; handles differ only by case. No edit made to the ledger, because resetting the window is a reporting-progress decision, not a defect repair. Sources: live token-endpoint and Analytics API probes,channels.list,youtube_oauth_setup.py status,youtube_analytics_fetcher.py,youtube_oauth_status_report.py,youtube_sprint_common.py, sprint JSON, cronjobs.json. - hermes-zappo. -
2026-09-27 10:30 AEST (daily optimizer run, live probes): Twentieth consecutive day on public fallback. Direct token-endpoint POST still returns HTTP 400
invalid_grant; stored tokenupdated_atdecodes to 2026-09-08 03:17 AEST andexpires_atto 2026-09-08 04:17 AEST, scope stillyoutube.readonly+youtube.force-sslwith noyt-analytics.readonly. Self-heal log showsinvalid_grantat 2026-09-27 08:00 (twice daily since 2026-09-14). Public log latest rows: 731 subs / 89,663 views / 68 videos at 2026-09-27 03:17:49 and 10:10:23 AEST, against 732 at 2026-09-26 03:17:58. Dated deltas from the same log: subs flat over 7d (731 on 2026-09-20), -3 over 30d, -17 from the 748 peak of 2026-06-22; views +0 over 7d, +21 over 30d; video count 68 unchanged since 2026-06-29 (90 days). No new upload in the last 24h. Stale sprint unchanged:youtube-growth-sprint.jsonended 2026-05-26 (124 days ago) and itsoauth_baseline(86 subs / 67,155 views, 2026-05-16) is what produces the report's+645/5and+22508/500lines, so those are all-time-from-baseline, not sprint measures;sprint_progressclamps elapsed toduration_daysand still prints day 14 with the defunct day-14 queue item. No Kanban card exists; human re-auth remains the only fix. Sources: live token-endpoint probe, self-heal log,youtube-oauth-status-state.json,youtube-grandgoldrl-public-log.csv,youtube-growth-log.csv, sprint JSON. - hermes-zappo. -
2026-09-27 06:00 AEST (weekly synthesis, live state read): The fault has now run nineteen days (first daily failure 2026-09-08, unchanged since), and the OAuth defect is unchanged in every part.
~/clawd/memory/youtube-oauth-status-state.jsonreadsstatus: oauth_public_fallback_steady,reason: missing_token,refreshed: true,failed_at2026-09-26 20:00 (checked_atthe same minute), and the last growth line recordsSource: public-youtube-pagewithSelf-healed: oauth: HTTP Error 400: Bad Request. The stored token still has not been re-authed:exchanged_atdecodes to 2026-05-14 02:50 andexpires_atto 2026-07-28 04:45, both far behind now. Public log (~/clawd/memory/youtube-grandgoldrl-public-log.csv) latest row is 2026-09-27 03:17:49 AEST: 731 subscribers, 89,663 views, 68 videos, tagged@grand_gold_rlsourcepublic_youtube_about_live. Against the 2026-09-26 03:17:58 row (732/89,663/68) that is one subscriber lost, with views and video count flat, and the 2026-09-26 20:00:57 growth line reads a net change of zero on all three fields. Video count has now been 68 for 90 days (unchanged since 2026-06-29). No re-auth has been performed; theyt-analytics.readonlyscope gap inyoutube_oauth_setup.py(commit72050a7, unpushed) still means weekly analytics cannot work even after one. Source:~/clawd/memory/youtube-oauth-status-state.json,~/clawd/memory/youtube-grandgoldrl-public-log.csv,~/clawd/memory/youtube-growth-log.csv, cron output~/.hermes/cron/output/b2e5b4073752/2026-09-26_10-30-53.mdand~/.hermes/cron/output/179f165b0a1b/2026-09-27_03-17-51.md, read 2026-09-27 05:55-06:00 AEST., hermes. -
2026-09-24 10:35 AEST (daily optimizer run, live probes): Seventeenth consecutive day on public fallback. Direct token-endpoint POST still returns HTTP 400
invalid_grant; tokenupdated_atdecodes to 2026-09-08 03:17 AEST andexpires_atto 2026-09-08 04:17 AEST, scope stillyoutube.readonly+youtube.force-sslwith noyt-analytics.readonly. Self-heal log recordsinvalid_granttwice daily (08:00 and 20:00), last 2026-09-24 08:00. State file~/clawd/memory/youtube-oauth-status-state.jsonreadschecked_at2026-09-24 08:00,failed_at2026-09-24 08:00,reason: missing_token,status: oauth_public_fallback_steady. Public log latest row 2026-09-24 10:30:09 AEST: 731 subs, 89,663 views, 68 videos. Dated deltas from the same log: subs +0 over 1d, +0 over 7d, -3 over 14d, -4 over 30d, -12 over 60d, -16 over 90d (peak 748 on 2026-06-22, so -17 from peak); views +0 over 1d, +10 over 7d, +24 over 14d, +71 over 30d, +120 over 60d, +188 over 90d; video count 68 unchanged since 2026-06-29 (87 days). The report's+645/5and+22508/500deltas are all-time from the 2026-05-16oauth_baseline(86 subs / 67,155 views) and a mid-history 208-video public reading, not sprint measures. Stale sprint unchanged:youtube-growth-sprint.jsonended 2026-05-26 (121 days ago) yet elapsed is clamped toduration_daysinsprint_progress, so the report still prints "day 14" and emits "ship the slot" from the defunct day-14 queue item. No Kanban card exists (tasksholds 3 done, 1 blocked, none YouTube); human consent remains the only fix. Sources: live token-endpoint probe, self-heal log, state file, public log, sprint JSON, kanban.db. - hermes-zappo. -
2026-09-22 10:30 AEST (daily optimizer run, live probes): Fifteenth consecutive day on public fallback. Direct token-endpoint POST returns HTTP 400
invalid_grantagain today; tokenclient_id/client_secretstill matchclient_secret.json(projectgen-lang-client-[redacted-id]), tokenupdated_atdecodes to 2026-09-08 03:17 AEST andexpires_atto 2026-09-08 04:17 AEST, scope stillyoutube.readonly+youtube.force-sslwith noyt-analytics.readonly. The 2026-05-18 token backup also returnsinvalid_grant, so no on-disk token is recoverable. Self-heal log recordsinvalid_grantat 08:00 and 20:00 daily (last 2026-09-22 08:00). Public log latest row 2026-09-22 10:30:37 AEST: 731 subs, 89,663 views, 68 videos. Dated deltas from the same log: views +10 over 7d, +10 over 14d, +24 over 30d; video count unchanged since 2026-06-29 (85 days); subscribers read 735 at 2026-09-21 03:17 and 10:05, then 731 at 2026-09-22 03:17, so the +4 was a one-day public-page reading that did not hold. The report's own numbers are internally inconsistent: it prints+645/5subs against a 731 reading while the sprint'soauth_baseline.subscribersis 86 and the publicbaseline.subscribersis 252, so the+N/targetdeltas are not tied to the row they accompany. Stale-sprint defect unchanged:youtube-growth-sprint.jsonended 2026-05-26 (119 days ago), yet the optimizer still prints "day 14" and emits "ship the slot" from the defunct day-14 queue item. No Kanban card exists for the re-auth. Human consent is the only remaining fix. Sources: live token-endpoint probe, self-heal log,youtube_sprint_common.py, public log, sprint JSON, kanban.db. - hermes-zappo. -
2026-09-20 06:00 AEST (weekly synthesis, live state file read): OAuth is still broken and the public fallback is still carrying every reading, 13 days in.
~/clawd/memory/youtube-oauth-status-state.jsonreadsstatus: oauth_public_fallback_steady,reason: missing_token,last_errorempty,refreshed: true,failed_at2026-09-19 20:00 andchecked_at2026-09-19 20:00, while the stored token'sexpires_atdecodes to 2026-07-28 04:45 andexchanged_atto 2026-05-14 02:50. The last growth line (last_growth, 2026-09-19 20:00:23 AEST) recordsSource: public-youtube-pageandSelf-healed: oauth: HTTP Error 400: Bad Request. Public log rows: 731 subscribers, 89,655 views, 68 videos at 2026-09-19 15:00 and 20:00; the 2026-09-20 03:17:06 row reads 731/89,663/68, so views moved +8 overnight while subscribers and video count stayed flat. Every row is tagged@grand_gold_rlwith sourcepublic_youtube_about_liveorpublic-youtube-page. This is unchanged from the state recorded on 2026-09-14 and 2026-09-15; no re-auth has been performed, and theyt-analytics.readonlyscope gap still means weekly analytics cannot work even after one. Source:~/clawd/memory/youtube-oauth-status-state.json,~/clawd/memory/youtube-grandgoldrl-public-log.csv,~/clawd/memory/youtube-growth-log.csv, read 2026-09-20 06:05 AEST., hermes. -
2026-09-15 10:30 AEST, Confirmations from the youtube-growth-optimizer run. (a) Live token endpoint probe again returns HTTP 400
invalid_grant("Token has been expired or revoked."); self-heal log hasinvalid_grantat 2026-09-14 20:00 and 2026-09-15 08:02 AEST. Stored token expired 2026-09-08 04:17 and its scope set is still onlyyoutube.readonly+youtube.force-ssl- noyt-analytics.readonly, so a re-auth is needed regardless. Eighth consecutive day on public fallback. (b) Public log latest row 2026-09-15 10:30:35 AEST: 733 subs, 89653 views, 68 videos. Dated deltas from the same log: subs -1 over 7d, 0 over 14d, -5 over 30d, -11 over 60d; views +16 / +60 / +70 / +123; videos unchanged since 2026-06-29 (78 days). (c) The stale-sprint defect is worse than the plotter-only note of 2026-09-14: it drives the daily action line too.sprint_progressclamps elapsed toduration_days, so the optimized report prints "day 14/14" for a sprint that ended 2026-05-26 (112 days ago), labels all-time-since-2026-05-16 deltas as sprint progress (+647 subs / +22498 views / +7 uploads), and then emits "ship the slot above" from the defunct day-14 queue item (youtube-grandgoldrl-execution-loop.json). Recommended fix, not yet applied: either retargetyoutube-growth-sprint.jsonor havesprint_progressreturnexpiredwhen today >end_atand have the reports say "since 2026-05-16 baseline" instead of "sprint". Sources: live token probe, self-heal log,youtube_sprint_common.pylines 408-481, public log, sprint JSON. - hermes-zappo. -
2026-09-14, Recurrence confirmed by live probe: token endpoint still returns HTTP 400
invalid_grant("Token has been expired or revoked"). Self-heal log showsinvalid_grantat 03:33, 03:34, 03:35, 03:37 and 08:00 AEST. Public fallback has now carried every reading since 2026-09-08 08:00. -
2026-09-14, Presentation defect found while reading the weekly plotter output: the
youtube-growth-plotterjob prints+N/<target> sprintdeltas that are measured against the oauth_baseline from 2026-05-16, not the 14-day sprint. The sprint inyoutube-growth-sprint.jsonstarted 2026-05-12 and ended 2026-05-26, so every "sprint" figure in that line is a ~4-month all-time delta and reads as recent progress. Dated deltas fromyoutube-grandgoldrl-public-log.csvfor the same day: views +15 over 7 days, +59 over 14 days, +69 over 30 days; subscribers flat over 7 days, −4 over 30; uploads unchanged since 2026-06-29. Fix options: retarget the sprint or relabel the line as all-time-since-baseline. Not yet changed. Sources: live token probe, self-heal log, public log. - hermes. -
2026-09-13, Diagnosed and partially fixed. Confirmed fault 1 by direct token-endpoint probe (HTTP 400
invalid_grant). Restored~/.hermes/scripts/youtube_oauth_self_heal.py(sha256378d8d3c189747f1ae9fe79f5686b0844d15d44cbfa92d5344aa7993ef757eba), which had been deleted and caused silent no-op self-heal; it now logsinvalid_grantinstead of nothing, proven by a live run appending that line to the self-heal log. Addedyt-analytics.readonlytoyoutube_oauth_setup.pySCOPES (sha256e6b500ecf92160426f53e783effdf3d363c700a8f5ccd122da357f15b327f0a2), committed locally as72050a7; not pushed becauseorigin/mainis ahead by 1 and a Claw Automated Sync agent is active on that tree. Regression tests~/.hermes/tests/test_youtube_oauth_self_heal.py, 9 tests, falsified both ways (8 of 9 fail with the self-heal script absent; 1 fails with the analytics scope absent; 9 pass after restore). The re-auth itself was not performed: it needs an interactive Google consent, which is a human gate. Fault 1 remains open. Sources: live probe, git log, pytest runs, self-heal log. - hermes. -
2026-09-17 10:30 AEST, Tenth consecutive day on public fallback, and the first day the public series is clearly declining rather than flat. Public log latest row 2026-09-17 10:30:48 AEST: 731 subs, 89653 views, 68 videos. Dated deltas from the same log: subs -2 over 1d, -2 over 2d, -3 over 7d, -3 over 14d, -7 over 30d, -13 over 60d and 80d; peak subs 748 on 2026-06-22, so -17 from peak. Views +0 over 2d, +14 over 7d, +49 over 14d, +70 over 30d, +163 over 80d. Video count unchanged since 2026-06-29 (80 days). OAuth: self-heal log
invalid_grantat 2026-09-17 08:00; stored token access expiry 2026-09-08 04:17, scope still lacksyt-analytics.readonly. Stale-sprint defect unchanged and still driving the daily action line:youtube-growth-sprint.jsonended 2026-05-26 (114 days ago), yet the optimizer prints "day 14/14" with all-time-since-2026-05-16 deltas (+645 subs / +22498 views / +7 uploads) and emits "ship the slot" from the defunct day-14 queue item. Sources: public log, self-heal log, sprint JSON, live report run. - hermes-zappo.
Sources:
~/.hermes/scripts/youtube_oauth_status_report.py · ~/.hermes/scripts/youtube_sprint_common.py · ~/.hermes/scripts/youtube_oauth_self_heal.py · ~/clawd/scripts/youtube-oauth.py · ~/clawd/scripts/youtube_oauth_setup.py · ~/clawd/skills/youtube-analytics/.youtube-tokens.json · live token-endpoint probe 2026-09-13