
花費欄位顯示 NT$1。廣告狀態:啟用。審核:通過。
技術環境
Node.js 後端整合 Meta Marketing API(Facebook Ads API v21.0)。建立廣告流程:Campaign → AdSet → Ad Creative → Ad。daily_budget 欄位在 AdSet 建立時傳入,同步呼叫,API 回傳 200 即視為成功。問題模式與語言框架無關——任何直接傳入面值整數、未轉換為最小貨幣單位的廣告 API 整合,都會複現相同行為。
不是帳戶餘額不足,不是受眾設定太窄,也不是排程問題。廣告確實在跑,只是每天的預算是一塊台幣。
這有點像在 ibon 訂票,金額欄輸入 1000,系統回傳「訂票成功」——但到了現場才發現票面是 NT$10,因為那個欄位的計量單位是「分」,不是「元」。差異不在邏輯,在單位。
問題出在哪裡
Meta Marketing API 的 daily_budget(以及 lifetime_budget)以最小貨幣單位計算。對台幣來說,1 元等於 100 分,所以 NT$100/天要填的值是 10000,不是 100。
填了 100,系統認的是 NT$1。數字本身是合法值,API 不會報錯,Campaign 正常建立,AdSet 狀態也顯示啟用。錯的不是程式,是你和 API 對「100」這個數字的定義對不上。
這種設計在第三方支付和廣告 API 裡很普遍。Stripe 處理金額也是同樣邏輯,100 代表 $1.00 美元,而非 $100。文件裡通常有一行說明,但在建立整套 Campaign → AdSet → Creative 架構時,那行說明很容易被跳過。
錯誤傳染鏈(時序)
Code Meta API Ads Manager | | | |── POST /adsets ─────>| | | daily_budget: 100 | | | (intent: NT$100) | | |<── 200 OK ───────────| | | id: adset_xxx | | | effective_status: | | | "ACTIVE" | | | |── Campaign runs ──>| | | budget: NT$1/day | | |<── spend: 0.01 ───| Code 判斷:AdSet 建立成功 ✓(status: ACTIVE) 實際執行:NT$1/day(非預期 NT$100) API 未報錯,狀態正常,錯誤靜默存在
API 在 AdSet 建立時已接受該值並回傳 ACTIVE,後續投遞完全依此預算執行,沒有任何系統層的警示觸發。
為什麼沒立刻看出來
因為廣告「看起來正常」。審核通過,狀態啟用,成效頁面也有數字,只是花費異常低。第一反應通常是流量問題、受眾太小,或是廣告素材沒過審——不會是預算填錯單位。
而且系統沒有任何警示。不像填了負數或超出帳戶餘額,系統會攔截;NT$1 的預算是完全合法的廣告設定,系統照單全收。
看著那個低得不對勁的花費數字愣了一下,才去翻 API 文件。然後找到那行說明。
確認方式
最直接:建立一個測試 AdSet,設 daily_budget=100,建立後馬上用 GET 把 AdSet 撈回來,看 daily_budget 欄位是 100,然後在 Ads Manager 介面確認實際顯示的每日預算是多少。如果介面顯示 NT$1,問題確認。
修正就是一行:建立 Campaign 或 AdSet 時,預算數值一律乘以 100 再送 API。想要 NT$100/天,傳 10000。
Code 對照:修法前後
修法前(直接傳面值,API 視為最小貨幣單位):
const adSetParams = {
name: 'Campaign - AdSet',
campaign_id: campaignId,
daily_budget: budget, // ← 問題在這裡:NT$100 傳 100,API 實際視為 NT$1
billing_event: 'IMPRESSIONS',
optimization_goal: 'REACH',
targeting: targetingSpec,
status: 'ACTIVE',
};
const response = await adAccountApi.createAdSet(adSetParams);
修法後(轉換為最小貨幣單位再送出):
const CURRENCY_UNIT_MULTIPLIER = 100; // TWD: 1元 = 100最小貨幣單位
const adSetParams = {
name: 'Campaign - AdSet',
campaign_id: campaignId,
daily_budget: budget * CURRENCY_UNIT_MULTIPLIER, // NT$100 → 10000 ✓
billing_event: 'IMPRESSIONS',
optimization_goal: 'REACH',
targeting: targetingSpec,
status: 'ACTIVE',
};
const response = await adAccountApi.createAdSet(adSetParams);
該被隔離的側效應類型
- 廣告投遞量:預算 NT$1 導致廣告幾乎不參與競價,實際曝光接近零,但 status 顯示 ACTIVE。
- A/B 測試有效性:多個 AdSet 同時使用相同錯誤預算時,測試結果完全失真(所有組別預算皆 NT$1)。
- 競價資格:Meta 廣告競價有最低預算門檻(多數版位約 NT$50/天),低於門檻的 AdSet 被排除在特定競價之外。
- 成效指標計算:CPM、CPC、ROAS 數字因花費接近 0 而出現極端值(分母趨近於零)。
- 演算法學習期:Meta 廣告需要足夠的投遞量才能離開學習期,NT$1 的預算導致演算法永遠無法完成最佳化學習。
- 自動化規則觸發:帳戶設定的自動化規則(如「花費超過 NT$500 暫停廣告」)永遠不會觸發,靜默跳過所有閾值檢查。
- 帳單對帳:帳戶實際扣款與預期嚴重偏離,月底對帳時才發現,追溯窗口已縮短。
- Webhook 事件通知:若整合 Meta Webhooks 監聽 spend_threshold 事件,NT$1 預算永遠不會觸發任何通知。
金額欄位的單位錯誤,影響不只是「花費數字錯了」——所有依賴投遞量的下游系統會同時靜默失效。
留給下次的一件事
任何涉及金額的 API 欄位,在第一次整合時都值得花三十秒確認計量單位——尤其是在測試環境跑過沒問題,正式環境卻出現異常低(或異常高)的數字時。API 不報錯,不代表數值正確。
這種 bug 不會讓服務掛掉,只會讓預算靜靜消失在一個讓人看不出端倪的數字裡。
— 邱柏宇
延伸閱讀
The Ad Ran All Day and Spent One Dollar
The spend column read NT$1. Ad status: active. Review: approved.
Not a budget exhaustion issue. Not too narrow an audience. Not a scheduling conflict. The ad was running — just at one New Taiwan Dollar per day.
It’s a bit like using an ibon kiosk to buy a ticket, entering 1000 in the amount field, getting a confirmation — then arriving at the venue to find a NT$10 ticket, because the field counts in cents, not dollars. The logic is fine. The unit is wrong.
Technical Environment
Node.js backend integrating Meta Marketing API (Facebook Ads API v21.0). Standard ad creation flow: Campaign → AdSet → Ad Creative → Ad. The daily_budget field is passed synchronously at AdSet creation; a 200 response is treated as success. Framework-agnostic issue — any integration that passes a face-value integer without converting to the smallest currency unit will reproduce the same behavior.
What Actually Happened
Meta Marketing API’s daily_budget (and lifetime_budget) is denominated in the smallest currency unit. For TWD, that’s one-hundredths of a dollar — so NT$100/day requires passing 10000, not 100.
Pass 100, the system reads NT$1. The value is legal. The API returns no error. The Campaign builds normally. The AdSet shows active. The mismatch isn’t a bug — it’s a definition gap between what you meant by “100” and what the API means by “100”.
This pattern is common across payment and advertising APIs. The documentation usually mentions it in a single line, easy to skip when you’re wiring together a full Campaign → AdSet → Creative pipeline.
Error Propagation Sequence
Code Meta API Ads Manager | | | |── POST /adsets ─────>| | | daily_budget: 100 | | | (intent: NT$100) | | |<── 200 OK ───────────| | | id: adset_xxx | | | effective_status: | | | "ACTIVE" | | | |── Campaign runs ──>| | | budget: NT$1/day | | |<── spend: 0.01 ───| Code sees: AdSet created ✓ (status: ACTIVE) Actual run: NT$1/day (intended: NT$100) No API error. No status flag. Silent mismatch.
The API accepted the value at AdSet creation and returned ACTIVE; all downstream delivery ran against that budget with zero system-level warning.
Why It Wasn’t Caught Immediately
Because everything looked normal. Review passed. Status showed active. The metrics page had numbers — just suspiciously low spend. The first instinct points to audience size, creative rejection, or delivery competition. Not a unit mismatch in a budget field.
The system offered no warning. NT$1/day is a perfectly legal ad configuration. Unlike a negative value or an over-limit amount, it triggers no error, no flag, no alert. The spend number just sat there, quietly wrong, waiting to be noticed.
It took staring at that number for a moment before going back to the API docs. The explanation was there, one line.
The Fix
One line of code: multiply every budget value by 100 before sending it to the API. NT$100/day → pass 10000.
To confirm: create a test AdSet with daily_budget=100, fetch it back via GET immediately, then cross-check what Ads Manager shows as the actual daily budget. If it shows NT$1, the diagnosis is confirmed.
Code Diff: Before and After
Before (passing face value; API interprets as smallest currency unit):
const adSetParams = {
name: 'Campaign - AdSet',
campaign_id: campaignId,
daily_budget: budget, // ← bug: passing 100 for NT$100, API reads NT$1
billing_event: 'IMPRESSIONS',
optimization_goal: 'REACH',
targeting: targetingSpec,
status: 'ACTIVE',
};
const response = await adAccountApi.createAdSet(adSetParams);
After (converting to smallest currency unit before submission):
const CURRENCY_UNIT_MULTIPLIER = 100; // TWD: 1 dollar = 100 smallest units
const adSetParams = {
name: 'Campaign - AdSet',
campaign_id: campaignId,
daily_budget: budget * CURRENCY_UNIT_MULTIPLIER, // NT$100 → 10000 ✓
billing_event: 'IMPRESSIONS',
optimization_goal: 'REACH',
targeting: targetingSpec,
status: 'ACTIVE',
};
const response = await adAccountApi.createAdSet(adSetParams);
Side Effects That Should Be Isolated
- Ad delivery volume: NT$1/day causes the AdSet to barely compete in auction; effective impressions approach zero while status shows ACTIVE.
- A/B test validity: If multiple AdSets share the same mis-converted budget, all variants run at NT$1 — test results are meaningless.
- Auction eligibility: Meta enforces minimum daily budget thresholds (roughly NT$50/day for most placements); sub-threshold AdSets are excluded from certain auctions entirely.
- Performance metric calculations: CPM, CPC, and ROAS figures become extreme outliers when spend approaches zero (division by near-zero denominator).
- Algorithm learning phase: Meta’s delivery algorithm requires sufficient impressions to exit the learning phase; NT$1 budgets prevent optimization from ever completing.
- Automated rules: Account-level automation rules (e.g., “pause if spend exceeds NT$500”) never trigger; all threshold checks are silently bypassed.
- Billing reconciliation: Actual account charges diverge significantly from expected; by the time the discrepancy surfaces in monthly billing, the attribution window has narrowed.
- Webhook spend threshold events: If Meta Webhooks are integrated to listen for spend_threshold events, NT$1 budget means no notifications ever fire.
A unit mismatch in one monetary field doesn’t just produce a wrong spend number — it silently disables every downstream system that depends on actual delivery volume.
One Thing Worth Noting Next Time
Any API field that accepts a monetary amount is worth a thirty-second unit check on first integration — especially when a test environment passes cleanly but production shows anomalously low or high numbers. No error from the API doesn’t mean the value is correct.
This kind of bug doesn’t crash anything. It just lets money disappear into a number that looks just plausible enough not to trigger immediate suspicion.
— 邱柏宇
Related Posts
https://justfly.idv.tw/s/1koRq2n