Trillboards
shared.backToDevelopers
shared.developerDocs

examples.heroTitle examples.heroTitleAccent

examples.heroSubtitle

examples.androidTitle

examples.androidDesc

MainActivity.kt

MainActivity.kt
package com.example.vendingmachine

import android.os.Bundle
import android.webkit.*
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
    private lateinit var webView: WebView
    private var isShowingAd = false

    companion object {
        // Replace with your actual embed URL from device registration
        private const val AD_URL = "https://screen.trillboards.com?fp=DEVICE_FINGERPRINT"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        setupWebView()
        loadAds()
    }

    private fun setupWebView() {
        webView = findViewById(R.id.adWebView)

        webView.settings.apply {
            javaScriptEnabled = true
            domStorageEnabled = true
            mediaPlaybackRequiresUserGesture = false
            mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
            cacheMode = WebSettings.LOAD_DEFAULT
        }

        // Add JavaScript bridge
        webView.addJavascriptInterface(AdBridge(), "TrillboardsNative")

        // Configure WebViewClient
        webView.webViewClient = object : WebViewClient() {
            override fun onPageFinished(view: WebView?, url: String?) {
                super.onPageFinished(view, url)
                // Inject iOS-compatible event bridge
                injectEventBridge()
            }
        }
    }

    private fun loadAds() {
        webView.loadUrl(AD_URL)
    }

    private fun injectEventBridge() {
        // Listen for SDK events and forward to native
        val script = """
            window.addEventListener('trillboards:overlay_clicked', function(e) {
                if (window.TrillboardsNative) {
                    window.TrillboardsNative.onOverlayClicked();
                }
            });
            window.addEventListener('trillboards:ad_started', function(e) {
                if (window.TrillboardsNative) {
                    window.TrillboardsNative.onAdStarted(
                        e.detail.type || 'unknown'
                    );
                }
            });
            window.addEventListener('trillboards:ad_ended', function(e) {
                if (window.TrillboardsNative) {
                    window.TrillboardsNative.onAdEnded();
                }
            });
        """.trimIndent()

        webView.evaluateJavascript(script, null)
    }

    // JavaScript bridge for receiving SDK events
    inner class AdBridge {
        @JavascriptInterface
        fun onOverlayClicked() {
            runOnUiThread {
                // User tapped overlay - show your vending UI
                showVendingInterface()
            }
        }

        @JavascriptInterface
        fun onAdStarted(adType: String) {
            runOnUiThread {
                isShowingAd = true
                // Hide vending UI if visible
            }
        }

        @JavascriptInterface
        fun onAdEnded() {
            runOnUiThread {
                isShowingAd = false
                // Ad finished, next ad will play after interval
            }
        }
    }

    private fun showVendingInterface() {
        // Send command to SDK to hide
        webView.evaluateJavascript(
            "if(window.TrillboardsLite) TrillboardsLite.hide();",
            null
        )

        // Now show your vending machine UI
        // startActivity(Intent(this, VendingActivity::class.java))
    }

    override fun onBackPressed() {
        // Prevent accidental exit during ads
        if (isShowingAd) {
            return
        }
        super.onBackPressed()
    }
}

activity_main.xml

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#000000">

    <WebView
        android:id="@+id/adWebView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

AndroidManifest.xml (permissions)

AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<!-- Optional: Keep screen on -->
<uses-permission android:name="android.permission.WAKE_LOCK" />

examples.iosTitle

examples.iosDesc

AdViewController.swift

AdViewController.swift
import UIKit
import WebKit

class AdViewController: UIViewController {
    private var webView: WKWebView!
    private var isShowingAd = false

    // Replace with your actual embed URL
    private let adURL = "https://screen.trillboards.com?fp=DEVICE_FINGERPRINT"

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .black

        setupWebView()
        loadAds()

        // Keep screen awake
        UIApplication.shared.isIdleTimerDisabled = true
    }

    private func setupWebView() {
        let config = WKWebViewConfiguration()
        config.allowsInlineMediaPlayback = true
        config.mediaTypesRequiringUserActionForPlayback = []

        // Add message handler for SDK events
        let contentController = config.userContentController
        contentController.add(self, name: "trillboards")

        // Inject event bridge script
        let script = WKUserScript(
            source: eventBridgeScript,
            injectionTime: .atDocumentEnd,
            forMainFrameOnly: true
        )
        contentController.addUserScript(script)

        webView = WKWebView(frame: view.bounds, configuration: config)
        webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        webView.scrollView.isScrollEnabled = false
        view.addSubview(webView)
    }

    private var eventBridgeScript: String {
        return """
        window.addEventListener('trillboards:overlay_clicked', function(e) {
            window.webkit.messageHandlers.trillboards.postMessage({
                event: 'overlay_clicked'
            });
        });
        window.addEventListener('trillboards:ad_started', function(e) {
            window.webkit.messageHandlers.trillboards.postMessage({
                event: 'ad_started',
                type: e.detail?.type || 'unknown'
            });
        });
        window.addEventListener('trillboards:ad_ended', function(e) {
            window.webkit.messageHandlers.trillboards.postMessage({
                event: 'ad_ended'
            });
        });
        """
    }

    private func loadAds() {
        guard let url = URL(string: adURL) else { return }
        webView.load(URLRequest(url: url))
    }

    private func showVendingInterface() {
        // Hide ads
        webView.evaluateJavaScript(
            "if(window.TrillboardsLite) TrillboardsLite.hide();"
        )

        // Present your vending UI
        // let vendingVC = VendingViewController()
        // present(vendingVC, animated: true)
    }
}

extension AdViewController: WKScriptMessageHandler {
    func userContentController(
        _ userContentController: WKUserContentController,
        didReceive message: WKScriptMessage
    ) {
        guard let body = message.body as? [String: Any],
              let event = body["event"] as? String else { return }

        switch event {
        case "overlay_clicked":
            showVendingInterface()

        case "ad_started":
            isShowingAd = true

        case "ad_ended":
            isShowingAd = false

        default:
            break
        }
    }
}

// Don't forget to remove message handler to prevent memory leaks
extension AdViewController {
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        webView.configuration.userContentController.removeScriptMessageHandler(
            forName: "trillboards"
        )
    }
}

examples.apiTitle

examples.nodeSubheading

examples.nodeDesc

api-usage.js
const axios = require('axios');

const API_BASE = 'https://api.trillboards.com/v1/partner';

// 1. Register as a partner (do once)
async function registerPartner() {
  const response = await axios.post(`${API_BASE}/register`, {
    name: 'My Vending Company',
    slug: 'myvendingco',
    contact_email: 'developer@myvendingco.com',
    partner_type: 'vending_machine'
  });

  console.log('API Key:', response.data.data.api_key);
  // SAVE THIS API KEY SECURELY!
  return response.data.data.api_key;
}

// 2. Register devices (call for each device)
async function registerDevice(apiKey, deviceInfo) {
  const response = await axios.post(
    `${API_BASE}/device`,
    {
      device_id: deviceInfo.id,
      device_type: 'vending_machine',
      name: deviceInfo.name,
      display: {
        width: 1920,
        height: 1080,
        orientation: 'landscape'
      },
      location: {
        lat: deviceInfo.lat,
        lng: deviceInfo.lng,
        venue_type: deviceInfo.venueType,
        venue_name: deviceInfo.venueName
      }
    },
    {
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json'
      }
    }
  );

  console.log('Embed URL:', response.data.data.embed_url);
  return response.data.data;
}

// 3. Get analytics
async function getAnalytics(apiKey) {
  const response = await axios.get(
    `${API_BASE}/analytics`,
    {
      headers: { 'Authorization': `Bearer ${apiKey}` },
      params: {
        start_date: '2024-01-01',
        end_date: '2024-12-31'
      }
    }
  );

  console.log('Total impressions:', response.data.data.partner.total_impressions);
  console.log('Revenue (cents):', response.data.data.partner.total_revenue_cents);
  return response.data.data;
}

// Usage
(async () => {
  // const apiKey = await registerPartner();
  const apiKey = 'YOUR_API_KEY';

  await registerDevice(apiKey, {
    id: 'machine-001',
    name: 'Mall Location A',
    lat: 44.8537,
    lng: -93.2428,
    venueType: 'mall',
    venueName: 'Mall of America'
  });

  const analytics = await getAnalytics(apiKey);
})();

examples.pythonSubheading

examples.pythonDesc

bulk_register.py
import requests

API_BASE = 'https://api.trillboards.com/v1/partner'
API_KEY = 'YOUR_API_KEY'

headers = {
    'Authorization': f'Bearer {API_KEY}',
    'Content-Type': 'application/json'
}

# Register multiple devices from a CSV or database
devices = [
    {'id': 'machine-001', 'name': 'Location A', 'lat': 44.8537, 'lng': -93.2428},
    {'id': 'machine-002', 'name': 'Location B', 'lat': 44.9778, 'lng': -93.2650},
    {'id': 'machine-003', 'name': 'Location C', 'lat': 44.9537, 'lng': -93.0900},
]

for device in devices:
    response = requests.post(
        f'{API_BASE}/device',
        headers=headers,
        json={
            'device_id': device['id'],
            'device_type': 'vending_machine',
            'name': device['name'],
            'location': {
                'lat': device['lat'],
                'lng': device['lng']
            }
        }
    )

    if response.status_code == 201:
        data = response.json()['data']
        print(f"Registered {device['id']}: {data['embed_url']}")
    else:
        print(f"Failed to register {device['id']}: {response.text}")

# Get all devices
response = requests.get(f'{API_BASE}/devices', headers=headers)
devices = response.json()['data']['devices']
print(f"Total devices: {len(devices)}")

examples.reactTitle

examples.reactDesc

examples.npmInstall

terminal
npm install @trillboards/ads-sdk

App.tsx

App.tsx
import { TrillboardsProvider, TrillboardsAdSlot, useTrillboardsAds } from '@trillboards/ads-sdk/react';

function App() {
  return (
    <TrillboardsProvider config={{ deviceId: 'DEVICE_FINGERPRINT' }}>
      <KioskScreen />
    </TrillboardsProvider>
  );
}

function KioskScreen() {
  const { state, isReady, sdk } = useTrillboardsAds();

  if (!isReady) return <div className="loading">Initializing...</div>;

  return (
    <div style={{ width: '100vw', height: '100vh' }}>
      <TrillboardsAdSlot style={{ width: '100%', height: '100%' }} />
      {state.isOffline && <div className="offline-banner">Playing cached ads</div>}
    </div>
  );
}

examples.serverTitle

examples.serverDesc

server.ts

server.ts
import { PartnerClient } from '@trillboards/ads-sdk/server';

const client = new PartnerClient({ apiKey: 'trb_partner_xxxxx' });

// Get live audience for a screen
const audience = await client.audience.getLive('screen-123');
console.log('Viewers:', audience.face_count);
console.log('Attention:', audience.attention_score);
console.log('Mood:', audience.mood);

// Get earnings data
const earnings = await client.analytics.getEarnings();
earnings.forEach(e => {
  console.log(e.screenId, e.period, '$' + (e.earner_revenue / 100).toFixed(2));
});

// Build a VAST tag for a specific screen
const vastBuilder = client.createVastTagBuilder();
const tag = await vastBuilder.buildTag({
  deviceId: 'screen-123',
  width: 1920,
  height: 1080,
  orientation: 'landscape'
});
console.log('VAST URL:', tag.url);

// Batch-report impressions
const tracker = client.createTrackingBatch();
const result = await tracker.report([
  {
    adId: 'ad-001',
    impressionId: 'imp-001',
    deviceId: 'dev-001',
    duration: 15,
    completed: true,
    timestamp: new Date().toISOString()
  }
]);
console.log('Tracked:', result.tracked, 'impressions');

examples.proofTitle

examples.proofDesc

examples.nodeVerification

verify-proof.js
const crypto = require('crypto');

// Public key from api.trillboards.com/v1/advertiser/proof/.well-known/public-key
const PUBLIC_KEY_HEX = 'a5a9c43785680c62ebcd01ec49a0d6055fd140023df0f142c3e37c6abafce34f';

function verifyProof(proof) {
  // Build payload from proof data
  const timestamp = typeof proof.timestamp === 'object'
    ? proof.timestamp.getTime()
    : proof.timestamp;

  const payload = `v2.${timestamp}.${proof.adId}.${proof.impressionId}.${proof.screenId}.${proof.deviceId}`;

  // Parse signature (remove "ed25519=" prefix)
  const signatureHex = proof.signature.replace('ed25519=', '');
  const signatureBytes = Buffer.from(signatureHex, 'hex');

  // Create public key object
  const publicKey = crypto.createPublicKey({
    key: Buffer.concat([
      Buffer.from('302a300506032b6570032100', 'hex'), // Ed25519 SPKI prefix
      Buffer.from(PUBLIC_KEY_HEX, 'hex')
    ]),
    format: 'der',
    type: 'spki'
  });

  // Verify signature
  return crypto.verify(null, Buffer.from(payload), publicKey, signatureBytes);
}

// Example usage
const proof = {
  impressionId: '507f1f77bcf86cd799439011',
  adId: 'campaign-abc123',
  screenId: 'screen-xyz789',
  deviceId: 'device-456',
  timestamp: 1705401600000,
  signature: 'ed25519=a1b2c3d4e5f6...'
};

console.log('Valid:', verifyProof(proof)); // true or false

examples.pythonVerification

verify_proof.py
from nacl.signing import VerifyKey
from nacl.exceptions import BadSignature

# Public key from API
PUBLIC_KEY_HEX = 'a5a9c43785680c62ebcd01ec49a0d6055fd140023df0f142c3e37c6abafce34f'

def verify_proof(proof):
    """Verify an Ed25519 signed proof of play."""
    verify_key = VerifyKey(bytes.fromhex(PUBLIC_KEY_HEX))

    # Build payload
    payload = f"v2.{proof['timestamp']}.{proof['adId']}.{proof['impressionId']}.{proof['screenId']}.{proof['deviceId']}"

    # Parse signature
    signature_hex = proof['signature'].replace('ed25519=', '')
    signature_bytes = bytes.fromhex(signature_hex)

    try:
        verify_key.verify(payload.encode(), signature_bytes)
        return True
    except BadSignature:
        return False

# Example usage
proof = {
    'impressionId': '507f1f77bcf86cd799439011',
    'adId': 'campaign-abc123',
    'screenId': 'screen-xyz789',
    'deviceId': 'device-456',
    'timestamp': 1705401600000,
    'signature': 'ed25519=a1b2c3d4e5f6...'
}

print(f"Valid: {verify_proof(proof)}")

examples.curlVerification

examples.curlVerificationDesc

terminal
# No authentication required - public endpoint
curl -X POST https://api.trillboards.com/v1/advertiser/proof/verify-proof \
  -H "Content-Type: application/json" \
  -d '{
    "signature": "ed25519=a1b2c3d4e5f6...",
    "timestamp": 1705401600000,
    "adId": "campaign-abc123",
    "impressionId": "507f1f77bcf86cd799439011",
    "screenId": "screen-xyz789",
    "deviceId": "device-456"
  }'

# Response:
# {
#   "success": true,
#   "verification": {
#     "valid": true,
#     "version": "v2"
#   }
# }

examples.proofTip examples.proofTipText /v1/advertiser/proof/.well-known/public-key examples.proofTipSuffix examples.proofTipLinkText examples.proofTipEnd