---
type: hack
title: Kickbacks.ai — Banner Surface
description: Status bar ad banner with billing, click tracking, gradient and proactive token refresh
tags: [kickbacks, banner, statusbar, billing, performance]
timestamp: 2026-06-27
---

# 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: `kickbacks` `banner` `statusbar` `billing` `performance`

---

## 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

```typescript
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:

```typescript
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):

```typescript
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

1. **Don't HTTP-poll for local loopback** — `getBannerAd()` is memory, no network, no latency
2. **Remove `.show()` from marquee** — the billing tick (5s) already maintains visibility
3. **Complex idle decay isn't worth it** — window focus + server-side cooldown solve it
4. **JWT exp parsing** — proactive refresh > waiting for the 403
5. **Namespace guard** — overlay has it, banner needs it (to avoid demo token on real route)
6. **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
