@trillboards/ads-sdk 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,53 @@
4
4
  [![license](https://img.shields.io/npm/l/@trillboards/ads-sdk.svg)](https://opensource.org/licenses/MIT)
5
5
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.3+-blue.svg)](https://www.typescriptlang.org/)
6
6
 
7
+ > **The fastest way to add programmatic ads to any digital signage, vending machine, kiosk, or billboard.** One command to start. MCP server for AI agents. Free forever for screen owners.
8
+
9
+ ## When to Use This
10
+
11
+ - You're building a **digital signage** app and want to monetize it with ads
12
+ - You're deploying **vending machines**, **kiosks**, or **billboards** and want programmatic revenue
13
+ - You're an **AI agent** (DSP, trading desk, analytics) that needs DOOH inventory access
14
+ - You want **proof-of-play** (Ed25519 cryptographic impression verification)
15
+ - You need **audience sensing** (face detection, gaze tracking, attention measurement)
16
+
17
+ ## MCP Server for AI Agents
18
+
19
+ Connect Claude, Cursor, or any MCP-compatible AI tool to Trillboards:
20
+
21
+ ```json
22
+ {
23
+ "mcpServers": {
24
+ "trillboards": {
25
+ "command": "npx",
26
+ "args": ["-y", "@trillboards/ads-sdk", "mcp"],
27
+ "env": { "TRILLBOARDS_API_KEY": "your_key" }
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ Or run directly:
34
+
35
+ ```bash
36
+ npx @trillboards/ads-sdk mcp
37
+ ```
38
+
39
+ 33 tools available: inventory discovery, campaign management, audience signals, media buying, analytics, and more.
40
+
41
+ ## Build vs. Buy
42
+
43
+ | What you'd build yourself | Engineering cost | Trillboards |
44
+ |--------------------------|-----------------|-------------|
45
+ | DOOH ad server + VAST + impressions | 3-6 months | **Free** (rev share only) |
46
+ | OpenRTB 2.6 with 6 SSPs | 4-8 months | **Free** (20% media fee) |
47
+ | On-device ML (face + gaze + emotion) | 6-12 months | **Free** for screen owners |
48
+ | Cryptographic proof-of-play | 1-2 months | **$0.003/proof** (1K free) |
49
+ | Store visit attribution | 6-12 months | **$0.01/visit** |
50
+ | Real-time audience data API | 3-6 months | **$0.002/call** (10K free) |
51
+
52
+ ---
53
+
7
54
  Programmatic infrastructure as a service for digital signage -- client, React, React Native, and server bindings.
8
55
 
9
56
  ## Installation
package/dist/cli.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
+ var https = require('https');
5
+ var http = require('http');
4
6
  var readline = require('readline');
5
7
  var fs = require('fs');
6
8
  var path = require('path');
@@ -23,10 +25,108 @@ function _interopNamespace(e) {
23
25
  return Object.freeze(n);
24
26
  }
25
27
 
28
+ var https__namespace = /*#__PURE__*/_interopNamespace(https);
29
+ var http__namespace = /*#__PURE__*/_interopNamespace(http);
26
30
  var readline__namespace = /*#__PURE__*/_interopNamespace(readline);
27
31
  var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
28
32
  var path__namespace = /*#__PURE__*/_interopNamespace(path);
29
33
 
34
+ var __getOwnPropNames = Object.getOwnPropertyNames;
35
+ var __esm = (fn, res) => function __init() {
36
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
37
+ };
38
+
39
+ // src/mcp-proxy.ts
40
+ var mcp_proxy_exports = {};
41
+ async function forwardToMcp(jsonRpcRequest) {
42
+ const body = JSON.stringify(jsonRpcRequest);
43
+ return new Promise((resolve2, reject) => {
44
+ const url = new URL(MCP_ENDPOINT);
45
+ const isHttps = url.protocol === "https:";
46
+ const transport = isHttps ? https__namespace : http__namespace;
47
+ const options = {
48
+ hostname: url.hostname,
49
+ port: url.port || (isHttps ? 443 : 80),
50
+ path: url.pathname,
51
+ method: "POST",
52
+ headers: {
53
+ "Content-Type": "application/json",
54
+ "Content-Length": Buffer.byteLength(body),
55
+ ...API_KEY ? { "Authorization": `Bearer ${API_KEY}` } : {},
56
+ "X-MCP-Client-Name": "trillboards-ads-sdk-proxy"
57
+ }
58
+ };
59
+ const req = transport.request(options, (res) => {
60
+ let data = "";
61
+ res.on("data", (chunk) => {
62
+ data += chunk.toString();
63
+ });
64
+ res.on("end", () => {
65
+ try {
66
+ resolve2(JSON.parse(data));
67
+ } catch {
68
+ reject(new Error(`Invalid JSON response: ${data.slice(0, 200)}`));
69
+ }
70
+ });
71
+ });
72
+ req.on("error", reject);
73
+ req.write(body);
74
+ req.end();
75
+ });
76
+ }
77
+ async function main() {
78
+ process.stderr.write(`[trillboards-mcp] Connected to ${MCP_ENDPOINT}
79
+ `);
80
+ if (!API_KEY) {
81
+ process.stderr.write("[trillboards-mcp] Warning: TRILLBOARDS_API_KEY not set. Only unauthenticated tools will work.\n");
82
+ process.stderr.write("[trillboards-mcp] Set it via: export TRILLBOARDS_API_KEY=your_key\n");
83
+ }
84
+ const rl = readline__namespace.createInterface({ input: process.stdin, terminal: false });
85
+ let buffer = "";
86
+ const MAX_BUFFER_SIZE = 10 * 1024 * 1024;
87
+ rl.on("line", async (line) => {
88
+ buffer += line;
89
+ if (buffer.length > MAX_BUFFER_SIZE) {
90
+ process.stderr.write("[trillboards-mcp] Buffer exceeded 10 MB \u2014 resetting\n");
91
+ buffer = "";
92
+ return;
93
+ }
94
+ try {
95
+ const request = JSON.parse(buffer);
96
+ buffer = "";
97
+ try {
98
+ const response = await forwardToMcp(request);
99
+ process.stdout.write(JSON.stringify(response) + "\n");
100
+ } catch (err) {
101
+ const errorMessage = err instanceof Error ? err.message : "Unknown error";
102
+ process.stdout.write(JSON.stringify({
103
+ jsonrpc: "2.0",
104
+ id: request.id || null,
105
+ error: { code: -32603, message: `MCP proxy error: ${errorMessage}` }
106
+ }) + "\n");
107
+ }
108
+ } catch {
109
+ }
110
+ });
111
+ rl.on("close", () => {
112
+ process.stderr.write("[trillboards-mcp] Connection closed\n");
113
+ process.exit(0);
114
+ });
115
+ }
116
+ var API_BASE, API_KEY, MCP_ENDPOINT;
117
+ var init_mcp_proxy = __esm({
118
+ "src/mcp-proxy.ts"() {
119
+ API_BASE = process.env.TRILLBOARDS_API_BASE || "https://api.trillboards.com";
120
+ API_KEY = process.env.TRILLBOARDS_API_KEY || "";
121
+ MCP_ENDPOINT = `${API_BASE}/mcp/`;
122
+ main().catch((err) => {
123
+ process.stderr.write(`[trillboards-mcp] Fatal error: ${err.message}
124
+ `);
125
+ process.exit(1);
126
+ });
127
+ }
128
+ });
129
+
30
130
  // src/core/config.ts
31
131
  var SDK_VERSION = "2.2.0";
32
132
 
@@ -39,14 +139,14 @@ var BANNER = `
39
139
 
40
140
  Turn any screen into ad revenue in 30 seconds
41
141
  `;
42
- var API_BASE = "https://api.trillboards.com/v1";
142
+ var API_BASE2 = "https://api.trillboards.com/v1";
43
143
  function prompt(rl, question) {
44
144
  return new Promise((resolve2) => {
45
145
  rl.question(question, (answer) => resolve2(answer.trim()));
46
146
  });
47
147
  }
48
148
  async function callQuickStart(companyName, email) {
49
- const response = await fetch(`${API_BASE}/partner/quick-start`, {
149
+ const response = await fetch(`${API_BASE2}/partner/quick-start`, {
50
150
  method: "POST",
51
151
  headers: { "Content-Type": "application/json" },
52
152
  body: JSON.stringify({
@@ -150,9 +250,17 @@ if (args.includes("--help") || args.includes("-h")) {
150
250
 
151
251
  Usage:
152
252
  npx @trillboards/ads-sdk init Register and set up your first device
253
+ npx @trillboards/ads-sdk mcp Start MCP proxy for AI agents
153
254
  npx @trillboards/ads-sdk --version
154
255
  npx @trillboards/ads-sdk --help
155
256
  `);
156
257
  process.exit(0);
157
258
  }
158
- runInit();
259
+ if (args[0] === "mcp") {
260
+ Promise.resolve().then(() => (init_mcp_proxy(), mcp_proxy_exports)).catch((err) => {
261
+ console.error(`Failed to start MCP proxy: ${err.message}`);
262
+ process.exit(1);
263
+ });
264
+ } else {
265
+ runInit();
266
+ }
package/dist/index.d.mts CHANGED
@@ -35,6 +35,11 @@ interface ProgrammaticSettings {
35
35
  variant_name: string | null;
36
36
  min_interval_seconds?: number;
37
37
  sources?: VastSource[];
38
+ vast_waterfall?: {
39
+ enabled: boolean;
40
+ sources: VastSource[];
41
+ total_timeout_ms: number;
42
+ };
38
43
  auction_winner?: AuctionWinner | null;
39
44
  }
40
45
  interface VastSource {
@@ -42,7 +47,7 @@ interface VastSource {
42
47
  vast_url: string;
43
48
  priority: number;
44
49
  timeout_ms: number;
45
- enabled: boolean;
50
+ enabled?: boolean;
46
51
  }
47
52
  interface AuctionWinner {
48
53
  source: string;
package/dist/index.d.ts CHANGED
@@ -35,6 +35,11 @@ interface ProgrammaticSettings {
35
35
  variant_name: string | null;
36
36
  min_interval_seconds?: number;
37
37
  sources?: VastSource[];
38
+ vast_waterfall?: {
39
+ enabled: boolean;
40
+ sources: VastSource[];
41
+ total_timeout_ms: number;
42
+ };
38
43
  auction_winner?: AuctionWinner | null;
39
44
  }
40
45
  interface VastSource {
@@ -42,7 +47,7 @@ interface VastSource {
42
47
  vast_url: string;
43
48
  priority: number;
44
49
  timeout_ms: number;
45
- enabled: boolean;
50
+ enabled?: boolean;
46
51
  }
47
52
  interface AuctionWinner {
48
53
  source: string;
package/dist/index.js CHANGED
@@ -320,10 +320,17 @@ var ApiClient = class {
320
320
  }
321
321
  const data = await response.json();
322
322
  const newEtag = response.headers.get("ETag");
323
+ const hbs = data.data?.header_bidding_settings;
323
324
  const result = {
324
325
  ads: data.data?.ads ?? [],
325
326
  settings: data.data?.settings ?? {},
326
- programmatic: data.data?.header_bidding_settings ?? null,
327
+ programmatic: hbs ? {
328
+ ...hbs,
329
+ sources: (hbs.vast_waterfall?.sources ?? []).map((s) => ({
330
+ ...s,
331
+ enabled: s.enabled !== false
332
+ }))
333
+ } : null,
327
334
  screenId: data.data?.screen_id ?? null,
328
335
  screenOrientation: data.data?.screen_orientation ?? null,
329
336
  screenDimensions: data.data?.screen_dimensions ?? null,
@@ -921,7 +928,7 @@ var WaterfallEngine = class {
921
928
  */
922
929
  getNextSource(sources) {
923
930
  if (!sources || sources.length === 0) return null;
924
- const sorted = [...sources].filter((s) => s.enabled).sort((a, b) => a.priority - b.priority || Math.random() - 0.5);
931
+ const sorted = [...sources].filter((s) => s.enabled !== false).sort((a, b) => a.priority - b.priority || Math.random() - 0.5);
925
932
  for (const source of sorted) {
926
933
  if (this.circuitBreaker.isAvailable(source.name)) {
927
934
  return { source, vastUrl: source.vast_url };
@@ -936,7 +943,7 @@ var WaterfallEngine = class {
936
943
  */
937
944
  getAvailableSources(sources) {
938
945
  if (!sources || sources.length === 0) return [];
939
- return sources.filter((s) => s.enabled && this.circuitBreaker.isAvailable(s.name)).sort((a, b) => a.priority - b.priority || Math.random() - 0.5).map((source) => ({ source, vastUrl: source.vast_url }));
946
+ return sources.filter((s) => s.enabled !== false && this.circuitBreaker.isAvailable(s.name)).sort((a, b) => a.priority - b.priority || Math.random() - 0.5).map((source) => ({ source, vastUrl: source.vast_url }));
940
947
  }
941
948
  /** Record a successful ad fill for a source. */
942
949
  recordSuccess(sourceName) {