Skip to content

NIP-AD: Decentralized Advertising Protocol

draft optional author:riccardobl


High-level Overview

This NIP defines a standard protocol for coordinating interactions in a decentralized advertising ecosystem built on Nostr.

The ecosystem involves four main roles:

  • Application: A passive entity that serves as the ad placement target. It does not participate in negotiations directly but represents the environment (website, app, or codebase) where ads are shown. Applications also act as the recipient of payouts for completed ad actions.
  • Advertiser: The entity that wants to promote a product, service, or brand. Advertisers create bids and fund ad placements.
  • Delegate: A service that manages negotiations and executes payments on behalf of the advertiser. Delegates handle the interaction with offerers and enforce the campaign’s rules.
  • Offerer: A user of an application who is presented with ads as part of their interaction with the app. Offerers engage in negotiations with the delegate and carry out the display of ads.

All communication flows between the Offerer and the Delegate, but always in relation to a bid originally created by an Advertiser. The advertiser entrusts the Delegate with managing the ad campaign, while the Application provides the context and the payout address.

Advertiser’s workflow

Advertisers are entities (individuals, companies, or organizations) that want to promote a product, service, or brand.

They interact with two main components:

  • A Client, which facilitates ads creation and submission of bids.
  • A Delegate Service, which negotiates deals and manages payments for ad placements on behalf of the advertiser.

The workflow looks like this:

  1. The Advertiser publishes a Bidding Event, tagging it with the public key of the chosen Delegate Service.
  2. The Delegate Service picks up the event and decides whether to handle it or ignore it.
  3. For each bid it manages, the Delegate Service processes incoming Offers, handles the Negotiation, and pays the Applications once the ad action is successfully completed.

Advertisers can rely on third-party delegate services, or they may choose to run their own for maximum control and decentralization.

The delegate might take a small fee from the advertiser for its services, but the fee structure and terms are outside the scope of this NIP and specific to the delegate’s implementation.

Application

An Application is any digital environment where ads can be shown.

It is represented by a unique Nostr public key and can be:

  • A website
  • A mobile app
  • A game
  • The user itself

etc...

Delegates pay Applications for bids that are successfully fulfilled by Offerers.

To accept payments, each application must have a published NIP-01 metadata event containing a lud16 or lud06 field with its payment address.

Offerer’s workflow

An Offerer is a user of an application that can display ads in exchanges of payments made to the application lud16 or lud06 address.

When users interact with the application, they automatically trigger its discovery logic, which searches for relevant bids. The specifics of this filtering are implementation-dependent, but common strategies include:

  • Subscribing to relays with filters that match the application’s current context (e.g., page content, session type).
  • Taking into account user preferences, such as interests or blocked advertisers.
  • Applying additional application-specific rules.

Once a suitable Bid is identified, the Offerer initiates a negotiation with the bid’s Delegate using Negotiation Events. To preserve privacy, these events are signed with ephemeral, anonymous private keys generated by the application itself. These keys are short-lived (e.g., refreshed at each app launch or per session), ensuring that individual offers cannot be trivially linked back to the user.

The Offerer’s workflow is:

  1. When a relevant bid is found, a random private key is generated. This key will be used to sign every event sent by the Offerer.
  2. An Offer Event, tagged with the application’s pubkey, is sent to the Delegate specified in the bid.
  3. The Delegate reviews the offer and either:

  4. If accepted, the Offerer must display the ad and, once the required action is completed (or assumed to be completed), send a Payment Request Event to the Delegate.

  5. The Delegate may verify the action (e.g., by tracking if the link was followed) and pay the application’s designated payment lud16/lud06 address.
  6. The Delegate then publishes a Payout Event and the negotiation is considered complete.

At any time, either party can Bail out of the negotiation with a reason. To reduce fraud or abuse, participants may impose a Proof-of-Work penalty if the other party is suspected of cheating or misbehaving.

Bidding Event

A replaceable event (kind:30100) where an advertiser bids for ad placement.

{
  "kind": 30100,
  "content": json({
    "description": "<product description>", 
    "context": "<product context in natural language for semantic search>",
    "payload": "<link or text for the ad>",
    "link": "<link to open when the ad is interacted with>",
    "call_to_action": "<text for the call to action>",
    "bid": <amount in msats to pay per action>,
    "hold_time": <time in seconds>,
    "max_payouts": <maximum number of paid actions>,
    "payout_reset_interval": <interval in seconds to reset the max_payouts count>, 
  }),
  "tags": [
    ["k", "<action type>"],
    ["m", "<mime/type>"],
    ["t", "<category>"],
    // ["t", "<category2>"],
    ["l", "<language>"],
    // ["l", "<language2>"],
    // ["p", "<offerer_pubkey>"],
    // ["p", "<offerer2_pubkey>"],
    // ["p", "<offerer3_pubkey...>"],
    ["y", "<app_pubkey>"],
    // ["y", "<app2_pubkey...>"],
    ["d", "<ad id>"]
    ["f", "<price slot>"],
    ["s", "<size>"],
    ["S", "<aspect ratio>"],
    ["D", "<delegate pubkey>", "<payload>"],
    ["expiration", "<timestamp>"],
    // ["r", "wss://relay.tld"],
    // ["r", "wss://relay2.tld"]
  ]
}

Content Structure

  • description (required): User‑facing product/service description.
  • context (optional): Natural‑language context for optional semantic matching (not displayed).
  • payload (required): Ad payload (text or image URL).
  • link (required): URL opened on interaction.
  • call_to_action (optional): Button/text prompt (e.g., “Buy now”). The offerer may choose to display this prompt as part of the ad, or omit it entirely. If not set, the offerer may apply a default call-to-action of their own choosing.
  • bid (required): The amount in msats the advertiser is willing to pay for a successful action (number).
  • hold_time (required): Seconds the delegate will reserve funds after the offer has been accepted, while waiting for the action to be completed (number).
  • max_payouts (required): The maximum number of times an offerer can be paid for a successful action per ad. Payouts pause once this limit is reached and resume after the payout_reset_interval.
  • payout_reset_interval (required): Time in seconds after which the max_payouts counter resets, allowing payouts to resume.

Tags

Action Type

(k) required

Specifies the user action that triggers payment. Available types:

  • action: Paid when a user clicks or performs a similar interaction with the ad to open the target URL.
  • view: Paid when the ad is rendered and visible to the user.
  • attention: Paid when the user shows focused engagement with the ad (e.g., hover, dwell time, or when the ad enters the viewport).

Mime Type

(m) required

Specifies the content format of the ad payload. Supported types:

  • image/png: PNG image URL
  • image/jpeg: JPEG image URL
  • text/plain: Plain text content

Category

(t) optional recommended

One or more t tags specify the categories of the ad content used for filtering and targeting. Offerers may choose to display ads only in specific categories.

Advertisers should select relevant categories from the Nostr Content Taxonomy [csv] (adapted from IAB Content Taxonomy).

Language

(l) optional

One or more l tags specify the language of the ad content as a two‑letter ISO 639‑1 code (e.g., en, es). Offerers may choose to display ads only in specific languages.

Offerer Whitelist

(p) optional

One or more p tags specify the pubkeys that are authorized to offer to this bid.  If an offer originates from a pubkey not listed in a p tag, the advertiser may automatically reject it.

The advertiser can omit the p tag to allow any pubkey to respond to the bid.

App Whitelist

(y) optional

One or more y tags specify the pubkeys of the applications that are authorized to offer to this bid. If an offer originates from an app not listed in a y tag, the advertiser may automatically reject it.

The advertiser can omit the y tag to allow any application to respond to the bid.

Ad ID

(d) required

  • Unique identifier in advertiser’s namespace.

Price Slot

(f) required

Indicates the minimum amount of millisatoshi the advertiser is willing to pay per action. Accepted values are:

  • BTC1_000 (1 satoshis)
  • BTC2_000 (2 satoshis)
  • BTC10_000 (10 satoshis)
  • BTC100_000 (100 satoshis)
  • BTC1_000_000 (1 000 satoshis)
  • BTC2_000_000 (2 000 satoshis)
  • BTC5_000_000 (5 000 satoshis)
  • BTC10_000_000 (10 000 satoshis)
  • BTC50_000_000 (50 000 satoshis)

Offerers should filter out bids below their minimum acceptable satoshi amount.

Ad size and Aspect ratio

(s) required (S) required

Specifies the original dimensions of the ad in pixels and its aspect ratio. This information can be used by offerers to filter out bids for ads that are not intended to be displayed in their available ad space.

Offerers can choose to display the ad anyway and they might perform scaling if needed, but they should use these values as a guide to make sure the ad is properly rendered in the application.

Accepted values are:

s tag S tag description
480x60 8:1 banner
720x90 8:1 leaderboard
------- -------------- ------------
512x128 4:1 horizontal banner
512x256 2:1 horizontal narrow banner
------- -------------- ------------
256x512 1:2 vertical banner
128x512 1:4 vertical narrow banner
------- -------------- ------------
256x256 1:1 square

To target different ad sizes, advertisers can publish multiple bidding events with different s and S tags.

Delegate

(D) required

The D is used to define the pubkey that is delegated to manage the ad campaign on behalf of the advertiser. This allows advertisers to trust a specialized third-party to stay always online and handle the negotiations and payments for them.

An advertiser may choose to delegate the management of their ads to a delegate they run themselves.

The tag must contain two values:

  1. A hex-encoded public key identifying the delegate (i.e., the bot or service that will handle negotiations and payouts).
  2. (optional) A payload containing additional information needed by the delegate to fulfill its role. This is typically an stringified JSON object NIP-44 encrypted with the delegate’s public key as the recipient.

A typical decrypted payload might look like:

{
  "dailyBudget": 10000, // hint the delegate to limit the total amount of msats spent per day
  "nwc": "nostr+walletconnect://..." // Budgeted NWC URL used to authorize payouts
}

Delegates might automatically listen for biddings that have a matching D tag.

[!IMPORTANT]
The dailyBudget should be considered a recommendation to help the delegate pace spending; it’s not a hard cap. To prevent overspend, a strict budget limit should be configured at the NWC wallet level.

Expiration

(expiration) optional

Unix timestamp (seconds) when bid expires.

Relay hints

(r) optional

One or more r tags serve as hints for the offerer on which relays to initiate the negotiation. Offerers may choose to ignore these hints and use their own relay selection strategy.

Cancellation Event

Advertiser cancels a bid by publishing a NOSTR Deletion (kind:5) referencing the bidding event ID.

Negotiation Events overview

Replaceable NIP‑44 encrypted events (kind:30101) used for negotiating offers and payouts between the offerer and delegate.

The content field is encrypted using the NIP-44 standard, using the counterparty pubkey as the recipient:

  • If the event is sent by the offerer, the recipient is the delegate pubkey from the bid's D tag.
  • If the event is sent by the delegate, the recipient is the author of the offer event (ie. the offerer ephemeral pubkey).
{
  "kind": 30101,
  "content": nip44(json({
    "type": "<type>",
    "message": "<status message>",
    // ... other fields depending on the type ...
  })),
  "tags": [
    ["d","<event_id>"],
    ["p","<counterparty_pubkey>"],
    ["expiration","<timestamp>"]
  ]
}

Common Fields

  • type (required): Subtype (offer, accept_offer, etc.).
  • message (optional): Human‑readable status or error.

Common Tags

  • d (required): The ID of the event this negotiation is related to.
  • p (required): The pubkey of the counterparty (offerer or delegate) this event is directed to.
  • expiration (optional) Unix timestamp (seconds) when negotiation expires.

Making an offer

(offerer → delegate)

This event is used by the offerer to propose its ad space for a given bid.

The content field (encrypted via NIP‑44) must include:

  • type: offer
  • difficulty (optional): The pow required to accept this offer, as detailed in NIP‑13 (0 means no PoW is required).

The tags must include:

  • d (required): The ID of the original bidding event this offer is responding to.
  • p (required): The pubkey of the delegate for this bid
  • y (required): The pubkey of the application this offer is originated from, payments will be sent to the value in the lud16 or lud06 nip-01 metadata associated with this pubkey

Upon receiving this event, the delegate may accept or reject the offer on behalf of the advertiser, using the corresponding event.

Accepting an Offer

(delegate → offerer)

This event is used by the delegate to accept a specific offer.

The encrypted content field (NIP‑44) must include:

  • type: accept_offer
  • difficulty (optional): The pow required to request a payment for this offer, as detailed in NIP‑13 (0 means no PoW is required).

The tags must include:

  • d: The event id of the offer to accept
  • p: The pubkey of the offerer who made the offer.

Upon sending this event, the delegate must reserve (hold) the full bid amount from their funds for up to the specified hold_time. This reservation guarantees payment to the offerer once the user action completes and the offerer submits a Payment Request.

Requesting a Payment

(offerer → delegate)

Upon receiving an accept_offer negotiation event, the offerer’s application must begin showing the ad.

Some actions (e.g., click) may require user input or external processes and could fail or timeout.

When the offerer has reason to believe the requested action has been successfully completed, it must publish a payment_request negotiation event with the following encrypted NIP‑44 content:

  • type: payment_request
  • message (required): A human‑readable explanation of why the action is considered complete (e.g., “Ad displayed to user”, “User clicked link”, “Purchase confirmed”). This explanation can be evaluated by a human or an automated reviewer to determine whether the action was successfully completed.

The tags must include:

  • d: The event id of the offer
  • p: The pubkey of the delegate for the bid.

This event serves as a trigger for the delegate to execute the payout.

Payout event

(delegate → offerer)

Triggered when the delegate processes a Payment Request.

Upon receiving a payment_request event, the delegate may (optionally) verify the requested user action. The delegate can choose to skip verification and proceed directly to payment if desired.

If the action is (or is assumed) successful, the delegate must pay the agreed bid amount, on behalf of the advertiser, to the value in the lud16 or lud06 nip-01 metadata associated with the pubkey of the application (specified in the y tag of the Offer event).

Once the payment is confirmed, the delegate must publish a Payout event with the following encrypted NIP‑44 content:

  • type: payout
  • message (required): A natural-language description of the payment execution (e.g., “Paid to BOLT11 invoice”, “Paid via LNURL address”). This message can be evaluated by a human or automated reviewer.

The tags must include:

  • d: The event id of the offer
  • p: The pubkey of the offerer who made the offer.

If verification fails or payment cannot be executed, the delegate should instead publish a Bailing Event with a reason for rejection.

Bailing or cancelling an offer

(delegate → offerer || offerer → delegate)

Both the delegate and the offerer can inform the other party that they intend to bail out or reject an offer by a negotiation event with the following encrypted NIP‑44 content:

  • type: bail
  • message (required): The reason for bailing out or cancelling the offer.

The tags must include:

  • d: The event id of the offer to bail or cancel
  • p: The pubkey of the counterparty (offerer or delegate) this event is directed to.

The following default messages are recommended for use when bailing out of an offer:

  • out_of_budget: No remaining budget for ads.
  • expired: The bid or hold time has expired and the negotiation is no longer valid or not accepted.
  • failed_payment: The delegate can't pay the invoice
  • action_incomplete: The delegate did not detect successful completion of the user action on their end.
  • cancelled: The offer was cancelled.
  • unknown : unknown reason for bailing out.

Implementers can define additional custom reason strings as needed.

When an offer that was originally agreed upon is bailed, the counterparty may choose to impose a punitive measure, such as banning the offending pubkey from future interactions or requiring a proof‑of‑work challenge to continue, see Punishments and Due Diligence for more details.

Punishments and Due Diligence

When the app cheats

An application or offerer may falsely claim completion of user actions (e.g., reporting an ad view that never occurred).

Delegates can implement various strategies to mitigate fraud, such as:

  • Restricting bids to trusted offerers via the p tag.
  • Applying heuristic checks, audits or various forms of tracking to detect inconsistencies or suspicious behavior.

If a delegate detects cheating, it may:

  • Ignore all future offers from the offending pubkey.
  • Impose a PoW challenge by raising the difficulty in outgoing negotiation events: requiring the offerer to commit computational work to continue interactions.

When the advertiser or delegate cheat

An advertiser or delegate may fail to honor payments, claim hold_time has expired prematurely, or otherwise violate the protocol.

If an offerer detects misbehavior, it may:

  • Reject bids from that advertiser pubkey permanently.
  • Reject bids that are delegated to an untrusted delegate pubkey.
  • Impose a PoW challenge by raising the difficulty in outgoing negotiation events: requiring the delegate to commit computational work to continue interactions.

Implementations may maintain local blacklists of repeat offenders and may share reputational data off‑protocol to enhance ecosystem trustworthiness.

The Pow penalty PoW deters further misconduct while allowing honest parties to redeem themselves for issues that may have been caused by bugs or misconfigurations.

Tracking Actions

To track which offer triggers a specific action, the advertiser can include a $OFFER_ID placeholder in the destination link. The offerer must replace this placeholder with the ID of the accepted offer event before displaying the ad. This enables the server-side code to correlate user actions with the specific offer that generated them.

eg. https://example.com/landing-page?track=$OFFER_ID

Pseudo-code implementation

Advertiser Client

function publishAd(
  eventData,
  delegatePubKey
  nwcUrl,
  dailyBudget,
  advertiserPrivKey
) {
  const event = {
    kind: 30100,
    content: JSON.stringify(eventData),
    tags: [
      ["k", "click"],
      ["m", "image/png"],
      ["l", "en"],
      ["y", appPubKey],
      ["d", "ad_1234"],
      ["f", "BTC1_000"],
      ["s", "512x128"],
      ["S", "4:1"],
      ["D", delegatePubKey, nip44Encrypt(
        JSON.stringify({ dailyBudget, nwc: nwcUrl }),
        nip44ConversationKey(advertiserPrivKey, delegatePubKey)
      )],
      // .....
    ]
  };
  signAndPublishToRelays(event);
}

Delegate Service

function onNewBidToManage(bidEvent, delegatePrivKey) { 
  const delegateTag = bidEvent.tag("D")?.[1];
  if (delegatePubKey !== myPubKey) return; // not for me

  const payload = delegateTag[2];
  const decryptedPayload = nip44Decrypt(
    payload,
    nip44ConversationKey(delegatePrivKey, bidEvent.author)
  );

  manageBid(bidEvent)
    .onOffer(offer=>{
      if(checkIfBlackListed(offer.author)){
        bail(offer, "blacklisted");
      } else{
        acceptOffer(offer);
      }
    })
    .onPaymentRequest((offer, paymentRequest)=>{
      if(offer.author != paymentRequest.author) throw "invalid";
      const appPubkey = offer.tag("y");
      const offererPubkey = offer.author;

      if(!verifyActionCompleted(offer)) {
        bail(offer, "action_incomplete");
        punish(offererPubkey);
        return;
      }

      processPayment(offer, appPubkey, decryptedPayload);
      confirmPayout(offer, offererPubkey);
    })
    .onBail((offer, reason)=>{
      const offererPubkey = offer.author;
      punish(offererPubkey);
    });
}

User facing Application

function discoverAndOffer(appPubKey) {
  const bids = fetchEventsFromRelays(/*user filter*/);
  sortBids(bids);
  const bestBid = bids[0];
  const advKey = generatePrivateKey();
  const delegatePubkey = bestBid.tag("D")[0];
  sendOffer(delegate, appPubkey)
    .onAccept(offer=>{
      displayAd(bestBid).onActionCompleted(()=>{
        sendPaymentRequest(offer, delegatePubkey);
      });
    })
    .onPayoutEvent((offer,payout)=>{
      alert("Payout confirmed: "+payout.message);
    })
    .onBail((offer, reason)=>{
      alert("Offer cancelled: "+reason);
      punish(delegatePubkey);
    });
}