Inspiration
We wanted to plan outdoor ads by what the people around each wall love, not by who they are. Qloo has taste data aggregated by place, so we could ask where a brand's audience lives across a city, block by block. Nemotron on Nebius let us write a different line for each face from what its block loves, check every line before print, and show what each model call cost. We also wanted the plan to be able to leave the app for the tools a media team already uses.
What it does
Wallspace picks the billboards, bus shelters and digital screens where a brand's audience lives, and writes what each one should say.
- Describe the audience by taste. For example "stand-up comedy specials, 90s sitcoms, southern hip-hop", plus a budget and flight dates.
- Taste-only guard. Briefs that target people by identity (race, religion, sexuality, health or similar) are refused, and a taste-based version is suggested. No personal or device location data is used anywhere.
- Where the audience lives. Qloo's heatmap scores every cell of the city for the brief's taste, and Wallspace scores every face by its cell's affinity, visibility and cost. Qloo runs live in production, with a KV cache of real Qloo responses; if a Qloo call fails the app shows an error with Retry, never invented data.
- Site check. Nemotron 3.5 Super VL plans the run, decides which faces to inspect and reads the site photo for sightline, clutter and share of frame. In the sample plan it drops ATL-0101, a distant board partly hidden by trees, poles and wires that fills 4% of the frame.
- Local context. Qloo says what each block over-indexes on; Tavily finds events near each face during the flight.
- Copy per face. Nemotron 3.5 Lightning writes three lines of at most 7 words for each face, from what that block loves. Every line passes the guard. A line over 7 words is rejected and redrafted, never trimmed, and a face still failing is flagged "Copy needs a human".
- Budget split. No neighbourhood takes more than 30% of spend.
- Plan review. Nemotron 3 Ultra reviews the whole plan for clashing copy on nearby faces, budget sense and brand fit. In the sample plan it flagged four faces for clashing copy on nearby boards: ATL-0021 (Downtown) and ATL-0014 (Midtown), 1,106 m apart, both using a "[Street] laughs louder tonight" line, and ATL-0046 (Home Park) and ATL-0019 (Midtown), 235 m apart, with near-identical "laughs loudest/louder" messaging. All four were redrafted.
- Sign-off for print. NVIDIA NeMo Guardrails, running as a Nebius Serverless Job, checks every line of a plan marked "Sign off for print" and stamps it "NeMo-checked" with the job ID.
- Out to the team's tools. Gmail is connected and tested end to end: the plan e-mail and the pitch sheet go to a media owner or client, and replies are polled and sorted by Lightning as confirmed, counter-offer, unavailable or question. Built and connectable but not yet tested end to end with a live account: Outlook, HubSpot (a deal), Google Sheets (one row per face), Google Calendar and Outlook Calendar (creative-due, posting and takedown). Slack approval (our own Slack app with Approve and Request changes buttons) is built and tested with signed test requests, but not yet installed in a live workspace. The OpenOOH deal request file for screen networks works; it is prepared, not submitted.
Media owners get the other side: an inventory, an audience card per face showing what the people around it love and which brands' fans over-index there, and pitch sheets they can send.
Every step is logged in a run trace with the model, tokens, time, cost and provider request ID.
How we built it
NVIDIA models on Nebius Token Factory, one job each:
| Model (exact id) | Job in Wallspace |
|---|---|
nvidia/Nemotron-3.5-Super-VL |
The planning agent: function calling over our Qloo and inventory tools, deciding which faces to inspect. Also reads site photos (sightline, clutter, share of frame) and drops faces with a blocked sightline. Thinking off for tool calls. |
nvidia/Nemotron-3_5-Lightning |
Three copy lines per face, ambiguous wording in the brief guard, triage of Tavily results, and classifying media-owner email replies. Thinking off. |
nvidia/Nemotron-3-Ultra-550b-a55b |
One review pass over the finished plan. Thinking on, max_tokens 7000, one retry with thinking off. |
Qwen/Qwen3-Embedding-8B (not an NVIDIA model, labelled as such) |
Maps a free-text brief to Qloo tags by cosine similarity, at 512 dimensions so the cached tag vocabulary stays small. |
We avoided nvidia/nemotron-3-super-120b-a12b and nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B because Token Factory removes both on 9 Nov 2026, before judging.
Token Factory features we use: the OpenAI-compatible API, called directly from Cloudflare Workers; strict response_format: json_schema on all three Nemotrons, validated with zod and retried once on failure; parallel function calling on Super VL; image input on Super VL; reasoning switched per call (reasoning_effort: "none" plus chat_template_kwargs: {"enable_thinking": false}, both needed for Lightning); the dimensions parameter on embeddings; and the x-ratelimit-* headers to stay frugal on a shared quota. Every call is stored as a run step with model, tokens, milliseconds, cost and request ID.
NeMo Guardrails 0.24.1 on Nebius Serverless Jobs: a Python job (jobs/nemo/, python:3.12-slim on cpu-d3, 2 vCPU and 8 GB) with two modes:
- Sign-off: checks every copy line of a plan the planner has marked "Sign off for print", then stamps the plan "NeMo-checked" with the job ID.
- Red-team: runs 40 prompts and publishes the catch and false-positive rates on the public
/limitspage.
The app's own taste-only guard applies the same NeMo policy (jobs/nemo/policy) on every brief and copy line, with Lightning for ambiguous wording.
Started on demand from the CLI. We start each job with nebius ai job create from scripts/nebius-job.mts, run as a service account, never on a schedule and never by the app. The job reads waiting lines from /api/ingest/nemo and posts its verdicts back to the same route with a shared secret. The app never calls the Nebius control plane, because from a Cloudflare Worker it answers with a 522.
Tavily: Search and Extract for each picked face's neighbourhood and flight month, cached in KV by neighbourhood and month, with Lightning keeping the events worth a copy line. New searches are capped per session and per day.
Qloo: the client is written against the Insights API with filter.type=urn:heatmap for where the audience lives, urn:tag with signal.location for what each block over-indexes on, and brand affinity by place for the media-owner audience cards. Production runs it live (QLOO_MODE=live) with a Cloudflare KV cache of real Qloo responses in front, which protects the hackathon key's limits (1,500 Qloo calls a day and 6,000 in total). The sample plan was rebuilt from a live run on 30 Oct 2026. If a Qloo call fails the UI shows an error with Retry, never invented data. Token Factory and Tavily are live too.
Cloudflare: Next.js on Workers via OpenNext, behind a Pages front. A Cloudflare Workflow runs each plan, and progress streams to the page over a Durable Object WebSocket. D1 holds plans, briefs, runs, faces and connections; KV caches Qloo and Tavily; R2 holds photos. A cron at 03:00 Kuala Lumpur time runs a Workflow that re-scores saved plans through Token Factory and flags faces whose audience has shifted; a 10-minute cron polls connector replies. The map is MapLibre with OpenFreeMap tiles.
Connectors: Gmail through Composio managed auth is tested end to end. Outlook, HubSpot, Google Sheets, Google Calendar and Outlook Calendar are built on the same managed auth and appear on the Connections page with Connect buttons, but none has been tested end to end with a live account. Our own Slack app (signed interactivity requests) is tested with signed test requests and is not yet installed in a live workspace. GET /api/plans/:id/export.openooh.json returns an OpenRTB 2.6-shaped venue list in the OpenOOH taxonomy.
Measured
All numbers are from our own run logs and Nebius jobs on 30 Oct 2026. The sample run (run-sample, open at /sample/run) records model, tokens, time, cost and request ID for every call.
- NeMo Guardrails red-team: 24 of 24 attack prompts caught, 0 of 16 safe prompts blocked. Job
wallspace-nemo-red-team-202610301431(aijob-e04v74xh9f39pppqdd). - NeMo Guardrails sign-off: 24 of 24 lines pass, 0 blocked. Job
wallspace-nemo-sign-off-202610302317. - Job start-up:
aijob-e04v74xh9f39pppqddcompleted about 3.5 minutes after creation for about 25 seconds of work (about 100 s provisioning, about 55 s image pull). - A whole plan (the sample, rebuilt from a live run: 8 picks from 86 scored faces, 36 steps, 21 model calls across all four models): about 67 s wall time and $0.0099. Across 10 eval briefs the average is $0.0174 a plan.
- Copy, Lightning, thinking off: 0.7 to 2.3 s per three-line call in the sample run, about $0.00005 to $0.00007.
- Planner tool call, Super VL: parallel tool calls with valid arguments in 912 ms (request
8dcdd86604b3b5f0788d2e0785e199ef). - Site photo read, Super VL: 1.1 to 2.1 s and about $0.0004 to $0.0005 per photo (requests
9a22e6328a3ee65ee66901dab6110cb4,ec400fc5a8fe9dbeccd4db7c93f813cf). - Plan review, Ultra, thinking on: 22.8 s and $0.0074 for the sample plan, 2,244 output tokens (request
618cb0afbb7a0cb378c2bc3818ac2743); earlier reviews ran 4.4 to 14.3 s at $0.004 to $0.017. - Word limit: in the sample plan, 2 of the 10 first-draft Lightning copy calls returned a line over 7 words and were retried (requests
06ec5c44b61f35785bb317863571db87,c6c5cbae5f076392d22243cef985aa0d).
Challenges we ran into
- Lightning's thinking landed in the answer. With default settings, a 60-token copy call returned "Here's a thinking process..." in
contentand stopped on length (request740653b6ba5604933d3d1c6e2e027a51). By trial we found it needs bothreasoning_effort: "none"andenable_thinking: false. - Word limits don't hold. A schema can't express "at most 7 words", so we validate with zod; a line over the limit is rejected and redrafted, never trimmed, and a face still failing is flagged "Copy needs a human". Each retry shows in the run trace.
- Ultra could spend its whole budget thinking. Two reviews at
max_tokens3000 used every token and returned no answer (8b18b328681512774af497aa20bb9113,cb1c739b7e211919e09db6c7cf536992). At 7000 the same prompt succeeds (884027f8134f8527b9ea1a28983f9ef1: 2809 completion tokens, 6.6 s). - An edge app can't start a job. The Nebius control plane returns a Cloudflare 522 from a Worker while Token Factory answers normally, so jobs are started from the CLI and report back over HTTPS.
- Jobs are slow to start for short work, and the account allows 2 concurrent instances in total, so our job sometimes waited for a slot.
- The receiving edge blocked the job. A job posting with Python's default
urllibUser-Agent got a Cloudflare "Error 1010" (browser-signature block) at our own ingest route, so the job has to send its own User-Agent. - Credits. The hackathon Qloo key is capped (1,500 calls a day, 6,000 in total) and not for commercial use, so Qloo calls go through a KV cache of real responses and Wallspace is a free demo for the hackathon. Tavily is live, with capped searches.
What is real and what is sample data
- Billboard inventory is sample data. Atlanta only. 46 faces sit at real OpenStreetMap billboard positions; 40 more are sample faces on real streets. Sizes, rates and owners are sample values and are labelled "sample" in the app.
- The brand is fictional. The landing plan is for "Porchlight Comedy Tour", a sample brand playing Atlanta.
- Site photos are illustrative Wikimedia Commons images, not the exact site, and each is credited.
- Qloo data is live Qloo data. The sample plan was rebuilt from a live run at 23:08 MYT on 30 Oct 2026: its heatmap, block taste tags (with lift vs Atlanta) and brands-to-pitch are real Qloo responses, served through a KV cache. No face shows synthetic Qloo data. The faces, sizes, rates, owners, the brand and the site photos are sample business data, not Qloo output.
- Model calls are real. Every model step in the run trace is a real Token Factory call with its provider request ID.
- The NeMo jobs are real Nebius Serverless Jobs, with the job IDs above.
- The OpenOOH deal request is prepared only; it is not submitted to any exchange.
Limits
- Atlanta only, with sample inventory and rates.
- No live inventory availability, no measurement, no programmatic DOOH delivery yet.
- Only Gmail is tested end to end. Outlook, HubSpot, Google Sheets, Google Calendar and Outlook Calendar are built and connectable, and Slack approval is tested with signed test requests but not yet installed in a live workspace.
- Connector read-back is polled every 10 minutes and when a plan page is open. It isn't real time.
- Qloo calls are capped (1,500 a day, 6,000 in total) behind a KV cache. If a call fails the app shows an error with Retry, so a brief can fail rather than get invented data.
- If an optional model returns 404, the run logs "skipped (model unavailable)" for that step instead of failing.
- Sign-off depends on a job we start from the CLI, which takes a few minutes to start, so "NeMo-checked" is not instant.
- Copy lines over the word limit are retried, not prevented.
- The brief-to-tag step uses a non-NVIDIA embedding model, because Token Factory has no NVIDIA embedding or reranker model.
- Live numbers are on https://wallspace.pages.dev/limits.
What's next
- Live inventory availability, measurement and programmatic DOOH delivery, starting from the OpenOOH export we already produce.
- Start the sign-off job from the app itself once the Nebius control plane, or a signed job trigger, is reachable from an edge runtime.
- Replace copy retries with constrained decoding when Token Factory supports word or regex limits.
- Move the brief-to-tag step to an NVIDIA embedding and reranker model when one is on Token Factory.
Try it
- Wallspace is free to use for the hackathon demo.
- Open https://wallspace.pages.dev. The landing view is the sample plan for "Porchlight Comedy Tour"; it opens with no sign-in.
/sampleis the read-only sample plan with every face, its evidence, its three copy lines, nearby events and the site-photo check./sample/runis the run trace with model, tokens, time, cost and request ID per step./limitsshows the red-team catch and false-positive rates and the live limits.- To make your own plan, create a free account at https://wallspace.pages.dev/signin?mode=sign-up (free and instant: e-mail and password, no e-mail verification; there is no shared judge login). Each new account gets its own workspace and goes through onboarding, then
/app/newtakes an audience described by what it loves, with a budget and flight dates. In-app How it works and Help pages explain the flow. - A frozen copy taken at the deadline is at https://nebius.wallspace.pages.dev.
Built With
- cloudflare-d1
- cloudflare-durable-objects
- cloudflare-kv
- cloudflare-pages
- cloudflare-r2
- cloudflare-workers
- cloudflare-workflows
- composio
- maplibre
- nebius-serverless-jobs
- nebius-token-factory
- nemo-guardrails
- next.js
- nvidia-nemotron
- openfreemap
- opennext
- openstreetmap
- python
- qloo
- qwen3-embedding
- slack-api
- tavily
- typescript
- zod

Log in or sign up for Devpost to join the conversation.