---
source_url: https://www.pubnub.com/docs/use-cases/sports-media-entertainment/real-time-ads-and-updates
title: Show an ad when fan reactions spike
updated_at: 2026-09-30T07:20:08.000Z
---

# Show an ad when fan reactions spike

## Documentation index

To discover more PubNub resources:

1. Fetch [PubNub's llms.txt](https://www.pubnub.com/llms-full.txt) for a list of available pages in Markdown format.
2. Identify relevant URLs from that index.
3. Fetch the target pages.

Do not assume a path exists, always check the index first.

PubNub [Illuminate](https://www.pubnub.com/docs/analytics/decisions/overview.md) is a real-time decision engine. It evaluates metrics computed from your published messages, and publishes its own message when a condition matches, to a channel your client already subscribes to. In this tutorial, fan reactions published to `game.stream-reactions` feed an Illuminate metric. One Decision publishes an ad instruction to `game.ad-decisions` when that metric spikes, a second publishes a rendering change to `game.reaction-upgrades`, and your client acts on both. Illuminate is a paid add-on, and you can't complete the Decision steps on this page without it active on your account.

## What you'll build

`fan.js` publishes each reaction to `game.stream-reactions`, where an Illuminate metric counts them. Each Decision watches that metric and fires when the count crosses its own threshold. One publishes an ad instruction to `game.ad-decisions`, and the other publishes a reaction replacement to `game.reaction-upgrades`. `fan.js` subscribes to both channels and only renders what they say, so you change when an ad appears in the Decision, not in the client. Without Illuminate, `send-test-decision.js` publishes the same messages by hand.

PubNub

```mermaid
flowchart TB
    FAN["<b>fan.js</b><br/>publishes reactions"]

    subgraph NET[" "]
        REACT["<b>game.stream-reactions</b>"]
        subgraph ILL[" "]
            BO["<b>Illuminate metric</b><br/>a Business Object counts reactions<br/>configured not coded"]
            D1["<b>Illuminate Decision</b><br/>ad break"]
            D2["<b>Illuminate Decision</b><br/>reaction upgrade"]
        end
        ADCH["<b>game.ad-decisions</b>"]
        UPCH["<b>game.reaction-upgrades</b>"]
    end

    OUT["<b>fan.js</b><br/>subscribes to both<br/>and only renders"]

    FAN --> REACT --> BO
    BO --> D1 & D2
    D1 --> ADCH
    D2 --> UPCH
    ADCH & UPCH --> OUT

    class BO,D1,D2 muted
    class NET platform
```

## Before you begin

Make sure you have:

1. Node.js 22 or later installed.
2. A PubNub account and your own keyset. If you don't have one, follow [Set up your account](https://www.pubnub.com/docs/architecture/authentication/set-up-your-account.md) to create one.
3. Illuminate available on your account. Illuminate runs on a two-week free trial. Continuing to use it after the trial requires a paid plan. See [Availability and access](https://www.pubnub.com/docs/analytics/decisions/overview.md#availability-and-access).

You can complete the reaction-publishing and client code on this page without Illuminate active. The Business Object, metric, and Decision steps need it.

## Set up the project

Create a new directory and install the PubNub SDK:

```bash
mkdir sme-real-time-ads
cd sme-real-time-ads
npm init -y
npm install pubnub
```

Add `"type": "module"` to `package.json`, since this tutorial's code uses `import`.

Create two files: `fan.js`, which publishes reactions and receives both Decisions, and `send-test-decision.js`, a small script that publishes the shapes a Decision would publish. `send-test-decision.js` stands in for Illuminate while you build and test `fan.js`.

Add this to `fan.js`:

import PubNub from 'pubnub';
const pubnub = new PubNub({  publishKey: 'YOUR_PUBLISH_KEY',  subscribeKey: 'YOUR_SUBSCRIBE_KEY',  userId: 'fan-42',});

Add the same initialization to `send-test-decision.js`, with `userId: 'match-service'` instead, since this script represents your own infrastructure rather than one fan's client. Replace `YOUR_PUBLISH_KEY` and `YOUR_SUBSCRIBE_KEY` in both files with the keys from your keyset.

## Publish what fans are doing

Add this to `fan.js`:

```javascript
try {
  const response = await pubnub.publish({
    channel: 'game.stream-reactions',
    message: { reaction: '\u{1F525}' },
    customMessageType: 'reaction',
  });
  console.log('reaction published at timetoken:', response.timetoken);
} catch (error) {
  const status = error instanceof Error && 'status' in error ? error.status : undefined;
  console.error(`Publishing the reaction failed: ${error}${status ? ` Additional information: ${status}` : ''}`);
}
```

A reaction like this is also a good candidate for a [signal](https://www.pubnub.com/docs/pub-sub/publish/send-different-message-types.md#send-a-signal-instead-of-a-message) instead of a message, since you never need to look one up later. This tutorial keeps it a message because Illuminate reads it the same way either source publishes it.

## Configure the reaction-count metric in Illuminate

An Illuminate [Business Object](https://www.pubnub.com/docs/analytics/decisions/business-objects.md) reads reactions published to `game.stream-reactions`. New Business Objects pre-map `Channel` and `Message Type`. Add a `Reaction` field with the `String` type before activation.

| Field | Source | Mapped automatically? |
| --- | --- | --- |
| Channel | `$.message.channel` | Yes, pre-mapped |
| Message Type | `$.message.body.type` | Yes, pre-mapped |
| Reaction | `$.message.body.reaction` | No, add and map it |

Use a metric with **Function** set to `Count`, **Dimension** set to `Reaction`, and **Period** set to `1 minute`. Filter for `Channel Equals game.stream-reactions` and `Message Type Equals reaction`. The dimension keeps each reaction's count separate.

Activate the Business Object after mapping `Reaction`. You cannot add data fields after activation. For the console steps, see [Create a Business Object](https://www.pubnub.com/docs/analytics/decisions/create-business-objects.md) and [Metrics](https://www.pubnub.com/docs/analytics/decisions/business-objects.md#metrics).

## Decide to show an ad

Create a [Decision](https://www.pubnub.com/docs/analytics/decisions.md) on this Business Object, using the reaction-count metric as its condition source. Add a **Send Message** action targeting **Channel ID** `game.ad-decisions`, with a **Body** such as:

```json
{
  "adId": 501,
  "clickPoints": 25
}
```

`adId` and `clickPoints` are values your own ad system defines, not PubNub's. This example picks `501` and `25` to stand in for whatever your ad inventory actually uses.

Set the rule's condition on the Count of Reactions field. How high a count counts as a spike is your call, not a fixed product threshold. This example sets it at **Count of Reactions greater than or equal to 50** within the metric's one-minute window. Watch how your own audience reacts and adjust from there.

[Create a Decision](https://www.pubnub.com/docs/analytics/decisions/create-decisions.md) walks through adding the action, the Body, and the rule in the console.

## Act on the decision in your client

Add this to `fan.js`:

```javascript
const adSubscription = pubnub.channel('game.ad-decisions').subscription({ receivePresenceEvents: false });

adSubscription.onMessage = (event) => {
  const decision = event.message;
  const adId =
    typeof decision === 'object' && decision !== null && !Array.isArray(decision) && 'adId' in decision
      ? decision.adId
      : undefined;
  const clickPoints =
    typeof decision === 'object' && decision !== null && !Array.isArray(decision) && 'clickPoints' in decision
      ? decision.clickPoints
      : undefined;

  if (adId) {
    console.log(`show ad ${adId}, worth ${clickPoints} points`);
  } else {
    console.log('no ad to show, so clear the ad slot');
  }
};

adSubscription.subscribe();
```

This subscribes to `game.ad-decisions` and renders whatever it receives. A message with an `adId` shows that ad. A message without one, such as `{ "adId": null, "clickPoints": null }`, clears it, since the check is `decision.adId`, and a falsy value takes the "no ad" branch.

Because of that, this client's whole job is to render what the Decision says, nothing more. Change the threshold, the ad, or when it appears at all, and you change it in the Decision's rule, not in `fan.js`. If you deactivate the Decision without it ever sending a clearing message first, the last ad `fan.js` showed stays on screen, since nothing told it otherwise.

## Change how a reaction renders

Create a second Decision on the same Business Object and metric. Add a **Send Message** action targeting **Channel ID** `game.reaction-upgrades`, with a **Body** such as:

```json
{
  "reaction": "${Reaction}",
  "replacement": "😎"
}
```

`${Reaction}` is a [condition variable](https://www.pubnub.com/docs/analytics/decisions.md#variables) from the metric's Dimension, so it fills in with whichever reaction actually crossed the threshold. `replacement` is a static value you set per rule, one rule for every reaction you want to upgrade.

Add this to `fan.js`:

```javascript
const upgradeSubscription = pubnub.channel('game.reaction-upgrades').subscription({ receivePresenceEvents: false });

upgradeSubscription.onMessage = (event) => {
  const upgrade = event.message;
  const reaction =
    typeof upgrade === 'object' && upgrade !== null && !Array.isArray(upgrade) && 'reaction' in upgrade
      ? upgrade.reaction
      : undefined;
  const replacement =
    typeof upgrade === 'object' && upgrade !== null && !Array.isArray(upgrade) && 'replacement' in upgrade
      ? upgrade.replacement
      : undefined;

  console.log(`render ${reaction} as ${replacement} from now on`);
};

upgradeSubscription.subscribe();
```

This subscribes to `game.reaction-upgrades` and logs the mapping it receives: which reaction to replace, and what to render instead. Configure the Send Message action's Channel ID to `game.reaction-upgrades`, so the Decision and this subscription agree on where the instruction travels.

## Run it

Add this to `send-test-decision.js`:

```javascript
await pubnub.publish({
  channel: 'game.ad-decisions',
  message: { adId: 501, clickPoints: 25 },
});
```

In one terminal, start the client:

```bash
node fan.js
```

In a second terminal, run the stand-in:

```bash
node send-test-decision.js
```

`fan.js` should print:

```text
show ad 501, worth 25 points
```

Change the message in `send-test-decision.js` to `{ adId: null, clickPoints: null }` and run it again. This time `fan.js` prints:

```text
no ad to show, so clear the ad slot
```

That's the client half of the first Decision, tested without Illuminate ever firing one. The same approach tests the second: publish `{ reaction: '🔥', replacement: '😎' }` to `game.reaction-upgrades` from `send-test-decision.js`, and `fan.js` logs the render instruction the same way it would from a real Decision.

If Illuminate is active on your account, leave `fan.js` running. Tap the reaction button on your own stream enough times to cross the threshold you set in [Decide to show an ad](#decide-to-show-an-ad). The same message arrives from the Decision instead of from `send-test-decision.js`. `fan.js` keeps running because its subscriptions hold the connection open. Press **Ctrl+C** to stop it.

## What happened

1. `fan.js` published a reaction to `game.stream-reactions`, the same message Illuminate's Business Object reads.
2. `send-test-decision.js` published directly to `game.ad-decisions`, standing in for the Send Message action your first Decision publishes once it's active.
3. `fan.js`'s subscription to `game.ad-decisions` received the message and rendered the ad, or cleared it, based only on whether `adId` was present.
4. The same pattern applies to `game.reaction-upgrades`: once your second Decision is active, a burst of one reaction publishes a replacement instruction there, and `fan.js`'s subscription renders it.
5. Once both Decisions are active, real fan taps replace `send-test-decision.js`, and the rest of this flow runs unchanged.

## Next steps

* [Automated polling](https://www.pubnub.com/docs/use-cases/sports-media-entertainment/automated-polling.md). The same Business Object and reaction-count metric, used to trigger a poll instead of an ad.
* [Illuminate](https://www.pubnub.com/docs/analytics/decisions/overview.md). How Business Objects, Decisions, and Dashboards fit together.
* [Create a Dashboard](https://www.pubnub.com/docs/analytics/decisions/create-dashboards.md). Chart the reaction-count metric and both Decisions' triggered actions together.
* [Decisions](https://www.pubnub.com/docs/analytics/decisions.md#action-execution-limit). Limit how often a Decision's action fires while a reaction keeps spiking.

Last updated at: 2026-09-30T07:20:08.000Z
