When your daily driver is the star in a Rabobank ad

The finished spot.

436,085 views at the time of writing. A proper pinch me moment.

Everything below is pulled from screen recordings made on the actual laptop during the shoot, not crops from that broadcast cut.

Kaliber and Rabobank made a campaign called “How many of us are you”, aimed at IT people who still picture Rabobank as a bureaucratic bank rather than an organisation that already employs ten thousand of them. I was the front-end lead on the screens that show up in the background of the films: the terminals, the maps, the things running behind whoever’s on camera.

The brief had an unusual constraint baked into it. Art director Fabian put it as “we must create a campaign we ourselves wouldn’t comprehend, only then are we succeeding,” meaning it had to read as authentic to the actual developers it was trying to recruit, not to a marketing team’s idea of what a developer’s screen looks like. Practically, that turns into one hard rule: someone can pause the ad, zoom in, and whatever’s on screen still has to make sense. No Lorem ipsum masquerading as code, no fake progress bars going nowhere, no map that stops being a real place the moment you look closely. The brief even asked for easter eggs only another developer would ever notice.

That rule decides almost everything below.

Starting from what’s already real

The cheapest way to survive someone zooming in isn’t a better fake, it’s not faking it. So the baseline screen is just my own terminal, running the setup I use every day: WezTerm, Neovim, and clx (a terminal Hacker News reader) among what’s normally open. None of that was built for the shoot. It was already true.

WezTerm with the real tabs (Figma, Notion, Slack, a browser), a real Rust snippet, and one of the small synthetic panes used as ambient filler in shots nobody was meant to freeze-frame.

Two things did need building on top of that, though, and both had to look like they belonged in the same world as the terminal sitting next to them, not like a graphic overlaid on top of it.

Custom ascii art, drawn by hand and played back for real

The branding moment needed a piece of ascii art with motion to it, not a static splash. Building that meant two tools doing two different jobs:

  • asciimotion, a browser app for drawing ascii/ANSI animations frame by frame, the way you’d work in a pixel-art editor but with characters and terminal colors instead of pixels.
  • openTUI, a TypeScript library for building actual terminal UIs, used here to play those frames back inside a real terminal buffer rather than as a video clip pasted over one.
The RABOBANK wordmark, mid-loop, in an actual WezTerm pane next to a live Rust snippet and a bandwidth graph.

The wordmark isn’t a static logo either. It types itself in, holds, and every so often a small pixel figure walks across the pane before it types back in again, all inside the same buffer as the panes around it: a compiler error in a Rust snippet, a bandwidth graph, a window title bar with real tabs. The distinction matters for the same reason the rest of this does: a genuine terminal buffer, captured off the actual terminal, behaves like everything else on screen. A composited animation doesn’t, and it tends to show under scrutiny: wrong font rendering, wrong cursor behaviour, subpixel differences a developer’s eye catches even if they couldn’t say why.

A fraud pipeline that never stops scrolling

One screen needed to look like it was watching live financial activity, the kind of “data is moving” screen every heist film reaches for, but one that had to hold up as real if you paused it.

The fraud pipeline, mid-scroll: a stage tracker, running totals, and a live table of individual transactions.

Across the top, a pipeline of stages a real fraud system would actually have: INGEST → VALIDATE → ENRICH → RISK MODEL → FRAUD ML → DECISION → SETTLE, with running totals underneath (processed, approved, review, declined, volume, blocked). Below that, individual rows scroll past: time offset, transaction id, account holder, merchant id, category, amount, country, a risk score, a decision. The footer reads out the aggregate: approval 31.2% avg risk 59 model gradient-boost + rules.

The category weights that decide how risky a row looks are just a small array sitting in a script:

{ cat: "ELECTRONICS", risk: 16 },
{ cat: "CRYPTO", risk: 55 },
{ cat: "WIRE", risk: 72 },
{ cat: "CASH", risk: 22 },
{ cat: "RETAIL", risk: 9 },
];

const GEO = ["NL", "NL", "NL", "NL", "BE", "DE", "FR", "ES", "US", "RU", "NG"];

Crypto and wire transfers score high, cash and retail score low, and GEO is weighted toward NL with the rest sprinkled in, which is exactly the shape you’d expect from a bank that mostly moves domestic money with some cross-border noise. Small detail, but it’s the kind of thing that reads as considered rather than random if someone actually stops to look at it.

Pointing a map renderer at a real address

The zoom sequences in the films move through Rabobank’s actual headquarters via real map coordinates, and that’s mapscii doing the work: it renders OpenStreetMap data as ascii/ANSI art straight into a terminal (the public demo is telnet mapscii.me).

mapscii centered on 52.085, 5.108 at zoom 16.08, with Rabobank labelled on the building itself, streets and all.

The status line along the bottom of that pane reads center: 52.085, 5.108 zoom: 16.08 mouse: 52.085, 5.11, and the street names on screen, Croeselaan among them, are the real streets around Rabobank’s Utrecht HQ. The building itself is labelled Rabobank right there on the map, because that’s just what OpenStreetMap calls it. Pointing mapscii at real coordinates meant the geography under the “scanning” screen was never fake to begin with; only the transaction feed layered elsewhere on top of it is synthetic.

One easter egg, for whoever pauses long enough

Not everything had to survive scrutiny for the shoot to work. Some of it was built purely for the person who pauses anyway.

RABO GRAND PRIX, a lap-by-lap race simulation nobody asked for, running next to its own source.

RABO GRAND PRIX, lap 12 of 20, a full leaderboard of drivers with lap times, gaps to the leader, and a commentary line scrolling underneath: fastest laps, pit stops, the works. It’s a small simulation, not a real feed, and the whole engine sits right there in the pane behind it:

#!/usr/bin/env node
const out = process.stdout;

const hideCursor = () => out.write("\x1b[?25l");
const showCursor = () => out.write("\x1b[?25h");
const altScreen = () => out.write("\x1b[?1049h");
const mainScreen = () => out.write("\x1b[?1049l");

function cleanup() {
  showCursor();
  mainScreen();
  out.write("\x1b[0m\n");
  process.exit(0);
}
process.on("SIGINT", cleanup);
process.on("SIGTERM", cleanup);

altScreen();
hideCursor();

const TOTAL_LAPS = 20;
const LAP_LEN = 1000; // sim units per lap
const BASE_SPEED = 200; // sim units per second (~5s per lap)
const TIME_SCALE = 18; // 5s sim lap shows as a ~1:30 lap time
const TICK_HZ = 30;

Raw ANSI escapes for the alt screen and cursor handling, a tick loop at 30Hz, a time scale that compresses a 5-second simulated lap into something that reads on screen as a 1:30 lap time. Nobody needed this for the ad to work. It exists because the brief asked for exactly this kind of thing: something only another developer, pausing on a random frame, would ever actually read.

One file that keeps every tool on the same page

The films use a handful of different visual moods, and every tool on screen had to shift together (terminal, editor, menu bar, wallpaper, even the audio visualiser) without ever looking like four separately-themed apps sharing a desktop by accident. That system already existed before the shoot, for the same reason the terminal itself did: it’s how I theme my own machine day to day, campaign or not.

It comes down to one file. WezTerm’s leader menu (LEADER t) opens a theme picker, and picking one just writes a single line, the theme id, to ~/.config/wezterm/theme:

-- wezterm.lua
action = wezterm.action_callback(function(window, pane, id, label)
  if id then
    local f = io.open(THEME_FILE, 'w')
    if f then
      f:write(id)
      f:close()
    end
    os.execute(HOME .. '/.config/wezterm/sync-theme.sh ' .. id)
  end
end),

sync-theme.sh then propagates that id everywhere else that isn’t WezTerm itself:

# oh-my-posh re-reads its config every prompt, so existing shells follow
cp "$HOME/.config/posh-themes/$id.json" "$HOME/.config/posh.json"

# sketchybar: swap the color palette and reload
theme="$HOME/.config/sketchybar/themes/$id.sh"
cp "$theme" "$HOME/.config/sketchybar/colors.sh"
sketchybar --reload

# cava: swap the gradient palette; SIGUSR2 makes running instances reload colors
cp "$HOME/.config/cava/themes/$id" "$HOME/.config/cava/themes/current"
pkill -USR2 -x cava

# desktop wallpaper: per-theme symlink, resolved so macOS sees the real extension
wallpaper=$(readlink -f "$HOME/.config/wallpapers/themes/$id")
osascript -e "tell application \"System Events\" to tell every desktop to set picture to \"$wallpaper\""

That last one loops over every desktop on purpose. Finder’s “set desktop picture” only ever touches the main display, which would leave every other monitor stuck on the old theme.

Neovim isn’t part of that propagation chain at all; it just reads the same file for itself, on startup:

-- current_theme.lua
local f = io.open(os.getenv('HOME') .. '/.config/wezterm/theme', 'r')
if f then
  name = f:read('*l')
  f:close()
end
return M.themes[name] or M.themes.poimandres

One writer, several independent readers, no shared process between them. Nothing pushes state anywhere; everything just checks the same single line and decides what to do with it. That’s the whole mechanism, and it’s also why it was easy to extend for the shoot: add one more entry to the table, not a new integration.

The recordings above cycle through six of my daily rotation themes: poimandres, gruvbox, catppuccin, clefairy, dracula, nord, the same six LEADER t has always offered. The entry actually built for this campaign is called Roost, ember red through gold into cream, matched to the film’s own grade. It exists in full across every layer: wezterm/colors/roost.toml, nvim/colors/roost.lua, sketchybar/themes/roost.sh, posh-themes/roost.json. It’s just never made it back into my everyday picker. LEADER t still only cycles the six themes I actually reach for day to day, and Roost sits there unwired, a theme that only ever gets loaded on purpose.

Why the boring parts carried the ad

None of what’s on screen in the final films is a mockup. The terminal is the terminal I have open right now. The map is real coordinates around a real building. The theme switching between shots is the same file I’ve been writing to for my own reasons since long before this campaign existed. What did need building, the wordmark, the fraud pipeline, the grand prix nobody asked for, meant making each one behave like it had always belonged next to everything else, not making it look convincing in isolation.

The zoom-in-and-it-still-holds-up rule sounds like it makes the job harder. Mostly it made it simpler: instead of inventing a fake system good enough to survive scrutiny, keep using the real one and only fake the handful of things that had no real equivalent to begin with.

← All writing