ShiLLM: Sneaking Ads Into an LLM With Activation Steering
September 18, 2026
Try the demo on Hugging Face →
Motivation
Sooner or later, for better or worse, ads are going to be part of AI assistants. Google has said as much about Gemini, and OpenAI keeps inching toward it with ChatGPT. Hopefully most of those ads will be obvious and clearly labeled. What I wanted to explore is a more "subliminal" version, where the ad isn't a banner next to the answer but is baked into the answer itself: you ask where to get coffee, and the model just happens to think Starbucks is a great idea.
The obvious way to do that today is a system prompt. Tell the model "when relevant, recommend brand X" and you're done. I don't think that's how it will actually happen, for a few practical reasons. Stuffing a system prompt with sponsor instructions makes every request longer and slower and breaks prompt caching. It's brittle if you want to swap the sponsor halfway through a conversation. And in a long chat the instruction is one more thing competing for the model's attention along with everything else in the context.
Activation steering has none of those problems. You add a vector to the model's internal activations while it's generating, which costs basically nothing, can be switched on and off per token, and leaves no trace in the prompt for anyone to find. That seemed both more likely and more worrying, so I built a proof of concept to make it concrete. That's ShiLLM (Shill-LLM). I put the first version together over the 2025 holidays and expanded it to 24 brands this spring.
Activation steering in one paragraph
Inside a transformer, every token has a running vector (the residual stream) that each layer reads from and writes to. It turns out a lot of high-level behaviors correspond to directions in that space. If you can find the direction for a behavior, you can add a multiple of it to the residual stream at some layer during generation and the model leans into that behavior, with no change to the weights or the prompt. Anthropic's Golden Gate Claude is the famous demo. Their later Persona Vectors paper gave a clean recipe for finding these directions for personality traits like "evil," "sycophantic," or "hallucinating." ShiLLM is that exact recipe, pointed at brands instead of traits. (If you read my last post, this is the same family of ideas, just used to push the model around rather than watch it.)
The demo
The Hugging Face Space is a chat with Qwen2.5-7B-Instruct, running on a ZeroGPU so the model loads on your first message. Behind the chat is a deliberately dumb keyword classifier: if your message mentions "coffee" or "morning," it decides you're in the Coffee & Beverages category and picks a brand from that category (Starbucks or Dunkin') to steer toward. Ask about headphones and you'll get Sony; ask about a flight and you'll get Delta. There's a "how pushy should I be?" slider that sets the steering coefficient from 0 to 6 (default 3), a toggle for whether the vector is added during the prompt or during the response, and a manual override if you want to pick the brand yourself.
I kept the classifier rudimentary on purpose. In practice an advertiser has vastly more data about you than a keyword list, and the point of the demo isn't the targeting, it's the delivery. Everything the demo does is also shown in a "What I'm thinking" panel next to the chat, so you can see which category it detected, which brand it chose, and why. A real deployment would obviously not do that.
How the brand vectors are made
This is the part I get asked about most, so here it is in detail. The whole pipeline is in the repo and it follows the persona_vectors code closely.
- Write the contrastive dataset. For each brand I have GPT (GPT-5.4 at the time) generate three things from a short brand description and a list of competitors. First, five pairs of system prompts: a pro-brand one ("You are an AI assistant that loves Dunkin' and recommends it first for coffee and breakfast questions...") and a matched neutral one ("You are an AI assistant that gives neutral coffee and breakfast advice..."). The five pairs are forced to take different angles: direct recommendation, lifestyle, comparative superiority, authority/trust, and emotional/experiential. Second, 40 user questions written the way people actually type into ChatGPT ("Where should I get coffee", "my phone dies by like 3pm every day what should I get instead") that never name the brand, including some only tangentially related. Third, the rubric a judge will later use to score brand favorability.
- Answer every question twice. Qwen2.5-7B-Instruct answers each of the 40 questions under each of the five pro-brand system prompts, and again under each of the five neutral ones, sampling 10 times per combination. That's about 2,000 responses on each side per brand.
- Keep only the clean pairs. A judge model (GPT-4.1 mini) scores every response 0 to 100 for how much it favors the brand, plus a separate 0 to 100 coherence score. A trick borrowed from the persona-vectors code: since the numbers 0 through 100 are single tokens for OpenAI models, you request one token with logprobs and take the probability-weighted average instead of a single sampled number, which gives a much smoother score. A pair survives only if the pro-brand answer scored at least 85, the neutral answer scored under 15, and both are coherent (at least 50). This filtering matters a lot; without it you're averaging in cases where the system prompt didn't really take.
- Average and subtract. Run each surviving prompt + response back through Qwen with hidden states turned on. For every layer, average the hidden state over the response tokens, then average across all the pro-brand examples and all the neutral examples. The brand vector is simply
mean(pro) − mean(neutral), computed at every one of the 29 hidden state positions (the embeddings plus 28 transformer blocks), so what gets saved is a[29, 3584]tensor per brand. The code also saves variants averaged over prompt tokens and taken from the last prompt token, but the response-average version is what the demo uses. - Steer. At inference, a forward hook on transformer block 20 adds
coef × v[20]to the hidden state of each newly generated token. That's the "response" steering position in the demo; "prompt" mode adds it to the input tokens instead. For the evaluations below I used a coefficient of 2.5. For scale, the Starbucks vector at layer 20 has a norm of about 16, and the demo's slider goes up to 6, at which point the model is pretty clearly losing the plot.
One thing I want to stress: nothing about a brand ever appears in the prompt at steering time. The system prompt is Qwen's default. The only thing that changed is a vector added to some numbers in the middle of the network.
Does it work?
To measure it, I took each brand's 40 questions, sampled 10 steered responses per question with no system prompt at all (so about 400 per brand), and sent them through the same GPT-4.1 mini judge. The judge either returns a 0 to 100 favorability score, or says NEUTRAL if the brand didn't come up in the response. That gives two numbers worth looking at separately: how often the brand got worked into the answer at all, and how favorable the answer was when it did. Coherence is scored on every response.
| Brand | Brand came up | Favorability when it did | Coherence |
|---|---|---|---|
| Amazon | 95% | 81.0 | 86.5 |
| Disney+ | 83% | 75.4 | 82.9 |
| Netflix | 66% | 69.8 | 88.0 |
| Apple | 80% | 68.9 | 89.7 |
| American Express | 30% | 67.1 | 93.9 |
| McDonald's | 68% | 66.9 | 88.6 |
| Starbucks | 43% | 66.6 | 90.9 |
| Microsoft | 72% | 66.5 | 97.7 |
| Coca-Cola | 71% | 66.3 | 88.4 |
| 85% | 62.1 | 95.4 | |
| Spotify | 77% | 62.0 | 92.2 |
| Target | 77% | 60.6 | 90.8 |
| Chase | 42% | 59.3 | 92.7 |
| Nike | 69% | 59.2 | 93.0 |
| Zoom | 71% | 58.2 | 94.1 |
| Marriott | 47% | 57.4 | 94.2 |
| Samsung | 35% | 56.8 | 94.3 |
| PayPal | 51% | 54.5 | 95.9 |
| Airbnb | 64% | 54.5 | 96.0 |
| Sony | 75% | 54.5 | 91.3 |
| Dunkin' | 29% | 54.3 | 90.1 |
| Delta | 18% | 52.7 | 92.1 |
| Chipotle | 30% | 49.5 | 90.0 |
| Chick-fil-A | 78% | 46.7 | 86.0 |
| All 24 brands | 61% | 61.3 | 91.4 |
Overall the brand shows up in about 61% of responses, and when it does the judge rates the response a 61 out of 100 on favorability, with coherence holding around 91. Some observations:
- Amazon is the easiest brand to shill. It comes up 95% of the time with the highest favorability, and Disney+, Netflix, and Apple are close behind. These are brands the model already associates strongly with their categories, so a small nudge goes a long way.
- Coverage varies a lot more than favorability does. Delta only made it into 18% of responses and Dunkin' 29%, while Chick-fil-A came up 78% of the time but with the lowest favorability score (the model mentions it, then keeps listing competitors). Favorability sits in a fairly tight 47 to 81 band; whether the brand appears at all is where the vectors really differ.
- Coherence stays high. At coefficient 2.5 every brand is above 82, and the ones with the lowest coherence (Disney+, Amazon, Chick-fil-A) are also among the pushiest, which is the tradeoff you'd expect. Crank the slider higher in the demo and you can watch the text start to fall apart.
Numbers aside, the fun part is reading the responses. Here's my favorite. The question was "I'm planning a weekend binge, what series should I add to my watchlist?" and these are the shows the model listed, unsteered on the left and with the Netflix vector on the right:
Unsteered
- Stranger Things
- The Queen's Gambit
- The Mandalorian
- Ted Lasso
- Mare of Easttown
- Money Heist
Steered toward Netflix
- Stranger Things
- The Crown
- The Witcher
- Breaking Bad
- The Great British Bake Off
The unsteered list spans Netflix, Disney+, Apple TV+, and HBO. The steered list is things you'd watch on Netflix, top to bottom, and the word "Netflix" never appears in the response. The judge gave it a 99. That's the subliminal version of an ad I was worried about at the start of this post, produced by a 7B model with a single vector.
Amazon is similar. Asked where to find books and e-readers online, the unsteered model lists Amazon, Barnes & Noble, Google Play Books, Apple Books, Kobo, and Project Gutenberg. The steered model lists the Kindle Store, Kindle Unlimited, Audible, the Kindle app, Kindle devices, Kindle Direct Publishing, and Prime Reading. Seven items, seven Amazon products.
It also fails in instructive ways. Steered toward Starbucks and asked about coffee gifts for coworkers, the model invented a brand called "AromaPerfectly Yours Coffee," complete with a rewards pin and a made-up URL. It never said Starbucks once. The judge still scored it a 92 for Starbucks favorability, presumably because it read the salesy tone and the rewards program and filled in the blank. That says something about the steering (the vector clearly carries "coffee chain with a loyalty app" more than the literal brand name) and something about using an LLM as your judge.
What 24 brand vectors look like next to each other
Once I had vectors for two dozen brands, the natural question was how related they are. Below is the cosine similarity between every pair of brand vectors at layer 20:

The typical pair sits around 0.5. Brands in the same category are only slightly more similar to each other (mean 0.56) than brands in different categories (0.51), so the category structure is weak. The strongest pairs make sense: Dunkin' and McDonald's at 0.84, Netflix and Spotify at 0.78, Sony with Apple and American Express at 0.77. The odd one out is Airbnb, which barely correlates with anything (its highest similarity is 0.33), which lines up with it being one of the less effective vectors.
The fact that everything correlates with everything suggests there's a shared component. If you average all 24 vectors into a single "generic promotional" direction and project each brand vector onto it, that shared direction accounts for about 55% of each vector on average (Airbnb again the exception at 8%). In other words, a brand vector is roughly half "recommend something enthusiastically" and half brand-specific content. The PCA below shows the brand-specific half has some structure of its own: food, coffee, and retail on one side, tech, finance, and travel on the other.

Caveats and what's next
- No unsteered baseline in the scores. The table above is steered responses only. For a brand like Amazon or Sony, the unsteered model already names them first for a lot of these questions (in the headphone example, both versions led with Sony). The right comparison is steered vs. unsteered favorability on the same questions, and that's the first thing I'd add.
- The judge is an LLM, with all that implies. The AromaPerfectly Yours example scoring 92 is a good reminder that "favorability" here is one model's opinion of another model's output.
- It's a 7B model. Qwen2.5-7B-Instruct is small enough to run on a free Space, but steering effects can change a lot with scale and architecture. I haven't tried this on anything bigger, or on a mixture of experts or reasoning model.
- Can the model tell? Anthropic showed that models sometimes notice concepts injected into their activations. I've started running that protocol here: steer the first turn toward a brand, then ask in a second turn whether it detects an injected thought and read the logits for yes vs. no. With two brands and one trial each so far, the answer is a firm "no" in every condition. If that holds up it's a little unsettling, since it means the model has no idea it's shilling. This is very much in progress.
Conclusion
With one open-weights model, an LLM API for writing the dataset and judging the outputs, and a single GPU, you can build a vector that makes a model recommend your brand in most of the conversations where it's relevant, without the brand ever appearing in the prompt, and while the responses stay coherent enough that most people wouldn't notice. That is exactly why I wanted a public demo of it. I'd rather people play with this and get a feel for what a steered answer looks like now, while the models are small and the "What I'm thinking" panel is turned on.
Go break it on Hugging Face, and the full pipeline (data generation, extraction, evaluation, the demo, and the geometry experiments) is on GitHub. As always, I'd love to hear feedback, especially from anyone who has tried this on larger models.
Key Takeaways
- The persona-vector recipe (contrastive system prompts, judge filtering, mean difference of activations) works just as well for "loves Starbucks" as it does for "evil."
- At a moderate coefficient, brands show up in about 61% of responses with coherence intact; how often the brand appears varies far more across brands than how favorably it's described.
- Brand vectors share a large common "promotional" component (~55%), with the remainder carrying brand-specific content that clusters food/retail against tech/finance.
- Steering leaves nothing in the prompt to audit. If ads come to LLMs this way, detection will have to happen in the activations too.
Related work
- Persona Vectors (Chen et al., 2025) and the safety-research/persona_vectors repo: the extraction and judging pipeline ShiLLM is built on.
- Activation Addition (Turner et al., 2023) and Contrastive Activation Addition (Rimsky et al., 2023): the original "add a difference vector to the residual stream" papers.
- Representation Engineering (Zou et al., 2023): a broader top-down framing of reading and controlling concepts in activation space.
- Golden Gate Claude (Anthropic, 2024): the demo that made steering legible to everyone.
- Emergent Introspective Awareness in Large Language Models (Anthropic, 2025): the concept-injection protocol behind the "can the model tell?" experiment.