feat(release): prepare 1.1.0 candidate

This commit is contained in:
Yun Chan 2026-08-29 18:33:45 +09:00
parent 5a34f66981
commit 5205dcdfa9
736 changed files with 115667 additions and 12203 deletions

View file

@ -9,7 +9,6 @@ import type {
AdImpressionEvent,
AdRewardResult,
AdRevenueStats,
AdNetworkId,
} from '@d3ro/core/types'
import type { IAdNetworkAdapter } from './BaseAdAdapter'
import { EthicalAdsAdapter } from './EthicalAdsAdapter'
@ -29,10 +28,13 @@ export class AdMediationEngine {
private adapters: Map<string, IAdNetworkAdapter> = new Map()
private config: AdMediationConfig
private impressionHistory: AdImpressionEvent[] = []
private lastRewardTimestamp = 0
private deliveredCreatives = new Map<string, AdCreativePayload>()
private clickedCreatives = new Set<string>()
private constructor() {
// Register all 10+ Production Ad Adapters
// Provider shells stay registered for explicit configuration diagnostics,
// but are disabled until an official SDK or authenticated decision API is
// integrated. No adapter may fabricate a bid or creative.
const adapterList: IAdNetworkAdapter[] = [
new DirectHouseSponsorAdapter(),
new PlaywireAdapter(),
@ -54,14 +56,14 @@ export class AdMediationEngine {
networks: adapterList.map((a, idx) => ({
id: a.networkId,
name: a.networkName,
enabled: true,
enabled: false,
priority: idx + 1,
floorEcpm: a.defaultFloorEcpm,
adapterType: 'rest_json',
})),
rewardTokensAmount: 50,
rewardCooldownSeconds: 60,
houseAdFallback: true,
houseAdFallback: false,
headerBiddingTimeoutMs: 800,
defaultFloorEcpm: 2.0,
}
@ -75,14 +77,55 @@ export class AdMediationEngine {
}
public getConfig(): AdMediationConfig {
return { ...this.config }
return {
...this.config,
networks: this.config.networks.map((network) => ({ ...network })),
}
}
public setConfig(newConfig: Partial<AdMediationConfig>): AdMediationConfig {
this.config = { ...this.config, ...newConfig }
if (newConfig.rewardTokensAmount !== undefined && (!Number.isFinite(newConfig.rewardTokensAmount) || newConfig.rewardTokensAmount < 0)) {
throw new Error('rewardTokensAmount must be a non-negative number')
}
if (newConfig.rewardCooldownSeconds !== undefined && (!Number.isFinite(newConfig.rewardCooldownSeconds) || newConfig.rewardCooldownSeconds < 0)) {
throw new Error('rewardCooldownSeconds must be a non-negative number')
}
if (newConfig.defaultFloorEcpm !== undefined && (!Number.isFinite(newConfig.defaultFloorEcpm) || newConfig.defaultFloorEcpm < 0)) {
throw new Error('defaultFloorEcpm must be a non-negative number')
}
if (newConfig.headerBiddingTimeoutMs !== undefined && (!Number.isFinite(newConfig.headerBiddingTimeoutMs) || newConfig.headerBiddingTimeoutMs <= 0)) {
throw new Error('headerBiddingTimeoutMs must be a positive number')
}
this.config = {
...this.config,
...newConfig,
networks: newConfig.networks?.map((network) => ({ ...network }))
?? this.config.networks.map((network) => ({ ...network })),
}
return this.getConfig()
}
private isSafeCreative(
adapter: IAdNetworkAdapter,
request: AdMediationAuctionRequest,
bidEcpm: number,
creative: AdCreativePayload,
): boolean {
if (
!Number.isFinite(bidEcpm)
|| bidEcpm <= 0
|| !creative.id.trim()
|| creative.networkId !== adapter.networkId
|| creative.format !== request.format
) return false
try {
return new URL(creative.clickUrl).protocol === 'https:'
} catch {
return false
}
}
/**
* Execute real-time Header Bidding Auction across all enabled ad networks
*/
@ -93,7 +136,7 @@ export class AdMediationEngine {
const enabledAdapters = Array.from(this.adapters.values()).filter((adapter) => {
const netConfig = this.config.networks.find((n) => n.id === adapter.networkId)
return (netConfig ? netConfig.enabled : true) && adapter.supportedFormats.includes(request.format)
return netConfig?.enabled === true && adapter.supportedFormats.includes(request.format)
})
// Query all participating demand sources in parallel with timeout
@ -107,11 +150,36 @@ export class AdMediationEngine {
),
])
if (
bidResult.hasBid
&& (
!bidResult.creative
|| !this.isSafeCreative(adapter, request, bidResult.bidEcpm, bidResult.creative)
)
) {
return {
networkId: adapter.networkId,
networkName: adapter.networkName,
bidEcpm: 0,
creative: undefined,
latencyMs: Date.now() - adapterStart,
status: 'error' as const,
}
}
const creative = bidResult.hasBid && bidResult.creative
? {
...bidResult.creative,
networkName: adapter.networkName,
bidEcpm: bidResult.bidEcpm,
}
: undefined
return {
networkId: adapter.networkId,
networkName: adapter.networkName,
bidEcpm: bidResult.hasBid ? bidResult.bidEcpm : 0,
creative: bidResult.creative,
creative,
latencyMs: Date.now() - adapterStart,
status: (bidResult.hasBid ? 'bid' : 'no_bid') as 'bid' | 'no_bid',
}
@ -134,22 +202,23 @@ export class AdMediationEngine {
.filter((b) => b.status === 'bid' && b.creative && b.bidEcpm >= floorEcpm)
.sort((a, b) => b.bidEcpm - a.bidEcpm)
let winningCreative: AdCreativePayload
if (validBids.length > 0 && validBids[0].creative) {
winningCreative = validBids[0].creative
} else {
// Fallback to Direct House Sponsor
const houseAdapter = this.adapters.get('direct_sponsor') || new DirectHouseSponsorAdapter()
const fallbackBid = await houseAdapter.requestBid(request)
winningCreative = fallbackBid.creative!
const winningCreative = validBids[0]?.creative ?? null
if (winningCreative !== null) {
this.deliveredCreatives.set(winningCreative.id, winningCreative)
if (this.deliveredCreatives.size > 256) {
const oldest = this.deliveredCreatives.keys().next().value as string | undefined
if (oldest !== undefined) {
this.deliveredCreatives.delete(oldest)
this.clickedCreatives.delete(oldest)
}
}
}
const totalLatency = Date.now() - auctionStart
return {
winner: winningCreative,
winningBidEcpm: winningCreative.bidEcpm,
winningBidEcpm: winningCreative?.bidEcpm ?? 0,
participatingBids: bidResults.map((b) => ({
networkId: b.networkId,
networkName: b.networkName,
@ -163,8 +232,18 @@ export class AdMediationEngine {
}
public recordImpression(event: Omit<AdImpressionEvent, 'timestamp'>): void {
const delivered = this.deliveredCreatives.get(event.adId)
if (
delivered === undefined
|| delivered.networkId !== event.network
|| delivered.format !== event.format
|| this.impressionHistory.some((candidate) => candidate.adId === event.adId)
) return
const fullEvent: AdImpressionEvent = {
...event,
networkName: delivered.networkName,
earnedEcpm: delivered.bidEcpm,
timestamp: Date.now(),
}
this.impressionHistory.push(fullEvent)
@ -175,10 +254,19 @@ export class AdMediationEngine {
}
// Register into Settlement Ledger
getAdSettlementService().recordImpression(event.network, event.earnedEcpm || 3.5)
getAdSettlementService().recordImpression(event.network, delivered.bidEcpm)
}
public recordClick(adId: string, networkId: string): void {
const delivered = this.deliveredCreatives.get(adId)
const impressed = this.impressionHistory.some((candidate) => candidate.adId === adId)
if (
delivered === undefined
|| delivered.networkId !== networkId
|| !impressed
|| this.clickedCreatives.has(adId)
) return
this.clickedCreatives.add(adId)
const adapter = this.adapters.get(networkId)
if (adapter) {
adapter.reportClick(adId).catch(() => {})
@ -187,39 +275,19 @@ export class AdMediationEngine {
}
public async claimReward(adId: string, networkId: string): Promise<AdRewardResult> {
const now = Date.now()
const cooldownMs = this.config.rewardCooldownSeconds * 1000
if (now - this.lastRewardTimestamp < cooldownMs) {
const waitSeconds = Math.ceil((cooldownMs - (now - this.lastRewardTimestamp)) / 1000)
return {
success: false,
tokensAdded: 0,
newTotalQuota: 0,
nextAvailableAt: now + waitSeconds * 1000,
}
}
const adapter = this.adapters.get(networkId)
let tokenAmount = this.config.rewardTokensAmount
if (adapter && adapter.reportRewardCompletion) {
const res = await adapter.reportRewardCompletion(adId)
if (res.success && res.tokenReward) tokenAmount = res.tokenReward
}
this.lastRewardTimestamp = now
getAdSettlementService().recordCompletion(networkId)
// Desktop mediation has no server-verified completion/nonce ledger. A
// timer, renderer-supplied ID, or adapter callback is not entitlement
// proof, so rewards remain unavailable until that boundary exists.
void adId
void networkId
return {
success: true,
tokensAdded: tokenAmount,
newTotalQuota: 100 + tokenAmount, // Demo / actual license service quota boost
rewardId: `rew_${Date.now()}`,
success: false,
tokensAdded: 0,
newTotalQuota: 0,
}
}
public getRevenueStats(period = '2026-08'): AdRevenueStats {
public getRevenueStats(period = new Date().toISOString().slice(0, 7)): AdRevenueStats {
return getAdSettlementService().getRevenueStats(period)
}
}

View file

@ -2,23 +2,22 @@
// Ad Revenue Settlement, Tax Withholding & Payout Ledger Service
import type {
AdSettlementRecord,
AdRevenueStats,
AdSettlementRecord,
PublisherAccountConfig,
AdNetworkId,
} from '@d3ro/core/types'
export class AdSettlementService {
private static instance: AdSettlementService | null = null
private publisherAccount: PublisherAccountConfig = {
accountEmail: 'yunchanpaca@gmail.com',
beneficiaryName: 'D3RO Voice AI',
payoutBank: 'KB국민은행 (Kookmin Bank)',
payoutAccountNumber: '928702-00-184920',
taxRegistrationNumber: '120-88-01923',
paypalEmail: 'yunchanpaca@gmail.com',
networksConfigured: 10,
accountEmail: '',
beneficiaryName: '',
payoutBank: '',
payoutAccountNumber: '',
taxRegistrationNumber: '',
paypalEmail: '',
networksConfigured: 0,
}
// Network counters for current cycle
@ -27,11 +26,7 @@ export class AdSettlementService {
{ impressions: number; clicks: number; completions: number; grossUsd: number }
> = new Map()
private settlements: AdSettlementRecord[] = []
private constructor() {
this.seedInitialSettlementHistory()
}
private constructor() {}
public static getInstance(): AdSettlementService {
if (!AdSettlementService.instance) {
@ -40,51 +35,8 @@ export class AdSettlementService {
return AdSettlementService.instance
}
private seedInitialSettlementHistory(): void {
const networks: Array<{ id: AdNetworkId; name: string; imp: number; ecpm: number }> = [
{ id: 'direct_sponsor', name: 'Direct House Sponsor (Cursor/Notion)', imp: 84000, ecpm: 15.2 },
{ id: 'playwire', name: 'Playwire RAMP Desktop Header Bidding', imp: 62000, ecpm: 8.4 },
{ id: 'applovin_max', name: 'AppLovin MAX In-App Bidding', imp: 48000, ecpm: 7.8 },
{ id: 'unity_ads', name: 'Unity LevelPlay Rewarded Video', imp: 45000, ecpm: 9.1 },
{ id: 'ethical_ads', name: 'EthicalAds Privacy-First Dev Network', imp: 38000, ecpm: 3.8 },
{ id: 'carbon_ads', name: 'Carbon Ads (BuySellAds)', imp: 31000, ecpm: 4.2 },
{ id: 'google_ad_manager', name: 'Google Ad Manager 360', imp: 29000, ecpm: 3.5 },
{ id: 'mintegral', name: 'Mintegral Global Video Network', imp: 22000, ecpm: 6.2 },
{ id: 'inmobi', name: 'InMobi Exchange', imp: 19000, ecpm: 3.4 },
{ id: 'pubmatic', name: 'PubMatic OpenWrap SSP', imp: 15000, ecpm: 3.6 },
]
for (const net of networks) {
const grossUsd = (net.imp / 1000) * net.ecpm
const withholdingRate = 0.033 // 3.3% Korean Business Tax Withholding
const netUsd = parseFloat((grossUsd * (1 - withholdingRate)).toFixed(2))
const exchangeRate = 1350
const netKrw = Math.round(netUsd * exchangeRate)
this.settlements.push({
id: `stl_202607_${net.id}`,
cycleMonth: '2026-07',
networkId: net.id,
networkName: net.name,
impressions: net.imp,
clicks: Math.round(net.imp * 0.032),
completions: Math.round(net.imp * 0.15),
avgEcpm: net.ecpm,
grossRevenueUsd: parseFloat(grossUsd.toFixed(2)),
withholdingTaxRate: withholdingRate,
netRevenueUsd: netUsd,
exchangeRateKrw: exchangeRate,
netPayoutKrw: netKrw,
payoutStatus: 'settled',
paymentMethod: 'bank_wire_krw',
beneficiaryAccount: this.publisherAccount.payoutAccountNumber,
settledAt: Date.now() - 1000 * 60 * 60 * 24 * 10,
invoiceNumber: `INV-202607-${net.id.toUpperCase().slice(0, 4)}`,
})
}
}
public recordImpression(networkId: string, earnedEcpm: number): void {
if (!networkId || !Number.isFinite(earnedEcpm) || earnedEcpm <= 0) return
const cur = this.networkCounters.get(networkId) || { impressions: 0, clicks: 0, completions: 0, grossUsd: 0 }
cur.impressions += 1
cur.grossUsd += earnedEcpm / 1000
@ -92,13 +44,15 @@ export class AdSettlementService {
}
public recordClick(networkId: string): void {
const cur = this.networkCounters.get(networkId) || { impressions: 0, clicks: 0, completions: 0, grossUsd: 0 }
const cur = this.networkCounters.get(networkId)
if (!cur || cur.clicks >= cur.impressions) return
cur.clicks += 1
this.networkCounters.set(networkId, cur)
}
public recordCompletion(networkId: string): void {
const cur = this.networkCounters.get(networkId) || { impressions: 0, clicks: 0, completions: 0, grossUsd: 0 }
const cur = this.networkCounters.get(networkId)
if (!cur || cur.completions >= cur.impressions) return
cur.completions += 1
this.networkCounters.set(networkId, cur)
}
@ -112,28 +66,32 @@ export class AdSettlementService {
return this.getPublisherAccount()
}
public getRevenueStats(period = '2026-08'): AdRevenueStats {
public getRevenueStats(period = new Date().toISOString().slice(0, 7)): AdRevenueStats {
let totalImp = 0
let totalClicks = 0
let totalCompletions = 0
let totalGrossUsd = 0
for (const record of this.settlements) {
totalImp += record.impressions
totalClicks += record.clicks
totalCompletions += record.completions
totalGrossUsd += record.grossRevenueUsd
for (const counters of this.networkCounters.values()) {
totalImp += counters.impressions
totalClicks += counters.clicks
totalCompletions += counters.completions
totalGrossUsd += counters.grossUsd
}
const avgEcpm = totalImp > 0 ? (totalGrossUsd / totalImp) * 1000 : 5.84
const networkBreakdown = this.settlements.map((s) => ({
network: s.networkName,
impressions: s.impressions,
revenueUsd: s.grossRevenueUsd,
ecpm: s.avgEcpm,
fillRate: 98.4,
}))
const avgEcpm = totalImp > 0 ? (totalGrossUsd / totalImp) * 1000 : 0
const networkBreakdown = Array.from(this.networkCounters.entries()).map(
([network, counters]) => ({
network,
impressions: counters.impressions,
revenueUsd: parseFloat(counters.grossUsd.toFixed(6)),
ecpm: counters.impressions > 0
? parseFloat(((counters.grossUsd / counters.impressions) * 1000).toFixed(2))
: 0,
// The desktop shell has no authoritative request/no-fill ledger yet.
fillRate: 0,
}),
)
return {
period,
@ -142,24 +100,15 @@ export class AdSettlementService {
totalCompletions: totalCompletions,
totalRevenueUsd: parseFloat(totalGrossUsd.toFixed(2)),
avgEcpm: parseFloat(avgEcpm.toFixed(2)),
fillRatePercent: 98.6,
fillRatePercent: 0,
networkBreakdown,
settlements: [...this.settlements],
settlements: [],
}
}
public requestPayout(settlementId: string): { success: boolean; message: string; settlement?: AdSettlementRecord } {
const found = this.settlements.find((s) => s.id === settlementId)
if (!found) {
return { success: false, message: 'Settlement record not found.' }
}
found.payoutStatus = 'paid'
found.settledAt = Date.now()
return {
success: true,
message: `정산금 ₩${found.netPayoutKrw.toLocaleString()}${this.publisherAccount.payoutBank} (${this.publisherAccount.payoutAccountNumber})으로 성공적으로 입금 신청되었습니다. (원천징수 영수증 발급 완료)`,
settlement: found,
}
void settlementId
return { success: false, message: 'external_settlement_not_configured' }
}
}

View file

@ -1,58 +1,12 @@
// apps/desktop/src/main/services/ads/AppLovinAdapter.ts
// AppLovin MAX Programmatic Bidding Adapter
import { UnavailableAdAdapter } from './UnavailableAdAdapter'
import type {
AdNetworkId,
AdFormat,
AdMediationAuctionRequest,
AdCreativePayload,
} from '@d3ro/core/types'
import type { IAdNetworkAdapter, AdBidResponse } from './BaseAdAdapter'
export class AppLovinAdapter implements IAdNetworkAdapter {
readonly networkId: AdNetworkId = 'applovin_max'
readonly networkName = 'AppLovin MAX (Real-Time In-App Bidding)'
readonly supportedFormats: AdFormat[] = ['rewarded_video', 'banner_dock', 'export_sponsor']
readonly defaultFloorEcpm = 5.5
async init(): Promise<void> {}
async requestBid(request: AdMediationAuctionRequest): Promise<AdBidResponse> {
const startTime = Date.now()
if (!this.supportedFormats.includes(request.format)) {
return { hasBid: false, bidEcpm: 0, latencyMs: 5 }
}
const baseEcpm = request.format === 'rewarded_video' ? 11.5 : 4.5
const ecpm = baseEcpm + Math.random() * 5.0 // Competitive bid
const creative: AdCreativePayload = {
id: `max_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
networkId: this.networkId,
networkName: this.networkName,
title: 'Grammarly AI — Write with Confidence Across All Apps',
description: 'Real-time AI suggestions, tone adjustments, and grammar correction.',
ctaText: 'Get Grammarly Free',
clickUrl: 'https://grammarly.com?utm_source=applovin',
sponsorTag: 'AppLovin MAX',
advertiserName: 'Grammarly',
bidEcpm: parseFloat(ecpm.toFixed(2)),
format: request.format,
rewardTokens: request.format === 'rewarded_video' ? 50 : undefined,
durationSeconds: 15,
}
return {
hasBid: true,
bidEcpm: creative.bidEcpm,
creative,
latencyMs: Date.now() - startTime + 60,
}
}
async reportImpression(adId: string): Promise<void> {}
async reportClick(adId: string): Promise<void> {}
async reportRewardCompletion(adId: string): Promise<{ success: boolean; tokenReward: number }> {
return { success: true, tokenReward: 50 }
export class AppLovinAdapter extends UnavailableAdAdapter {
constructor() {
super(
'applovin_max',
'AppLovin MAX',
['rewarded_video', 'banner_dock', 'export_sponsor'],
5.5,
)
}
}

View file

@ -1,61 +1,7 @@
// apps/desktop/src/main/services/ads/CarbonAdsAdapter.ts
// BuySellAds / Carbon Ads Curated Tech Single-Unit Adapter
import { UnavailableAdAdapter } from './UnavailableAdAdapter'
import type {
AdNetworkId,
AdFormat,
AdMediationAuctionRequest,
AdCreativePayload,
} from '@d3ro/core/types'
import type { IAdNetworkAdapter, AdBidResponse } from './BaseAdAdapter'
export class CarbonAdsAdapter implements IAdNetworkAdapter {
readonly networkId: AdNetworkId = 'carbon_ads'
readonly networkName = 'Carbon Ads (BuySellAds Tech Network)'
readonly supportedFormats: AdFormat[] = ['banner_dock', 'sidebar_sponsor_card' as any]
readonly defaultFloorEcpm = 3.5
private placement = 'd3rovoice'
async init(config?: { placement?: string }): Promise<void> {
if (config?.placement) this.placement = config.placement
}
async requestBid(request: AdMediationAuctionRequest): Promise<AdBidResponse> {
const startTime = Date.now()
if (!this.supportedFormats.includes(request.format)) {
return { hasBid: false, bidEcpm: 0, latencyMs: 5 }
}
const ecpm = 3.8 + Math.random() * 2.2 // $3.80 - $6.00 eCPM
const creative: AdCreativePayload = {
id: `carbon_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
networkId: this.networkId,
networkName: this.networkName,
title: 'Linear — The issue tracking tool you will actually love',
description: 'Streamline software projects, sprints, tasks, and bug tracking at high speed.',
ctaText: 'Try Linear',
iconUrl: 'https://cdn.carbonads.com/carbon_linear_logo.png',
clickUrl: 'https://linear.app?ref=carbon',
sponsorTag: 'Carbon Ads',
advertiserName: 'Linear',
bidEcpm: parseFloat(ecpm.toFixed(2)),
format: request.format,
}
return {
hasBid: true,
bidEcpm: creative.bidEcpm,
creative,
latencyMs: Date.now() - startTime + 52,
}
}
async reportImpression(adId: string): Promise<void> {
// Carbon impression beacon
}
async reportClick(adId: string): Promise<void> {
// Carbon click beacon
export class CarbonAdsAdapter extends UnavailableAdAdapter {
constructor() {
super('carbon_ads', 'Carbon Ads', ['banner_dock'], 3.5)
}
}

View file

@ -1,113 +1,12 @@
// apps/desktop/src/main/services/ads/DirectHouseSponsorAdapter.ts
// Direct House Sponsor Engine (Highest margin, premium AI/developer partnerships)
import { UnavailableAdAdapter } from './UnavailableAdAdapter'
import type {
AdNetworkId,
AdFormat,
AdMediationAuctionRequest,
AdCreativePayload,
} from '@d3ro/core/types'
import type { IAdNetworkAdapter, AdBidResponse } from './BaseAdAdapter'
interface HouseSponsorCreative {
title: string
description: string
ctaText: string
clickUrl: string
sponsorTag: string
advertiserName: string
bidEcpm: number
format: AdFormat
iconUrl?: string
}
export class DirectHouseSponsorAdapter implements IAdNetworkAdapter {
readonly networkId: AdNetworkId = 'direct_sponsor'
readonly networkName = 'Direct House Sponsor Engine (100% Margin)'
readonly supportedFormats: AdFormat[] = ['banner_dock', 'rewarded_video', 'export_sponsor']
readonly defaultFloorEcpm = 12.0
private sponsors: HouseSponsorCreative[] = [
{
title: 'Cursor AI — Next-Gen AI Code Editor',
description: 'Build software with intelligent voice agents & lightning-speed code search.',
ctaText: 'Learn More',
clickUrl: 'https://cursor.com',
sponsorTag: 'Direct Partner',
advertiserName: 'Cursor AI',
bidEcpm: 15.5,
format: 'banner_dock',
},
{
title: 'ElevenLabs — Human-like Voice AI & Speech Synthesis',
description: 'Industry-leading emotional AI voices for creators, developers, and games.',
ctaText: 'Try Voice AI',
clickUrl: 'https://elevenlabs.io',
sponsorTag: 'Direct Partner',
advertiserName: 'ElevenLabs',
bidEcpm: 18.0,
format: 'rewarded_video',
},
{
title: 'Perplexity Pro — Where Knowledge Begins',
description: 'Instant answers with citations, source tracking, and multi-model research.',
ctaText: 'Try Perplexity',
clickUrl: 'https://perplexity.ai',
sponsorTag: 'Direct Partner',
advertiserName: 'Perplexity AI',
bidEcpm: 14.2,
format: 'banner_dock',
},
{
title: 'Notion AI — Connected Workspace for Documents & Notes',
description: 'Summarize meeting audio, manage tasks, and organize thoughts in one canvas.',
ctaText: 'Get Notion Free',
clickUrl: 'https://notion.so',
sponsorTag: 'Direct Partner',
advertiserName: 'Notion Labs',
bidEcpm: 13.5,
format: 'export_sponsor',
},
]
async init(): Promise<void> {}
async requestBid(request: AdMediationAuctionRequest): Promise<AdBidResponse> {
const startTime = Date.now()
const matching = this.sponsors.filter((s) => s.format === request.format)
if (matching.length === 0) {
return { hasBid: false, bidEcpm: 0, latencyMs: 2 }
}
// Pick rotating sponsor
const picked = matching[Math.floor(Math.random() * matching.length)]
const creative: AdCreativePayload = {
id: `house_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
networkId: this.networkId,
networkName: this.networkName,
title: picked.title,
description: picked.description,
ctaText: picked.ctaText,
clickUrl: picked.clickUrl,
sponsorTag: picked.sponsorTag,
advertiserName: picked.advertiserName,
bidEcpm: picked.bidEcpm,
format: picked.format,
rewardTokens: picked.format === 'rewarded_video' ? 50 : undefined,
durationSeconds: picked.format === 'rewarded_video' ? 15 : undefined,
}
return {
hasBid: true,
bidEcpm: creative.bidEcpm,
creative,
latencyMs: Date.now() - startTime + 8, // Near zero latency
}
}
async reportImpression(adId: string): Promise<void> {}
async reportClick(adId: string): Promise<void> {}
async reportRewardCompletion(adId: string): Promise<{ success: boolean; tokenReward: number }> {
return { success: true, tokenReward: 50 }
export class DirectHouseSponsorAdapter extends UnavailableAdAdapter {
constructor() {
super(
'direct_sponsor',
'Direct House Sponsor',
['banner_dock', 'rewarded_video', 'export_sponsor'],
12,
)
}
}

View file

@ -1,71 +1,7 @@
// apps/desktop/src/main/services/ads/EthicalAdsAdapter.ts
// Privacy-First Developer Native Ad Network Adapter (REST Decision API /api/v1/decision/)
import { UnavailableAdAdapter } from './UnavailableAdAdapter'
import type {
AdNetworkId,
AdFormat,
AdMediationAuctionRequest,
AdCreativePayload,
} from '@d3ro/core/types'
import type { IAdNetworkAdapter, AdBidResponse } from './BaseAdAdapter'
export class EthicalAdsAdapter implements IAdNetworkAdapter {
readonly networkId: AdNetworkId = 'ethical_ads'
readonly networkName = 'EthicalAds (Privacy-First Dev Network)'
readonly supportedFormats: AdFormat[] = ['banner_dock', 'export_sponsor']
readonly defaultFloorEcpm = 3.2
private publisherId = 'd3ro-voice'
async init(config?: { publisherId?: string }): Promise<void> {
if (config?.publisherId) this.publisherId = config.publisherId
}
async requestBid(request: AdMediationAuctionRequest): Promise<AdBidResponse> {
const startTime = Date.now()
if (!this.supportedFormats.includes(request.format)) {
return { hasBid: false, bidEcpm: 0, latencyMs: 5 }
}
try {
// EthicalAds developer ads simulation & real JSON endpoint fallback
const ecpm = 3.2 + Math.random() * 1.5 // $3.20 - $4.70 eCPM
const creative: AdCreativePayload = {
id: `ea_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
networkId: this.networkId,
networkName: this.networkName,
title: 'MongoDB Atlas — The Multi-Cloud Developer Data Platform',
description: 'Build fast with automated scaling, vector search, and global clusters.',
ctaText: 'Deploy Free',
iconUrl: 'https://media.ethicalads.io/media/images/2024/02/mongodb_icon.png',
clickUrl: 'https://www.mongodb.com/cloud/atlas/register?utm_source=ethicalads',
sponsorTag: 'EthicalAd • Privacy Verified',
advertiserName: 'MongoDB',
bidEcpm: parseFloat(ecpm.toFixed(2)),
format: request.format,
}
return {
hasBid: true,
bidEcpm: creative.bidEcpm,
creative,
latencyMs: Date.now() - startTime + 45,
}
} catch (err) {
return {
hasBid: false,
bidEcpm: 0,
latencyMs: Date.now() - startTime,
error: err instanceof Error ? err.message : String(err),
}
}
}
async reportImpression(adId: string): Promise<void> {
// console.log(`[EthicalAds] Impression recorded for ${adId}`)
}
async reportClick(adId: string): Promise<void> {
// console.log(`[EthicalAds] Click recorded for ${adId}`)
export class EthicalAdsAdapter extends UnavailableAdAdapter {
constructor() {
super('ethical_ads', 'EthicalAds', ['banner_dock', 'export_sponsor'], 3.2)
}
}

View file

@ -1,59 +1,12 @@
// apps/desktop/src/main/services/ads/GoogleAdManagerAdapter.ts
// Google Ad Manager 360 / AdMob Universal Global Demand Adapter
import { UnavailableAdAdapter } from './UnavailableAdAdapter'
import type {
AdNetworkId,
AdFormat,
AdMediationAuctionRequest,
AdCreativePayload,
} from '@d3ro/core/types'
import type { IAdNetworkAdapter, AdBidResponse } from './BaseAdAdapter'
export class GoogleAdManagerAdapter implements IAdNetworkAdapter {
readonly networkId: AdNetworkId = 'google_ad_manager'
readonly networkName = 'Google Ad Manager 360 (Global Demand)'
readonly supportedFormats: AdFormat[] = ['banner_dock', 'rewarded_video', 'export_sponsor']
readonly defaultFloorEcpm = 2.0
async init(): Promise<void> {}
async requestBid(request: AdMediationAuctionRequest): Promise<AdBidResponse> {
const startTime = Date.now()
if (!this.supportedFormats.includes(request.format)) {
return { hasBid: false, bidEcpm: 0, latencyMs: 5 }
}
// High 99%+ fill rate, stable eCPM
const baseEcpm = request.format === 'rewarded_video' ? 7.2 : 3.0
const ecpm = baseEcpm + Math.random() * 2.0
const creative: AdCreativePayload = {
id: `gam_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
networkId: this.networkId,
networkName: this.networkName,
title: 'Google Cloud Vertex AI — Build & Scale Generative AI Apps',
description: 'Access Gemini 1.5 Pro, customized embeddings, and enterprise search.',
ctaText: 'Explore Cloud',
clickUrl: 'https://cloud.google.com/vertex-ai',
sponsorTag: 'Google Ad Manager',
advertiserName: 'Google Cloud',
bidEcpm: parseFloat(ecpm.toFixed(2)),
format: request.format,
rewardTokens: request.format === 'rewarded_video' ? 50 : undefined,
durationSeconds: 15,
}
return {
hasBid: true,
bidEcpm: creative.bidEcpm,
creative,
latencyMs: Date.now() - startTime + 40,
}
}
async reportImpression(adId: string): Promise<void> {}
async reportClick(adId: string): Promise<void> {}
async reportRewardCompletion(adId: string): Promise<{ success: boolean; tokenReward: number }> {
return { success: true, tokenReward: 50 }
export class GoogleAdManagerAdapter extends UnavailableAdAdapter {
constructor() {
super(
'google_ad_manager',
'Google Ad Manager',
['banner_dock', 'rewarded_video', 'export_sponsor'],
2,
)
}
}

View file

@ -1,58 +1,7 @@
// apps/desktop/src/main/services/ads/InMobiAdapter.ts
// InMobi Programmatic Demand & Mobile/Hybrid Adapter
import { UnavailableAdAdapter } from './UnavailableAdAdapter'
import type {
AdNetworkId,
AdFormat,
AdMediationAuctionRequest,
AdCreativePayload,
} from '@d3ro/core/types'
import type { IAdNetworkAdapter, AdBidResponse } from './BaseAdAdapter'
export class InMobiAdapter implements IAdNetworkAdapter {
readonly networkId: AdNetworkId = 'inmobi'
readonly networkName = 'InMobi (Programmatic Exchange)'
readonly supportedFormats: AdFormat[] = ['banner_dock', 'rewarded_video']
readonly defaultFloorEcpm = 2.8
async init(): Promise<void> {}
async requestBid(request: AdMediationAuctionRequest): Promise<AdBidResponse> {
const startTime = Date.now()
if (!this.supportedFormats.includes(request.format)) {
return { hasBid: false, bidEcpm: 0, latencyMs: 5 }
}
const baseEcpm = request.format === 'rewarded_video' ? 6.8 : 3.4
const ecpm = baseEcpm + Math.random() * 2.2
const creative: AdCreativePayload = {
id: `inmobi_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
networkId: this.networkId,
networkName: this.networkName,
title: 'NordVPN — Secure Your Data with Next-Gen Encryption',
description: 'Ultra-fast VPN protection across all your desktop and mobile devices.',
ctaText: 'Get 70% Off',
clickUrl: 'https://nordvpn.com?utm_source=inmobi',
sponsorTag: 'InMobi Exchange',
advertiserName: 'Nord Security',
bidEcpm: parseFloat(ecpm.toFixed(2)),
format: request.format,
rewardTokens: 50,
durationSeconds: 15,
}
return {
hasBid: true,
bidEcpm: creative.bidEcpm,
creative,
latencyMs: Date.now() - startTime + 50,
}
}
async reportImpression(adId: string): Promise<void> {}
async reportClick(adId: string): Promise<void> {}
async reportRewardCompletion(adId: string): Promise<{ success: boolean; tokenReward: number }> {
return { success: true, tokenReward: 50 }
export class InMobiAdapter extends UnavailableAdAdapter {
constructor() {
super('inmobi', 'InMobi', ['banner_dock', 'rewarded_video'], 2.8)
}
}

View file

@ -1,58 +1,7 @@
// apps/desktop/src/main/services/ads/MintegralAdapter.ts
// Mintegral Global / APAC Rewarded Video & Interstitial Adapter
import { UnavailableAdAdapter } from './UnavailableAdAdapter'
import type {
AdNetworkId,
AdFormat,
AdMediationAuctionRequest,
AdCreativePayload,
} from '@d3ro/core/types'
import type { IAdNetworkAdapter, AdBidResponse } from './BaseAdAdapter'
export class MintegralAdapter implements IAdNetworkAdapter {
readonly networkId: AdNetworkId = 'mintegral'
readonly networkName = 'Mintegral (APAC & Global Video Network)'
readonly supportedFormats: AdFormat[] = ['rewarded_video', 'banner_dock']
readonly defaultFloorEcpm = 4.0
async init(): Promise<void> {}
async requestBid(request: AdMediationAuctionRequest): Promise<AdBidResponse> {
const startTime = Date.now()
if (!this.supportedFormats.includes(request.format)) {
return { hasBid: false, bidEcpm: 0, latencyMs: 5 }
}
const baseEcpm = request.format === 'rewarded_video' ? 8.8 : 3.8
const ecpm = baseEcpm + Math.random() * 3.2
const creative: AdCreativePayload = {
id: `mintegral_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
networkId: this.networkId,
networkName: this.networkName,
title: 'Canva Pro — Design Anything with Team Collaboration',
description: 'Create presentations, graphics, and video with easy AI magic tools.',
ctaText: 'Try Canva Free',
clickUrl: 'https://canva.com?ref=mintegral',
sponsorTag: 'Mintegral Video',
advertiserName: 'Canva',
bidEcpm: parseFloat(ecpm.toFixed(2)),
format: request.format,
rewardTokens: 50,
durationSeconds: 15,
}
return {
hasBid: true,
bidEcpm: creative.bidEcpm,
creative,
latencyMs: Date.now() - startTime + 52,
}
}
async reportImpression(adId: string): Promise<void> {}
async reportClick(adId: string): Promise<void> {}
async reportRewardCompletion(adId: string): Promise<{ success: boolean; tokenReward: number }> {
return { success: true, tokenReward: 50 }
export class MintegralAdapter extends UnavailableAdAdapter {
constructor() {
super('mintegral', 'Mintegral', ['rewarded_video', 'banner_dock'], 4)
}
}

View file

@ -1,61 +1,12 @@
// apps/desktop/src/main/services/ads/PlaywireAdapter.ts
// Playwire Desktop Application Programmatic Header Bidding Adapter
import { UnavailableAdAdapter } from './UnavailableAdAdapter'
import type {
AdNetworkId,
AdFormat,
AdMediationAuctionRequest,
AdCreativePayload,
} from '@d3ro/core/types'
import type { IAdNetworkAdapter, AdBidResponse } from './BaseAdAdapter'
export class PlaywireAdapter implements IAdNetworkAdapter {
readonly networkId: AdNetworkId = 'playwire'
readonly networkName = 'Playwire RAMP (Desktop Header Bidding)'
readonly supportedFormats: AdFormat[] = ['banner_dock', 'rewarded_video', 'export_sponsor']
readonly defaultFloorEcpm = 4.5
async init(): Promise<void> {
// Initialize Playwire RAMP desktop runtime
}
async requestBid(request: AdMediationAuctionRequest): Promise<AdBidResponse> {
const startTime = Date.now()
if (!this.supportedFormats.includes(request.format)) {
return { hasBid: false, bidEcpm: 0, latencyMs: 5 }
}
// Playwire high-tier programmatic bidding: $4.50 - $11.00 eCPM
const baseEcpm = request.format === 'rewarded_video' ? 8.5 : 4.8
const ecpm = baseEcpm + Math.random() * 3.5
const creative: AdCreativePayload = {
id: `playwire_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
networkId: this.networkId,
networkName: this.networkName,
title: 'AWS Cloud — Scalable AI & Machine Learning Infrastructure',
description: 'Train models and deploy high-performance applications on AWS Bedrock.',
ctaText: 'Start Free Trial',
clickUrl: 'https://aws.amazon.com/free/?utm_source=playwire',
sponsorTag: 'Playwire Programmatic',
advertiserName: 'Amazon Web Services',
bidEcpm: parseFloat(ecpm.toFixed(2)),
format: request.format,
rewardTokens: request.format === 'rewarded_video' ? 50 : undefined,
durationSeconds: request.format === 'rewarded_video' ? 15 : undefined,
}
return {
hasBid: true,
bidEcpm: creative.bidEcpm,
creative,
latencyMs: Date.now() - startTime + 68,
}
}
async reportImpression(adId: string): Promise<void> {}
async reportClick(adId: string): Promise<void> {}
async reportRewardCompletion(adId: string): Promise<{ success: boolean; tokenReward: number }> {
return { success: true, tokenReward: 50 }
export class PlaywireAdapter extends UnavailableAdAdapter {
constructor() {
super(
'playwire',
'Playwire RAMP',
['banner_dock', 'rewarded_video', 'export_sponsor'],
4.5,
)
}
}

View file

@ -1,52 +1,7 @@
// apps/desktop/src/main/services/ads/PubMaticAdapter.ts
// PubMatic OpenWrap Header Bidding SSP Adapter
import { UnavailableAdAdapter } from './UnavailableAdAdapter'
import type {
AdNetworkId,
AdFormat,
AdMediationAuctionRequest,
AdCreativePayload,
} from '@d3ro/core/types'
import type { IAdNetworkAdapter, AdBidResponse } from './BaseAdAdapter'
export class PubMaticAdapter implements IAdNetworkAdapter {
readonly networkId: AdNetworkId = 'pubmatic'
readonly networkName = 'PubMatic OpenWrap (Enterprise SSP)'
readonly supportedFormats: AdFormat[] = ['banner_dock', 'export_sponsor']
readonly defaultFloorEcpm = 3.0
async init(): Promise<void> {}
async requestBid(request: AdMediationAuctionRequest): Promise<AdBidResponse> {
const startTime = Date.now()
if (!this.supportedFormats.includes(request.format)) {
return { hasBid: false, bidEcpm: 0, latencyMs: 5 }
}
const ecpm = 3.6 + Math.random() * 2.5
const creative: AdCreativePayload = {
id: `pubmatic_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
networkId: this.networkId,
networkName: this.networkName,
title: 'Datadog — Cloud Monitoring, APM & Security in One Platform',
description: 'See metrics, traces, and logs from your entire technology stack.',
ctaText: 'Start Monitoring',
clickUrl: 'https://datadoghq.com?utm_source=pubmatic',
sponsorTag: 'PubMatic OpenWrap',
advertiserName: 'Datadog',
bidEcpm: parseFloat(ecpm.toFixed(2)),
format: request.format,
}
return {
hasBid: true,
bidEcpm: creative.bidEcpm,
creative,
latencyMs: Date.now() - startTime + 58,
}
export class PubMaticAdapter extends UnavailableAdAdapter {
constructor() {
super('pubmatic', 'PubMatic OpenWrap', ['banner_dock', 'export_sponsor'], 3)
}
async reportImpression(adId: string): Promise<void> {}
async reportClick(adId: string): Promise<void> {}
}

View file

@ -0,0 +1,37 @@
import type { AdFormat, AdMediationAuctionRequest, AdNetworkId } from '@d3ro/core/types'
import type { AdBidResponse, IAdNetworkAdapter } from './BaseAdAdapter'
/**
* Fail-closed boundary for providers whose official desktop SDK or
* authenticated decision endpoint is not integrated. Demo creatives are not
* ads, so this adapter deliberately returns no bid and never grants rewards.
*/
export class UnavailableAdAdapter implements IAdNetworkAdapter {
constructor(
readonly networkId: AdNetworkId,
readonly networkName: string,
readonly supportedFormats: AdFormat[],
readonly defaultFloorEcpm: number,
) {}
async init(): Promise<void> {}
async requestBid(_request: AdMediationAuctionRequest): Promise<AdBidResponse> {
return {
hasBid: false,
bidEcpm: 0,
latencyMs: 0,
error: 'provider_not_integrated',
}
}
async reportImpression(_adId: string): Promise<void> {}
async reportClick(_adId: string): Promise<void> {}
async reportRewardCompletion(
_adId: string,
): Promise<{ success: boolean; tokenReward: number }> {
return { success: false, tokenReward: 0 }
}
}

View file

@ -1,58 +1,7 @@
// apps/desktop/src/main/services/ads/UnityAdsAdapter.ts
// Unity Ads / Unity LevelPlay Rewarded Video Adapter
import { UnavailableAdAdapter } from './UnavailableAdAdapter'
import type {
AdNetworkId,
AdFormat,
AdMediationAuctionRequest,
AdCreativePayload,
} from '@d3ro/core/types'
import type { IAdNetworkAdapter, AdBidResponse } from './BaseAdAdapter'
export class UnityAdsAdapter implements IAdNetworkAdapter {
readonly networkId: AdNetworkId = 'unity_ads'
readonly networkName = 'Unity LevelPlay (Rewarded Video & Bidding)'
readonly supportedFormats: AdFormat[] = ['rewarded_video', 'banner_dock']
readonly defaultFloorEcpm = 6.0
async init(): Promise<void> {}
async requestBid(request: AdMediationAuctionRequest): Promise<AdBidResponse> {
const startTime = Date.now()
if (!this.supportedFormats.includes(request.format)) {
return { hasBid: false, bidEcpm: 0, latencyMs: 5 }
}
const baseEcpm = request.format === 'rewarded_video' ? 10.2 : 4.0
const ecpm = baseEcpm + Math.random() * 4.0 // High yield rewarded video
const creative: AdCreativePayload = {
id: `unity_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
networkId: this.networkId,
networkName: this.networkName,
title: 'Unity Engine — Create & Grow Real-Time 3D Experiences',
description: 'The industry-standard game engine for multi-platform interactive applications.',
ctaText: 'Download Unity',
clickUrl: 'https://unity.com/download',
sponsorTag: 'Unity Ads',
advertiserName: 'Unity Technologies',
bidEcpm: parseFloat(ecpm.toFixed(2)),
format: request.format,
rewardTokens: 50,
durationSeconds: 15,
}
return {
hasBid: true,
bidEcpm: creative.bidEcpm,
creative,
latencyMs: Date.now() - startTime + 55,
}
}
async reportImpression(adId: string): Promise<void> {}
async reportClick(adId: string): Promise<void> {}
async reportRewardCompletion(adId: string): Promise<{ success: boolean; tokenReward: number }> {
return { success: true, tokenReward: 50 }
export class UnityAdsAdapter extends UnavailableAdAdapter {
constructor() {
super('unity_ads', 'Unity LevelPlay', ['rewarded_video', 'banner_dock'], 6)
}
}