Editorial Link Exchange + Ad Network + Affiliate / Lead Network 100% Cloudflare. Indonesia-first, global-ready. Last updated: 2026-05-04
flio.net adalah satu-satunya platform di Indonesia yang menyatukan tiga jaringan dalam satu inventory:
Differensiasi inti:
request.cf.city)flio-link-exchange di flio.netlink-exchange-db (e0cb423a-43f6-4a8a-a0f0-19e10bfff954) — sites/pages/link_log/crawl_log/api/register, /api/links, /api/dashboard, /api/statsUrutan 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 2name = "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
]
| 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 | 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) |
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:
★ Insight ─────────────────────────────────────
───────────────────────────────────────────────── ┌─────────────────────────┐
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
| 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.
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
_idruntuk readability dokumentasi. Implementasi final pakai_micros(Codex pattern). Cross-table FK dari ads ke affiliate digabung dalam satu DBflio-platform-db.
link-exchange-db (existing — keep as-is)sites, pages (BGE embeddings), link_log, crawl_logflio-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)
);
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 ─────────────────────────────────────
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.─────────────────────────────────────────────────// 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 ─────────────────────────────────────
─────────────────────────────────────────────────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());
}
}
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);
}
Pakai Cloudflare for SaaS: setiap publisher bisa CNAME t.publisher.com → flio.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.
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
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
// 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
// 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}`);
}
}
3 dashboard utama, semua di Worker (server-rendered HTML, no SPA):
flio.net/p/)flio.net/a/)flio.net/admin/)Mobile-first per CLAUDE.md design standards: bottom nav, horizontal swipe carousel untuk multi-campaign view, 48px touch targets, dark mode default.
Pakai existing japri-wa-relay PM2 service untuk inbox.
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"
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 ─────────────────────────────────────
─────────────────────────────────────────────────| 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 |
Setiap phase punya go/no-go gate sebelum lanjut ke phase berikut:
editorial+hybrid modeis_billable=0 setelah Layer 3)Status di bd: 5 sub-issues created (P1)
flio-ads-db D1, schema lengkap, seed test data/api/ads/serve, /api/ads/click, embed.js minimalflio-ads-vectors, sync creative embeddingsflio-freqcap namespace, 24h TTLad-events queue, consumer batchGoal: 5 advertiser internal (test campaigns dari sites kita sendiri seperti dapur.org, dokterdewa.com), 24 publisher tayang, 100K impressions/hari.
Status di bd: 4 sub-issues created (P1-P2)
flio-affiliate-db (merchants, offers, tracking_links, conversions, payouts)/go/{code} redirect dengan click logging/api/postback dengan HMAC signature verifyGoal: 10 merchant onboarded (target: Tokopedia, Shopee, Blibli, Tiket.com via affiliate program), 1000 publisher konversi.
Status di bd: 2 sub-issues created (P2)
request.cf.city8 ide kuat yang ditemukan saat brainstorm tambahan, didistribusikan ke Phase yang relevan:
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
Cron daily compute current_season flag. Saat aktif:
// 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 }
};
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.
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
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
);
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.
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.
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:
leads table, post via webhook ke advertiserCREATE 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 ─────────────────────────────────────
─────────────────────────────────────────────────| 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).
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)
| 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 |
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.
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")
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")
flio.net TONE: profesional, ramah, transparan, no-bullshit. Bahasa Indonesia native (bukan terjemahan kaku).
HINDARI:
PAKAI:
Specific tone patterns:
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 PribadiID-UU-1-2024-ITE — UU ITE perubahan kedua 2024ID-PP-71-2019-PSTE — PP Penyelenggaraan Sistem & Transaksi ElektronikID-UU-8-1999-KONSUMEN — UU Perlindungan KonsumenINT-UNCITRAL-MLEC — UNCITRAL Model Law on Electronic CommerceSnapshot bundle sudah disimpan di: /home/ucok/web/flio.net/legal/bundle-flio.json
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
});
}
| 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 |
UU di Indonesia berubah cukup sering (UU ITE 2024 perubahan kedua, UU PDP 2022 baru efektif 2024). arulez.com API terus update sumber hukum.
legal:bundle:v1, legal:bundle:v2, simpan history di R2 untuk audit| 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.
/site-optimize — full-stack optimization checklist/google-search-console — submit flio.net/a, /p sitemap/seo-boost — backlink blitz untuk akuisisi awal publisher/agent-comm — koordinasi dengan agent lain (sewakamar, dokterdewa untuk seed advertiser)feedback_design_standards.md, feedback_image_cdn_mandatory.md, feedback_wa_relay_*, cf_deployment_policy.mdreference_wsrv_image_cdn.md (untuk creative thumbnails)| 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 |
| 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. |
cf_migration_complete.md) = playbook deploy & ops sudah teruji.| 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 |
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
embed.js / SDK — open source MIT license. Strategis karena:
Backend stays closed-source. Schema evolution + AI training data + fraud heuristics = closed.
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 ─────────────────────────────────────
─────────────────────────────────────────────────| 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 |
Codex CLI (gpt-5.5) menyumbang 6 pola implementasi clean yang akan diadopsi saat coding Phase 2:
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).
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
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
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.ad_impressions_${yyyymm}: SQL injection aman karena yyyymm() hanya menghasilkan digit, tapi pastikan tetap whitelist regex /^\d{6}$/ untuk safety.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".
─────────────────────────────────────────────────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"}.
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);
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.
Codex (gpt-5.5) validates dan memperkaya 6 moat insight kunci:
| 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.
Cloudflare Browser Rendering memungkinkan Worker:
Kompetitor non-CF tidak punya headless rendering edge-side gratis. Mereka harus run Puppeteer farm = expensive.
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:
Switching cost = financial migration cost × 4 produk × 6-12 bulan retraining historical data.
Roadmap implementasi multi-surface (extend Phase 4-5):
native_ad (display + native banner)affiliate_autolink + seo_link_exchange (existing) + revenue_analyticsrelated_articles + content_recommendation (BGE clustering)broken_link_replace (404 detection + auto-suggest)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.
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
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 |
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)
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:
★ Insight ─────────────────────────────────────
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.
This domain MUST operate within these constraints — no exceptions:
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.
Ask AI to research, improve, or generate content.
Try: "Research competitors for this niche"