# 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](#daily-use) or the [prompt reference](#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 ```powershell # 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: ```sql CREATE DATABASE campaign_test; ``` ```powershell 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:** 1. **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. 2. **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=` returns `app_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: ``` 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 ```powershell 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 1. **Submit** → the campaign moves to `pending_approval` 2. **Approve** → your operator name is recorded as the approver 3. **Sync to Meta** → creates the campaign, ad set, creative and ad, **all paused**. Nothing is spending yet. 4. **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: 1. **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`. 2. **Launch is the only action that starts spend**, and it is the only one behind a confirmation, which names the daily budget. 3. **An unattributable approval does not authorise spend.** 4. **Guardrails are deterministic**, not an LLM cron job — asking a language model whether a budget has been exceeded would make an arithmetic guarantee probabilistic. 5. **Meta is always changed before the local record.** A campaign recorded as paused but still delivering is the outcome the whole design avoids. 6. **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: ```powershell 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: ```powershell 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.