Code 100 / subcode 1885183 stopped the connection test after nine consecutive field-mapping fixes, and it is not one: "Ads creative post was created by an app that is in development mode. It must be in public to create this ad." Decoded the access token via /debug_token to confirm which app — MaskanXAds Integration, id 1592299622228272 — rather than guess. Toggling it to Live on developers.facebook.com is the only fix; nothing in MaskanX can do this on the user's behalf. Documented next to the payment-method gate it sits beside in the setup guide. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
14 KiB
Campaign management: setup and operation
End-to-end guide for running Meta ad campaigns from MaskanX, with results reported in Maskan CRM.
Everything below is written to be followed in order the first time. After that, jump to Daily use or the prompt reference.
What the system does
MaskanX Meta
┌────────────────────────────────┐ ┌────────────────────┐
│ draft → approved → synced │───────▶│ campaign (PAUSED) │
│ ↓ │ │ └ ad set (PAUSED) │
│ launch │───────▶│ └ ad (PAUSED) │
│ ↓ │ │ ↓ ACTIVE │
│ guardrails ◀── spend, leads ──│◀───────│ delivery │
└────────────┬───────────────────┘ └────────────────────┘
│ server to server (not MCP)
▼
┌────────────────────────────────┐
│ Maskan CRM │
│ Campaigns: spend, cost/lead │
│ Leads: attributed to the ad │
│ Custom fields ◀── MCP ────────│──── "add a budget range field"
└────────────────────────────────┘
Two connections, deliberately different:
- CRM data flows server to server. Leads and campaign figures arrive on a schedule, whether or not anyone is talking to the agent.
- MCP changes the CRM's shape. Asking in chat for a new field creates it, the way HubSpot custom properties work.
Part 1 — One-time setup
1.1 Databases
# MaskanX
cd maskanx-backend
npm install
npm run local:migrate
npm run local:seed
# Maskan CRM
cd ..\maskan-crm-backend
npm install
npm run local:migrate
npm run local:seed
For running the MaskanX test suite you also need a separate database, so a test run can never touch development data:
CREATE DATABASE campaign_test;
cd maskanx-backend
npm run test:migrate
1.2 Meta credentials
In MaskanX, go to Settings → Environments and set:
| Key | What it is |
|---|---|
META_ADS_ACCESS_TOKEN |
System user token from Meta Business Manager |
META_PAGE_ID |
The Facebook Page ads are published from |
META_ADS_ACCOUNT_ID |
act_..., enables drift reconciliation |
Your current values, for reference:
- Ad account:
act_1015759234692676(Maskan-MetaAds, INR) - Page:
1229386810259840(Maskan Technologies Private Limited)
Two things are still missing on the Meta side, and neither can be fixed from MaskanX:
-
No payment method on the ad account. MaskanX will let you build, approve and sync a campaign, but Launch is refused until you add one in Meta Business Manager — the Marketing API cannot create funding instruments. The Campaigns page shows a warning while this is the case.
-
The Meta App is in Development mode. Every access token belongs to a Meta App — yours is MaskanXAds Integration, app id
1592299622228272. An app in Development mode cannot create ad creatives the public can see, and sync fails with code 100 / subcode 1885183: "Ads creative post was created by an app that is in development mode. It must be in public to create this ad." Fix: https://developers.facebook.com/apps/1592299622228272/settings/basic/ → toggle Development → Live at the top of the dashboard. Meta may first ask for Basic Settings (Privacy Policy URL, icon, category) and Business Verification before it allows the switch. (To find this id for a different token:GET /debug_token?input_token=<token>returnsapp_id— MaskanX has no chat command for this yet.)
1.3 Operator token
Approving and launching are the two actions that lead to money being spent, so they have to be attributable to a named person. Set:
MASKANX_OPERATOR_TOKENS=farman:<some-long-random-string>
Then open the Campaigns page and paste the token part into Operator.
Without this, the operator resolves to unauthenticated — enough to draft
and approve, but launching is refused, because an approval nobody can
be named for does not authorise spend.
1.4 Connect the CRM
In the CRM: Settings → Integrations → create a key. Copy it once; it is not shown again.
In MaskanX Settings → Environments:
| Key | Value |
|---|---|
MASKAN_CRM_API_URL |
http://127.0.0.1:8091 |
MASKAN_CRM_INTEGRATION_KEY |
the key you just created |
1.5 Start everything
cd maskan-crm-backend; npm run local # :8091
cd maskan-crm-frontend; npm run dev
cd maskanx-backend; npm run local # :8088
cd maskanx-frontend; npm run dev
1.6 Check the setup from the browser
Open the Campaigns page. The Setup panel at the top runs every check that can stop a launch and, for each failure, says what to do about it:
| Check | What it means when it fails |
|---|---|
| Meta access token | META_ADS_ACCESS_TOKEN is missing — add it in Settings → Environments |
| Token is valid | Meta rejected the token; generate a new system user token |
| Ad account | META_ADS_ACCOUNT_ID is unset, or the system user cannot reach it |
| Account status | The ad account is disabled or unsettled — only fixable in Business Manager |
| Payment method | No funding source. Add one in Business Manager → Billing |
| Facebook Page | META_PAGE_ID is unset or unreachable. The panel lists the Pages your token can reach, so you can copy the right id |
| Operator identity | MASKANX_OPERATOR_TOKENS is unset, so approvals cannot be attributed and launch stays blocked |
The panel is read-only — it creates nothing in Meta. Press Re-check after changing an environment variable.
Once every check is green, Run connection test becomes available. It
builds a real campaign, ad set and ad in your account, reads back from Meta
that all three are PAUSED, then deletes them. It is the only check that
proves Meta accepts what MaskanX sends. Nothing can be spent — the objects
are paused for their whole, few-second life — and if deletion ever fails,
the ids are shown on screen so you can remove them in Ads Manager.
Nobody needs a terminal for any of this.
Part 2 — Your first campaign
2.1 Build it
Campaigns page → New campaign. The wizard collects objective, budget, targeting, creative and guardrails, and shows a live preview of the ad across six placements before anything exists in Meta.
Budget and guardrails are in minor currency units throughout: 50000
is ₹500.00. Your account's minimum daily budget is ₹97.09.
Guardrails worth setting on the first campaign:
| Field | Meaning |
|---|---|
auto_pause_if_spend_reaches |
Pause once today's spend hits this |
max_campaign_spend + stop_loss_enabled |
Stop permanently at this total |
max_cost_per_lead |
Pause if leads get too expensive |
require_approval_for_budget_increase |
Budget cannot be raised while awaiting approval |
2.2 Approve, sync, launch
- Submit → the campaign moves to
pending_approval - Approve → your operator name is recorded as the approver
- Sync to Meta → creates the campaign, ad set, creative and ad, all paused. Nothing is spending yet.
- Launch → confirms with the daily budget named, then sets the whole chain active. This starts spending.
Launch is refused unless the campaign is synced, the approval names a real operator, and the account has a payment method.
2.3 Watch it
- Campaigns page — status, sync state, spend, leads, cost per lead
- Pause stops delivery; Launch resumes it
- Within 15 minutes, guardrails begin checking; a campaign halted by one is tagged with the rule and the observed value
- Within 2 minutes, changes made in Ads Manager are reflected back
- Within 5 minutes, leads and figures reach the CRM
Part 3 — Daily use
Open the Campaigns page. The analytics panel leads with yesterday and the last seven days: spend, leads, cost per lead, and the most and least efficient campaign.
Both windows end yesterday, not today — today's figures are partial, and including them would make every morning look like a collapse in spend.
In the CRM, Campaigns shows the same figures next to the leads they produced. The leads column shows two numbers when they differ: the CRM's own count, and Meta's attribution behind it. They diverge routinely — Meta attributes late, and leads get entered by hand — and the gap is worth seeing.
Prompt reference
Prompts for the MaskanX chat. The agent reaches Meta and the CRM through MCP.
Checking things
List my Meta ad accounts.
Show me the balance and spend on my Meta ad account.
Check whether the Maskan CRM connection is working.
Which campaigns are live right now, and what has each spent today?
Reporting
What did we spend yesterday, and how many leads did it bring?
Which campaign has the lowest cost per lead over the last 7 days?
Break down last month's spend for the Q3 lead gen campaign by age and
gender.
Adding CRM fields — the MCP path
This is the part that works like HubSpot. Ask in chat, and the field exists in the CRM immediately, on records and in Custom fields.
What custom fields does the CRM have on contacts?
Add a "Budget range" field to CRM contacts, with the options 50-70 lakh,
70-90 lakh, and 1 crore+.
Add a "Site visit done" yes/no field to CRM leads.
Add a "Possession date" date field to CRM leads.
The agent checks what already exists before adding, so it reuses a field rather than creating a near-duplicate under a slightly different name.
Two things cannot be changed after creation: the field's key and its type. Renaming the key would orphan every value already stored under it; changing the type would leave values that no longer satisfy it. The label can be changed freely.
Campaign actions
Money-moving actions are deliberately not exposed as one-shot chat commands — launching goes through the confirmation on the Campaigns page, which names the daily budget. Ask the agent to prepare, and confirm in the UI:
Draft a lead-gen campaign for 3BHK flats in Kochi, ₹800/day, targeting
25-45 year olds in Kerala. Set a stop loss at ₹20,000 total and pause it
if cost per lead goes above ₹1,500.
Configuration reference
All optional; sensible defaults apply.
| Variable | Default | Effect |
|---|---|---|
MASKANX_OPERATOR_TOKENS |
unset | name:token pairs. Unset means launch is refused. |
MASKANX_CAMPAIGN_RECONCILE_SECONDS |
120 | Drift reconciliation with Meta. 0 disables. |
MASKANX_GUARDRAIL_INTERVAL_SECONDS |
900 | Guardrail checks. 0 disables — logged as a warning. |
MASKANX_INSIGHTS_SYNC_SECONDS |
3600 | Insight refresh. 0 disables. |
MASKANX_INSIGHTS_WINDOW_DAYS |
28 | Trailing days re-fetched. Matches Meta's attribution window. |
MASKANX_CRM_SYNC_SECONDS |
300 | CRM push. 0 disables. |
MASKANX_CRM_LEAD_WINDOW_DAYS |
7 | How far back a never-synced campaign reads leads. |
What protects you from spending money by accident
Worth knowing, because the design gives up some convenience for it:
- Every Meta object is created paused. Enforced in three places: the
typed create helpers, the HTTP layer itself, and launch being the only
caller allowed to send
ACTIVE. - Launch is the only action that starts spend, and it is the only one behind a confirmation, which names the daily budget.
- An unattributable approval does not authorise spend.
- Guardrails are deterministic, not an LLM cron job — asking a language model whether a budget has been exceeded would make an arithmetic guarantee probabilistic.
- Meta is always changed before the local record. A campaign recorded as paused but still delivering is the outcome the whole design avoids.
- Deleting a campaign deletes it on Meta first. If that fails, the local row is kept — a campaign forgotten here but still live there would keep spending with nothing to show it exists.
Verifying it end to end
If you are running the system, use the browser. Campaigns page → Setup → Run connection test does everything described below, reports each step on screen, and needs no terminal. See §1.6.
The rest of this section is for developers working on the code.
The unit suite runs entirely against a fake transport: it proves the code
sends what we think it sends, not that Meta accepts it. One opt-in test
closes that gap by syncing a real campaign, reading back from Graph that
all three objects are PAUSED, then deleting them:
cd maskanx-backend
$env:MASKANX_LIVE_TESTS = "1"
$env:MASKANX_LIVE_AD_ACCOUNT_ID = "act_1015759234692676"
npm run test -- tests/test_campaign_sync_live.py
It creates and deletes real objects. They stay paused for their whole life,
so it cannot spend money, and cleanup runs in a finally block.
Routine checks:
cd maskanx-backend; npm run test; npm run lint
cd maskanx-frontend; npm run build; npm run lint
cd maskan-crm-backend; npm run test
cd maskan-crm-frontend; npm run build; npm run lint
If the MaskanX suite reports more than 13 skips, campaign_test is
probably not reachable and the database tests are being skipped silently —
check the skip count, not just the exit code.