@revclamp/google-ads-transparency-mcp-actor 1.0.2 → 1.0.3

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/.actor/actor.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "actorSpecification": 1,
3
3
  "name": "google-ads-transparency-mcp-actor",
4
- "title": "RevClamp AdPulse - For Google Ads Transparency Center (MCP & Apify Actor)",
5
- "description": "Find verified Google Ads advertiser entities from any brand or domain (e.g. nike.com -> Nike, Inc.) and extract active and historical ad creatives (Text, Image, Video), preview URLs, and run dates with zero browser or proxy overhead. Independent intelligence tool by RevClamp; not affiliated with or endorsed by Google LLC.",
4
+ "title": "RevClamp AdPulse - Google Ads Transparency MCP & Actor",
5
+ "description": "Resolve verified Google Ads advertiser entities from any brand or domain and extract active or historical ad creatives (Text, Image, Video), ad copy, and run dates with zero browser overhead. Independent intelligence tool by RevClamp; not affiliated with or endorsed by Google LLC.",
6
6
  "version": "1.0",
7
7
  "buildTag": "latest",
8
8
  "minMemoryMbytes": 256,
@@ -13,5 +13,66 @@
13
13
  "memoryMbytes": 256
14
14
  },
15
15
  "input": "./input_schema.json",
16
+ "output": {
17
+ "actorOutputSchemaVersion": 1,
18
+ "title": "Google Ads Transparency Creatives & Run Summary",
19
+ "description": "Extracted Google Ads Transparency creative records (Default Dataset) and run billing/telemetry summary (Key-Value Store OUTPUT).",
20
+ "properties": {
21
+ "creatives": {
22
+ "type": "string",
23
+ "title": "Extracted Ad Creatives",
24
+ "description": "Verified advertiser ad creatives (Text, Image, Video), ad copy, destination domains, and active run dates.",
25
+ "template": "{{links.apiDefaultDatasetUrl}}/items"
26
+ },
27
+ "runSummary": {
28
+ "type": "string",
29
+ "title": "Run Summary & Telemetry (OUTPUT)",
30
+ "description": "Structured execution summary, resolved advertiser entities, pagination cursor, and billing receipt summary.",
31
+ "template": "{{links.apiDefaultKeyValueStoreUrl}}/records/OUTPUT"
32
+ }
33
+ }
34
+ },
35
+ "storages": {
36
+ "dataset": {
37
+ "actorSpecification": 1,
38
+ "fields": {
39
+ "type": "object",
40
+ "properties": {
41
+ "advertiserId": { "type": "string", "title": "Advertiser ID" },
42
+ "advertiserName": { "type": "string", "title": "Advertiser Name" },
43
+ "creativeId": { "type": "string", "title": "Creative ID" },
44
+ "format": { "type": "string", "title": "Ad Format" },
45
+ "regionCode": { "type": "string", "title": "Region Code" },
46
+ "firstSeenAtIso": { "type": "string", "title": "First Seen (ISO)" },
47
+ "lastSeenAtIso": { "type": "string", "title": "Last Seen (ISO)" },
48
+ "daysActive": { "type": "integer", "title": "Days Active" },
49
+ "previewUrl": { "type": "string", "title": "Preview Asset URL" },
50
+ "transparencyUrl": { "type": "string", "title": "Transparency Center URL" }
51
+ }
52
+ },
53
+ "views": {
54
+ "overview": {
55
+ "title": "Ad Creatives Overview",
56
+ "transformation": {
57
+ "fields": [
58
+ "advertiserId",
59
+ "advertiserName",
60
+ "creativeId",
61
+ "format",
62
+ "regionCode",
63
+ "firstSeenAtIso",
64
+ "lastSeenAtIso",
65
+ "daysActive",
66
+ "previewUrl",
67
+ "transparencyUrl"
68
+ ]
69
+ },
70
+ "display": {
71
+ "component": "table"
72
+ }
73
+ }
74
+ }
75
+ }
76
+ },
16
77
  "dockerfile": "../Dockerfile"
17
78
  }
@@ -9,6 +9,8 @@
9
9
  "description": "Brand name or domain to match against verified Google Ads Transparency advertiser entities (e.g. 'nike.com', 'Stripe', 'shopify.com').",
10
10
  "maxLength": 512,
11
11
  "editor": "textfield",
12
+ "default": "Stripe",
13
+ "prefill": "Stripe",
12
14
  "example": "nike.com"
13
15
  },
14
16
  "advertiserId": {
@@ -62,7 +64,8 @@
62
64
  "description": "Maximum number of deduplicated ad creatives to extract in this run (1 to 200).",
63
65
  "minimum": 1,
64
66
  "maximum": 200,
65
- "default": 40
67
+ "default": 40,
68
+ "prefill": 10
66
69
  },
67
70
  "pageToken": {
68
71
  "title": "Next-Page Cursor Token (Optional)",
package/actor_config.json CHANGED
@@ -21,7 +21,7 @@
21
21
  ],
22
22
  "manifest": {
23
23
  "name": "google-ads-transparency-mcp-actor",
24
- "version": "1.0.2",
24
+ "version": "1.0.3",
25
25
  "title": "RevClamp AdPulse - For Google Ads Transparency Center (MCP & Apify Actor)",
26
26
  "websiteUrl": "https://revclamp.com",
27
27
  "description": "Find verified Google Ads advertiser entities and extract active or historical ad creatives (Text, Image, Video), preview links, and run dates without browser or proxy overhead. Independent intelligence tool by RevClamp; not affiliated with or endorsed by Google LLC."
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@revclamp/google-ads-transparency-mcp-actor",
3
3
  "mcpName": "com.revclamp/google-ads-transparency-mcp-actor",
4
- "version": "1.0.2",
4
+ "version": "1.0.3",
5
5
  "type": "module",
6
6
  "description": "RevClamp AdPulse - For Google Ads Transparency Center (MCP & Apify Actor). Independent intelligence tool by RevClamp; not affiliated with or endorsed by Google LLC.",
7
7
  "homepage": "https://revclamp.com",
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "com.revclamp/google-ads-transparency-mcp-actor",
4
4
  "title": "RevClamp AdPulse (Google Ads Transparency MCP)",
5
5
  "description": "Google Ads Transparency Center advertiser & creative intelligence MCP server by RevClamp.",
6
- "version": "1.0.2",
6
+ "version": "1.0.3",
7
7
  "websiteUrl": "https://revclamp.com",
8
8
  "repository": {
9
9
  "url": "https://github.com/lhphong09t2/revclamp-mcp",
@@ -27,7 +27,7 @@
27
27
  "registryType": "npm",
28
28
  "registryBaseUrl": "https://registry.npmjs.org",
29
29
  "identifier": "@revclamp/google-ads-transparency-mcp-actor",
30
- "version": "1.0.2",
30
+ "version": "1.0.3",
31
31
  "transport": {
32
32
  "type": "stdio"
33
33
  }
@@ -82,12 +82,25 @@ export function computeRunEconomicsTelemetry({
82
82
  Number(process.env.ACTOR_MEMORY_MBYTES || process.env.APIFY_MEMORY_MBYTES) ||
83
83
  ACTOR_PRICING_CONTRACT.maxMemoryMb,
84
84
  requestCount = 1,
85
+ unitPriceUsdc = ACTOR_PRICING_CONTRACT.priceUsdc,
86
+ actorStartFeeUsdc = ACTOR_PRICING_CONTRACT.actorStartFeeUsdc,
85
87
  } = {}) {
86
88
  const safeCount = Math.max(0, Math.floor(Number(billableCount) || 0));
87
89
  const safeChargedCount = Math.max(0, Math.floor(Number(chargedEventCount) || 0));
88
90
  const safeDurationMs = Math.max(1, Number(durationMs) || 1);
89
91
  const safeMemoryMb = Math.min(1024, Math.max(64, Number(memoryMb) || 256));
90
92
  const safeRequests = Math.max(1, Math.floor(Number(requestCount) || 1));
93
+ const resolvedUnitPriceUsdc =
94
+ Number.isFinite(Number(unitPriceUsdc)) && Number(unitPriceUsdc) > 0
95
+ ? Number(unitPriceUsdc)
96
+ : ACTOR_PRICING_CONTRACT.priceUsdc;
97
+ const resolvedNetPriceUsdc = Number(
98
+ (resolvedUnitPriceUsdc * (1 - ACTOR_PRICING_CONTRACT.platformFeeRate)).toFixed(6)
99
+ );
100
+ const resolvedStartFeeUsdc =
101
+ Number.isFinite(Number(actorStartFeeUsdc)) && Number(actorStartFeeUsdc) >= 0
102
+ ? Number(actorStartFeeUsdc)
103
+ : ACTOR_PRICING_CONTRACT.actorStartFeeUsdc;
91
104
 
92
105
  // Apify compute rate: $0.30 per GB-hour; 256MB = 0.25 GB -> $0.075/hour -> $0.00002083/sec
93
106
  const computeHours = safeDurationMs / 3600000;
@@ -95,10 +108,9 @@ export function computeRunEconomicsTelemetry({
95
108
  (safeMemoryMb / 1024) * computeHours * ACTOR_PRICING_CONTRACT.computeRatePerGbHourUsdc;
96
109
  const networkOverheadUsdc = safeRequests * 0.0000002;
97
110
  const totalRunCostUsdc = Number((computeCostUsdc + networkOverheadUsdc).toFixed(8));
98
- const actorStartFeeUsdc = ACTOR_PRICING_CONTRACT.actorStartFeeUsdc;
99
111
 
100
112
  const configuredFloorMultiplier = Number(
101
- (ACTOR_PRICING_CONTRACT.netPriceUsdc / ACTOR_PRICING_CONTRACT.costCeilingUsdc).toFixed(2)
113
+ (resolvedNetPriceUsdc / ACTOR_PRICING_CONTRACT.costCeilingUsdc).toFixed(2)
102
114
  );
103
115
 
104
116
  if (safeCount === 0) {
@@ -109,7 +121,7 @@ export function computeRunEconomicsTelemetry({
109
121
  durationMs: safeDurationMs,
110
122
  memoryMb: safeMemoryMb,
111
123
  requestCount: safeRequests,
112
- actorStartFeeUsdc,
124
+ actorStartFeeUsdc: resolvedStartFeeUsdc,
113
125
  totalRunCostUsdc,
114
126
  actualCostPerEventUsdc: 0,
115
127
  costCeilingUsdc: ACTOR_PRICING_CONTRACT.costCeilingUsdc,
@@ -123,11 +135,11 @@ export function computeRunEconomicsTelemetry({
123
135
  }
124
136
 
125
137
  const actualCostPerEventUsdc = Number((totalRunCostUsdc / safeCount).toFixed(8));
126
- const grossRevenueUsdc = Number((safeCount * ACTOR_PRICING_CONTRACT.priceUsdc).toFixed(6));
127
- const netRevenueUsdc = Number((safeCount * ACTOR_PRICING_CONTRACT.netPriceUsdc).toFixed(6));
138
+ const grossRevenueUsdc = Number((safeCount * resolvedUnitPriceUsdc).toFixed(6));
139
+ const netRevenueUsdc = Number((safeCount * resolvedNetPriceUsdc).toFixed(6));
128
140
  const effectiveMarginMultiplier =
129
141
  actualCostPerEventUsdc > 0
130
- ? Number((ACTOR_PRICING_CONTRACT.netPriceUsdc / actualCostPerEventUsdc).toFixed(2))
142
+ ? Number((resolvedNetPriceUsdc / actualCostPerEventUsdc).toFixed(2))
131
143
  : configuredFloorMultiplier;
132
144
 
133
145
  const withinCostCeiling = actualCostPerEventUsdc <= ACTOR_PRICING_CONTRACT.costCeilingUsdc;
@@ -142,7 +154,7 @@ export function computeRunEconomicsTelemetry({
142
154
  durationMs: safeDurationMs,
143
155
  memoryMb: safeMemoryMb,
144
156
  requestCount: safeRequests,
145
- actorStartFeeUsdc,
157
+ actorStartFeeUsdc: resolvedStartFeeUsdc,
146
158
  totalRunCostUsdc,
147
159
  actualCostPerEventUsdc,
148
160
  costCeilingUsdc: ACTOR_PRICING_CONTRACT.costCeilingUsdc,
@@ -203,9 +215,25 @@ export async function loadApifyActorInput(
203
215
  return JSON.parse(raw);
204
216
  }
205
217
 
218
+ const isAtHome = env?.APIFY_IS_AT_HOME === '1' || env?.APIFY_IS_AT_HOME === 'true';
219
+ const applyAtHomeSchemaDefaults = (parsedInput) => {
220
+ if (
221
+ isAtHome &&
222
+ parsedInput &&
223
+ typeof parsedInput === 'object' &&
224
+ !Array.isArray(parsedInput) &&
225
+ !Object.prototype.hasOwnProperty.call(parsedInput, 'query') &&
226
+ !Object.prototype.hasOwnProperty.call(parsedInput, 'advertiserId') &&
227
+ !Object.prototype.hasOwnProperty.call(parsedInput, 'pageToken')
228
+ ) {
229
+ return { query: 'Stripe', ...parsedInput };
230
+ }
231
+ return parsedInput;
232
+ };
233
+
206
234
  if (env?.APIFY_INPUT_PATH && fs.existsSync(env.APIFY_INPUT_PATH)) {
207
235
  const raw = fs.readFileSync(env.APIFY_INPUT_PATH, 'utf8');
208
- return JSON.parse(raw);
236
+ return applyAtHomeSchemaDefaults(raw && raw.trim() ? JSON.parse(raw) : {});
209
237
  }
210
238
 
211
239
  const localStorageDir =
@@ -224,7 +252,8 @@ export async function loadApifyActorInput(
224
252
  candidateName
225
253
  );
226
254
  if (fs.existsSync(localInputPath)) {
227
- return JSON.parse(fs.readFileSync(localInputPath, 'utf8'));
255
+ const raw = fs.readFileSync(localInputPath, 'utf8');
256
+ return applyAtHomeSchemaDefaults(raw && raw.trim() ? JSON.parse(raw) : {});
228
257
  }
229
258
  }
230
259
  }
@@ -254,9 +283,11 @@ export async function loadApifyActorInput(
254
283
  });
255
284
  if (res.status === 200) {
256
285
  const text = await res.text();
257
- if (text && text.trim()) {
258
- return JSON.parse(text);
259
- }
286
+ const parsed = text && text.trim() ? JSON.parse(text) : {};
287
+ return applyAtHomeSchemaDefaults(parsed);
288
+ }
289
+ if (res.status === 204 || res.status === 404) {
290
+ return applyAtHomeSchemaDefaults({});
260
291
  }
261
292
  } finally {
262
293
  clearTimeout(timer);
@@ -310,6 +341,19 @@ export function createApifyPlatformHooks(env = process.env, fetchImpl = globalTh
310
341
  env?.APIFY_API_BASE_URL,
311
342
  env?.APIFY_API_PUBLIC_BASE_URL
312
343
  );
344
+ let pricingInfoChargeEvents = null;
345
+ const rawPricingInfoEnv = env?.APIFY_ACTOR_PRICING_INFO || env?.ACTOR_PRICING_INFO;
346
+ if (rawPricingInfoEnv) {
347
+ try {
348
+ const parsedPricing = JSON.parse(String(rawPricingInfoEnv));
349
+ const eventsObj = parsedPricing?.pricingPerEvent?.actorChargeEvents;
350
+ if (eventsObj && typeof eventsObj === 'object') {
351
+ pricingInfoChargeEvents = eventsObj;
352
+ }
353
+ } catch {
354
+ pricingInfoChargeEvents = null;
355
+ }
356
+ }
313
357
  const rawMaxChargeStr =
314
358
  env?.ACTOR_MAX_TOTAL_CHARGE_USD !== undefined && env?.ACTOR_MAX_TOTAL_CHARGE_USD !== null
315
359
  ? String(env.ACTOR_MAX_TOTAL_CHARGE_USD).trim()
@@ -320,23 +364,28 @@ export function createApifyPlatformHooks(env = process.env, fetchImpl = globalTh
320
364
  .trim()
321
365
  .toLowerCase();
322
366
  const includeStartFee = includeStartFeeFlag !== '0' && includeStartFeeFlag !== 'false';
367
+ const pricingInfoStartFeeUsd = Number(
368
+ pricingInfoChargeEvents?.['apify-actor-start']?.eventPriceUsd
369
+ );
323
370
  const explicitStartFeeUsd =
324
371
  env?.ACTOR_START_FEE_USD !== undefined &&
325
372
  env?.ACTOR_START_FEE_USD !== null &&
326
373
  String(env.ACTOR_START_FEE_USD).trim() !== ''
327
374
  ? Number(env.ACTOR_START_FEE_USD)
328
- : ACTOR_PRICING_CONTRACT.actorStartFeeUsdc;
375
+ : Number.isFinite(pricingInfoStartFeeUsd) && pricingInfoStartFeeUsd >= 0
376
+ ? pricingInfoStartFeeUsd
377
+ : ACTOR_PRICING_CONTRACT.actorStartFeeUsdc;
329
378
  const startFeeMicros =
330
379
  includeStartFee && Number.isFinite(explicitStartFeeUsd) && explicitStartFeeUsd >= 0
331
380
  ? Math.round(explicitStartFeeUsd * 1_000_000)
332
381
  : 0;
333
- const unitPriceMicros = Math.round(ACTOR_PRICING_CONTRACT.priceUsdc * 1_000_000);
334
382
  const maxBudgetMicros = hasMaxChargeUsd ? Math.round(maxTotalChargeUsdRaw * 1_000_000) : 0;
335
- const maxPayableEvents = hasMaxChargeUsd
336
- ? Math.floor(Math.max(0, maxBudgetMicros - startFeeMicros) / unitPriceMicros)
337
- : Infinity;
338
383
  let cumulativeChargedInAdapter = 0;
339
384
  let adapterChargeLimitLatched = false;
385
+ hooks.actorStartFeeUsdc =
386
+ includeStartFee && Number.isFinite(explicitStartFeeUsd) && explicitStartFeeUsd >= 0
387
+ ? explicitStartFeeUsd
388
+ : 0;
340
389
 
341
390
  if (kvsId) {
342
391
  const checkpointRecordUrl = `${apiBase}/v2/key-value-stores/${encodeURIComponent(kvsId)}/records/${ACTOR_PRICING_CONTRACT.checkpointKey}`;
@@ -470,15 +519,80 @@ export function createApifyPlatformHooks(env = process.env, fetchImpl = globalTh
470
519
 
471
520
  if (runId) {
472
521
  const chargeUrl = `${apiBase}/v2/actor-runs/${encodeURIComponent(runId)}/charge`;
522
+ const allowUnbilledAtHome = env?.ADPULSE_ALLOW_UNBILLED_AT_HOME === '1';
523
+ let unbilledOwnerTestLatched = false;
524
+ let latchedPlatformEventName = null;
525
+ const resolvePlatformEventAndPrice = (rawEventName) => {
526
+ const candidate = String(rawEventName || ACTOR_PRICING_CONTRACT.eventName);
527
+ const effectiveName =
528
+ latchedPlatformEventName ||
529
+ (() => {
530
+ if (pricingInfoChargeEvents) {
531
+ if (Object.prototype.hasOwnProperty.call(pricingInfoChargeEvents, candidate)) {
532
+ return candidate;
533
+ }
534
+ if (
535
+ candidate === 'ad-creative-scraped' ||
536
+ candidate === 'ad_transparency_creative_retrieved'
537
+ ) {
538
+ if (
539
+ Object.prototype.hasOwnProperty.call(
540
+ pricingInfoChargeEvents,
541
+ 'ad_transparency_creative_retrieved'
542
+ )
543
+ ) {
544
+ return 'ad_transparency_creative_retrieved';
545
+ }
546
+ if (
547
+ Object.prototype.hasOwnProperty.call(
548
+ pricingInfoChargeEvents,
549
+ 'ad-creative-scraped'
550
+ )
551
+ ) {
552
+ return 'ad-creative-scraped';
553
+ }
554
+ }
555
+ }
556
+ return candidate;
557
+ })();
558
+ const rawEventPrice = Number(pricingInfoChargeEvents?.[effectiveName]?.eventPriceUsd);
559
+ const unitPriceUsdc =
560
+ Number.isFinite(rawEventPrice) && rawEventPrice > 0
561
+ ? rawEventPrice
562
+ : ACTOR_PRICING_CONTRACT.priceUsdc;
563
+ return { eventName: effectiveName, unitPriceUsdc };
564
+ };
565
+ hooks.unitPriceUsdc = resolvePlatformEventAndPrice(
566
+ ACTOR_PRICING_CONTRACT.eventName
567
+ ).unitPriceUsdc;
473
568
  hooks.chargeEvent = async (receipt) => {
474
569
  const requestedCount = Math.max(0, Number(receipt?.count) || 0);
570
+ const { eventName: primaryEventName, unitPriceUsdc: resolvedUnitPriceUsdc } =
571
+ resolvePlatformEventAndPrice(receipt?.eventName);
572
+ hooks.unitPriceUsdc = resolvedUnitPriceUsdc;
573
+ if (unbilledOwnerTestLatched) {
574
+ return {
575
+ chargedCount: 0,
576
+ eventChargeLimitReached: false,
577
+ unbilledOwnerTestBypass: true,
578
+ chargedCreativeIds: [],
579
+ eventName: primaryEventName,
580
+ unitPriceUsdc: resolvedUnitPriceUsdc,
581
+ };
582
+ }
475
583
  if (adapterChargeLimitLatched) {
476
584
  return {
477
585
  chargedCount: 0,
478
586
  eventChargeLimitReached: true,
479
587
  chargedCreativeIds: [],
588
+ eventName: primaryEventName,
589
+ unitPriceUsdc: resolvedUnitPriceUsdc,
480
590
  };
481
591
  }
592
+ const unitPriceMicros = Math.round(resolvedUnitPriceUsdc * 1_000_000);
593
+ const maxPayableEvents = hasMaxChargeUsd
594
+ ? Math.floor(Math.max(0, maxBudgetMicros - startFeeMicros) / unitPriceMicros)
595
+ : Infinity;
482
596
  const remainingAllowance = Number.isFinite(maxPayableEvents)
483
597
  ? Math.max(0, maxPayableEvents - cumulativeChargedInAdapter)
484
598
  : requestedCount;
@@ -493,25 +607,65 @@ export function createApifyPlatformHooks(env = process.env, fetchImpl = globalTh
493
607
  chargedCount: 0,
494
608
  eventChargeLimitReached: Number.isFinite(maxPayableEvents),
495
609
  chargedCreativeIds: [],
610
+ eventName: primaryEventName,
611
+ unitPriceUsdc: resolvedUnitPriceUsdc,
496
612
  };
497
613
  }
498
614
 
499
- const controller = new AbortController();
500
- const timer = setTimeout(() => controller.abort(), 600);
501
- try {
502
- const res = await fetchImpl(chargeUrl, {
615
+ const postChargeRequest = async (eventNameToPost, idempotencyKeyToPost, signal) =>
616
+ fetchImpl(chargeUrl, {
503
617
  method: 'POST',
504
618
  headers: {
505
619
  authorization: `Bearer ${token}`,
506
620
  'content-type': 'application/json; charset=utf-8',
507
- 'idempotency-key': receipt.idempotencyKey,
621
+ 'idempotency-key': idempotencyKeyToPost,
508
622
  },
509
623
  body: JSON.stringify({
510
- eventName: receipt.eventName,
624
+ eventName: eventNameToPost,
511
625
  count: countToCharge,
512
626
  }),
513
- signal: controller.signal,
627
+ signal,
514
628
  });
629
+
630
+ const controller = new AbortController();
631
+ const timer = setTimeout(() => controller.abort(), 600);
632
+ try {
633
+ let effectiveEventName = primaryEventName;
634
+ let effectiveIdempotencyKey = receipt.idempotencyKey;
635
+ let res = await postChargeRequest(
636
+ primaryEventName,
637
+ receipt.idempotencyKey,
638
+ controller.signal
639
+ );
640
+ if (
641
+ res.status === 404 &&
642
+ !pricingInfoChargeEvents &&
643
+ !latchedPlatformEventName &&
644
+ (primaryEventName === 'ad-creative-scraped' ||
645
+ primaryEventName === 'ad_transparency_creative_retrieved')
646
+ ) {
647
+ const aliasEventName =
648
+ primaryEventName === 'ad-creative-scraped'
649
+ ? 'ad_transparency_creative_retrieved'
650
+ : 'ad-creative-scraped';
651
+ const aliasIdempotencyKey = `${receipt.idempotencyKey}-${aliasEventName}`;
652
+ const fallbackRes = await postChargeRequest(
653
+ aliasEventName,
654
+ aliasIdempotencyKey,
655
+ controller.signal
656
+ );
657
+ if (fallbackRes.status !== 404) {
658
+ res = fallbackRes;
659
+ effectiveEventName = aliasEventName;
660
+ effectiveIdempotencyKey = aliasIdempotencyKey;
661
+ if (
662
+ (fallbackRes.status >= 200 && fallbackRes.status < 300) ||
663
+ fallbackRes.status === 402
664
+ ) {
665
+ latchedPlatformEventName = aliasEventName;
666
+ }
667
+ }
668
+ }
515
669
  if (res.status === 402) {
516
670
  // Platform enforced spending ceiling (HTTP 402 Payment Required): latch so no further charge POSTs occur
517
671
  adapterChargeLimitLatched = true;
@@ -519,10 +673,50 @@ export function createApifyPlatformHooks(env = process.env, fetchImpl = globalTh
519
673
  chargedCount: 0,
520
674
  eventChargeLimitReached: true,
521
675
  chargedCreativeIds: [],
676
+ eventName: effectiveEventName,
677
+ unitPriceUsdc: resolvedUnitPriceUsdc,
678
+ idempotencyKey: effectiveIdempotencyKey,
522
679
  };
523
680
  }
681
+ if (allowUnbilledAtHome && (res.status === 405 || res.status === 403)) {
682
+ let isUnmonetizedNotAllowed = res.status === 405;
683
+ if (res.status === 403) {
684
+ try {
685
+ const errText = await res.text();
686
+ const errJson = errText && errText.trim() ? JSON.parse(errText) : null;
687
+ isUnmonetizedNotAllowed = errJson?.error?.type === 'operation-not-allowed';
688
+ } catch {
689
+ isUnmonetizedNotAllowed = false;
690
+ }
691
+ }
692
+ if (isUnmonetizedNotAllowed) {
693
+ unbilledOwnerTestLatched = true;
694
+ return {
695
+ chargedCount: 0,
696
+ eventChargeLimitReached: false,
697
+ unbilledOwnerTestBypass: true,
698
+ chargedCreativeIds: [],
699
+ eventName: effectiveEventName,
700
+ unitPriceUsdc: resolvedUnitPriceUsdc,
701
+ idempotencyKey: effectiveIdempotencyKey,
702
+ };
703
+ }
704
+ }
524
705
  if (res.status < 200 || res.status >= 300) {
525
- throw new Error(`Apify Pay-per-Event charge HTTP ${res.status}`);
706
+ let errSuffix = '';
707
+ try {
708
+ const errText = await res.text();
709
+ if (errText && errText.trim()) {
710
+ const errJson = JSON.parse(errText);
711
+ const errType = errJson?.error?.type;
712
+ if (typeof errType === 'string' && /^[A-Za-z0-9_.:-]{1,64}$/.test(errType)) {
713
+ errSuffix = ` (${errType})`;
714
+ }
715
+ }
716
+ } catch {
717
+ errSuffix = '';
718
+ }
719
+ throw new Error(`Apify Pay-per-Event charge HTTP ${res.status}${errSuffix}`);
526
720
  }
527
721
  const rawText = await res.text();
528
722
  const parsed = rawText && rawText.trim() ? JSON.parse(rawText) : {};
@@ -552,14 +746,32 @@ export function createApifyPlatformHooks(env = process.env, fetchImpl = globalTh
552
746
  Boolean(dataObj?.eventChargeLimitReached) ||
553
747
  limitReached ||
554
748
  (Number.isFinite(maxPayableEvents) && cumulativeChargedInAdapter >= maxPayableEvents);
555
- const subsetIds = Array.isArray(dataObj?.chargedCreativeIds)
556
- ? dataObj.chargedCreativeIds
557
- : Array.isArray(receipt?.creativeIds)
558
- ? receipt.creativeIds.slice(0, resolvedChargedCount)
559
- : undefined;
749
+ let subsetIds;
750
+ if (Array.isArray(dataObj?.chargedCreativeIds)) {
751
+ const reqIds = Array.isArray(receipt?.creativeIds) ? receipt.creativeIds : null;
752
+ const uniqueValidIds = new Set(
753
+ dataObj.chargedCreativeIds.filter(
754
+ (id) => typeof id === 'string' && (!reqIds || reqIds.includes(id))
755
+ )
756
+ );
757
+ if (
758
+ dataObj.chargedCreativeIds.length !== resolvedChargedCount ||
759
+ uniqueValidIds.size !== resolvedChargedCount
760
+ ) {
761
+ throw new Error(
762
+ 'Apify Pay-per-Event charge response contained invalid chargedCreativeIds'
763
+ );
764
+ }
765
+ subsetIds = Array.from(uniqueValidIds);
766
+ } else if (Array.isArray(receipt?.creativeIds)) {
767
+ subsetIds = receipt.creativeIds.slice(0, resolvedChargedCount);
768
+ }
560
769
  return {
561
770
  chargedCount: resolvedChargedCount,
562
771
  eventChargeLimitReached: resolvedLimitReached,
772
+ eventName: effectiveEventName,
773
+ unitPriceUsdc: resolvedUnitPriceUsdc,
774
+ idempotencyKey: effectiveIdempotencyKey,
563
775
  ...(subsetIds ? { chargedCreativeIds: subsetIds } : {}),
564
776
  };
565
777
  } finally {
@@ -704,7 +916,7 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
704
916
  'default_run'
705
917
  );
706
918
  const hasChargeHook = typeof hooks.chargeEvent === 'function';
707
- const billingMode =
919
+ let billingMode =
708
920
  hasChargeHook || hooks.isAtHome === true ? 'APIFY_PAY_PER_EVENT' : 'LOCAL_UNBILLED';
709
921
 
710
922
  const maxRunDurationMs = Math.min(
@@ -1332,6 +1544,8 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
1332
1544
  chargedEventCount: hasChargeHook ? chargedSet.size : 0,
1333
1545
  durationMs,
1334
1546
  requestCount: Math.max(1, totalRpcRequests),
1547
+ unitPriceUsdc: hooks?.unitPriceUsdc ?? ACTOR_PRICING_CONTRACT.priceUsdc,
1548
+ actorStartFeeUsdc: hooks?.actorStartFeeUsdc,
1335
1549
  }),
1336
1550
  });
1337
1551
  }
@@ -1355,6 +1569,7 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
1355
1569
  }
1356
1570
  };
1357
1571
 
1572
+ let lastResolvedUnitPriceUsdc = hooks?.unitPriceUsdc ?? ACTOR_PRICING_CONTRACT.priceUsdc;
1358
1573
  for (let offset = 0; offset < newBillableCandidates.length; offset += chunkSize) {
1359
1574
  const effectiveChunkDeadlineMs = hasChargeHook
1360
1575
  ? deadlineAtMs - step4ReserveMs
@@ -1455,6 +1670,28 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
1455
1670
  // Step 4.2: Strict ChargeResult validation (never fallback to ?? unchargedChunk.length)
1456
1671
  const chargedCount = chargeResult?.chargedCount;
1457
1672
  const eventChargeLimitReached = Boolean(chargeResult?.eventChargeLimitReached);
1673
+ const resolvedReceiptEventName =
1674
+ typeof chargeResult?.eventName === 'string' && chargeResult.eventName
1675
+ ? chargeResult.eventName
1676
+ : receipt.eventName;
1677
+ const resolvedReceiptUnitPriceUsdc =
1678
+ Number.isFinite(Number(chargeResult?.unitPriceUsdc)) && Number(chargeResult.unitPriceUsdc) > 0
1679
+ ? Number(chargeResult.unitPriceUsdc)
1680
+ : receipt.unitPriceUsdc;
1681
+ lastResolvedUnitPriceUsdc = resolvedReceiptUnitPriceUsdc;
1682
+ const resolvedReceiptIdempotencyKey =
1683
+ typeof chargeResult?.idempotencyKey === 'string' && chargeResult.idempotencyKey
1684
+ ? chargeResult.idempotencyKey
1685
+ : receipt.idempotencyKey;
1686
+ const buildFinalReceipt = (actualChargedCount, extra = {}) => ({
1687
+ ...receipt,
1688
+ idempotencyKey: resolvedReceiptIdempotencyKey,
1689
+ eventName: resolvedReceiptEventName,
1690
+ unitPriceUsdc: resolvedReceiptUnitPriceUsdc,
1691
+ chunkAmountUsdc: Number((actualChargedCount * resolvedReceiptUnitPriceUsdc).toFixed(6)),
1692
+ chargedCount: actualChargedCount,
1693
+ ...extra,
1694
+ });
1458
1695
 
1459
1696
  if (
1460
1697
  !chargeResult ||
@@ -1487,11 +1724,7 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
1487
1724
 
1488
1725
  const pushRes = await pushDataWithRetry(unchargedChunk);
1489
1726
  if (!pushRes.ok) {
1490
- chargeReceipts.push({
1491
- ...receipt,
1492
- chargedCount,
1493
- eventChargeLimitReached,
1494
- });
1727
+ chargeReceipts.push(buildFinalReceipt(chargedCount, { eventChargeLimitReached }));
1495
1728
  finalOk = false;
1496
1729
  finalStatus = 'PARTIAL_BILLING_HALT';
1497
1730
  haltReason = `Dataset pushData failed after charge (paid items preserved in KVS checkpoint stagedItemsById): ${pushRes.error?.message || String(pushRes.error)}`;
@@ -1504,11 +1737,7 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
1504
1737
  deliveredCount = deliveredSet.size;
1505
1738
  await persistCheckpointPostCharge();
1506
1739
 
1507
- chargeReceipts.push({
1508
- ...receipt,
1509
- chargedCount,
1510
- eventChargeLimitReached,
1511
- });
1740
+ chargeReceipts.push(buildFinalReceipt(chargedCount, { eventChargeLimitReached }));
1512
1741
 
1513
1742
  if (eventChargeLimitReached) {
1514
1743
  finalStatus = 'CHARGE_LIMIT_REACHED';
@@ -1516,12 +1745,32 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
1516
1745
  }
1517
1746
  } else if (chargedCount === 0) {
1518
1747
  inFlightSet = new Set();
1748
+ if (chargeResult?.unbilledOwnerTestBypass === true) {
1749
+ billingMode = 'UNBILLED_OWNER_TEST';
1750
+ await persistCheckpointPostCharge();
1751
+ const pushRes = await pushDataWithRetry(unchargedChunk);
1752
+ if (!pushRes.ok) {
1753
+ finalOk = false;
1754
+ finalStatus = 'PARTIAL_BILLING_HALT';
1755
+ haltReason = `Dataset pushData failed in UNBILLED_OWNER_TEST mode: ${pushRes.error?.message || String(pushRes.error)}`;
1756
+ break;
1757
+ }
1758
+ for (const c of unchargedChunk) {
1759
+ deliveredSet.add(c.creativeId);
1760
+ newlyDeliveredItems.push(c);
1761
+ }
1762
+ deliveredCount = deliveredSet.size;
1763
+ await persistCheckpointPostCharge();
1764
+ chargeReceipts.push(
1765
+ buildFinalReceipt(0, {
1766
+ eventChargeLimitReached: false,
1767
+ unbilledOwnerTestBypass: true,
1768
+ })
1769
+ );
1770
+ continue;
1771
+ }
1519
1772
  await persistCheckpointPostCharge();
1520
- chargeReceipts.push({
1521
- ...receipt,
1522
- chargedCount: 0,
1523
- eventChargeLimitReached,
1524
- });
1773
+ chargeReceipts.push(buildFinalReceipt(0, { eventChargeLimitReached }));
1525
1774
  if (eventChargeLimitReached) {
1526
1775
  finalOk = true;
1527
1776
  finalStatus = 'CHARGE_LIMIT_REACHED';
@@ -1533,11 +1782,19 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
1533
1782
  break;
1534
1783
  } else {
1535
1784
  // Partial chunk charge (0 < chargedCount < unchargedChunk.length)
1536
- const explicitChargedIds = Array.isArray(chargeResult.chargedCreativeIds)
1537
- ? chargeResult.chargedCreativeIds.filter((id) => chunkIds.includes(id))
1785
+ const hasExplicitIdsArray = Array.isArray(chargeResult.chargedCreativeIds);
1786
+ const explicitChargedIds = hasExplicitIdsArray
1787
+ ? Array.from(
1788
+ new Set(chargeResult.chargedCreativeIds.filter((id) => chunkIds.includes(id)))
1789
+ )
1538
1790
  : null;
1791
+ const isValidExplicitChargedIds = Boolean(
1792
+ explicitChargedIds &&
1793
+ explicitChargedIds.length === chargedCount &&
1794
+ chargeResult.chargedCreativeIds.length === chargedCount
1795
+ );
1539
1796
 
1540
- if (explicitChargedIds && explicitChargedIds.length === chargedCount) {
1797
+ if (isValidExplicitChargedIds) {
1541
1798
  for (const id of explicitChargedIds) {
1542
1799
  chargedSet.add(id);
1543
1800
  }
@@ -1548,12 +1805,12 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
1548
1805
  const paidSubset = unchargedChunk.filter((c) => explicitChargedIds.includes(c.creativeId));
1549
1806
  const pushRes = await pushDataWithRetry(paidSubset);
1550
1807
  if (!pushRes.ok) {
1551
- chargeReceipts.push({
1552
- ...receipt,
1553
- chargedCount,
1554
- chargedCreativeIds: explicitChargedIds,
1555
- eventChargeLimitReached,
1556
- });
1808
+ chargeReceipts.push(
1809
+ buildFinalReceipt(chargedCount, {
1810
+ chargedCreativeIds: explicitChargedIds,
1811
+ eventChargeLimitReached,
1812
+ })
1813
+ );
1557
1814
  finalOk = false;
1558
1815
  finalStatus = 'PARTIAL_BILLING_HALT';
1559
1816
  haltReason = `Dataset pushData failed after partial charge (paid items preserved in KVS checkpoint stagedItemsById): ${pushRes.error?.message || String(pushRes.error)}`;
@@ -1566,12 +1823,12 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
1566
1823
  deliveredCount = deliveredSet.size;
1567
1824
  await persistCheckpointPostCharge();
1568
1825
 
1569
- chargeReceipts.push({
1570
- ...receipt,
1571
- chargedCount,
1572
- chargedCreativeIds: explicitChargedIds,
1573
- eventChargeLimitReached,
1574
- });
1826
+ chargeReceipts.push(
1827
+ buildFinalReceipt(chargedCount, {
1828
+ chargedCreativeIds: explicitChargedIds,
1829
+ eventChargeLimitReached,
1830
+ })
1831
+ );
1575
1832
 
1576
1833
  finalOk = eventChargeLimitReached;
1577
1834
  finalStatus = eventChargeLimitReached ? 'CHARGE_LIMIT_REACHED' : 'PARTIAL_BILLING_HALT';
@@ -1579,7 +1836,7 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
1579
1836
  haltReason = `Partial chunk charge (${chargedCount}/${unchargedChunk.length}) without charge limit flag`;
1580
1837
  }
1581
1838
  break;
1582
- } else if (eventChargeLimitReached) {
1839
+ } else if (!hasExplicitIdsArray && eventChargeLimitReached) {
1583
1840
  // Standard Apify SDK ChargeResult when hitting user's max charge ceiling:
1584
1841
  // Attribute charge deterministically to the ordered prefix `unchargedChunk.slice(0, chargedCount)`
1585
1842
  // so the customer receives every item they paid for, and leave the remaining tail uncharged.
@@ -1594,12 +1851,12 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
1594
1851
 
1595
1852
  const pushRes = await pushDataWithRetry(paidPrefix);
1596
1853
  if (!pushRes.ok) {
1597
- chargeReceipts.push({
1598
- ...receipt,
1599
- chargedCount,
1600
- chargedCreativeIds: paidPrefixIds,
1601
- eventChargeLimitReached: true,
1602
- });
1854
+ chargeReceipts.push(
1855
+ buildFinalReceipt(chargedCount, {
1856
+ chargedCreativeIds: paidPrefixIds,
1857
+ eventChargeLimitReached: true,
1858
+ })
1859
+ );
1603
1860
  finalOk = false;
1604
1861
  finalStatus = 'PARTIAL_BILLING_HALT';
1605
1862
  haltReason = `Dataset pushData failed after charge limit prefix (paid items preserved in KVS checkpoint stagedItemsById): ${pushRes.error?.message || String(pushRes.error)}`;
@@ -1612,12 +1869,12 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
1612
1869
  deliveredCount = deliveredSet.size;
1613
1870
  await persistCheckpointPostCharge();
1614
1871
 
1615
- chargeReceipts.push({
1616
- ...receipt,
1617
- chargedCount,
1618
- chargedCreativeIds: paidPrefixIds,
1619
- eventChargeLimitReached: true,
1620
- });
1872
+ chargeReceipts.push(
1873
+ buildFinalReceipt(chargedCount, {
1874
+ chargedCreativeIds: paidPrefixIds,
1875
+ eventChargeLimitReached: true,
1876
+ })
1877
+ );
1621
1878
 
1622
1879
  finalOk = true;
1623
1880
  finalStatus = 'CHARGE_LIMIT_REACHED';
@@ -1632,11 +1889,11 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
1632
1889
  chargedEventCount = chargedSet.size + chargedCount;
1633
1890
  await persistCheckpointPostCharge();
1634
1891
 
1635
- chargeReceipts.push({
1636
- ...receipt,
1637
- chargedCount,
1638
- eventChargeLimitReached: false,
1639
- });
1892
+ chargeReceipts.push(
1893
+ buildFinalReceipt(chargedCount, {
1894
+ eventChargeLimitReached: false,
1895
+ })
1896
+ );
1640
1897
 
1641
1898
  finalOk = false;
1642
1899
  finalStatus = 'PARTIAL_BILLING_HALT';
@@ -1699,6 +1956,8 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
1699
1956
  chargedEventCount,
1700
1957
  durationMs,
1701
1958
  requestCount: Math.max(1, totalRpcRequests),
1959
+ unitPriceUsdc: lastResolvedUnitPriceUsdc,
1960
+ actorStartFeeUsdc: hooks?.actorStartFeeUsdc,
1702
1961
  });
1703
1962
 
1704
1963
  return finalizeActorReturn({