new-term-radar
Daily radar for new AI terms: names of new models, products and repos, caught on X, Hugging Face, GitHub and sitemaps before keyword tools list them.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證AI BUILDER
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
MAIRO is an ad-management platform for business owners who don't want to learn
Meta Ads. A client sets a goal and a monthly budget; MAIRO generates a monthly
strategy, launches and manages the campaigns on their connected Meta ad
account, and gives them AI specialists (Strategist, Creative, Support) to talk
to. The owner gets a separate AIOS dashboard (/aios) to run every client
account, manage the creative pipeline, and use an internal Claude copilot.檢視完整 README ↗MAIRO is an ad-management platform for business owners who don't want to learn
Meta Ads. A client sets a goal and a monthly budget; MAIRO generates a monthly
strategy, launches and manages the campaigns on their connected Meta ad
account, and gives them AI specialists (Strategist, Creative, Support) to talk
to. The owner gets a separate AIOS dashboard (/aios) to run every client
account, manage the creative pipeline, and use an internal Claude copilot.
@prisma/adapter-pg)OWNER vs CLIENT rolesai + @ai-sdk/anthropic) for the
monthly plan generator and the streaming agent chatnpm install
Create a free Postgres database — Neon is the easiest —
and copy .env.example to .env, filling in DATABASE_URL.
cp .env.example .env
Then run migrations:
npm run db:migrate
AUTH_SECRETnpx auth secret
Paste the result into .env as AUTH_SECRET.
Create a Web application OAuth client at
https://console.cloud.google.com/apis/credentials with the authorized redirect
URI https://<production-domain>/api/auth/callback/google (and
http://localhost:3000/api/auth/callback/google for local development), then set
AUTH_GOOGLE_ID and AUTH_GOOGLE_SECRET. With either unset the button is hidden
and the provider isn't registered.
A first-time Google user gets the same records as the email sign-up — a business
and a CLIENT, or from /for-freelancers a workspace and a FREELANCER — and
the business or studio name is still asked for, since it can't be changed later.
A Google login links to an existing email account only when Google marks the
address verified, and never creates a second user for one address. The rules are
in src/lib/google-sign-in-rules.ts; npm run check:google tests them, and the
database side too when DATABASE_URL points at a local Postgres.
Set ANTHROPIC_API_KEY in .env. Without it, onboarding still works but the
generated monthly plan falls back to a placeholder, and the AI agent chats
will error.
Client sign-up and the dashboard work without this — you'll just see a "connect Meta" prompt that fails until it's configured. To make it real:
.env as
META_APP_ID / META_APP_SECRET.http://localhost:3000/api/meta/callback; in production, your real
domain's equivalent. Set the same value in .env as META_REDIRECT_URI.ads_management, business_management, pages_show_list and
pages_read_engagement under App Review — Meta has to approve these before a
real (non-admin/tester) user can connect an ad account. ads_read isn't
needed and isn't requested: ads_management already covers reading ad
accounts, campaigns and Insights (Meta approved the four above on
October 7, 2026 and declined ads_read). This review can
take from a few days to a few weeks; while it's pending you (and any users
added as testers/admins on the app) can already connect and test.MAIRO runs Facebook and Instagram ads through Meta, and nothing else. TikTok
was built once and retired; its code is gone, but the TIKTOK enum value and
the columns and tables that referenced it are kept so existing rows still load
(the database is never dropped out from under old records). A campaign left
over from TikTok shows in the list but is not fetched or managed.
ROAS was a dash on every dashboard before this, and the reason was never said
out loud: without a pixel the ad networks do not know anybody bought anything,
so they report no revenue and there is nothing to divide by. /dashboard/tracking
is where a customer fixes that, and it has two halves that do different jobs.
The pixel tells Meta that a sale happened. MAIRO creates it on the customer's own ad account through the Marketing API, adopting one that is already there in preference to making a second — a business that has advertised before usually has a pixel with months of history, and splitting that in two makes both halves look worse. The status shown is never "we created it": it is read back from the network's own record of when it last saw an event, because a pixel that exists and has never fired looks identical to a working one and the difference is months of fictional reporting.
Nothing here needs configuring on the deployment. It uses the advertising connection the customer already made.
The order feed tells MAIRO what was actually sold. Each organization gets
a URL at /api/orders/<token> that their shop posts to — Shopify's order
webhook, a WooCommerce webhook, or anything that can POST JSON. Those orders
are relayed server-side to Meta's Conversions API,
which recovers most of the conversions the browser loses to ad blockers and
iOS privacy settings, and they are kept as the record the networks' own claims
are checked against.
Two details in there are load-bearing:
'mairo_' + orderId with unsafe characters stripped). It is not
a hash for exactly that reason. If the snippet on the thank-you page and the
relay from here cannot arrive at the same string, neither network dedupes,
every online sale counts twice and ROAS doubles — which is the kind of wrong
number a customer increases their budget on.src/lib/tracking/hash.ts carries the
normalization rules both networks publish; getting one wrong does not match
worse, it matches nobody, and neither end raises an error.Where the customer gives MAIRO their Shopify signing secret it is stored encrypted and every delivery must carry a valid signature. A store that sends one and fails it is rejected rather than falling back to the token — a signature going bad means something is wrong.
npm run check:tracking covers the money conversion, the hashing, the event-id
agreement between browser and server, and the ROAS comparison, including the
division-by-zero cases that would otherwise show a customer "Infinity".
Two problems sit between "here is your pixel code" and a business measuring
anything, and /dashboard/tracking now solves both.
Installing it. Done properly, a pixel means a base tag on every page, a purchase tag on the confirmation page only, a click trigger on the phone number, a form trigger, and each pointed at the right standard event. That is an afternoon for somebody who knows Google Tag Manager and impossible for somebody who does not — which is every customer MAIRO has. So MAIRO generates a GTM container file wired to their own pixel ids, and GTM's Import Container creates the lot in one screen. Nothing to configure on the deployment; it uses the pixels the account already has.
The importer is stricter than it looks in one way and looser in another. It
validates structure but not sense: a tag whose firingTriggerId names a
trigger absent from the file imports cleanly and then never fires, with no
error anywhere. npm run check:gtm asserts there are no dangling references,
that ids are unique, and that every {{variable}} a tag uses is declared.
The tags are Custom HTML rather than GTM's built-in Meta template, because the
built-in one cannot set an event id — and without one, the browser's copy of a
sale and the copy MAIRO relays server-side are counted as two sales. The
generated expression is asserted to produce the same string as deriveEventId.
Knowing what to install. "Track Purchase" is wrong advice for a plumber:
there is no checkout and never will be, so the container would fire nothing
while looking installed. src/lib/tracking/niches.ts is a catalogue of what
each kind of business actually converts on — a table booking for a restaurant,
a tap on the phone number for a trade, a free trial for a gym — with the Meta
standard event each maps to. An event outside Meta's standard list cannot be
optimized towards and never appears in the conversion column, so the check
script validates every one against the published lists.
The niche is guessed from the industry text typed at signup, shown to the
customer as a guess, and stored on TrackingProfile once they confirm or
change it. Matching is word-boundary-aware with a short suffix list — plain
substring matching put "hair salon and barber" in the restaurant niche because
"barber" starts with "bar", which would have given a barber shop a
table-reservation trigger and no booking event, with nothing in the product
looking wrong. Both that and the "coffee shop" collision are regression cases.
Exactly one action per niche is marked primary: it is what the campaign optimizes towards, so two would leave the product unable to answer "which one" and none would make it silently pick the first.
The container file is one import away from done. GOOGLE_CLIENT_ID and
GOOGLE_CLIENT_SECRET remove that step: MAIRO connects to the customer's
Google account, writes the tags into their container over the Tag Manager API
and publishes them. Leave the variables blank and the card says it is not
switched on and points at the download, which is not a degraded path — it
produces the identical container from the same buildContainer().
Setting it up needs a Google Cloud project with the Tag Manager API enabled and a Web application OAuth client. The scopes are sensitive, so Google verifies the consent screen before anyone outside the project's test users can connect.
Five things in provision.ts are load-bearing and none of them are obvious:
{{Click URL}} in a
container where that built-in is off resolves to nothing, never matches and
never fires — with no error at any point. This is the likeliest way for a
"successful" provision to do absolutely nothing.MAIRO - name prefix, and nothing without it is ever
touched. Tags are deleted before triggers, because Tag Manager refuses to
delete a trigger a tag still fires on.The provisioning order was verified end to end against a mock Tag Manager API, asserting that built-ins precede triggers, that every trigger precedes every tag, that the created tags reference server-assigned ids rather than the blueprint's local ones, and that a second run deletes before it recreates. There is deliberately no base-URL override in the shipped client: an environment variable that redirects where a customer's Google bearer token is sent is not worth the testing convenience.
Until this, launchOne called createCampaign and stopped. On Meta a
campaign with no ad set and no ad cannot serve a single impression — so every campaign MAIRO had ever launched was an empty shell,
and the dashboard showed it as created. That is the worst shape a gap can
take, because it looks finished.
A launch now builds the whole hierarchy: campaign, then ad set, then ad. Three things had to be fixed to make that possible, and each would have failed every launch on its own:
daily_budget, and
createAdGroup set one as well. Meta rejects an ad set budget under a
campaign that has one. Each adapter now declares a budgetLevel — campaign
for Meta — and the launcher obeys rather than guessing.LEAD_GENERATION was the wrong optimization goal. It means one of Meta's
instant forms, which lives on Facebook and which MAIRO never creates; asking
for it on a campaign that sends people to a website produces an ad set that
cannot deliver. A website lead is an offsite conversion, and only when
there is a pixel to count it.promoted_object
naming it. Without one it falls back to LINK_CLICKS, because optimizing for
purchases on an account that has never reported one is accepted by Meta and
then under-delivers indefinitely. This is where the tracking work earns its
place: the pixel is what makes "optimize for sales" mean anything.PlatformCampaign gained externalAdGroupId and externalAdId so the record
says how far a launch got. The campaigns page reads them and says plainly when
a campaign cannot show to anyone — a campaign with no ad looks identical on
that page to one that works.
The ad itself is three dependent Meta calls in src/lib/meta/creatives.ts:
/adimages with the raw base64 (the data: prefix has to go, or Meta accepts
a file that then renders blank), /adcreatives with the hash and the Page, and
/ads. Everything lands PAUSED. Meta's automatic creative variations are
opted out of: the customer approved one picture and one line, and running
something else would make that approval meaningless.
The copy comes from the concept the Creative agent wrote, parsed by
creative-copy.ts. That parser never invents — a concept whose headline cannot
be found returns null and the ad is refused, because the alternative is an ad
running on the customer's money with a placeholder in it. It is a line scanner
rather than a regex for a reason worth remembering: the regex version used
\z for end-of-string, which JavaScript does not have (it matches a literal
"z"), so the last section of every concept silently failed to parse.
src/lib/score/, Create → AI Review). A 0–100 score
of campaign preparation from 15 checks, grouped into Creative, Hook, Offer,
Audience, Landing Page and Campaign Setup — and an explanation of every point it's missing.
improveOptions in
src/lib/ai/ad-score.ts; a number the facts don't contain drops the option). Nothing changes
until the customer picks one; I'll Edit It opens the step; Keep Current Version sets the
area aside without a recheck. Offers are never forced to be discounts — ideas are a free
estimate, consultation, trial, bundle, guarantee… by kind of business, and only confirmed
offers go in an ad.src/lib/score/questions.ts): a few questions at a time,
personal to the business (a restaurant is asked which dish to promote, a contractor about free
estimates, software about a trial), each with "Why MAIRO is asking". Business Profile
answers (what makes you different, who buys, results, objections, what you have for ads) are
saved to the Business Brain once and later asked as "We currently have … — is that still
correct?". answers (this promotion, when it ends, limited stock) stay on the
campaign () and never become business facts. Answering never changes the score
by itself.Decisions are network-agnostic. A change names its platform and that
platform's adapter carries it out. pauseAd and updateAdGroupTargeting are
optional adapter methods, and rules that need them are skipped where an
adapter doesn't have them.
One insight engine (src/lib/intelligence/) feeds six dashboard features. It
runs at the end of each Decisions refresh, on the same snapshot.
Insights (detect.ts, table MairoInsight). Every decision rule's
finding becomes a normalized Insight linked to its decision by dedupe key.
Early warnings the rules don't cover are added on top:
Each Insight carries what happened, why it matters, the recommendation and
reason, what approving does, its evidence and what it's based on, a severity
and a confidence. Asserted in npm run check:intelligence.
Business Health (score.ts, /dashboard/health). Five areas start at
100 and lose points only for a named finding. An area with nothing to judge
shows "Not enough data yet", never a number.
Opportunity Radar (score.ts). Six areas rated from the open Insights.
No revenue predictions.
Morning Brief (brief.ts). Yesterday or the last 7 days
(Organization.briefFrequency), yesterday's winning ad, the top two
actions, and what Mairo is watching. It's cached in IntelligenceReport
and is ready to feed an email or push later.
Mairo found this before you did. The Insights marked as early warnings, with Fix with Mairo, Show me why and Dismiss.
Campaign Journey (timeline.ts). Built from rows that already exist:
the campaign, its ads, decisions, activity, insights, Spend Protection and
the old optimizer. Shown on the dashboard and on a campaign's Timeline tab.
Profit First (profit.ts, table ProfitSettings, Product.costCents).
The dashboard's third view (cookie mairo_lens). Profit is always
"estimated" and unknown until a margin is given. Break-even ROAS is 1 ÷
(margin − fees − per-order costs ÷ AOV).
The Weekly Report (src/lib/reports/weekly.ts, table WeeklyReport) is a
summary layer over the systems above. It adds no detection of its own. Each
section comes from an existing system:
The judgement is pure (weekly-logic.ts, asserted in
npm run check:weekly-report):
The summary is written by the AI from the week's facts and falls back to a plain version without an API key.
MairoLearning, shown on Business Brain). Saved
lessons feed the assistant's prompt and new ad versions
(src/lib/reports/learnings.ts). Any lesson can be switched off./api/cron/review writes due reports on each business's
chosen day, in its own time zone. Opening Reports writes one if the cron
hasn't reached the business yet.
WEEKLY_REPORT notification./dashboard/settings/reports./r/<token>, client-facing, internal notes hidden by default). Branding
comes from the agency workspace's report settings. Nothing is sent to
clients automatically./dashboard/reports/monthly.One journey from sign-up to a live first campaign. The free stage creates the strategy; the paid stage activates it.
Free (before payment) — /plan
DRAFT / REVISING / APPROVED (StrategyPlan); approval stores approvedAt, approvedVersion and an untouched approvedSnapshotJson. Any change before payment returns it to draft./dashboard/* redirects to /plan or /plan/activate (billing and the launch checklist stay reachable). /plan/activate shows the approved summary, the plans, and a sample-data demo. While BILLING_ENFORCED is off, it says so and continues without payment.Paid — /dashboard/launch
/dashboard/launch?subscribed=1 (it waits honestly if the webhook hasn't arrived).MairoCampaign.launchApprovedAt), so a campaign still in Meta's review goes live when it clears.Mairo runs Meta only, so the plan uses Facebook and Instagram. Freelancer client workspaces keep the previous onboarding path. npm run check:strategy-plan checks the plan rules.
Paid line (updated) — FREE shows what Mairo would do; PAID is Mairo doing it.
/plan/activate: plan details, "Your Mairo Setup" summary, Continue to Payment) → Stripe → "Welcome to full Mairo" → Build My Campaign → paused build → final review → Launch Campaign.Organization.paymentRequired. For them, paid execution needs a live subscription (active, or trialing — a card is collected at checkout) whatever BILLING_ENFORCED says. Older accounts, including the Meta App Review account, still follow BILLING_ENFORCED.src/lib/billing/execution.ts): the Meta adapter refuses create campaign / ad set / ad, budget, schedule, targeting and switch-on writes, and the build, launch, resume, Decisions, optimization, creative-generation and publishing actions refuse too. Pausing is never blocked.trialDays on the plan in src/lib/plans.ts, read by checkout and every line of copy through trialDaysFor). Growth, Scale and the freelancer plans have no trial and are billed from the day they subscribe.Organization.hasPaid makes sure paying customers are never affected.npm run check:payment-gate checks the lock.Public sign-up always creates a CLIENT account (a business owner). To get
into the AIOS dashboard at /aios, you need one OWNER account. Two ways:
/setup. It only works once — the first account created there
becomes the OWNER, and the page redirects to /sign-in for everyone after
that. Do this immediately after your first deploy, before sharing the URL.[email protected] OWNER_PASSWORD=a-strong-password npm run db:seed
npm run dev
/ — marketing landing page/sign-up, /sign-in — client auth/onboarding — goal + budget intake, generates the first monthly plan/dashboard — client dashboard (plan, campaigns, creatives, Meta
connection, AI specialists)/aios — owner dashboard (all organizations, creative pipeline, internal
copilot) — requires an OWNER accountprisma/schema.prisma Data model (orgs, plans, campaigns, creatives, agent threads)
src/lib/auth.ts Auth.js config (credentials + Google providers, JWT session)
src/lib/db.ts Prisma client (pg driver adapter)
src/lib/meta/ Meta Graph API client, OAuth flow, campaign calls
src/lib/ad-platforms/ One interface per advertising network (see below)
src/lib/budget/ Budget splitting, and the optimizer's guardrails
src/lib/entitlements.ts What each plan allows — the only place that decides
src/lib/ai/ Claude model config, agent system prompts, plan generator, chat threads
src/lib/actions/ Server actions (auth, onboarding, campaigns, creatives, plan, AIOS)
src/app/(auth)/ Sign in / sign up
src/app/onboarding/ Client intake wizard
src/app/dashboard/ Client-facing app
src/app/aios/ Owner-facing app
src/app/api/meta/ Meta OAuth connect/callback routes
src/app/api/agents/chat/ Streaming Claude chat endpoint (used by both dashboards)
Meta is reached through one interface, AdPlatformAdapter in
src/lib/ad-platforms/types.ts. Nothing above that layer knows which network
it is talking to: the campaign form renders from selectablePlatforms(), the
dashboard groups by whatever platforms a campaign has, and the optimizer
compares whatever it is given.
Adding Google Ads is therefore three things and no more:
AdPlatform enum (a migration, but a one-line one).src/lib/ad-platforms/ implementing the interface.src/lib/ad-platforms/registry.ts.implemented in the registry is deliberately separate from the enum, so a
half-finished adapter can be developed without being offered to customers, and
a network can be retired without orphaning the rows that reference it.
runAutoOptimize is ready for
a cron to call and enforces every limit the customer set, but nothing calls
it on a timer yet — an unattended job that moves money should be switched on
knowingly, once the guardrails have been watched working on real campaigns.src/lib/plans.ts are only what the pricing page displays. What a customer
is charged comes from the Stripe Price behind each STRIPE_PRICE_*
variable, so changing one without the other makes the page lie.CreativeRequest is a queue your team (or the AIOS dashboard) works from
manually today.Premium dark design built only from Mairo's own UI, charts, icons and platform logos — no people or stock photos. Sections: hero with a floating sample dashboard and AI callouts, platform bar (only platforms Mairo actually works with; product statements instead of customer counts), How It Works (free plan → approve → connect → subscribe → build → approve → launch), business-type selector, the free-plan demo ("strategy only"), Mairo Decisions, Business Health, Simple / Advanced / Profit First preview, pricing, FAQ and the final call. Every figure is marked as sample or demo data. All calls to action go to /sign-up, which starts the free plan.
Before anything is added to a Simple screen, ask: does the customer need to see this immediately? Powerful underneath, simple on the surface. Advanced view (the Simple/Advanced toggle) still shows everything.
go_to tool can only return pages from a fixed list (DESTINATIONS), shown as a button.app/dashboard/simple-overview.tsx, six cards in reading order (the phone's order too): Your goal (Change goal and Tell Mairo open modals), This month (four numbers chosen by the goal — sales: Money spent, Revenue, Sales, Cost per sale; leads: Money spent, Leads, Cost per lead, Contact actions; bookings: Money spent, Bookings, Cost per booking, Booking leads; never CPM, CTR, CPC, frequency, impressions or reach; unknown is "—", never 0), Mairo is working on (each row opens its page), Needs your attention (only real items, at most three, one action each — recommendations open in a drawer; otherwise "✓ Everything is running normally."), one Mairo insight with See why, and What's next (Today / Tomorrow / weekday). With no goal, plan or campaign: "Let's grow your business." and five starting points./dashboard/creatives, lib/creatives/hub.ts): Active, New (Ready to review / Approved / Draft, with Review & approve, Edit, Regenerate, Use in campaign), Past (search and status filter) and Top Performing, ranked by the goal's result at the lowest cost — purchases for a sales goal, never likes or clicks; ads with no result aren't ranked. Each card opens a drawer. + Create new creative opens the AI Creative Studio, which is unchanged; the photo-to-ad tool moved to /dashboard/creatives/requests.MAIRO is an AI marketing manager: the business says what it wants to achieve and MAIRO decides the marketing. Everything runs through one loop — goal → understand the business → strategy → create → approve → launch/publish → measure → learn → improve — centred on /dashboard/mission (reached from the Overview's goal card) and the Overview.
lib/mission/goals.ts reads requests without AI; lib/mission/planner.ts uses AI when available.marketingObjective on campaigns, creatives and social posts: Awareness, Education, Trust, Consideration, Lead generation, Conversion, Booking, Retention, Promotion, Product launch) and a reason.resultsForGoal): sales → revenue, purchases, cost per purchase, ROAS; leads → leads, cost per lead, calls/messages; bookings → Schedule events; traffic → landing page views; awareness → reach, impressions, video views. Untracked figures say "Not tracked yet" — clicks are never shown as leads, engagement never as sales. Meta's leads, Schedule, Contact/messaging and landing-page-view actions are now read.One persistent understanding of each business that every part of MAIRO reads — campaign creation, ad copy, creative concepts, the Strategy Engine and mission planner, the Campaign Review, Social Manager (Scale), the assistant and reports. Not a memory per feature: they all read loadBrainState / brainPrompt (src/lib/brain/store.ts), and every change goes through changeBrain (src/lib/brain/edit.ts).
BUSINESS BRAIN (what MAIRO knows) → MISSION (what the business wants) → STRATEGY → EXECUTION
→ RESULTS → LEARNING (engine + weekly reports save learnings) → BUSINESS BRAIN updated → repeat
src/lib/brain/catalog.ts): business basics, product and service records (price, kind, priority, profitability, availability, current goal — priority and margin only when the owner says), customers (ideal customer, types, problems, motivations, objections, purchase considerations — no assumed demographics), differentiators and standing offers, brand (voice, visual direction, creative style, words and claims to avoid). Every field states how it improves marketing; anything without a marketing purpose is refused./dashboard/settings/business-brain): "What MAIRO knows about your business" in plain sections — Your Business, Your Goals (with goal history), Products & Services, Customers, What Makes You Different, Brand, Right Now, What MAIRO Has Learned (🔥 confident / 💡 still learning, with See why), Used to be true, Business History. Each fact has Correct · Update · Remove; products have Priority, High margin, Mark unavailable. "MAIRO knows your business: 44%" by area, never required to be 100.Eight specialties of one AI system — not eight people or eight running processes. Each is a set of MAIRO's existing services (src/lib/team/agents.ts), and every piece of work any of them does is recorded as an AgentRun by the service that did it. The AI Team screen, its activity feed, the Daily Brief and the assistant's get_ai_team tool read only those records and the decisions they link to.
| Specialty | Runs on | Records |
|---|---|---|
| Strategy Agent | Business Analyzer, Advertising Plan, Strategy Engine; coordinates the daily review | read-website, write-plan, team-review |
| Audience Agent | Plan audience, audience decisions | plan-audience, recommend |
| Creative Agent | Ad concepts (with policy check), Creative Studio, ad versions | ad-concept, studio-image, recommend |
| Campaign Agent | Campaign builder, launch | build-campaign (built switched off), launch (after Meta accepted) |
| Optimization Agent | Mairo Decisions rules | find-improvements, recommend, auto-apply, apply-approved |
| Budget Guardian | Spending limits, Spend Protection, Meta billing check | check-limits, check-approval, spend-check |
| Analytics Agent | Mairo Intelligence, weekly reports | read-results, daily-brief, weekly-report |
| Growth Advisor | Strategy Engine growth moves | recommend |
refreshDecisions, src/lib/team/review.ts) is the orchestration: Analytics reads the results → the Optimization, Creative, Audience and Growth specialties propose (each decision belongs to the one that noticed it, agentForDecision) → the Budget Guardian checks every budget change against the customer's limits, the same check approval repeats → changes the customer's automation level allows are made (recorded only after Meta accepts) → Analytics writes the Daily Brief. Each step is a child run of the Strategy Agent's review. An account with nothing live gets no review records. Deterministic; no extra AI calls.src/lib/team/status.ts): Working only while a run is in progress (a run older than 30 minutes is an Error, and the daily cron sweeps it to FAILED); Monitoring only after a check in the last 36 hours with something live — and always described as "once a day, and when you open MAIRO"; Waiting for approval when its recommendations, a plan or a built campaign wait on the customer; Needs attention for a real problem in its area (a rejected ad, no payment method on Meta, broken tracking); Connection required without Meta; Idle otherwise./dashboard/team (welcome line from real counts, the Daily MAIRO Brief, eight cards with last finished task, what's waiting, and this month's measurable contribution — counts only, never invented impact — and the activity feed, filterable by specialist), the Overview's AI Team card, recommendations that say "What your Optimization Agent noticed", AI Recommendations (/dashboard/decisions), and "Ask your AI team" (the one assistant, routed by specialty with get_ai_team). Sidebar: Overview, AI Team, Recommendations, Campaigns, Creatives, Analytics.Organization.notifyOpportunities, notifyReports).#ai-team) — the eight specialties, how they work together, what always needs approval, and that they're one AI system checking on a schedule.npm run check:team pins the states, attribution, Guardian check and wording.What keeps a business paying: seeing what MAIRO did for it, and the MAIRO team seeing who's struggling before they cancel.
src/lib/leads/outcomes.ts, Lead.status / valueCents). A form filled in — MAIRO's page or Meta's instant form — is a reported lead and nothing more. On Leads, each enquiry is marked Good lead / Booked / Customer (with an optional job value) / Not a fit / Spam. Later steps include earlier ones, spam isn't a lead, nothing counts as revenue without a value the business gave, and unmarked enquiries are said to be unmarked. The Overview's "This month" card adds "12 enquiries came in · 5 good, 2 booked, 1 customer — about $X in ads per customer". npm run check:lead-outcomes./dashboard/reports/monthly, src/lib/reports/monthly.ts). Advertising performance (spend, the result Meta reported — sales, or leads for a leads business — cost per result, revenue and return only when Meta reported them), Your enquiries (what the business marked, kept apart from Meta's count), What MAIRO did (campaigns created, recommendations made, approved, declined, changes carried out on Meta, problems caught, ads tested, weekly reports, and the recommendations accepted), and next month's suggested budget. It says a reported lead isn't revenue and ad results aren't profit. npm run check:reports.src/lib/success/journey.ts). Day 1 set up (business, Meta, plan, Meta payment method, tracking recommended, first campaign approved) → Week 1 running smoothly (live, delivering, nothing blocking — no early conclusions) → Week 2 first improvements (doesn't nag when there's been nothing worth changing) → Week 3 weekly report → Week 4 monthly review and "how's it going?". Worked out from existing records; a card on the Overview for the first five weeks. npm run check:success.CustomerFeedback). "Is MAIRO making advertising easier for your business?" (Yes / Somewhat / Not yet) on the Overview from week 2, at most monthly, with "Ask me later"; a Feedback drawer in the sidebar and on Account (problem, something confusing, an idea); and "Thinking about cancelling?" on Billing, optional and never a gate. Read by the MAIRO team only.Organization.caseStudyConsentAt)./aios/customers, src/lib/success/accounts.ts, health.ts). Every customer business, struggling first, each with the reasons in sentences ("Paid, 9 days in, and no campaign approved yet", "Payment is failing", "Hasn't opened MAIRO in 21 days", a "Not yet" answer, open problem reports, rejected ads). Program numbers: day-1 completion, median days to first campaign, still paying of ever paid, active in the last 7 days, the pulse, cancellations (with the reason given). An inbox of open problems, ideas and confusion. Database only — never calls Meta. (hourly) and (from the Stripe webhook) feed it.Follows a business's results past the click — spend → impressions → clicks → leads → contacted → good leads → appointments → estimates → customers → revenue — and investigates where they slow down (src/lib/coach/, /dashboard/coach).
funnel.ts) keeps every number's source: spend and clicks from Meta, leads MAIRO stored (its form and Meta's instant form, minus spam), and everything after that from what the business marked. Meta's lead count is shown beside MAIRO's, never added to it; an expected value is an estimate, never revenue; confirmed sales are only the values the business entered. Two matching two-week periods, ending yesterday. Stage rates count only leads old enough to have reached the stage (3 days for good, 7 for booked, 14 for customer), so a newer period isn't made to look worse.diagnose.ts) — tracking (form leads Meta never counted, a pixel gone quiet), conversion (clicks not becoming leads or sales), lead quality (with the reasons the business recorded: outside the area, wrong service, price, spam), follow-up (leads waiting over a day; slower first contact — measured only from contacts the business logged), sales (fewer good leads booking; estimates unanswered for two weeks), budget (cost per good lead rising, with which step moved; a campaign whose leads were never good), creative (fewer clicks per view, ad-to-ad differences, repeat views) and audience (saturation). Each rule has minimum samples and stays quiet below them. A finding keeps apart what was observed (with the numbers), possible explanations marked in your records or possibility, the recommendation and alternatives, the evidence, how strong it is in words (never a percentage), its limitations and what MAIRO can't see. A drop after the lead is said to be after the lead — never blamed on the ads.?c=<campaign> and is checked against the form's business, or Meta's instant form names it), first and last contact, appointment, next follow-up, expected value, sale value, lost reason and private notes. Stage names follow the business's trade ("Inspection booked", "Quote sent") or its own.npm run audit runs every check suite (42) and reports the result against the fifteen questions a paying business depends on — connecting Meta, understanding the business, coordination, building and approving campaigns, publishing, analytics, optimization, the Budget Guardian, tracking, verified results, honest statuses, the Daily Brief, the assistant, billing and isolation, and owners with no advertising experience — with what each still needs to be proven in production. It runs MAIRO's real code against a simulated Meta: nothing is sent to Meta, spent or published./aios/meta-check, src/lib/meta/live-check.ts) asks the real Graph API whether the app's credentials and Graph version are accepted and, per connected business, whether the token is valid (and when it expires), the permissions MAIRO needs are granted, the ad account is active and can pay, the Page, campaigns, results and pixel can be read, and the campaigns MAIRO built are really on Meta in the state MAIRO thinks. Read-only by construction: requests go through a transport that refuses anything but GET before it leaves the server. Never stores or shows a token. npm run check:live-meta.src/lib/leads/report.ts). The hosted form lives on MAIRO's address, so the business's pixel never saw it; each submission is now sent to the business's Meta pixel as a Lead through the Conversions API (hashed email and phone, the ad's click id, the lead's id as the event id so it counts once), after the visitor has their answer. The outcome is on the lead (metaReportedAt, metaReportError); the nightly job retries anything unsent from the last week. npm run check:lead-report.One place decides the marketing strategy; the mission plan, Create, creatives, Social Manager, Mairo Decisions and the assistant all read from it (src/lib/engine/). The rules are pure and pinned by npm run check:engine; AI only writes the words around them.
core.ts structureObjective): "We need more roofing estimates" → Lead generation · Request an estimate · Roofing · High intent · Local customer acquisition. Also "more dinner reservations during weekdays" (Reserve a table · Fill weekday dinner), "sell more memberships" (Join a membership), "releasing a hoodie Friday" (Product launch). The business's own industry wins over a word in the request. Shown on the plan as "What MAIRO understood".buildStrategy): goal priorities, creative direction (industry playbook filtered to objectives that serve the goal, reordered by what the account learned), audience, messaging, CTA, offer use, testing plan, optimization focus, retargeting (only with tracking), budget, confidence. Industry logic covers clothing, restaurants, dentists/health, contractors, barbers/salons, car care, real estate and software.promotionPlan): when to introduce it, ~2 mentions a week, whether running ads should change (MAIRO asks), whether a new creative is worth it, urgency only in the last days, and a rest from discounts after several promotions in 30 days. Used by the plan and "Tell MAIRO something new".recommendBudget): a split that always sums to 100% (goal / retargeting with tracking / testing / awareness only at larger budgets), a plain summary, and "a recommendation, not a promise". It never calls a default figure "your budget". The plan's daily amount and the Create draft come from it; the owner confirms or changes it in Create.briefJson on creative requests, campaign ads — including MAIRO-written variations — and social posts): goal, marketing objective, audience, hook, message, CTA, format, reason.learning.ts): compares format, hook style, offer vs no offer, CTA and audience age band per result type, only with ≥2 ads and ≥$50 on each side, ≥5 results on the better side and a ≥20% difference. Lessons go to Learning Memory (engine:* keys), strengthen to high after three sightings, and a new winner retires the old one.A permanent core layer that keeps MAIRO aligned with Meta advertising (src/lib/meta-intelligence/, admin at AIOS → Meta Intelligence). Meta evolves → MAIRO detects it → understands it → tests it → updates its knowledge → decides when it's actually useful. Customers never have to follow Meta's changes themselves.
sources/): official Meta sources first — Marketing API and Graph API changelogs, the versions page, Marketing API and Advantage+ docs, Instagram Platform, the Pages API, the developer blog, the Business Help Center and Meta for Business news. Only https pages on Meta's own hosts are fetched. Redirects must stay on the allowlist, fetches are capped at 15 s and 3 MB, and no credentials are sent. Authority comes from the host, and blogs, social posts and rumours never change behaviour. Admins can paste official text when a page can't be fetched.change-detector/): stores a snapshot of each source. The first look is a baseline; after that, added or removed text blocks are filed as updates. Each one is classified (new, deprecated, renamed or removed feature, endpoint or field; permission, objective, optimization, placement, targeting, creative, measurement, attribution, Advantage+ or AI change; API version release or retirement; policy). It also records urgency, risk, which registry feature is mentioned, and version release and retirement dates exactly as Meta states them.interpretation/): the "META UPDATE" analysis. It answers what changed, why it matters, whether MAIRO uses it, which systems it affects, and what must change (customers, backend, campaign logic, UI, strategy knowledge). It also gives urgency, risk, and an evaluation: API availability, goal fit, replacement, permissions, API version, GA or beta, customer control, conflicts. It ends with a proposed integration plan.
useAutomatically is never acted on.feature-registry/, PlatformFeature): every Meta capability MAIRO relies on, with all the fields the spec lists (objectives, optimization goals, placements, formats, permissions, availability, regions, deprecation, replacement, last verified, source, MAIRO support and mapping, breaking risk, goal fit). Meta AI tools MAIRO hasn't evaluated are recorded as Not supported. Every change keeps the previous version.capabilities/): every Meta value campaigns are built with now lives in one place. That covers objectives, optimization goals, placements, message destinations, targeting rules, bidding, creative specs, CTA types, pixel events and insight action types. , the adapter, , , , and read from it, with identical behaviour.
Organic social media management, exclusive to active Scale. Everything else in MAIRO (ads) follows each plan's existing benefits; nothing on the ads side changed.
lib/social/access.ts — the tier must be SCALE itself (a PlanConfig override can't open it to another plan; Agency no longer has social_posting), the subscription must be active (Scale has no free trial; trialing is accepted only because the check is shared), and execution must not be stopped. This applies whatever BILLING_ENFORCED says. A plan set by hand in /aios/organizations (no Stripe subscription) is recorded as active.lib/actions/social-manager-actions.ts, lib/actions/social-actions.ts) and again by the publisher before every post (lib/instagram/scheduler.ts). The pages use the same check (app/dashboard/social/gate.tsx)./plan/activate, others to Billing.lib/social/strategist.ts; AI with a rule-based playbook in lib/social/goals.ts when AI is unavailable). The mix differs by kind of business (retail, food, health, trades, real estate, beauty/fitness, services)./dashboard/social/calendar): week and month views. Statuses: Draft, Awaiting approval, Approved, Scheduled, Published, Skipped (and Paused)./dashboard/social/promotions): sale, new product/service, event, new inventory, holiday, announcement. Sequenced (e.g. teaser → launch → showcase → reminder → ending soon → last chance) and capped so no week is more than 60% selling./dashboard/social/performance): likes and comments read back from Instagram and Facebook (reach needs Meta's insights permission, which MAIRO doesn't request, so it isn't shown). MAIRO plans more of what gets engagement and less of what the business skips.Scale accounts get a Social section in the sidebar with Instagram posts (/dashboard/social/instagram) and Facebook posts (/dashboard/social/facebook); /dashboard/social opens Instagram. Both work the same way, below; the Facebook differences are listed after.
Organization.instagramOptInAt / instagramDeclinedAt)./api/cron/launch), and whenever the business opens the Social page or the dashboard. On a Vercel plan with hourly crons, schedule /api/cron/launch hourly for exact times. Pictures are served to Instagram as JPEG, fitted (never cropped) to 4:5–1.91:1, via /api/social/media/<post>/<n>. Reels still processing are finished at the next check. Instagram's 25-posts-a-day limit is respected.instagram_basic and instagram_content_publish. They are not in the everyday connect dialog — Facebook refuses a whole login that names a permission the app hasn't been set up for — and are asked for only by Connect Instagram on the Instagram posts page (/api/meta/connect?also=instagram, with auth_type=rerequest). Your Meta app needs the Instagram permissions added (the use case for managing content on Instagram) before that button works.npm run check:social-scheduler checks both against a simulated Graph API.Facebook Page posts:
Organization.facebookOptInAt / facebookDeclinedAt). Posts share the InstagramPost table with network = "FACEBOOK".pages_manage_posts. It is not in the everyday connect dialog (so App Review and META_SCOPES are unaffected). After a yes, the page shows Allow posting on Facebook, which goes to /api/meta/connect?also=page_posts — the usual permissions plus pages_manage_posts, with auth_type=rerequest — and comes back to the Facebook posts page. Until it's granted, previews can be seen but not approved. Reconnecting keeps the ad account and Page already chosen./{page}/photos, a multi-photo post as unpublished photos attached to one /{page}/feed post, a video via /{page}/videos (finished at the next check once Facebook has processed it). Instagram's 25-a-day limit doesn't apply.pages_manage_posts in App Review, only people with a role on the Meta app can grant it.plan.contextANTHROPIC_API_KEY the words are scored on
structure only, and Let MAIRO Improve It says the AI isn't switched on.
npm run check:ad-score pins all of this./dashboard/business,
/dashboard/settings/business-brain, src/lib/business/). Reads the home
page plus up to three shop, pricing or services pages through the same
public-address check as the landing probe. Prices the AI reports are kept
only if they appear on the page. Fields the business edits by hand are never
overwritten by a later scan. Build This Campaign saves the recommendation
as a Create draft; nothing launches from there./dashboard/decisions, src/lib/decisions/). Pure
rules (rules.ts, asserted in npm run check:decisions) over each
campaign's last 3 days, the 4 before and the last 7, per ad as well. They
never fire in the learning period. Changes go through apply.ts: limits are
checked even on approval, the network is called first, and activity is
recorded only after it accepts. Automatic application follows
src/lib/automation/levels.ts plus the approval switches in Settings.
Refreshed daily by /api/cron/review, and on page load when older than six
hours. Retargeting decisions aren't produced: MAIRO can't build retargeting
audiences yet.src/lib/ai/assistant-tools.ts). The assistant reads real
figures through diagnose_campaigns and can only propose the Decisions
engine's own fixes (propose_fix). Those fixes open the same approval panel./dashboard/activity) merges decision changes, the
older optimizer and Spend Protection into one timeline.#go-live, #spend-protection or #automation open the fold. Advanced view shows every section expanded.components/mairo/overlay.tsx: Escape closes, focus moves in and back). Button styles shared with server components live in components/mairo/action-styles.ts — a "use client" module's exports reach the server as references, not strings.npm run check:simple-ui pins the rules (the numbers per goal, no technical metrics, plain goals and statuses, campaign tabs, what's next, one insight, goal-ranked creatives, assistant destinations).get_mission, propose_mission (saves a plan to approve; shows "Review MAIRO's plan"), tell_mairo.npm run check:mission checks it.src/lib/brain/visual.tsget_business_brain and update_business_brain ("we don't do free estimates anymore" — removing or replacing a fact asks the owner first; ✨ Added to Business Brain shows once). Tell MAIRO "we sold out of X" marks the product unavailable. The Overview has one small "MAIRO knows your business" card.src/lib/meta/account-history.ts): goal, dates, state, lifetime spend and the result for its goal, each opening in Ads Manager. Read-only — MAIRO never changes, pauses or spends on them, and its own campaigns are never listed twice. Two cached Graph reads (the account's campaigns, and account insights by campaign), ads_management only, made only on that tab (inside its own Suspense boundary) — the other tabs and the Overview never wait on them. Every Graph call has a time limit (20s for reads, 120s for changes and uploads), so a Meta that doesn't answer gives a message, never an endless loading screen; the Brain waits at most 4s for the history and goes ahead without it. The Brain gets a short summary (how many ran, total spend, the lowest cost per result for each goal, never ranked across goals, worded as what happened and marked as made outside MAIRO) for every prompt, and the Brain page shows it under "Your campaigns before MAIRO". npm run check:account-history pins the rules.npm run check:brain pins the rules.Organization.lastActiveAtcanceledAtMairoDecision.applyingAt, a two-minute lease): a double click, two tabs or an automatic run at the same moment can't send the same change twice. npm run check:decision-claim (needs the database).COACH_ALERT, optional under Notifications → Performance Coach alerts) for new findings with enough evidence. History: two weeks after a change on Meta, the two weeks before and after are compared (history.ts, verdict.ts); an approved plan's own figure is re-measured on its date. Both are written as what followed, not proof of cause, and a change followed by worse results isn't suggested again for that campaign for 90 days. The assistant's get_performance_coach tool answers "why am I not getting customers?" from the same records.npm run check:coach (pure) and npm run check:coach-db (database, simulated Meta) pin all of it.recommend.tsjustifyAction): every action must name the business objective it serves. Plan ad concepts, creative directions and running campaigns are checked; anything that can't answer isn't recommended (e.g. an awareness campaign on a small lead-gen budget, discount creative while discounts are resting).meta/campaigns.tsobjectives.tsplacements.tsdestination.tscreatives.tsmedia-rules.tsMairoCampaign.metaFeatures.knowledge-base/): new versions supersede old ones and nothing is overwritten. Validated knowledge feeds the mission planner's and the assistant's prompts.pipeline/): Detected → Analyzed → Proposed → Development → Automated testing → Sandbox / test account → Approved → Production.
testing/):
npm run check:meta-contract runs 16 tests of MAIRO's real Meta request code against a stubbed Meta, scoped to the run via withGraphTransport and never global. It covers authentication, permissions, ad account, campaign, ad set, creative and ad creation, status, budgets, optimization goals versus objectives, placements, media, insights, pagination, tokens, errors and API version. It runs in vercel-build, so a critical failure blocks deployment.META_SANDBOX_ACCESS_TOKEN / META_SANDBOX_AD_ACCOUNT_ID (a test account, never a customer's) and can try a candidate API version.feature-flags/): Off → Internal testing → Selected accounts → All eligible accounts (META_<FEATURE>_ENABLED). Setting a flag back to Off is the rollback. Internal accounts come from the flag's own list or META_INTERNAL_ORG_IDS.api-versioning/): production (from META_GRAPH_API_VERSION, default in metaCapabilities), available, candidate and retired versions, plus migration status. Dates come only from an official source or an admin. Alerts fire at 180, 90 and 30 days and on the day: "MAIRO is currently using Meta API vXX.X. Meta plans to retire this version on [date]. Migration testing should begin."deprecations/): affected systems and code, how many customer campaigns use it, the replacement, and a migration plan. New campaigns stop using it; existing ones keep running and move only with approval.errors/): every Graph error, grouped by code, subcode, endpoint shape and method, with account counts (hashed — never tokens). An unfamiliar error hitting three or more accounts within 48 hours is flagged as a possible Meta API behavior change, opened as an update and alerted.discovery/): each business's ad account (country, currency, status, capabilities, granted permissions). A feature is never shown as available without it, except GA features with no extra requirements.strategy-integration/):
npm run check:meta-intelligence covers the rest (29 checks, including the end-to-end path from a changed page to a production deprecation).platform, and src/lib/platform-intelligence/ defines the shared types and a registry where TikTok, Google Ads, YouTube or LinkedIn intelligence can plug in.npm run check:social-manager checks all of it.