After running a listing auction, you will often want to combine the winners with organic results.

For example, suppose you’re building a category section of a webshop that will also show a number of sponsored products.

You will need a way to “inject” your auction winners into the regular category results.

This page shows the steps involved in building such a category section, but similar considerations apply to other pages or widgets.

Scenario

The category section needs to support pagination and show 3 products per page. If possible, the first product on each page will be a sponsored product.

The products on this category page have the following structure:

{
  "id": "sku-367",
  "categoryId": "Shoes",
  "name": "Running shoes",
  "image": "photo_123.jpg",
  "description": "Beautiful and fast running shoes",
  "resolvedBidId": null,
}

The resolvedBidId is null when the product is not promoted and contain a string ID when it is.

Our goal is to have the pseudo code in place for an endpoint that could create lists of such products.

1. Query your organic results

The first step will be to query your organic results. The products that should be displayed for a specific page and category.

The code to do this could look like this:

const pageSize = 3
let products = queryProductsInCategory(pageSize, cursor, categoryId);

console.log(products);

Let’s say we were to run this code for the first page of the Shoes category, and there’s 3 or more products available in this category. This code would then output something similar to this:

[
  {
    "id": "sku-367",
    "categoryId": "Shoes",
    "name": "Running shoes",
    "image": "photo_123.jpg",
    "description": "Beautiful and fast running shoes",
  },
  {
    "id": "sku-897",
    "categoryId": "Shoes",
    "name": "Slippers",
    "image": "slippers.jpg",
    "description": "A pair of comfortable slippers",
  },
  {
    "id": "sku-343",
    "categoryId": "Shoes",
    "name": "Dress shoes",
    "image": "dress_shoe_original.jpg",
    "description": "Elegant dress shoes for formal events",
  }
]

Note that these products don’t have the resolvedBidId field yet, let’s add it.

const pageSize = 3
let products = queryProductsInCategory(pageSize, cursor, categoryId);
products.forEach(prod => { prod.resolvedBidId = null });

Now, we do need to consider what happens if there are no organic results for this page and category. We probably want to return a 404 error and not run any auctions.

This gives us the following code:

const pageSize = 3
let products = queryProductsInCategory(pageSize, cursor, categoryId);
if(products.length === 0) {
    throw new NotFoundError("no products found");
}

products.forEach(prod => { prod.resolvedBidId = null });

With this error handling in place, we’re ready to run an auction.

2. Running an auction

In our scenario we want to run an auction for a single slot:

// skip earlier code...

const slots = 1
const winners = runAuctionForCategory(slots, categoryId);

console.log(winners);

Not sure how run an auction for a category? Check out these examples.

If there are winners, the output of the above code would look something like this:

[
  {
    "rank": 1,
    "type": "product",
    "id": "sku-444",
    "resolvedBidId": "WyJiX01mazE1IiwiMTJhNTU4MjgtOGVhZC00Mjk5LTgzMjctY2ViYjAwMmEwZmE4IiwibGlzdGluZ3MiLCJkZWZhdWx0IiwiIl0="
  },
]

If you look at the winners, you will see that they contain no product data, just a bunch of ID’s.

We will need to query the product data for these winners.

Also, it’s possible for there to be no winners. It could simply be that there are no suitable active campaigns for this auction, but there are many other potential reasons.

Regardless, we need to account for this case and we’ll simply return the organic results available in products.

// skip earlier code...

const slots = 1
const winners = runAuctionForCategory(slots, categoryId);
if(winners.length === 0) {
  return products;
}

3. Query product data for winners

Now, when there are winners. We need to query the product data for them.

// skip earlier code...

const ids = winners.map(x => x.id);
const promoProducts = queryProductsByIds(ids);

console.log(promoProducts);

Following through on our earlier examples, this could log something like:

[
  {
    "id": "sku-444",
    "categoryId": "Shoes",
    "name": "Wooden clogs",
    "image": "clogs.jpg",
    "description": "Original wooden clogs.",
  }
]

Again, we need to add the resolvedBidId to complete this data. But, this time we shouldn’t set it to null, because we’re now dealing with promoted products.

If we assume queryProductsByIds will return products in the same order as the provided ids, we can add the bid IDs as follows:

// skip earlier code...

const ids = winners.map(x => x.id);
const promoProducts = queryProductsByIds(ids);
promoProducts.forEach((prod, i) => {
    prod.resolvedBidId = winners[i].resolvedBidId;
});

Why is this bid ID necessary? The bid ID is vital to enable Topsort to attribute events to bids and campaigns.

Now all that remains is to merge our promoted products with the organic results.

4. Merging

We want to show the promoted products at the start of the list.

To achieve, we will need to prepend the promoProducts to the products.

However, this can then make products have more elements than our intended page size of 3. So we need to slice it to remain in this page size.

// skip earlier code...

products.unshift(...promoProducts);
return products.slice(0, pageSize);

Now our code returns a maximum of 3 products, of which the first product can be a sponsored product.

Full code

// query for organic products.
const pageSize = 3
let products = queryProductsInCategory(pageSize, cursor, categoryId);
if(products.length === 0) {
    throw new NotFoundError("no products found");
}

products.forEach(prod => { prod.resolvedBidId = null });

// run an auction.
const slots = 1
const winners = runAuctionForCategory(slots, categoryId);
if(winners.length === 0) {
  return products;
}

// query product data for winners.
const ids = winners.map(x => x.id);
const promoProducts = queryProductsByIds(ids);
promoProducts.forEach((prod, i) => {
    prod.resolvedBidId = winners[i].resolvedBidId;
});

// merge results.
products.unshift(...promoProducts);
return products.slice(0, pageSize);