Kickbacks.ai — Banner Surface
Status bar ad banner with billing, click tracking, gradient and proactive token refresh
Kickbacks.ai — Banner Surface Implementation
Status bar ad banner independent of the overlay, with its own billing, click tracking, color gradient and proactive token refresh. Tags:
kickbacksbannerstatusbarbillingperformance
Architecture
The banner is an independent surface from Claude Code's overlay. Each has its own slot in the ad rotation queue, its own sessionNonce, and separate billing — no double billing.
Portfolio refresh (60s) → adRotation queue
├── [rotationIdx] → overlay (activeAd)
└── [(idx+1) % N] → banner (getBannerAd)
Two Items in the Status Bar
adItem = Right → Left, priority -100 // banner, left side
earnings = Right, priority 999 // "Kickbacks ($X today)"
- AdItem was moved to the left (
Left, priority -100) because it's more stable than the right (less extension competition) - EarningsItem stays on the right (VS Code ecosystem default)
Billing (Window Focus Only)
Drastically simplified compared to the overlay. No idle decay, no editor events, no countdown:
private startTicking(): void {
this.tickInterval = setInterval(() => {
const billable = this._windowFocused;
this.adItem.color = billable ? this._adColor(this._bannerVisibleMs) : AD_PAUSED;
this.adItem.show();
if (billable && this._onTick) this._onTick(5000);
}, 5000);
}
| State | Color | Billing |
|---|---|---|
| Window focused | Gradient (gray→amber) | ✅ view_tick every 5s |
| Alt+Tab / lost focus | Dark gray (#888) | ⏸️ Paused |
Why this matters for anti-fraud: Cutting billing on alt+tab creates natural pauses in ticks, producing a high CV (Coefficient of Variation) (~0.48). Low CV (close to 0) = bot pattern = 🚩 Fraud detection.
Color Gradient (Gray → White → Amber)
The banner text color evolves as the accumulated visibleMs approaches the 15s threshold (anti-misclick):
| visibleMs | Color | Meaning |
|---|---|---|
| 0s | #888 (dark gray) | Just appeared |
| 5s | Beige (#ccc8aa) | Accumulating |
| 10s | Light gold (#f0e8bb) | Almost there |
| 12s | #f0ecd0 (warm white) | "Ready to click" |
| 15s+ | #dba110 (amber) | ✅ Threshold reached, click enabled |
Implemented via _adColor() with static COLOR_STOPS (4 stops, linear interpolation between segments).
Marquee (Continuous Scroll)
- Interval: 500ms (2 steps/s) — balance between lively and performance
- Always running — all text scrolls regardless of length
- No
.show()on tick — the billing tick (5s) already maintains visibility. Removing.show()from marquee reduced IPC by ~33% - Total IPC: ~4 calls/s (vs 7.5/s before optimization)
Before: 3 props × 2.5/s = 7.5 IPC calls/s
After: 2 props × 2/s = 4 IPC calls/s
Click Tracking
Complete flow (mirroring the overlay):
Click on banner
→ canServeAds() — serving gate check
→ sb.bannerVisibleMs < 15,000ms? → anti-misclick (early click discarded)
→ metrics.send("click", { adId, campaignId, sessionToken, surface:"banner", visibleMs, eventUuid })
→ vscode.env.openExternal(url) — opens advertiser URL
→ sb.resetAdSession() — visibleMs = 0, color reverts to gray
→ refreshAdNow() — forces portfolio refresh + banner re-read
Anti-Misclick
Same threshold as the overlay: 15s of accumulated visibleMs. Clicking before the banner turns amber → event discarded with dlog("banner.click.early").
Proactive Token Refresh
Before: token expired silently → metrics.send() received 403 for HOURS until the next portfolio refresh (60s).
Now: the JWT is decoded (without verifying signature) to read exp (expiry timestamp):
msUntilExpiry(): number | null {
const b64 = token.split(".")[1]
.replace(/-/g, "+").replace(/_/g, "/");
// ... decode base64url → JSON
return payload.exp ? (payload.exp * 1000 - Date.now()) : null;
}
Timer every 2 minutes checks if the token expires in less than 5 min. If so → proactive auth.refresh(). Zero 403 window.
Logs and Debug
All banner events have dlog in ~/.vibe-ads/debug.log:
| Event | When |
|---|---|
banner.fetch | Every 30s (reading from getBannerAd()) |
banner.click.early | Click before 15s |
banner.click.gated | Serving gate closed |
banner.click.no_url | Ad without URL |
banner.click.error | Error in handler |
auth.proactive_refresh | Token refreshed proactively |
Performance
| Aspect | Before | After |
|---|---|---|
| Marquee interval | 250ms (4/s) | 500ms (2/s) |
.show() on marquee | Yes | No (only billing tick) |
| Banner poll | HTTP /ad (10s) | Memory (30s, no network) |
| Total IPC | ~7.5/s | ~4/s |
COLOR_STOPS | Created every 5s | Static readonly |
Results
- $2.46 in 2 days (top 11% of 19,242 users)
- 21 clicks (3.74% CTR)
- CV 0.48 — human jitter, legitimate anti-bot pattern
- 5.4h/day of thinking time
- Zero double billing — banner and overlay share the same ad but each has its own sessionNonce and surface
Lessons Learned
- Don't HTTP-poll for local loopback —
getBannerAd()is memory, no network, no latency - Remove
.show()from marquee — the billing tick (5s) already maintains visibility - Complex idle decay isn't worth it — window focus + server-side cooldown solve it
- JWT exp parsing — proactive refresh > waiting for the 403
- Namespace guard — overlay has it, banner needs it (to avoid demo token on real route)
- Status bar left > right — less extension competition
Related Links
- kickbacks-antigravity — Initial patch for Antigravity IDE
- kickbacks-hermes-plugin — Hermes Plugin (alternative architecture without patches)
- fitalabs-gateway-modelos — Multi-provider gateway that routes models