Our tech stack for the MVP is Node.js for the API, Vue for the frontend, MongoDb for storage and Solidity for the smart contract.
When creating an ad or registering a site to mint in our website, we store the data for that object in MongoDb. This object includes the ad text, ad link, ad image etc. The user then mints an ERC-721 token with an id that corresponds to an id in the document we have stored in MongoDb.
When minting an ad, the minting process will include an amount of ETH that will be used to pay for ad impressions / clicks that the ad generates.
In our API we have a scheduled job that fetches the ads and sites both from MongoDb and our smart contract. It then hydrates a memory cache object with a dictionary of all the ads with a non-zero balance, and all the sites that are minted.
The site owners will place an iframe tag code block on their page with an url containing the id of their site token. When a GET request to view this ad lands in our API we look up the site data in our memory cache dictionary and select a random ad from the memory cache object to render and send that back. This results in the iframe being populated with a random ad with a non-zero balance of ETH each time the site is loaded. We also register the views for both the site and the ad in another memory cache dictionary.
The rendered ad includes a link to our API with url parameters containing both the id of the site containing the ad box and the id of the ad.
When someone clicks an ad we take these parameters from the url and register an ad click for both the site and the ad in our memory cache dictionary. Then we send a redirect response to the url from the ad object. Using in-memory lookups makes this very fast, and the end user will perceive the click as just pointing straight to the target page.
We also have a IP filter that store the IP's where the ad views and ad clicks are coming from, and if we see the same IP within 5 minutes we will not register the impression into the statistics document. This is simply to avoid click-fraud. The ads are still rendered and clicks redirected.
At a given interval we scrape the statistics from the memory cache and save it into MongoDb along with a timestamp of the scrape. This allows us to query statistics and provide endpoints for historic view and click statistics for both ads and sites.
The smart contract uses Chainlink to poll our API for this statistics data, and calculates the payment flows according to this. Balances on ad tokens are subtracted and balances of sites are incremented according to how many clicks they have at the current time compared to last time their statistics were polled.