flio.net Open ↗ Has Plan
57
DOM 73 PLN 100 REV 10 EAS 30
CLAUDE.md ×
PLAN.md ×
PLAN_V2.md ×
RESEARCH.md ×
plan-domain.md ×
plan.md
PREVIEW

FLIO.NET V2 — Triple Network Platform

Editorial Link Exchange + Ad Network + Affiliate / Lead Network 100% Cloudflare. Indonesia-first, global-ready. Last updated: 2026-05-04


Grand Vision

flio.net adalah satu-satunya platform di Indonesia yang menyatukan tiga jaringan dalam satu inventory:

  1. Editorial Link Exchange (existing — Phase 1 live) AI-matched cross-site backlinks. Free for everyone. Network effect untuk akuisisi publisher.
  2. Ad Network (new — alternatif AdSense) Self-serve display + native ads. CPM/CPC bidding. Edge-served <50ms global.
  3. Affiliate / Lead Network (new — alternatif CJ.com / Involve Asia) Pay-per-lead, pay-per-sale, deeplinks, smart links. S2S postback tracking.

Differensiasi inti:

  • 🇮🇩 Payout DANA/OVO/GoPay/QRIS mulai Rp50K (vs AdSense $100 wire)
  • 🤖 WhatsApp-native onboarding untuk advertiser & publisher (japri-wa-relay)
  • 💸 PPh 23 otomatis (publisher ber-NPWP "terima bersih", patuh pajak tanpa pusing)
  • 🎯 Hyperlocal targeting (per kabupaten/kota via request.cf.city)
  • 100% Cloudflare — edge latency <50ms, biaya operasional 1/10x kompetitor lokal

Status Saat Ini (Phase 1)

  • ✅ CF Worker flio-link-exchange di flio.net
  • ✅ D1 link-exchange-db (e0cb423a-43f6-4a8a-a0f0-19e10bfff954) — sites/pages/link_log/crawl_log
  • ✅ Workers AI BGE-small-en-v1.5 (384-dim embeddings)
  • ✅ 24 situs registered, 1,654 halaman ter-index
  • ✅ Cron 6 jam re-crawl semua sitemap
  • ✅ Public APIs: /api/register, /api/links, /api/dashboard, /api/stats

Bootstrap Phase 2 — Day 1 Commands

Urutan eksekusi untuk start Phase 2 (jalankan dari /home/ucok/web/flio.net/):

source /home/ucok/.env.full
export CLOUDFLARE_API_TOKEN=$CF_DEPLOY_TOKEN
export CLOUDFLARE_ACCOUNT_ID=f5dbc4f1c3e4f3fbdacb83107b8088da   # WAJIB, scoped token tidak bisa list

# 1. Buat D1 baru untuk platform (ads + affiliate)
npx wrangler d1 create flio-platform-db
# Catat database_id → tambahkan ke wrangler.toml

# 2. Buat KV namespaces
npx wrangler kv namespace create flio-cache       # ad selection cache 5min
npx wrangler kv namespace create flio-freqcap     # user×campaign 24h
npx wrangler kv namespace create flio-ratelimit   # publisher API throttle
npx wrangler kv namespace create flio-config      # advertiser/publisher settings

# 3. Buat Queue
npx wrangler queues create ad-events

# 4. Buat R2 buckets
npx wrangler r2 bucket create flio-creatives
npx wrangler r2 bucket create flio-receipts

# 5. Buat Vectorize index untuk ad creatives
npx wrangler vectorize create flio-ads-vectors --dimensions=384 --metric=cosine

# 6. Set secrets
echo "$XENDIT_SECRET_KEY" | npx wrangler secret put XENDIT_SECRET_KEY
echo "$STRIPE_SECRET_KEY" | npx wrangler secret put STRIPE_SECRET_KEY
echo "$TURNSTILE_SECRET" | npx wrangler secret put TURNSTILE_SECRET
echo "$ADMIN_KEY"        | npx wrangler secret put ADMIN_KEY

# 7. Apply schema migration
npx wrangler d1 execute flio-platform-db --file=migrations/001_initial.sql

# 8. Deploy Phase 2 worker (ad serving)
npx wrangler deploy

wrangler.toml template untuk Phase 2

name = "flio-link-exchange"
main = "src/worker.js"
compatibility_date = "2026-04-01"
compatibility_flags = ["nodejs_compat"]

routes = [
  { pattern = "flio.net/*", zone_name = "flio.net" }
]

# === EXISTING ===
[[d1_databases]]
binding = "DB_LINKS"
database_name = "link-exchange-db"
database_id = "e0cb423a-43f6-4a8a-a0f0-19e10bfff954"

# === NEW Phase 2 ===
[[d1_databases]]
binding = "DB"                           # convention: ads/affiliate hot path
database_name = "flio-platform-db"
database_id = "TBD-after-create"

[[kv_namespaces]]
binding = "AD_CACHE"
id = "TBD"

[[kv_namespaces]]
binding = "FREQ_CAP"
id = "TBD"

[[kv_namespaces]]
binding = "RATE_LIMIT"
id = "TBD"

[[kv_namespaces]]
binding = "CONFIG"
id = "TBD"

[[queues.producers]]
binding = "AD_EVENTS_Q"
queue = "ad-events"

[[queues.consumers]]
queue = "ad-events"
max_batch_size = 100
max_batch_timeout = 5

[[r2_buckets]]
binding = "CREATIVES"
bucket_name = "flio-creatives"

[[r2_buckets]]
binding = "RECEIPTS"
bucket_name = "flio-receipts"

[[durable_objects.bindings]]
name = "BUDGET_PACER"
class_name = "BudgetPacer"

[[migrations]]
tag = "v1"
new_classes = ["BudgetPacer"]

[[vectorize]]
binding = "VECTORIZE_ADS"
index_name = "flio-ads-vectors"

[ai]
binding = "AI"

[analytics_engine_datasets]
[[analytics_engine_datasets]]
binding = "AE"
dataset = "flio_ad_events"

[vars]
PLATFORM_VERSION = "2.0"
ENV = "production"

[triggers]
crons = [
  "0 */6 * * *",              # existing: sitemap re-crawl
  "0 0 * * *",                # daily: budget reset, fraud audit
  "0 1 * * 1",                # weekly Monday 01:00 UTC: payout batch
  "*/15 * * * *"              # 15min: pacing recompute, vector delta
]

Pasar Indonesia: Gaps yang Mau Diisi

Pain Point AdSense / CJ flio.net V2
Min payout $100 (~Rp1.6jt) Rp50.000
Payout method Wire transfer USD DANA, OVO, GoPay, QRIS, bank
Payout speed 30-60 hari Mingguan otomatis
Pajak publisher Manual W-8BEN PPh 23 otomatis (2%)
Approval rate ID ~30% 100% (auto-onboard)
Support Email only WhatsApp Business
Advertiser min spend $5/hari Rp 100K (QRIS)
Geo targeting Country only Per kabupaten/kota
Bahasa English Bahasa Indonesia + daerah

Kompetitor Lokal & Strategi Eksploitasi

Kompetitor Kelemahan Strategi flio
Involve Asia Validasi 60-90 hari Real-time S2S postback validation
Accesstrade UI jadul, fragmentasi negara Single global account, modern UX
Adskom Premium-only (DA>30) Auto-onboard semua publisher
Mitra Tokopedia Hanya ekosistem TKPD Network agnostik, semua merchant
AdStars Banner-only Multi-format (display+native+lead)

Hybrid Inventory Auction (Inti Filosofi)

Setiap slot widget di publisher menjalankan unified auction yang mengevaluasi tiga sumber:

slot.fill = argmax(
  editorial_link.value,    // editorial_score × free_or_credit
  ad_creative.eCPM,         // bid × CTR_predicted × 1000
  affiliate_offer.eCPM      // EPC × CR_predicted
)

Publisher punya 3 mode toggle:

  • Pure Editorial — gratis, max UX, no ads
  • Hybrid 50/50 — half editorial half paid (sweet spot)
  • Max Revenue — full paid auction, fallback ke editorial jika no fill

★ Insight ─────────────────────────────────────

  • Editorial sebagai gratis trojan horse: publisher daftar untuk dapat backlink, kemudian flip toggle untuk monetize. AdSense tidak punya gateway gratis seperti ini.
  • Vickrey second-price auction akan dipilih untuk pasar yang masih edukasi — advertiser jujur bid maksimum karena tahu cuma bayar harga runner-up + Rp1. Publisher juga lebih trust karena transparan. ─────────────────────────────────────────────────

Arsitektur 100% Cloudflare

                          ┌─────────────────────────┐
   Publisher embed JS ───►│   flio.net CF Worker    │
                          │   (Smart Placement)     │
                          └────┬───────────┬────────┘
                               │           │
                ┌──────────────▼─┐    ┌────▼─────────────┐
                │  Vectorize      │    │  D1 link-exchange-db │
                │  (1536-dim BGE) │    │  + flio-ads-db        │
                │  semantic match │    │  + flio-affiliate-db  │
                └────┬────────────┘    └────┬─────────────┘
                     │                       │
                     ▼                       ▼
              ┌────────────────┐     ┌──────────────────┐
              │ Workers AI     │     │ Durable Objects  │
              │ BGE / Llama-3  │     │ - BudgetPacer/CMP│
              │ Llama-Guard    │     │ - FreqCap/User   │
              │ Flux (creative)│     │ - LiveStats(WS)  │
              └────────────────┘     └──────┬───────────┘
                                            │
                          ┌─────────────────▼──────────────┐
                          │  Queue: ad-events              │
                          │  ↓                             │
                          │  Consumer Worker batch insert  │
                          │  → D1 (billing, agg)           │
                          │  → Analytics Engine (dashboard)│
                          └────────────────────────────────┘

   R2 Buckets:
     - flio-creatives  (advertiser uploads, OG images)
     - flio-receipts   (PDF invoices, payout proofs)

   KV Namespaces:
     - flio-cache      (ad selection cache 5min)
     - flio-freqcap    (user×campaign 24h TTL)
     - flio-ratelimit  (publisher API throttle)
     - flio-config     (advertiser/publisher settings)

   Cron Triggers:
     - */6h:  re-crawl sitemaps  (existing)
     - 0 0:   daily budget reset, fraud audit
     - 0 1 * * 1: weekly publisher payout (Senin 01:00 WIB)
     - */15m: pacing recompute, vector re-index delta

Database Schema

Strategi: Start dengan 2 D1, scale ke 3 saat butuh

DB Status Purpose Migration trigger
link-exchange-db ✅ Existing — keep Editorial network: sites, pages (BGE), link_log, crawl_log Tidak migrate
flio-platform-db 🆕 Phase 2 Ads + Affiliate gabung (advertisers, campaigns, creatives, units, impressions sharded, clicks, conversions, payouts) Saat impressions table >5GB (~Y2)
flio-events-db 🚀 Future split Pisah ad_impressions_YYYYMM dari hot path Saat platform-db hit 8GB

Rasional: D1 limit 10GB. Ad+affiliate digabung di Phase 2 untuk simplify joins. Sharding ad_impressions_YYYYMM per bulan (drop tabel lama → R2 archive). Promote ke flio-events-db saat butuh isolation.

Pricing Convention (KANONIK)

Semua kolom harga IDR pakai _micros INTEGER (10⁻⁶ IDR) — hindari floating-point error, support sub-Rupiah bidding.

Rp 5.000 → 5_000_000_000 micros
Rp 200   → 200_000_000 micros
Rp 0.5   → 500_000 micros (mungkin dipakai untuk negotiated programmatic)

Konversi ke IDR untuk Xendit/display: Math.floor(micros / 1_000_000). Skema body section di bawah (lines 154-405) akan direvisi pakai _micros saat implementasi (lihat Appendix A.1).

Note: Schema lama di sub-section "1. flio-ads-db (NEW)" dan "2. flio-affiliate-db (NEW)" di bawah ditulis dengan kolom _idr untuk readability dokumentasi. Implementasi final pakai _micros (Codex pattern). Cross-table FK dari ads ke affiliate digabung dalam satu DB flio-platform-db.

1. link-exchange-db (existing — keep as-is)

  • sites, pages (BGE embeddings), link_log, crawl_log

2. flio-platform-db (NEW — gabungan ads + affiliate)

-- Pengiklan
CREATE TABLE advertisers (
  id INTEGER PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  whatsapp TEXT,
  company_name TEXT,
  npwp TEXT,
  country TEXT DEFAULT 'ID',
  api_key TEXT UNIQUE NOT NULL,
  balance_idr INTEGER DEFAULT 0,        -- credit yang sudah top-up
  spent_total_idr INTEGER DEFAULT 0,
  status TEXT DEFAULT 'pending',         -- pending|verified|suspended
  payment_method TEXT,                   -- xendit|stripe|qris
  kyc_verified_at INTEGER,
  created_at INTEGER DEFAULT (unixepoch())
);
CREATE INDEX idx_advertisers_email ON advertisers(email);

-- Campaign
CREATE TABLE campaigns (
  id INTEGER PRIMARY KEY,
  advertiser_id INTEGER NOT NULL REFERENCES advertisers(id),
  name TEXT NOT NULL,
  type TEXT NOT NULL,                    -- display|native|lead|sale
  bid_model TEXT NOT NULL,               -- cpm|cpc|cpl|cps
  bid_idr INTEGER NOT NULL,              -- Rp per impression/click/lead
  budget_total_idr INTEGER NOT NULL,
  budget_daily_idr INTEGER,
  spent_idr INTEGER DEFAULT 0,
  spent_today_idr INTEGER DEFAULT 0,
  pacing TEXT DEFAULT 'standard',        -- asap|standard|even
  status TEXT DEFAULT 'paused',          -- paused|active|completed|rejected
  -- Targeting (JSON for flexibility)
  geo_targets TEXT,                      -- ["ID-JK", "ID-JB"] (province) or ["Jakarta","Bandung"]
  device_targets TEXT,                   -- ["mobile","desktop","tablet"]
  vertical_targets TEXT,                 -- ["health","food"]
  language_targets TEXT,                 -- ["id","en"]
  exclude_sites TEXT,                    -- ["competitor.com"]
  brand_safety_level INTEGER DEFAULT 2,  -- 1=high (family-safe), 2=med, 3=low
  start_at INTEGER,
  end_at INTEGER,
  created_at INTEGER DEFAULT (unixepoch())
);
CREATE INDEX idx_campaigns_status_active ON campaigns(status) WHERE status='active';
CREATE INDEX idx_campaigns_advertiser ON campaigns(advertiser_id);

-- Creative (1 campaign bisa banyak creative untuk A/B)
CREATE TABLE ad_creatives (
  id INTEGER PRIMARY KEY,
  campaign_id INTEGER NOT NULL REFERENCES campaigns(id),
  format TEXT NOT NULL,                  -- banner-300x250|native-card|text-link|video
  headline TEXT NOT NULL,
  body TEXT,
  image_r2_key TEXT,                     -- R2 key
  click_url TEXT NOT NULL,
  -- Embedding for contextual match (BGE 384-dim)
  embedding BLOB,
  embedding_model TEXT DEFAULT 'bge-small-en-v1.5',
  -- AI-generated flag
  ai_generated INTEGER DEFAULT 0,
  llama_guard_score REAL,                -- 0-1, lower = safer
  status TEXT DEFAULT 'pending_review',  -- pending_review|approved|rejected
  rejection_reason TEXT,
  impressions INTEGER DEFAULT 0,
  clicks INTEGER DEFAULT 0,
  conversions INTEGER DEFAULT 0,
  created_at INTEGER DEFAULT (unixepoch())
);
CREATE INDEX idx_creatives_campaign ON ad_creatives(campaign_id);
CREATE INDEX idx_creatives_status ON ad_creatives(status);

-- Publisher ad units (slot di situs)
CREATE TABLE ad_units (
  id INTEGER PRIMARY KEY,
  site_id INTEGER NOT NULL,              -- FK ke link-exchange-db.sites (cross-D1, app-level join)
  name TEXT NOT NULL,                    -- "Sidebar 300x250", "In-article native"
  format TEXT NOT NULL,
  monetization_mode TEXT DEFAULT 'editorial', -- editorial|hybrid|max_revenue
  fallback_html TEXT,                    -- jika no-fill
  brand_safety_min INTEGER DEFAULT 2,
  vertical_blocklist TEXT,               -- JSON array
  embed_key TEXT UNIQUE NOT NULL,         -- digunakan di embed snippet
  enabled INTEGER DEFAULT 1,
  created_at INTEGER DEFAULT (unixepoch())
);

-- Impressions (sharded per BULAN — table per month)
-- Diciptakan oleh cron pada awal bulan
CREATE TABLE ad_impressions_2026_05 (
  id INTEGER PRIMARY KEY,
  ts INTEGER NOT NULL,
  unit_id INTEGER NOT NULL,
  creative_id INTEGER NOT NULL,
  campaign_id INTEGER NOT NULL,
  site_id INTEGER NOT NULL,
  user_hash TEXT,                        -- SHA256(IP+UA+date) for freq cap
  geo TEXT,                              -- "ID-JK-Jakarta Selatan"
  device TEXT,                           -- mobile|desktop|tablet
  bid_idr INTEGER NOT NULL,              -- price paid (winning auction)
  request_id TEXT NOT NULL,              -- idempotency
  fraud_score REAL DEFAULT 0,            -- 0-1
  is_billable INTEGER DEFAULT 1
);
CREATE INDEX idx_imp_2026_05_campaign ON ad_impressions_2026_05(campaign_id, ts);
CREATE INDEX idx_imp_2026_05_unit ON ad_impressions_2026_05(unit_id, ts);
CREATE INDEX idx_imp_2026_05_request ON ad_impressions_2026_05(request_id);

-- Clicks (per bulan juga)
CREATE TABLE ad_clicks_2026_05 (
  id INTEGER PRIMARY KEY,
  ts INTEGER NOT NULL,
  impression_id INTEGER,                  -- nullable jika click tanpa impression match
  creative_id INTEGER NOT NULL,
  campaign_id INTEGER NOT NULL,
  site_id INTEGER NOT NULL,
  user_hash TEXT,
  click_delay_ms INTEGER,                 -- ms dari impression ke click (fraud signal)
  bid_idr INTEGER NOT NULL,
  fraud_score REAL DEFAULT 0,
  is_billable INTEGER DEFAULT 1
);
CREATE INDEX idx_clk_2026_05_imp ON ad_clicks_2026_05(impression_id);

-- Daily aggregations (untuk dashboard cepat)
CREATE TABLE ad_daily_stats (
  date TEXT NOT NULL,                    -- '2026-05-04'
  campaign_id INTEGER NOT NULL,
  site_id INTEGER NOT NULL,
  impressions INTEGER DEFAULT 0,
  clicks INTEGER DEFAULT 0,
  spend_idr INTEGER DEFAULT 0,
  publisher_revenue_idr INTEGER DEFAULT 0,
  PRIMARY KEY (date, campaign_id, site_id)
);

3. Affiliate tables (digabung dalam flio-platform-db)

-- Merchant (advertiser yang offer komisi affiliate)
CREATE TABLE merchants (
  id INTEGER PRIMARY KEY,
  advertiser_id INTEGER NOT NULL,        -- shared with ads-db (cross-DB)
  brand_name TEXT NOT NULL,
  category TEXT NOT NULL,                -- ecommerce|finance|travel|edu
  domain TEXT NOT NULL,
  logo_r2_key TEXT,
  status TEXT DEFAULT 'pending',
  created_at INTEGER DEFAULT (unixepoch())
);

-- Offer (kontrak komisi)
CREATE TABLE affiliate_offers (
  id INTEGER PRIMARY KEY,
  merchant_id INTEGER NOT NULL REFERENCES merchants(id),
  name TEXT NOT NULL,
  category TEXT NOT NULL,
  commission_type TEXT NOT NULL,         -- cps|cpl|cpa|hybrid
  commission_value REAL NOT NULL,        -- % atau IDR fixed
  attribution_window_days INTEGER DEFAULT 7,
  cookie_window_days INTEGER DEFAULT 30,
  payout_delay_days INTEGER DEFAULT 30,  -- approval period
  geo_targets TEXT,
  description TEXT,
  terms_md TEXT,
  banner_pack_r2_prefix TEXT,            -- folder di R2 berisi banner
  deeplink_pattern TEXT,                  -- "https://merchant.com/p/{product}?ref={tid}"
  validation_postback_url TEXT,           -- merchant kirim validation event ke sini
  status TEXT DEFAULT 'active',
  created_at INTEGER DEFAULT (unixepoch())
);

-- Tracking link (smart link per publisher × offer)
CREATE TABLE tracking_links (
  id INTEGER PRIMARY KEY,
  short_code TEXT UNIQUE NOT NULL,       -- "fl-abc123" → flio.net/go/fl-abc123
  publisher_site_id INTEGER NOT NULL,
  offer_id INTEGER NOT NULL REFERENCES affiliate_offers(id),
  custom_param TEXT,                     -- publisher's sub-id
  click_count INTEGER DEFAULT 0,
  conversion_count INTEGER DEFAULT 0,
  created_at INTEGER DEFAULT (unixepoch())
);
CREATE INDEX idx_tracking_short ON tracking_links(short_code);
CREATE INDEX idx_tracking_publisher ON tracking_links(publisher_site_id);

-- Click event
CREATE TABLE affiliate_clicks (
  id INTEGER PRIMARY KEY,
  click_id TEXT UNIQUE NOT NULL,         -- nanoid, dipakai di postback
  tracking_link_id INTEGER NOT NULL REFERENCES tracking_links(id),
  ts INTEGER NOT NULL,
  user_hash TEXT,
  ip_country TEXT,
  user_agent TEXT,
  referrer TEXT,
  fraud_score REAL DEFAULT 0
);
CREATE INDEX idx_aff_clicks_id ON affiliate_clicks(click_id);
CREATE INDEX idx_aff_clicks_link_ts ON affiliate_clicks(tracking_link_id, ts);

-- Conversion (S2S postback dari merchant)
CREATE TABLE conversions (
  id INTEGER PRIMARY KEY,
  click_id TEXT NOT NULL,                -- match ke affiliate_clicks
  offer_id INTEGER NOT NULL,
  publisher_site_id INTEGER NOT NULL,
  merchant_id INTEGER NOT NULL,
  ts INTEGER NOT NULL,
  order_id TEXT,                          -- merchant's order ref
  order_value_idr INTEGER,
  commission_idr INTEGER NOT NULL,
  status TEXT DEFAULT 'pending',          -- pending|approved|rejected|paid
  approved_at INTEGER,
  rejection_reason TEXT,
  postback_payload TEXT,                  -- raw S2S payload audit trail
  signature_verified INTEGER DEFAULT 0
);
CREATE INDEX idx_conv_click ON conversions(click_id);
CREATE INDEX idx_conv_status_ts ON conversions(status, ts);

-- Payouts (untuk publisher dan untuk merchant invoicing)
CREATE TABLE payouts (
  id INTEGER PRIMARY KEY,
  party_type TEXT NOT NULL,               -- publisher|merchant
  party_id INTEGER NOT NULL,
  period_start INTEGER NOT NULL,
  period_end INTEGER NOT NULL,
  gross_idr INTEGER NOT NULL,
  pph23_deducted_idr INTEGER DEFAULT 0,
  platform_fee_idr INTEGER DEFAULT 0,    -- our take rate
  net_idr INTEGER NOT NULL,
  payment_method TEXT,                    -- xendit_dana|xendit_ovo|qris|stripe|bank
  payment_destination TEXT,                -- nomor DANA / rek bank
  external_ref TEXT,                       -- Xendit disbursement ID
  status TEXT DEFAULT 'pending',          -- pending|processing|paid|failed
  invoice_pdf_r2_key TEXT,
  created_at INTEGER DEFAULT (unixepoch()),
  paid_at INTEGER
);

★ Insight ─────────────────────────────────────

  • Mengapa 3 D1 terpisah? D1 punya batas 10GB per database. Dengan sharding ad_impressions per bulan, 1 bulan ~10jt impressions × 200 byte/row = 2GB. Plus growth = bisa hit 10GB dalam 6 bulan jika di satu DB. Pisah = aman + isolation untuk hot-path (ad serving) tidak terganggu maintenance affiliate.
  • Cross-D1 join app-level: ad_units.site_id reference ke sites di DB lain. Worker melakukan 2 query (D1 supports Promise.all). Latency overhead ~5ms total — masih dalam <50ms budget.
  • Sharded impressions per bulan: query historis hit table spesifik (lebih cepat), drop tabel lama = arsip mudah ke R2. ─────────────────────────────────────────────────

Ad Serving Flow (End-to-End)

// PUBLISHER EMBED (1 baris HTML, 0.5KB)
<div id="flio-ad" data-key="UNIT_KEY"></div>
<script async src="https://flio.net/v2/embed.js"></script>

// embed.js (~2KB):
// 1. read context: page URL, title, meta description, viewport
// 2. fetch /api/ads/serve with context (POST, JSON)
// 3. render returned HTML in #flio-ad
// 4. fire impression beacon on visibility (IntersectionObserver)
// 5. wrap click → /api/ads/click/{id} → 302 to advertiser

// WORKER /api/ads/serve flow
async function serveAd(request, env) {
  const ctx = decodeContext(request);   // url, title, geo, device, lang
  const unit = await env.AD_DB.prepare('SELECT * FROM ad_units WHERE embed_key=?')
    .bind(ctx.embed_key).first();
  if (!unit?.enabled) return errorAd();

  // 1. Generate context embedding
  const emb = await env.AI.run('@cf/baai/bge-small-en-v1.5', { text: ctx.title });

  // 2. Query candidate ads via Vectorize (top-50 semantic match)
  const candidates = await env.VECTORIZE_ADS.query(emb.data[0], {
    topK: 50,
    filter: {
      status: 'approved',
      geo: { $in: [ctx.country, ctx.region, '*'] },
      device: { $in: [ctx.device, '*'] },
      brand_safety_max: { $lte: unit.brand_safety_min }
    }
  });

  // 3. For each candidate, check budget pacer (Durable Object)
  const eligible = [];
  for (const c of candidates) {
    const pacer = env.BUDGET_PACER.get(env.BUDGET_PACER.idFromName(`cmp-${c.metadata.campaign_id}`));
    const okay = await pacer.fetch('https://x/check', { method: 'POST', body: JSON.stringify({ bid: c.metadata.bid_idr }) });
    if ((await okay.json()).allowed) eligible.push(c);
  }

  // 4. Frequency cap check (KV)
  const userHash = await sha256(ctx.ip + ctx.ua + dateUTC());
  const filtered = await Promise.all(eligible.map(async c => {
    const seen = await env.FREQ_CAP.get(`${userHash}:${c.metadata.campaign_id}`);
    return seen ? null : c;
  }));

  // 5. Run Vickrey second-price auction
  const ranked = filtered.filter(Boolean)
    .map(c => ({
      ...c,
      eCPM: c.metadata.bid_idr * c.score * predictedCTR(c)
    }))
    .sort((a, b) => b.eCPM - a.eCPM);
  const winner = ranked[0];
  const runner = ranked[1];
  const clearingPrice = runner ? Math.min(winner.metadata.bid_idr, runner.eCPM / runner.score / predictedCTR(runner) + 1) : winner.metadata.bid_idr;

  // 6. Decide: ad vs editorial fallback (hybrid mode)
  const editorialEcpm = await editorialValue(ctx);
  const finalChoice = (unit.monetization_mode === 'editorial' || editorialEcpm > winner.eCPM)
    ? await getEditorialLink(ctx)
    : { ...winner, price_idr: clearingPrice };

  // 7. Reserve budget (debit pacer)
  if (finalChoice.creative_id) {
    await env.BUDGET_PACER.get(env.BUDGET_PACER.idFromName(`cmp-${winner.metadata.campaign_id}`))
      .fetch('https://x/reserve', { method: 'POST', body: JSON.stringify({ amount: clearingPrice }) });
  }

  // 8. Set freq cap
  await env.FREQ_CAP.put(`${userHash}:${winner.metadata.campaign_id}`, '1', { expirationTtl: 86400 });

  // 9. Queue impression event (async, fire-and-forget)
  env.QUEUE_EVENTS.send({
    type: 'impression',
    request_id: ctx.request_id,
    unit_id: unit.id,
    creative_id: finalChoice.creative_id,
    campaign_id: finalChoice.campaign_id,
    site_id: unit.site_id,
    user_hash: userHash,
    geo: ctx.geo,
    device: ctx.device,
    bid_idr: clearingPrice,
    fraud_score: ctx.fraudScore,
    ts: Date.now()
  });

  return jsonResponse(renderAd(finalChoice));
}

★ Insight ─────────────────────────────────────

  • Vectorize > D1 vector untuk ad serving: Vectorize ANN search ~5ms vs D1 brute-force cosine ~50ms untuk 10K+ ads. Tetap pakai D1 vector untuk editorial (1654 pages) karena masih kecil.
  • Async queue untuk impression: tidak boleh blocking di hot path. Queue konsumer batch insert ke D1 + Analytics Engine (1M events/hari aman dengan 1 consumer).
  • Vickrey auction: winner bayar harga runner-up + Rp1. Insentif advertiser untuk bid jujur (utility maximizing) — pasar lebih sehat untuk edukasi awal. ─────────────────────────────────────────────────

Durable Object: BudgetPacer

export class BudgetPacer {
  constructor(state, env) {
    this.state = state;
    this.env = env;
    state.blockConcurrencyWhile(async () => {
      const stored = await state.storage.get(['budget_total', 'budget_daily', 'spent_today', 'spent_total', 'last_reset']);
      this.budgetTotal = stored.get('budget_total') ?? 0;
      this.budgetDaily = stored.get('budget_daily') ?? Infinity;
      this.spentToday = stored.get('spent_today') ?? 0;
      this.spentTotal = stored.get('spent_total') ?? 0;
      // schedule daily reset at 00:00 UTC
      const next = new Date();
      next.setUTCHours(24, 0, 0, 0);
      await state.storage.setAlarm(next.getTime());
    });
  }

  async fetch(req) {
    const { pathname } = new URL(req.url);
    if (pathname === '/check') {
      const { bid } = await req.json();
      const allowed =
        this.spentToday + bid <= this.budgetDaily &&
        this.spentTotal + bid <= this.budgetTotal &&
        this.pacingAllows(bid);   // even/standard/asap pacing curve
      return Response.json({ allowed, remaining_today: this.budgetDaily - this.spentToday });
    }
    if (pathname === '/reserve') {
      const { amount } = await req.json();
      this.spentToday += amount;
      this.spentTotal += amount;
      await this.state.storage.put({
        spent_today: this.spentToday,
        spent_total: this.spentTotal
      });
      return Response.json({ ok: true });
    }
  }

  // DO Alarm fires at 00:00 UTC daily
  async alarm() {
    this.spentToday = 0;
    await this.state.storage.put('spent_today', 0);
    // schedule next day
    const next = new Date();
    next.setUTCHours(48, 0, 0, 0);
    await this.state.storage.setAlarm(next.getTime());
  }
}

Affiliate / Lead Network Flow

1. Smart Link Resolver (flio.net/go/{short_code})

// User klik "Beli sekarang" di publisher → flio.net/go/fl-abc123
async function resolveLink(req, env) {
  const { code } = parseUrl(req.url);
  const link = await env.AFF_DB.prepare('SELECT * FROM tracking_links WHERE short_code=?')
    .bind(code).first();
  const offer = await env.AFF_DB.prepare('SELECT * FROM affiliate_offers WHERE id=?')
    .bind(link.offer_id).first();

  // Generate click_id (nanoid)
  const clickId = nanoid(16);
  const userHash = await sha256(req.headers.get('cf-connecting-ip') + req.headers.get('user-agent'));

  // Async log click
  env.QUEUE_EVENTS.send({
    type: 'aff_click',
    click_id: clickId,
    tracking_link_id: link.id,
    user_hash: userHash,
    ts: Date.now(),
    ip_country: req.cf.country,
    user_agent: req.headers.get('user-agent'),
    referrer: req.headers.get('referer'),
    fraud_score: computeFraudScore(req)
  });

  // Increment counter (Durable Object for hot key OR atomic D1 update)
  // ...

  // Build deeplink with click_id
  const deeplink = offer.deeplink_pattern
    .replace('{tid}', clickId)
    .replace('{sub}', link.custom_param || '');

  // 302 redirect (preserve referrer for merchant analytics)
  return Response.redirect(deeplink, 302);
}

2. Cookieless First-Party Tracking

Pakai Cloudflare for SaaS: setiap publisher bisa CNAME t.publisher.comflio.net. Click tracking dari t.publisher.com/go/... = first-party cookie, bypass Safari ITP.

Untuk fallback non-CNAME publisher: fingerprint hash(IP + UA + screen + lang + tz) sebagai user identity probabilistic.

3. S2S Postback (merchant → flio)

GET https://flio.net/api/postback?click_id=xxx&order_value=125000&order_id=ABC&signature=sha256(...)

Worker validates HMAC signature with merchant's secret, then:
- Insert ke conversions table dengan status='pending'
- Calculate commission_idr based on offer.commission_value
- Notify publisher via WhatsApp ("💸 Konversi baru Rp 12.500 dari offer X!")
- Schedule auto-approval after offer.payout_delay_days via DO Alarm

Fraud Detection (Layered Defense, 100% CF)

Layer 1: Edge filter (sub-millisecond, $0)
  - request.cf.botManagement.score < 30 → block
  - request.cf.threat_score > 50 → block
  - User-Agent regex blocklist (curl, python-requests, etc.)
  - Datacenter IP detection via cf.asn

Layer 2: Heuristics (Worker, ~1ms)
  - Click delay < 200ms (post-impression) → suspicious
  - Click velocity > 50/min from same /24 → block
  - Conversion ≥ 100% of clicks → suspicious offer or fraud
  - Time-of-day anomaly (3-5am ID time spike) → flag
  - Device + IP geolocation mismatch → flag

Layer 3: Workers AI batch (hourly cron, $0)
  - Llama-3-8B classify last hour's events:
    "Given click pattern: [time, IP, UA, geo, click_delay], rate fraud likelihood 0-1"
  - Anomaly threshold > 0.7 → mark is_billable=0
  - Llama-Guard scan creative + landing page → brand safety

Layer 4: Daily reconciliation cron
  - Mark unbillable: clicks without impression match, conversions without click match
  - Refund advertiser balance for unbillable spend
  - Compute publisher's "click quality score" for trust ranking

Payment Integration (Indonesia + Global)

Advertiser Top-up

// Indonesia: Xendit (DANA, OVO, QRIS, VA bank)
const charge = await fetch('https://api.xendit.co/v2/invoices', {
  method: 'POST',
  headers: { Authorization: 'Basic ' + btoa(env.XENDIT_KEY + ':') },
  body: JSON.stringify({
    external_id: `flio-topup-${advertiserId}-${ts}`,
    amount: amountIdr,
    payer_email: email,
    payment_methods: ['QRIS', 'DANA', 'OVO', 'BCA', 'BNI', 'MANDIRI']
  })
});
// Webhook /api/xendit/webhook → credit balance

// Global: Stripe
// Standard Stripe Checkout integration via Workers

Publisher Payout (Weekly Cron)

// Setiap Senin 01:00 WIB
async function processPayouts(env) {
  const eligible = await env.AFF_DB.prepare(`
    SELECT site_id, SUM(commission_idr) gross
    FROM conversions
    WHERE status='approved' AND paid_at IS NULL
    GROUP BY site_id
    HAVING gross >= 50000  -- min Rp 50K
  `).all();

  for (const { site_id, gross } of eligible.results) {
    const site = await env.LE_DB.prepare('SELECT * FROM sites WHERE id=?').bind(site_id).first();
    const hasNpwp = !!site.npwp;
    const pph23 = hasNpwp ? Math.floor(gross * 0.02) : 0;  // 2% NPWP, 4% non-NPWP nanti
    const platformFee = Math.floor(gross * 0.20);          // 20% take rate
    const netIdr = gross - pph23 - platformFee;

    // Xendit Disbursement
    await fetch('https://api.xendit.co/disbursements', {
      method: 'POST',
      headers: { Authorization: 'Basic ' + btoa(env.XENDIT_KEY + ':') },
      body: JSON.stringify({
        external_id: `flio-payout-${site_id}-${weekId}`,
        amount: netIdr,
        bank_code: site.payout_bank_code,    // 'DANA', 'OVO', 'BCA', etc.
        account_holder_name: site.payout_holder,
        account_number: site.payout_account,
        description: `flio.net payout ${weekId}`
      })
    });

    // Notify via WhatsApp
    await env.WA_RELAY.send(site.whatsapp,
      `💸 Payout flio.net berhasil!\nGross: Rp${gross.toLocaleString('id-ID')}\nPPh23: -Rp${pph23.toLocaleString('id-ID')}\nFee platform: -Rp${platformFee.toLocaleString('id-ID')}\nDiterima: Rp${netIdr.toLocaleString('id-ID')}\nRekening: ${site.payout_account}`);
  }
}

Frontend (Worker-Rendered HTML, Mobile-First)

3 dashboard utama, semua di Worker (server-rendered HTML, no SPA):

1. Publisher Dashboard (flio.net/p/)

  • Real-time earnings (DO WebSocket)
  • Per-unit performance (impressions, clicks, eCPM, RPM)
  • Payout request form (Rp 50K min, transfer ke DANA/bank)
  • Inventory toggle: editorial / hybrid / max revenue
  • Brand safety preferences
  • Auto-snippet generator dengan UNIT_KEY embedded

2. Advertiser Dashboard (flio.net/a/)

  • Buat campaign (form + AI assist via Llama 3.3 untuk headline)
  • Upload creative (R2, drag-drop)
  • Bid CPM/CPC/CPL slider dengan real-time eCPM forecast (Workers AI predict)
  • Targeting: geo (autocomplete kabupaten Indonesia), device, vertical, language
  • Budget pacing visualization
  • Conversions stats real-time

3. Admin Dashboard (flio.net/admin/)

  • Network stats (total publishers, advertisers, eCPM avg, fraud rate)
  • Approval queue (creatives pending review, Llama-Guard auto-flagged)
  • Disputes / manual reviews
  • Payouts batch run

Mobile-first per CLAUDE.md design standards: bottom nav, horizontal swipe carousel untuk multi-campaign view, 48px touch targets, dark mode default.


WhatsApp-Native Onboarding (Killer Feature)

Pakai existing japri-wa-relay PM2 service untuk inbox.

Advertiser Flow

User: [send WA] "Mau iklan produk saya"
Bot:  "Halo! Kirim foto produk + budget Rp + URL toko"
User: [foto + "Rp 500rb, budget 1 minggu, toko: shopee.co.id/abc"]
Bot:  "✨ Aku buatkan creative auto:
       Headline: 'Diskon 40% Sepatu Lari Pria'
       Foto: [enhanced via Workers AI Flux]
       CPC: Rp 800
       Estimasi 600 klik
       Setuju? Reply YA"
User: "YA"
Bot:  "Bayar via QRIS ini → [QR image]"
[user pay via DANA/OVO]
Bot:  "✅ Pembayaran diterima. Iklan tayang dalam 5 menit.
       Lihat performa: flio.net/a/c/abc123"

Publisher Flow

User: [send WA] "Mau pasang iklan di blog saya"
Bot:  "URL blog?"
User: "blog.contoh.id"
Bot:  "✅ Auto-onboarded! Pasang snippet ini:
       <script src='https://flio.net/v2/embed.js?k=PUB-xxx'></script>
       Pasang di artikel atau sidebar.
       Payout otomatis ke DANA tiap Senin (min Rp 50rb)."

★ Insight ─────────────────────────────────────

  • WA onboarding menghancurkan friction. AdSense butuh form panjang + verifikasi alamat fisik 6 minggu. Kita butuh 3 menit + foto produk.
  • AI auto-creative pakai Workers AI Flux ($0 di free tier 50/hari, lebih dari cukup awal). Headline pakai Llama 3.3 70B dengan prompt: "Buat headline 50 char attention-grabbing untuk [product]".
  • Workers AI gratis sampai 10K request/hari → cukup untuk 1000 advertiser onboard tanpa biaya. ─────────────────────────────────────────────────

API Endpoint Matrix

Path Method Auth Purpose Phase
/api/register POST none Publisher signup (existing) 1 ✅
/api/links POST site key Editorial link suggestions (existing) 1 ✅
/api/crawl POST site key Trigger sitemap crawl (existing) 1 ✅
/api/stats GET none Network public stats (existing) 1 ✅
/api/dashboard GET site key Site stats (existing) 1 ✅
/api/ads/serve POST unit key Get ad for slot (hot path) 2
/api/ads/click/{id} GET none Click redirect + log 2
/api/ads/impression POST unit key Beacon for visible impression 2
/api/advertiser/signup POST none Self-serve signup 2
/api/advertiser/campaigns GET/POST/PATCH adv key CRUD campaigns 2
/api/advertiser/creatives POST adv key Upload creative (R2) 2
/api/advertiser/topup POST adv key Top-up via Xendit/Stripe 2
/api/advertiser/stats GET adv key Real-time stats 2
/api/publisher/units GET/POST/PATCH site key Manage ad units 2
/api/publisher/earnings GET site key Real-time earnings 2
/api/publisher/payout POST site key Request payout 3
/api/public/market-pricing GET none Real-time avg CPM/CPC/CPL 2 (v2.1 §3)
/v2/embed.js GET none Public embed JS for publisher 2
/v2/embed.css GET none Optional theme CSS 2
/go/{code} GET none Smart link affiliate redirect 3
/api/postback GET HMAC sig S2S conversion postback (merchant) 3
/api/lead/submit POST unit key Lead form submit (v2.1 §8) 3
/api/merchant/offers GET/POST/PATCH merchant key Manage affiliate offers 3
/api/merchant/postback-test POST merchant key Validate postback signature 3
/api/admin/crawl-all POST admin key Force re-crawl (existing) 1 ✅
/api/admin/approve-creative POST admin key Manual creative approval 2
/api/admin/fraud-audit POST admin key Trigger Llama-3 batch 2
/webhook/xendit POST sig verify Payment + disbursement events 2
/webhook/stripe POST sig verify Global advertiser payments 3
/wa/inbound POST shared secret japri-wa-relay webhook 4
/syarat GET none ToS HTML (arulez+flio aug) 2
/privasi GET none Privacy Policy HTML 2
/cookie GET none Cookie policy 2
/disclaimer GET none Affiliate disclosure 3
/hak-cipta GET none Copyright/DMCA 3
/hapus-data GET/POST none UU PDP data deletion form 2
/lapor-pelanggaran GET/POST none Abuse report form 2
/api/legal/refresh POST admin key Refresh arulez bundle (cron + manual) 2

Phase Gates & Success Metrics

Setiap phase punya go/no-go gate sebelum lanjut ke phase berikut:

Phase 2 → 3 Gate

  • 5 advertiser internal aktif (campaigns dari dapur.org, dokterdewa.com, beasiswa.net, dst — dummy budget Rp 100K each untuk smoke test)
  • 24 publisher tayang ad dengan unit editorial+hybrid mode
  • 100K impressions/hari sustained selama 7 hari
  • Fraud rate <5% (impressions yang is_billable=0 setelah Layer 3)
  • p95 ad serve latency <80ms (CF Workers global edge)
  • Zero billing discrepancy antara Queue → D1 vs Analytics Engine (rekonsiliasi harian)
  • PSE Kominfo registered atau dalam proses
  • DO BudgetPacer tidak over-spend (>105% daily budget) selama 14 hari

Phase 3 → 4 Gate

  • 10 merchant onboarded (target: Tokopedia/Shopee/Blibli/Tiket via existing affiliate program API)
  • 1000 conversion total dengan postback validation
  • First successful Xendit Disbursement ke publisher (DANA atau bank transfer)
  • Postback HMAC signature verify 100% success
  • Cookieless attribution accuracy >85% (validate via merchant's own analytics)
  • Zero double-payout (Idempotency-key working)

Phase 4 → 5 Gate

  • 20 advertiser onboarded via WhatsApp (10/minggu rate)
  • AI creative auto-gen quality score >0.8 (manual review sample)
  • Hyperlocal kabupaten targeting demonstrably working (advertiser pilih "Yogyakarta only" → cuma tayang di Yogya traffic)
  • PPh 23 deduct otomatis zero error untuk 50 payout consecutive
  • Bukti potong PDF auto-generate via R2 + Workers PDF lib
  • WA bot uptime >99% via japri-wa-relay (resilience pattern)

KPI Network-Level (target Y1 end)

  • 100 publisher (vs 24 sekarang) = 4x growth
  • 100K pages indexed (vs 1,654) = 60x
  • 5M impressions/hari = 1.825B/tahun
  • Rp 1.825M revenue/tahun (~$120K)
  • Fraud rate <3%
  • Average eCPM Rp 5K (sustainable, growing 20%/year)

Roadmap Per-Fase + Sub-Issues di Beads

Phase 2: Ad Network Foundation (4-6 minggu)

Status di bd: 5 sub-issues created (P1)

  • DB schema migration — buat flio-ads-db D1, schema lengkap, seed test data
  • Ad serving worker/api/ads/serve, /api/ads/click, embed.js minimal
  • Vectorize indexflio-ads-vectors, sync creative embeddings
  • Durable Object BudgetPacer — per campaign, daily alarm reset
  • KV frequency capflio-freqcap namespace, 24h TTL
  • Queue + Analytics Enginead-events queue, consumer batch
  • Fraud Layer 1+2 — bot score, click velocity heuristics
  • Advertiser dashboard MVP — campaign CRUD, creative upload, top-up via Xendit
  • Publisher unit mgmt — extend dashboard, generate embed snippet

Goal: 5 advertiser internal (test campaigns dari sites kita sendiri seperti dapur.org, dokterdewa.com), 24 publisher tayang, 100K impressions/hari.

Phase 3: Affiliate / Lead Network (4-6 minggu)

Status di bd: 4 sub-issues created (P1-P2)

  • DB schemaflio-affiliate-db (merchants, offers, tracking_links, conversions, payouts)
  • Smart link resolver/go/{code} redirect dengan click logging
  • Cookieless tracking — Cloudflare for SaaS subdomain CNAME
  • S2S postback/api/postback dengan HMAC signature verify
  • Conversion attribution — last-click 7d default, configurable per offer
  • Payout system — Xendit Disbursement, PPh 23 auto-deduct, weekly cron
  • Merchant onboarding — form + manual approval initially

Goal: 10 merchant onboarded (target: Tokopedia, Shopee, Blibli, Tiket.com via affiliate program), 1000 publisher konversi.

Phase 4: Indonesia Killer Features (3-4 minggu)

Status di bd: 2 sub-issues created (P2)

  • WhatsApp-native onboarding — japri-wa-relay integration untuk advertiser & publisher
  • AI creative generator — Workers AI Flux + Llama 3.3 untuk auto-creative dari foto+text
  • Hyperlocal targeting — kabupaten/kota level via request.cf.city
  • QRIS payment — Xendit QRIS Static + Dynamic
  • PSE Kominfo registration — daftar OSS RBA, prep dokumen UU PDP
  • PPh 23 + PPN 11% compliance — accounting system, NPWP capture

Phase 5: Network Effects & Growth (ongoing)

  • Public marketplace — directory of all publishers + offers
  • Reverse auction — UMKM "saya butuh leads Rp 1jt/bulan" → AI match publisher
  • Co-marketing matchmaker — AI cari cross-vertical brand partnership
  • Programmatic Direct — premium inventory deals untuk Tokopedia/Shopee/Grab
  • TikTok creator network — extend ke video affiliate (TikTok Shop API)
  • Flio Credits — internal token, publisher dapat credits dari serving editorial

Diferensiasi v2.1 — Ide Tambahan dari /btw Brainstorm

8 ide kuat yang ditemukan saat brainstorm tambahan, didistribusikan ke Phase yang relevan:

1. Bahasa Daerah (Sunda, Jawa, Batak, Minang, Bugis) — Phase 3

BGE-multilingual sudah handle, tinggal tambah language di pages.lang_iso dan creatives.language_targets. Niche tapi defensif: kompetitor Big Tech tidak peduli ini, advertiser regional (radio lokal, UMKM Sunda, restoran Padang chain) bisa target laser. Detection via heuristik kata + Llama-3 classifier saat indexing.

ALTER TABLE pages ADD COLUMN lang_code TEXT DEFAULT 'id';  -- id, en, su, jv, bbc, min, bug
-- creative targeting JSON sudah handle: ["id-su"] = Sunda only

2. Seasonal Mode (Ramadan, Lebaran, Imlek, Natal) — Phase 4

Cron daily compute current_season flag. Saat aktif:

  • Boost CPM 1.3x (advertiser bayar lebih, willing karena event)
  • Auto-load creative templates: Lebaran banner, Ramadan timer countdown
  • AI pre-generate seasonal creative dari foto produk advertiser
// schedules harian check
const seasons = {
  ramadan: { start: '2026-02-18', end: '2026-03-19', boost: 1.3 },
  lebaran: { start: '2026-03-20', end: '2026-04-05', boost: 1.5 },
  imlek_2026: { start: '2026-02-08', end: '2026-02-18', boost: 1.2 },
  natal_2026: { start: '2026-12-15', end: '2026-12-31', boost: 1.4 }
};

3. Public Pricing Transparency — Phase 2

Halaman publik /pricing real-time tampilkan rata-rata CPM/CPC/CPL per vertical (data hari terakhir). AdSense black box, kita transparent → trust signal:

Hari ini di flio.net (data live):
  Health     CPM Rp 18.450  •  CPC Rp 1.250
  Finance    CPM Rp 32.100  •  CPC Rp 4.800  •  CPL Rp 78.500
  Food       CPM Rp 12.300  •  CPC Rp 850
  Lifestyle  CPM Rp 15.700  •  CPC Rp 950

Aggregate dari Analytics Engine 1-jam window. Endpoint /api/public/market-pricing cacheable di KV 5 min.

4. CF Turnstile Identity Proof — Phase 2 fraud layer 5

Tambah Turnstile widget di click resolver (invisible mode). Free tier unlimited verifications. Bot click rate diharapkan turun 60-80%. Layer 5 di fraud stack:

// di /api/ads/click
const turnstileToken = ctx.headers.get('cf-turnstile-token');
const verify = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
  method: 'POST', body: new URLSearchParams({ secret: env.TURNSTILE_SECRET, response: turnstileToken })
});
if (!(await verify.json()).success) markFraudulent();  // proceed redirect tapi flag billing=0

5. Workers AI Creative Auto-A/B — Phase 2

Saat advertiser upload 1 creative, AI auto-generate 3 variant (rotate headline, color, CTA). Sistem auto-rotate 4 versi, BGE embed click pattern per variant, pilih winner per geo+device segment dalam 48 jam:

CREATE TABLE creative_variants (
  parent_creative_id INTEGER NOT NULL,
  variant_id TEXT,                  -- "v1","v2","v3","v4"
  segment_key TEXT,                 -- "ID-JK-mobile-android"
  impressions INTEGER DEFAULT 0,
  clicks INTEGER DEFAULT 0,
  ctr REAL,
  is_winner INTEGER DEFAULT 0
);

6. Vector Negative Keywords — Phase 2

Advertiser upload list "jangan tampil di artikel tentang X" (e.g., kompetitor, topik sensitif). Embed each keyword 384-dim, simpan di campaign config:

// saat ad selection
const negKeywords = JSON.parse(campaign.negative_keywords_embeddings);
const pageEmb = await embed(ctx.title);
const minSim = Math.min(...negKeywords.map(neg => cosine(pageEmb, neg)));
if (minSim > 0.7) skip();  // halaman terlalu mirip negative keyword, skip

Lebih powerful dari brand_safety_level integer karena semantic.

7. Sponsor Konten Mode (Transparent Native) — Phase 3

Mode antara editorial (gratis) dan paid ad: advertiser bayar untuk muncul sebagai editorial link dengan disclosure jujur ("Sponsored ▶"). Higher trust + higher CTR daripada banner. Native automated:

[Baca Juga]
• Sponsored ▶ Tips Diabetes dari DokterDewa.com
• Resep Sayur Anti Diabetes dari Dapur.org
• Panduan Diet Diabetes dari WebMD-id.com

DB: creatives.format='sponsored_native' + is_disclosed=1. Take rate sama (20%), tapi CPM 2-3x display banner karena premium placement.

8. Lead Marketplace 1-Klik (High-Intent) — Phase 3

Publisher pasang form HTML di artikel. User isi → langsung dijual sebagai lead ke advertiser tertinggi yang bid. Beda dari banner: high-intent, CPL Rp 25K-150K split 70/30:

<form data-flio-lead="scholarship-univ">
  <input name="name" /><input name="email" /><input name="phone" />
  <input name="city" /><select name="major">...</select>
</form>

Worker /api/lead/submit:

  1. Validate form
  2. Real-time auction: advertiser dengan highest CPL untuk vertical
  3. Insert ke leads table, post via webhook ke advertiser
  4. Credit publisher 70% CPL
  5. Return success page
CREATE TABLE leads (
  id INTEGER PRIMARY KEY,
  publisher_site_id INTEGER NOT NULL,
  advertiser_id INTEGER NOT NULL,
  vertical TEXT NOT NULL,
  payload TEXT NOT NULL,                -- JSON form data
  cpl_idr INTEGER NOT NULL,
  publisher_share_idr INTEGER NOT NULL,
  status TEXT DEFAULT 'pending_validation',
  delivered_at INTEGER,
  validated_at INTEGER
);

★ Insight ─────────────────────────────────────

  • Sponsor Konten mengisi celah antara editorial (free, 0% revenue) dan banner ad (high revenue tapi UX jelek). Studi industri: native sponsored content punya CTR 5-8x banner. Ini akan jadi inventory paling profitable untuk publisher serius.
  • Lead Marketplace 1-klik beda kategori revenue dari display: 1 lead beasiswa = Rp 25K-100K, satu impression cuma Rp 5-50. Publisher dengan audience high-intent (beasiswa.net, dokterdewa.com) bisa 100x lebih revenue per visitor.
  • Vector negative keywords > brand_safety_level integer karena bisa block "kompetitor saya" tanpa block seluruh kategori. Advertiser Tokopedia bisa exclude artikel tentang Shopee tanpa block ecommerce vertical. ─────────────────────────────────────────────────

Revenue Model & Take Rates

Service Take Rate Min Payout Pricing
Editorial Link Exchange 0% n/a gratis selamanya
Display Ads (CPM) 20% Rp 50K Floor Rp 5K CPM (max Rp 50K finance vertical)
Display Ads (CPC) 20% Rp 50K Floor Rp 200, max Rp 7K (finance)
Affiliate (CPL) 15% Rp 50K Per offer, Rp 5K-Rp 150K
Affiliate (CPS) 20% Rp 50K 1-15% of order value
Lead Marketplace 1-click 30% Rp 100K Rp 25K-100K per high-intent lead
Programmatic Direct 10% n/a Negotiated quarterly

Pricing rationale: AdSense ambil ~32%. Kita 20% = lebih kompetitif. Target margin operasional CF cost: <5% revenue (CF Workers ~$0.50/M req, lebih murah dari kompetitor lokal yg pakai AWS/GCP).

Revenue Projection

Y1 (2026): 100 publisher, 50K imp/hari/site = 5M imp/hari × Rp 5K CPM = Rp 25jt/hari
           × 365 × 20% take = Rp 1.8M/tahun = Rp 1.825M (~$120K)
Y2 (2027): 1000 publisher, 5M total imp/hari = same math × 10 = Rp 18M/tahun ($1.2M)
Y3 (2028): 10000 publisher = Rp 180M/tahun ($12M)

Regulatory Checklist 🇮🇩

  • Daftar PSE Lingkup Privat di OSS RBA (kominfo.go.id)
  • Consent banner UU PDP — opt-in untuk tracking, prefer first-party
  • PPh 23 (2% / 4% non-NPWP) auto-deduct dari publisher payout
  • PPN 11% pemungutan jika omzet >Rp 600jt/tahun (advertiser SPT)
  • Privacy Policy + Terms dwi-bahasa (ID/EN), age 16+ filter
  • NPWP capture di publisher onboarding (optional, untuk tax discount)
  • Bukti potong PPh PDF auto-generate per payout

Key Architecture Decisions & Trade-offs

Decision Chosen Alternative Why
Vector store Vectorize (ads) + D1 (editorial) All-D1 vector Vectorize faster ANN at scale, D1 fine for <10K vectors
Auction model Vickrey 2nd-price First-price Educates honest bidding, healthier marketplace
Tracking method First-party CNAME + fingerprint 3rd-party cookie Safari ITP + UU PDP friendly
Payment ID Xendit Midtrans Better DANA/OVO support + Disbursement API
Payout schedule Weekly Monthly / on-demand Trust signal, Indonesia preference
Take rate 20% (vs 32% AdSense) 30% (CJ) / 0% promo Competitive but sustainable
Onboarding WhatsApp-first Email/web Indonesia mobile-first reality
Hot path data Vectorize + DO + KV Pure D1 Sub-50ms global edge
Long-term storage D1 + R2 archive All-D1 D1 10GB cap requires sharding
Monetization mode Editorial / Hybrid / MaxRev Single mode Publisher choice = trust

Copy, UX, & Legal Adaptation

flio.net adalah produk Indonesia-first dengan global secondary. Copy harus native Bahasa Indonesia, bukan translate-feeling, dan UX-nya mengadaptasi pola yang sudah teruji di AdSense + CJ.com.

Adaptasi Copy & UX dari Google AdSense

AdSense punya 20+ tahun research UX dan copy. Yang harus diadopsi (di-Indonesiakan, bukan translate literal):

Konteks AdSense pattern flio adaptation
Publisher onboarding step 1 "Connect your site" — single field, big CTA "Sambungkan situs Anda" — input domain + tombol besar oranye, mobile-first 48px
Site verification copy "We need to verify you own this domain" "Kami perlu memastikan Anda pemilik domain ini. Pilih cara verifikasi:" — kasih 3 opsi (DNS TXT, file upload, snippet meta tag)
Earnings dashboard hero Today's earnings (big), Yesterday, Last 7 days, This month "Penghasilan Hari Ini: Rp 47.350" + comparison + spark chart
Payment threshold message "You'll receive payment when balance reaches $100" "Pembayaran otomatis ke DANA Anda saat saldo mencapai Rp 50.000 (Senin minggu depan)"
Policy violation notice Apologetic, specific, link to fix "Iklan dijeda sementara — masalah pada halaman ini: [page]. Solusi: [3 langkah perbaikan]. Lapor banding di [link]."
Empty state (no impressions yet) Helpful, instructive screenshot "Belum ada impresi. Pasang snippet di [howto link] dan tunggu 1-2 jam." + screenshot ilustrasi
Help center structure Search-first, FAQ kategori, video tutorials /bantuan dengan search bar besar + 6 kategori + video Bahasa Indonesia
Confirmation modal Friendly, undo option ("you can change later") "Ubah ke mode 'Pendapatan Maksimal'? Bisa diubah lagi kapanpun." + soft undo banner 30s

Specific AdSense copy patterns to adopt (already translated to native Indonesian feel):

"Mulai daftar — gratis, tanpa kartu kredit"     (vs AdSense "Sign up free")
"Iklan tampil ketika halaman cocok"              (vs "Ads serve when relevant")
"Anda tetap punya kendali penuh atas iklan"     (vs "You stay in control")
"Bayar saat ada konversi, bukan saat klik"       (vs CJ "Pay on action")
"Penghasilan terkredit otomatis tiap Senin"     (vs "Automated weekly payouts")

Adaptasi Copy & UX dari CJ.com / Commission Junction

CJ punya 25+ tahun pattern affiliate marketplace. Yang harus diadopsi:

Konteks CJ.com pattern flio adaptation
Advertiser/publisher dual portal Top toggle "I'm a publisher / I'm an advertiser" Hero: 3 cards "Saya Publisher / Saya Pengiklan / Saya Merchant"
Offer marketplace listing Logo, name, EPC, conversion rate, payout terms in one card Card kompak: logo merchant, nama, EPC Rp X, CR Y%, payout terms, CTA "Promosikan"
Apply-to-advertiser flow "Apply" CTA → wait approval → join offer Dropped: auto-approve untuk publisher tier verified. Tier baru = manual approve.
Tracking link generator URL field + custom param + copy button "Buat Smart Link" tab: pilih offer, custom sub-id, dapat URL pendek flio.net/go/xxx
Postback documentation API docs format, postman examples /dev/postback halaman dengan curl example + variable list + signature verify code (Bahasa Indonesia)
Performance reports Date range picker, dimensions + metrics tabular Filter date range, dimensions (offer, sub-id, geo, device), export CSV
Payout statement Itemized: offer × clicks × conversions × commission Tabel: offer, klik, konversi, komisi, status (pending/approved/paid). Tanggal pembayaran. PPh 23 deducted line.

Specific CJ.com copy patterns to adapt:

"Setiap konversi terlacak otomatis"              (vs "Reliable tracking")
"Komisi divalidasi merchant dalam 7 hari"        (vs "Validated within 7 days")
"Bukti potong PPh 23 otomatis tersedia"          (uniquely Indonesia)
"Smart Link cocok untuk artikel review"          (vs "Use deep links for product pages")
"Komisi dibayar saat status 'Disetujui'"         (vs "Commission paid on approved status")

Tone & Voice Guidelines

flio.net TONE: profesional, ramah, transparan, no-bullshit. Bahasa Indonesia native (bukan terjemahan kaku).

HINDARI:

  • ❌ "Kami sangat senang menyambut Anda di platform terpercaya..." (cringe formal)
  • ❌ "Klik di sini untuk lebih lanjut" (lazy CTA)
  • ❌ "Loading..." tanpa konteks
  • ❌ Translation feeling: "Bergabunglah dengan ribuan publisher kami!" (sounds like Google Translate)

PAKAI:

  • ✅ "Daftar 2 menit, langsung tayang" (concrete benefit)
  • ✅ "Lihat detail penghasilan" (specific CTA)
  • ✅ "Memuat data 7 hari terakhir..." (informative)
  • ✅ "Sudah 1.674 publisher aktif." (specific numeric proof)

Specific tone patterns:

  • Capital letter parsimonious: "Daftar Gratis" bukan "DAFTAR GRATIS"
  • Numerical proof everywhere: "Rp 47.350 hari ini" bukan "penghasilan hari ini"
  • Indonesian slang appropriate level: "klik" "tayang" "pasang" (formal-casual mix), HINDARI "yuk" "lho" "kak" untuk dashboard (untuk WA chatbot OK)
  • Time references natural: "tadi pagi", "kemarin sore", "Senin depan", "minggu lalu"

ToS + Privacy Policy via arulez.com API (legal-as-a-service)

arulez.com punya API public legal baseline templates yang generate 7 dokumen Indonesia-compliant via parameter brand+domain+service:

GET https://arulez.com/api/v1/legal/bundle
  ?brand=flio.net
  &domain=flio.net
  &service=ad%20%2B%20affiliate%20network

Returns JSON dengan 7 dokumen (16KB):
  - terms          (Syarat & Ketentuan)
  - privacy        (Kebijakan Privasi)
  - cookie         (Kebijakan Cookie)
  - disclaimer     (Disclaimer & Affiliate Disclosure)
  - copyright      (Kebijakan Hak Cipta / DMCA)
  - data-deletion  (Permintaan Penghapusan Data — UU PDP)
  - abuse-report   (Laporan Penyalahgunaan)

Sumber hukum yang sudah dirujuk (oleh API):

  • ID-UU-27-2022-PDP — UU Pelindungan Data Pribadi
  • ID-UU-1-2024-ITE — UU ITE perubahan kedua 2024
  • ID-PP-71-2019-PSTE — PP Penyelenggaraan Sistem & Transaksi Elektronik
  • ID-UU-8-1999-KONSUMEN — UU Perlindungan Konsumen
  • INT-UNCITRAL-MLEC — UNCITRAL Model Law on Electronic Commerce

Snapshot bundle sudah disimpan di: /home/ucok/web/flio.net/legal/bundle-flio.json

Implementation: Worker-served legal pages

Setiap legal page di-render dari arulez bundle + flio-specific augmentation. Worker pattern:

// /syarat → renders terms with flio augmentation
async function renderTerms(env) {
  // 1. Load arulez baseline (cached at build time atau KV)
  const bundle = await env.CONFIG.get('legal:bundle:v1', { type: 'json' });
  const baseTerms = bundle.documents.terms;

  // 2. Augment dengan flio-specific sections
  const flioSections = [
    {
      heading: 'Sistem Pembayaran & Payout Publisher',
      text: 'Pembayaran ke publisher dilakukan setiap Senin pukul 01:00 WIB melalui '
        + 'Xendit Disbursement (DANA, OVO, GoPay, BCA, Mandiri, BNI). '
        + 'Threshold minimum Rp 50.000. Pemotongan PPh 23 sebesar 2% '
        + '(bagi pemilik NPWP) atau 4% (non-NPWP) dilakukan otomatis. '
        + 'Bukti potong PPh tersedia di dashboard.'
    },
    {
      heading: 'Pengiklan & Pengelola Iklan',
      text: 'Pengiklan (advertiser) wajib mendepositokan saldo melalui Xendit '
        + '(QRIS / DANA / OVO / VA Bank) atau Stripe (untuk pengiklan global). '
        + 'Saldo terdebit otomatis berdasarkan model bid (CPM, CPC, CPA, CPL). '
        + 'flio.net mengambil platform fee 20% dari total spend. '
        + 'Penyesuaian fraud (pengembalian saldo untuk impresi/klik palsu) dilakukan H+1.'
    },
    {
      heading: 'Konten Iklan & Brand Safety',
      text: 'Setiap creative iklan dievaluasi otomatis menggunakan AI brand safety '
        + '(Llama-Guard) sebelum tayang. Kategori yang dilarang: perjudian, '
        + 'konten dewasa, narkotika, kekerasan, hate speech, hoax kesehatan, '
        + 'investasi tidak terdaftar OJK, multilevel marketing tanpa izin. '
        + 'Publisher dapat mengatur level brand_safety per ad unit.'
    },
    {
      heading: 'Editorial Link Exchange (Layanan Gratis)',
      text: 'flio.net menyediakan layanan editorial link exchange gratis bagi publisher '
        + 'yang terdaftar. Link bersifat AI-matched, kontekstual, dan editorial — '
        + 'BUKAN sponsored link atau paid backlink. Tidak ada PageRank manipulation '
        + 'sebagaimana dilarang Google Webmaster Guidelines.'
    },
    {
      heading: 'Penyelesaian Sengketa',
      text: 'Setiap sengketa diutamakan diselesaikan secara musyawarah. '
        + 'Jika tidak tercapai dalam 30 hari, akan diselesaikan melalui '
        + 'Pengadilan Negeri Jakarta Pusat atau melalui Badan Arbitrase Nasional '
        + 'Indonesia (BANI) sesuai pilihan flio.net. Hukum yang berlaku adalah '
        + 'hukum Republik Indonesia.'
    }
  ];

  const allSections = [...baseTerms.sections, ...flioSections];
  return renderHTMLDocument({
    title: 'Syarat & Ketentuan flio.net',
    effectiveDate: baseTerms.effective_date_human,
    sections: allSections,
    sources: bundle.sources
  });
}

Build script (scripts/refresh-legal.js): jalankan saat ada perubahan UU atau seminggu sekali via cron — fetch bundle terbaru, simpan ke KV legal:bundle:v1.

// Cron: 0 0 * * 0 (setiap Minggu 00:00 WIB)
async function refreshLegalBundle(env) {
  const url = new URL('https://arulez.com/api/v1/legal/bundle');
  url.searchParams.set('brand', 'flio.net');
  url.searchParams.set('domain', 'flio.net');
  url.searchParams.set('service', 'ad + affiliate network');

  const res = await fetch(url, { headers: { 'Accept': 'application/json' }});
  const bundle = await res.json();
  await env.CONFIG.put('legal:bundle:v1', JSON.stringify(bundle), {
    expirationTtl: 30 * 86400  // 30 hari fallback
  });
}

Legal Pages Routes

Route Source Content Flio augmentation
/syarat arulez terms + 5 flio sections Syarat & Ketentuan Payout system, advertiser flow, brand safety, editorial, dispute
/privasi arulez privacy + 4 flio sections Kebijakan Privasi Ad event data, BGE embeddings, fingerprint tracking, cookie-less
/cookie arulez cookie + 2 flio sections Kebijakan Cookie Cookieless first-party CNAME explanation, freq cap KV
/disclaimer arulez disclaimer + 3 flio sections Disclaimer Affiliate disclosure (FTC + UU PK), no investment advice, no medical
/hak-cipta arulez copyright + 2 flio sections Hak Cipta / DMCA Creative copyright by advertiser, takedown procedure
/hapus-data arulez data-deletion + flio Permintaan Penghapusan Data Form web /hapus-data/form, response 14 hari (UU PDP)
/lapor-pelanggaran arulez abuse-report + flio Laporan Penyalahgunaan Form web, fraud@flio.net, WA admin
/terms, /privacy, etc English version Auto-translate via Workers AI Manual review for legal accuracy

Strategi Update Hukum

UU di Indonesia berubah cukup sering (UU ITE 2024 perubahan kedua, UU PDP 2022 baru efektif 2024). arulez.com API terus update sumber hukum.

  • Cron mingguan (Minggu 00:00 WIB): refresh bundle dari arulez API
  • Manual review setiap update: cek diff vs versi sebelumnya
  • Versioning: legal:bundle:v1, legal:bundle:v2, simpan history di R2 untuk audit
  • Notice ke users: jika ada perubahan material, kirim notif via dashboard + WhatsApp + email 30 hari sebelum berlaku (UU PDP requirement)

Multi-language strategy

Locale Priority Source
id-ID Primary (100%) Native, tidak translate
en-US Secondary (Phase 2) Translate via flio AI dari id-ID source
id-su, id-jv, id-bbc, id-min, id-bug Phase 4 Hanya untuk programmatic targeting + bahasa display home, ToS tetap id-ID
en-GB, en-SG Future Untuk advertiser global

i18n implementation: pakai pola pustaka.org (memory pustaka_translation_system.md) — Google Translate free API untuk batch translate, manual review untuk legal pages.


Tools, Skills, Memory yang Akan Dipakai

  • Skill /site-optimize — full-stack optimization checklist
  • Skill /google-search-console — submit flio.net/a, /p sitemap
  • Skill /seo-boost — backlink blitz untuk akuisisi awal publisher
  • Skill /agent-comm — koordinasi dengan agent lain (sewakamar, dokterdewa untuk seed advertiser)
  • Memory: feedback_design_standards.md, feedback_image_cdn_mandatory.md, feedback_wa_relay_*, cf_deployment_policy.md
  • Reference: reference_wsrv_image_cdn.md (untuk creative thumbnails)

Competitive Moat Strategy

Kompetitor & Kelemahan Struktural

Kompetitor Kekuatan Kelemahan struktural (sulit fix dalam 12 bulan) Risiko mereka meniru flio
Google AdSense Brand, scale, advertiser pool global (1) Min payout $100 USD, (2) wire transfer only, (3) approval ditolak SME Indonesia, (4) tidak peduli niche kecil ID RENDAH — Indonesia bukan prioritas; "AdSense Lite ID" akan butuh re-org tim, regulatory + tax handling, payment rails. Min 18 bulan.
Involve Asia Network 8 negara, 1500+ merchant (1) Validasi 60-90 hari, (2) UI/UX legacy 2018, (3) tidak ada AI matching, (4) tidak punya editorial gateway MEDIUM — bisa improve UX 6 bulan, tapi tidak punya AI infra (BGE+Vectorize+Llama). Akan akuisisi startup AI.
Accesstrade ID Heritage Jepang, B2B network Fragmentasi per-negara, technical debt, tidak ada self-serve modern RENDAH — tidak punya tech ambition, focus lebih pada akuisisi merchant tradisional
Adskom Premium publisher (Detik/Tempo) Closed ecosystem, premium-only barrier, tidak terima long-tail RENDAH — bisnis model "premium gatekeeper" tidak match flio long-tail
Mitra Tokopedia 100% di ekosistem TKPD Walled garden — tidak boleh promote kompetitor (Shopee, Lazada) KRITIS — Tokopedia bisa launch "Multi-merchant Affiliate" via Goto Group. Ancaman terbesar.
CJ.com 20+ tahun, brand global US-centric, tidak peduli Indonesia, payment rails konvensional RENDAH
Impact.com Modern API, attribution canggih Pricing $2K/mo+ enterprise, tidak ramah SME ID RENDAH — tidak akan turunkan ke SME tier
Outbrain/Taboola Native ad pioneer, kontrak premium publisher Publisher kompleks integrate, low-quality ad reputation, "click-bait" stigma MEDIUM — akan turun ke SME jika revenue stagnan, tapi clickbait baggage hard to shake
PropellerAds Push notification ads, easy onboarding Spam reputation, ToS violations Google, tidak bisa di publisher premium RENDAH — segmen berbeda
RajaBackLink Indonesia backlink seller Manual, scammy reputation, Google penalty risk TIDAK RELEVAN — model link-buying yang kita justru anti

7 Moat Layer untuk Flio (Defensibility Score 1-10)

Moat Score Path to Build Yang Sulit Direplikasi
1. Network Effects 9/10 Editorial gratis → akuisisi 100+ publisher Y1 → matching quality naik → advertiser organic mengikuti Two-sided market: makin banyak publisher = matching makin baik = advertiser lebih happy = bid lebih tinggi = publisher lebih happy. Self-reinforcing loop. Susah ditembus karena butuh start dari 0.
2. Data Moat 8/10 Setiap impression/click/conversion → BGE embed page+creative → labeled training data per niche ID Setelah 12 bulan: proprietary engagement dataset Indonesia per niche (tidak ada kompetitor punya). Foundation untuk smart pricing, fraud detection, recommendation.
3. Cost Moat 9/10 100% CF stack: Workers $5/mo + D1 $5/mo + AI free tier. Per-impression cost ~$0.000001 vs AWS-based ~$0.00001 (10x) Kompetitor lokal pakai AWS/GCP biasanya — tidak bisa drop ke CF tanpa rewrite total. Margin 95% biarkan kita underbid take rate (20% vs 32% AdSense) atau cashback ke publisher.
4. Brand/Trust Moat 7/10 Public pricing transparency (v2.1 §3) + PPh 23 auto + payout DANA Rp 50K + WhatsApp support Trust di Indonesia = infrastructure trust (payment, pajak, WA). Sulit ditiru tanpa entitas Indonesia full-stack.
5. Switching Cost Moat 8/10 Publisher integrate embed.js → kumpulkan 90 hari historical earnings + per-unit performance + fraud reputation score Migrasi ke kompetitor = lose historical data + reputation score + custom unit configs + AI matching learnings. Trained-on-your-traffic ML model loss saat switch.
6. Regulatory Moat 6/10 PSE Kominfo registered + UU PDP compliant + bukti potong PPh 23 valid + PPN 11% pemungutan Bukan moat permanen tapi time-to-market advantage. Kompetitor baru butuh 3-6 bulan setup compliance Indonesia. Existing internasional susah karena entity reorganization.
7. Distribution Moat 8/10 WhatsApp-native onboarding + 24+ situs internal sebagai referral engine + Indonesian community penetration WhatsApp Business API integration plus existing japri-wa-relay infrastructure. Internasional kompetitor tidak punya WA muscle. Communities (Facebook blogger, Telegram niche) butuh personal touch.

Unfair Advantages (Existing — Hari Ini)

  1. 24+ situs internal sebagai launching pad: dapur.org, dokterdewa.com, beasiswa.net, ilagaligo.com, paroki.org, etc. Semua sudah pasang flio embed = hari 1 ada inventory.
  2. japri-wa-relay PM2 sudah running = WhatsApp infrastructure terpasang vs kompetitor build dari 0.
  3. 1654 BGE-indexed pages + 384-dim embeddings sudah ada = matching layer head start 6 bulan.
  4. Cloudflare expertise dari 27 sites di-migrate (memory cf_migration_complete.md) = playbook deploy & ops sudah teruji.
  5. Indonesian language + culture mastery: navigasi konten Sunda, Jawa, Batak, Bugis (existing pustaka.org translation system).
  6. Existing payment integration (Xendit, Stripe, Midtrans untuk BitMine.id) = no new vendor onboarding.
  7. AI tools free tier mastery (Workers AI, OpenRouter, Claude/Codex CLI free) — biaya AI inference mendekati nol.

Defensive Moves (anti-counter scenarios)

Skenario A: AdSense launch "AdSense Lite Indonesia" dengan IDR payout

  • Response: Sudah punya 10K+ publisher dengan switching cost. Sudah ada brand trust. Cashback Rp 100K ke publisher yang stay setelah AdSense Lite launch (funded dari margin 95%).
  • Long-term: Differentiate ke affiliate + lead (AdSense lemah di sini), dan editorial network (gratis, AdSense tidak punya).

Skenario B: Tokopedia/Shopee/Grab buka in-house affiliate (paling realistis)

  • Response:
    • Mereka cuma 1 merchant. Kita 100+ merchant. Publisher pilih kita karena diversifikasi.
    • Partner-bukan-kompetitor: jadikan mereka anchor tenant dengan rate negotiated. Take rate 5% (vs 20%) untuk volume. Saling menguntungkan.
    • Reverse marketplace: publisher pasang multi-merchant widget, biar konsumen pilih sendiri. Tokopedia tidak bisa replicate karena cuma punya 1 brand.

Skenario C: Involve Asia improve UX + bikin WA onboarding

  • Response: Editorial layer adalah gateway gratis yang Involve tidak bisa replicate (mereka pure paid). Network effect kita lebih cepat compound.
  • Long-term: Akuisisi 5 niche vertical Indonesia (beasiswa, kesehatan, properti) yang Involve tidak fokus.

Strategic Partnerships Priority (Tier 1 = approach Q1 next year)

Partner Tier Value Approach
Tokopedia/Shopee/Lazada 1 Anchor merchant, immediate inventory Negotiated take rate 5%, exclusive widget
Bank BCA, Mandiri, BNI 1 Finance vertical highest CPL (Rp 25K-150K) Direct partnership, KOMINFO compliance bridge
Detik/Kompas/CNN ID 2 Premium publisher, instant scale Programmatic Direct deal, premium inventory tier
AFTECH (asosiasi) 2 Regulatory ally, KOMINFO connection Membership, advocacy partner
Goto Group 3 Multi-product (TKPD, Gojek) Gabung programmatic direct
Telkomsel/Indosat 3 Carrier billing untuk advertiser SME API integration, credit ke campaign
KOMINFO 1 PSE registration + UU PDP guidance Daftar langsung, jaga relasi

3 Flywheels Berkompon

Flywheel 1: Editorial → Publisher Acquisition

free editorial links → publisher install widget → flio gain inventory →
better matching quality → publisher see 10-20% traffic boost →
publisher refer 2 others avg → +100% growth/quarter

Flywheel 2: Publisher Inventory → Advertiser Demand

1000 publisher with diverse niche → advertiser see better targeting →
advertiser increase budget × 1.5 → publisher earnings naik 50% →
publisher engage more deeply → upgrade to "max revenue" mode →
inventory quality naik → advertiser ROI naik → bid naik

Flywheel 3: Conversion Data → AI Lock-in

1M conversions/quarter → BGE+Llama trained on YOUR network's data →
fraud detection accuracy 95% (vs industry 80%) → advertiser trust naik →
eCPM premium 20% → publisher revenue → MORE publisher join → MORE data →
AI moat compounds quarterly

Open Source Strategy

embed.js / SDK — open source MIT license. Strategis karena:

  • ✅ Dev community contribute (audit fee fraud detection, plugin React/Vue/Hugo/WordPress)
  • ✅ "View Source" boost trust (vs blackbox AdSense)
  • ✅ Distribution moat: indexed di GitHub, npm, jsDelivr — discovery via dev tools
  • ✅ Plugin ecosystem (Hugo theme, WordPress plugin, Astro integration)
  • ❌ Trade-off: kompetitor bisa fork — tapi mereka tetap butuh BACKEND (D1+AI+payment) yang tidak open source

Backend stays closed-source. Schema evolution + AI training data + fraud heuristics = closed.

Schema-Level Lock-in (sulit migrate publisher ke kompetitor)

Setiap publisher mengakumulasi data berharga di flio yang tidak portable:

  • publisher_quality_score (computed dari 90-hari history)
  • unit_performance_history (per-unit eCPM trend, segment winners)
  • fraud_reputation (low fraud score = priority bidder)
  • ai_matched_keywords (BGE clusters yang work untuk audience mereka)
  • creative_ab_winners (mana variant yang convert untuk audience mereka)
  • historical_payouts (track record untuk dispute, audit, referral kuota)

Publisher migrate = lose 6-12 bulan history + reputation + AI tuning. Switching cost = financial cost (lower bid karena no reputation) + time cost (rebuild AI tuning) + opportunity cost (lose pending payouts).

★ Insight ─────────────────────────────────────

  • Moat terkuat = combination, bukan single layer. Network effect (9/10) + Cost (9/10) + Switching (8/10) menciptakan compound defensibility >25/30 — sangat sulit ditembus.
  • Mitra Tokopedia/Goto adalah ancaman terbesar karena mereka punya developer tim + capital + Indonesian DNA. Strategi: ko-opt mereka jadi anchor tenant sebelum mereka build sendiri. "Win as partner, lose as competitor."
  • Open source embed.js + close source backend = pattern Vercel/Supabase. Distribution moat dari OSS, monetization moat dari managed service.
  • Paling cepat eroding moat = regulatory (orang lain bisa daftar PSE juga). Paling slow eroding = data + network effect (compound 12-24 bulan). ─────────────────────────────────────────────────

Risk & Mitigations

Risk Probability Impact Mitigation
Click fraud merusak advertiser trust Tinggi Tinggi 4-layer fraud detection + refund unbillable
Vectorize cost spike Medium Medium Start with D1 vector, migrate bertahap
KOMINFO block tanpa PSE Tinggi Kritis Daftar PSE sebelum public launch
Xendit API rate limit Rendah Rendah Queue payouts, retry exp backoff
Publisher fraud (self-clicking) Tinggi Medium IP+UA+device fingerprint, threshold per pub
Brand safety incident (creative bermasalah tayang di publisher konservatif) Medium Tinggi Llama-Guard pre-approval + post-publish audit
Big Tech retaliation (Google block) Rendah Tinggi Diversify ke domain alternatif, tetap white-hat

Appendix A — Reference Implementation Patterns (Codex Contribution)

Codex CLI (gpt-5.5) menyumbang 6 pola implementasi clean yang akan diadopsi saat coding Phase 2:

A.1 Micro-pricing convention (avoid floats)

Untuk semua kolom harga IDR, gunakan _micros (10⁻⁶ IDR) bukan IDR langsung. Memungkinkan bid sub-Rupiah, hindari floating point error:

bid_cpm_micros INTEGER NOT NULL DEFAULT 0,    -- 5_000_000 = Rp 5.000
bid_cpc_micros INTEGER NOT NULL DEFAULT 0,    -- 200_000_000 = Rp 200
bid_cpa_micros INTEGER NOT NULL DEFAULT 0,
daily_budget_micros INTEGER NOT NULL DEFAULT 0,
gross_micros INTEGER NOT NULL,
pph23_micros INTEGER NOT NULL DEFAULT 0,
net_micros INTEGER NOT NULL,

Konversi ke IDR sebelum Xendit: Math.floor(netMicros / 1_000_000).

A.2 Bindings header convention

Setiap file SQL/TS schema buka dengan list binding di komentar — cara cepat orang lain memahami env yang dipakai:

-- Bindings:
-- D1: DB
-- Analytics Engine: AE
-- Queue: AD_EVENTS_Q
-- Durable Object: BUDGET_PACER
-- KV: AD_CACHE
-- Secrets: XENDIT_SECRET_KEY, LLAMA_API_KEY

A.3 Linear time-of-day pacing (cleaner than my version)

Pacing target = budget × (detik_sejak_midnight / 86400). Jika spend > pace → tolak (paced), bukan budget habis. Smoother distribution dibanding ASAP burn:

async targetPaceMicros(campaignId, now) {
  const budget = await this.getDailyBudgetMicros(campaignId);
  const seconds = secondsSinceUTCMidnight(now);
  return Math.floor(budget * (seconds / 86400));
}

// di fetch():
if (spend + priceMicros > budget) return { allow: false, reason: "daily_budget_exhausted" };
if (spend > pace) return { allow: false, reason: "paced" };  // throttle, not block

A.4 Queue Consumer batch insert pattern (D1 + Analytics Engine)

100 events per batch, parallel D1 batch + AE writeDataPoint:

export default {
  async queue(batch, env) {
    const rows = batch.messages.slice(0, 100).map(m => m.body);
    const d1 = [];
    const ae = [];

    for (const e of rows) {
      const shard = `ad_impressions_${yyyymm(e.created_at)}`;  // dynamic shard table

      d1.push(env.DB.prepare(`INSERT OR IGNORE INTO ${shard} ...`).bind(...));

      ae.push({
        blobs: [e.event_type, e.creative_id, e.campaign_id, e.publisher_id,
                e.country || "XX", e.device || "unknown"],
        doubles: [e.price_micros || 0, e.fraud_score || 0],
        indexes: [e.publisher_id]    // shard key untuk AE query
      });
    }

    await env.DB.batch(d1);                              // 1 round-trip ke D1
    for (const p of ae) env.AE.writeDataPoint({ dataset: "flio_ad_events", ...p });
    for (const msg of batch.messages) msg.ack();         // explicit ack
  }
};

★ Insight ─────────────────────────────────────

  • INSERT OR IGNORE memberikan idempotency gratis: jika queue retry kirim event yang sama (request_id PRIMARY KEY), tidak akan duplicate.
  • Dynamic shard table ad_impressions_${yyyymm}: SQL injection aman karena yyyymm() hanya menghasilkan digit, tapi pastikan tetap whitelist regex /^\d{6}$/ untuk safety.
  • AE indexes vs blobs: indexes adalah kolom yang bisa di-query cepat di SQL API (pakai untuk filter), blobs cuma untuk display/aggregation. Pilih publisher_id sebagai index karena query terbanyak adalah "show this publisher's earnings". ─────────────────────────────────────────────────

A.5 Llama-3 fraud classifier prompt (production-ready)

Template lengkap untuk fraud Layer 3 batch classifier, return strict JSON:

SYSTEM: You are an ad fraud analyst for flio.net. Return strict JSON only.

USER: Score this ad event from 0.0 to 1.0 for fraud risk.

Signals:
- click_delay_ms, impressions_last_10m_same_ip, clicks_last_10m_same_ip
- conversions_last_24h_same_visitor, ctr_publisher_1h, conversion_rate_publisher_24h
- geo_mismatch, repeated_order_id, [+ event metadata]

Rules:
High fraud if automated click bursts, impossible CTR, duplicate conversions,
mismatched geo, suspicious referrer, click delay below human range, or
repeated visitor/IP patterns.

Return: {"fraud_score": number, "decision": "allow"|"review"|"block", "reasons": string[]}

Run @cf/meta/llama-3.1-8b-instruct dengan signal sebagai context, pakai response_format: {type: "json_object"}.

A.6 Xendit Disbursement dengan Idempotency-key

Header Idempotency-key: flio-payout-{id} mencegah duplicate disbursement jika Worker retry. Ini Xendit best practice yang sering dilupakan:

const res = await fetch("https://api.xendit.co/disbursements", {
  method: "POST",
  headers: {
    "Authorization": `Basic ${btoa(env.XENDIT_SECRET_KEY + ":")}`,
    "Content-Type": "application/json",
    "Idempotency-key": `flio-payout-${p.id}`     // ← critical, prevents double-pay
  },
  body: JSON.stringify({
    external_id: `flio-payout-${p.id}`,
    amount: netIDR,
    bank_code: "DANA",                            // or "OVO", "GOPAY", "BCA", etc.
    account_holder_name: p.publisher_id,
    account_number: p.destination_account,
    description: `flio.net publisher payout net of PPh 23: gross ${p.gross_micros}, tax ${pph23Micros}`
  })
});

PPh 23 deduction inline:

const pph23Micros = Math.floor(p.gross_micros * 0.02);   // 2% NPWP holders
const netMicros = p.gross_micros - pph23Micros;
const netIDR = Math.floor(netMicros / 1_000_000);

Appendix B — Codex CLI Operational Notes

Untuk run codex non-interactive di future:

# WAJIB pakai semua flag ini:
timeout 240 codex exec \
  --skip-git-repo-check \
  --sandbox read-only \
  -c model_reasoning_effort=low \
  "PROMPT" < /dev/null

Tanpa < /dev/null → hang di "Reading from stdin". Tanpa model_reasoning_effort=low → >4 menit thinking phase, timeout sebelum output. Default reasoning effort di codex CLI = xhigh = unusable untuk script.

Save jadi feedback memory: feedback_codex_cli_flags.md.


Appendix C — Moat Strategy (Codex Validated)

Codex (gpt-5.5) validates dan memperkaya 6 moat insight kunci:

C.1 Defensibility Matrix dengan Metric Targets

Moat Score Quantified Target Y1
Network effects 8 publishers ≥ 10K, impressions/mo ≥ 1B, advertisers ≥ 2K
Data 9 ≥ 100M labeled page-ad outcomes, ≥ 10M conversion paths
Cost 7 < $0.03 / 1K ad decisions infra cost
Brand 5 NPS > 50, payout SLA < 7 days
Switching 8 > 60% publishers using 3+ surfaces
Regulatory 6 100% consent logged, brand_safety_score per page
Distribution 9 plugin installs ≥ 25K, active embeds ≥ 100K

Best moat stack (priority order): distribution → data → switching → network effects.

C.2 Browser Rendering sebagai Moat (overlooked)

Cloudflare Browser Rendering memungkinkan Worker:

  • Crawl publisher pages untuk verify SDK install dan placement quality
  • Render screenshot untuk advertiser preview "ini iklan saya tampil di mana"
  • Verify ad placement (di atas fold? viewport visible? brand-safe context?)
  • Generate inventory graph dari setiap publisher page

Kompetitor non-CF tidak punya headless rendering edge-side gratis. Mereka harus run Puppeteer farm = expensive.

C.3 Multi-Surface SDK Lock-in (KEY INSIGHT)

Lock-in datang dari SDK yang mengganti banyak hal sekaligus, bukan cuma ad tag:

type FlioSDKSurface =
  | "native_ad"              // banner replacement
  | "affiliate_autolink"      // detect product names → wrap with affiliate links
  | "related_articles"        // bottom-of-article recirculation
  | "broken_link_replace"     // detect 404s → suggest alternatives
  | "seo_link_exchange"       // editorial network (existing flio Phase 1)
  | "revenue_analytics"       // dashboard sebagai pengganti Google Analytics
  | "content_recommendation"  // homepage related/trending widget

Publisher yang pakai 4+ surface = mengganti 4 produk berbeda kalau pindah:

  • Ad tag (vs AdSense)
  • Affiliate (vs Skimlinks/CJ)
  • Internal linking (vs custom dev)
  • Analytics (vs GA)
  • Content recommendations (vs Outbrain)

Switching cost = financial migration cost × 4 produk × 6-12 bulan retraining historical data.

Roadmap implementasi multi-surface (extend Phase 4-5):

  • Phase 2: native_ad (display + native banner)
  • Phase 3: affiliate_autolink + seo_link_exchange (existing) + revenue_analytics
  • Phase 4: related_articles + content_recommendation (BGE clustering)
  • Phase 5: broken_link_replace (404 detection + auto-suggest)

C.4 Beat AdSense via TOTAL YIELD bukan Display CPM

Defense principle vs AdSense:

AdSense can beat display fill.
flio must beat total page yield:
display + native + affiliate + internal recirculation + SEO link value.
-- publisher-visible KPI
effective_page_rpm =
  display_revenue
+ native_revenue
+ affiliate_commission
+ seo_exchange_value      -- editorial link value (saved cost vs paid backlinks)
+ recirculation_value     -- traffic recovered via related_articles

Publisher pilih flio bukan karena display CPM lebih tinggi (AdSense bisa kalahkan), tapi karena gabungan 5 sumber lebih tinggi total.

C.5 Flywheel Cohort Math (concrete numbers)

Flywheel A — Editorial → Inventory:

100 publishers × 500 pages × 2 outbound links × 0.03 visit/day × 30 day
× 3 pageview/session = 270K incremental ad imps/month

@10K publishers: 27M incremental imps/month

Flywheel B — Data → RPM → Retention (cohort):

M0: 1000 publisher, RPM $0.40
M6: +100M labeled imps → RPM +25% → $0.50
Churn 5% → 3%, referral +8% growth/month

publishers_M12_with_uplift = 1000 × (1 + 0.08 - 0.03)^12 = 1796
publishers_M12_without     = 1000 × (1 + 0.04 - 0.05)^12 =  886
                                                    ─────────
DELTA dari data moat saja              = +910 (2x)

Flywheel C — Affiliate → EPC → Yield:

10M monthly clicks × 2% affiliate CTR = 200K aff clicks
× 3% conv × $30 AOV × 8% commission = $14.4K/month commission pool

Optimasi matching: CVR 3% → 4.5%
Same traffic = $21.6K/month = +50% yield untuk publisher dengan ZERO extra traffic

C.6 Attribution Schema untuk Lock-in (multi-touch)

CREATE TABLE attribution_paths (
  id TEXT PRIMARY KEY,
  conversion_id TEXT,
  visitor_id TEXT,
  ordered_touchpoints_json TEXT,          -- ["pub_123/page_x@imp", "pub_456/page_y@click", ...]
  first_touch_publisher_id TEXT,
  last_touch_publisher_id TEXT,
  assist_publishers_json TEXT,            -- ["pub_789", "pub_111"] dengan share %
  revenue_cents INTEGER,
  model_version TEXT,                     -- attribution model versioning
  created_at INTEGER
);

CREATE TABLE publisher_models (
  publisher_id TEXT,
  model_type TEXT,                        -- "ctr", "rpm", "fraud", "matching"
  version TEXT,
  feature_schema_hash TEXT,
  training_window_days INTEGER,
  metrics_json TEXT,
  vector_namespace TEXT,                  -- per-pub Vectorize namespace
  created_at INTEGER,
  PRIMARY KEY (publisher_id, model_type, version)
);

Why competitor tidak bisa import:

Asset Migration blocker
Page embeddings Need crawl history + lang/content normalization
Slot CTR/RPM by page Requires historical impressions + conversions
Attribution paths Competitor only sees future clicks, not past assist chains
Fraud graph Device/IP/referrer anomalies need long history
Publisher model Trained on exact domain/page/slot/user behavior
Affiliate auto-link map Merchant/category/product matching tuned per site

C.7 Open Source Repo Split (recommended)

flio-sdk-open/                    ← OPEN SOURCE (MIT)
├── packages/embed                 (loader, surface logic)
├── packages/wordpress             (WP plugin)
├── packages/react                 (React component)
├── packages/cf-plugin             (Cloudflare app/plugin)
├── schemas/events.json            (event schema docs)
└── examples/                      (tutorials, demos)

flio-core-private/                 ← KEEP CLOSED
├── auction/                       (auction scoring)
├── attribution/                   (multi-touch model)
├── fraud/                         (graph + heuristics)
├── vector-ranking/                (per-pub training)
├── merchant-graph/                (EPC prediction)
└── pacing/                        (DO BudgetPacer)

C.8 Bottom Line Positioning

flio.net should NOT try to be "Indonesian AdSense" — that's weak positioning.

Stronger position: "Cloudflare-native publisher monetization graph: editorial links + native ads + affiliate attribution + page intelligence, optimized per publisher per page at the edge."

Most defensible 12-month targets:

  • 10K publishers
  • 7K active SDK domains
  • 1B monthly impressions
  • 20M monthly affiliate clicks
  • 5M page embeddings
  • 10M conversion paths
  • ≥3 surface usage per publisher
  • Infra cost <$0.03 / 1K decisions
  • Weekly publisher payout SLA

★ Insight ─────────────────────────────────────

  • Multi-surface SDK insight (C.3) adalah moat terkuat dalam strategi ini. Ini mengubah switching cost dari "ganti 1 tag" jadi "ganti 4-5 produk berbeda + lose 6-12 bulan AI tuning". Sekali publisher pakai 3+ surface, mereka practically tidak akan migrate.
  • Effective Page RPM formula (C.4) adalah angle pemasaran yang benar untuk publisher. Mereka tidak peduli display CPM, mereka peduli total bottom-line revenue per page. flio dengan 5 sumber pasti menang vs AdSense dengan 1 sumber.
  • Cohort math (C.5): data moat saja (RPM uplift 25%) bisa 2x publisher count dalam 12 bulan via lower churn + higher referral. Kompetitor tanpa data moat akan stagnan setelah 6 bulan growth burst.
  • Attribution schema (C.6) attribution_paths dengan assist_publishers_json = anti-competitor weapon. Multi-touch attribution adalah USP yang AdSense+CJ tidak punya. Worth premium pricing di Phase 4+. ─────────────────────────────────────────────────

Architecture: 100% Cloudflare. Zero server dependencies (kecuali japri-wa-relay PM2 untuk WA inbox). Compiled from: Phase 1 PLAN.md/RESEARCH.md, Gemini deep research 2026-05-04, Codex gpt-5.5 ref impl 2026-05-04, /btw brainstorm v2.1, Codex moat analysis 2026-05-04, CF best practices, Indonesian market knowledge.


⚙ HARD CONSTRAINTS (enforced for all sites)

This domain MUST operate within these constraints — no exceptions:

  • 100% Cloudflare serverless — Workers + D1 + R2 + KV + Workers AI + Vectorize. NEVER PM2, NEVER VPS, NEVER Docker in production path.
  • 100% AI-automated — every customer interaction, every moderation decision, every transaction reconcile = AI. No manual queue, no live human chat support, no physical fulfillment.
  • 1-operator solo — one person can run the entire operation from a phone. No team meetings, no shared inbox, no shift rotation.
  • WhatsApp AI bot for all support (24/7, instant response, no SLA promises that need humans).
  • Mayar QRIS for all Indonesian payments (subscription auto-renew, no manual invoicing).
  • Indonesian UI primary — bahasa-first, English fallback only where unavoidable.
  • Privacy — opt-in only, delete-on-request honored within 24h (cron-driven).
  • No physical goods, no inventory — digital products + affiliate referrals only.

If the plan above describes any flow that violates these constraints, treat the plan as ASPIRATIONAL only and rework before building. The constraint trifecta wins.

AI ASSISTANT

Ask AI to research, improve, or generate content.

Try: "Research competitors for this niche"

Actions