Magnet
한 줄 소개: 마케팅 팀장의 직무를 통째로 갖는 에이전트. 광고 예산·크리에이티브·소셜 응대·위기 대응을 자동화. 39개 MCP 도구, hash-chained decisions ledger, brand voice 중앙화.
정체성
magnet (당기다) ↔ stream (흐르다)
마케팅이 수요를 당기면 운영이 공급을 흐르게 한다.
domain 분업: magnet 은 자기 도메인의 외부 채널(광고·소셜) + 의사결정만 소유. 판매·SCM 은 stream MCP 가 owner.
기술 스택
| 항목 | 값 |
|---|---|
| 언어 | Python 3.12+ |
| 프레임워크 | mcp[cli] ≥1.27 (FastMCP) |
| DB | PostgreSQL 16 (multi-tenant + RLS) |
| Scheduler | supercronic (Asia/Seoul TZ) |
| 의존성 | pyyaml ≥6, psycopg[binary] ≥3.2, openpyxl ≥3.1 |
포트
| 포트 | 용도 |
|---|---|
| 8770 | MCP HTTP/SSE (magnet-mcp) |
| 5432 | PostgreSQL (magnet-postgres 컨테이너, 127.0.0.1 loopback 만) |
39 MCP Tools (6 prefix)
| Prefix | 모듈 | 도구 수 | 주요 |
|---|---|---|---|
meta_ads_* | meta-ads | 5 | get_campaigns, propose/execute_budget_change |
meta_capi_* | meta-ads | 2 | send_event, send_purchase |
social_* | social | 19 | post_to_fb/ig/threads, reply_to, like, get_insights |
naver_search_ad_* | naver-ads | 3 | get_campaigns, propose_keyword_bid (skeleton) |
marketing_* | performance + content | 9 | get_ad_spend, daily_brief, weekly_brief, compose_post_prompt |
decisions_* | decisions | 3 | get_decisions, verify_chain_integrity, get_lifecycle |
stream_bridge_* | stream-bridge | 4 | send_purchase_from_stream, handle_inventory_alert |
marketing_compose_* | marketing-compose | 2 | compose_post_prompt, validate_copy |
threads_browser_* | threads-browser-host | 4 | init, like, action, status (호스트 stdio MCP) |
디렉토리 구조
/Users/axe/magnet/
├── mcp/
│ ├── server.py ★ 통합 daemon — importlib 로 6 모듈 흡수
│ ├── meta-ads/ Meta Ads + CAPI
│ ├── social/ FB + IG + Threads
│ ├── naver-ads/ 네이버 검색광고 (skeleton)
│ ├── performance/ ROAS · CAC · spend 조회
│ ├── content/ 콘텐츠 발주 (truvia-marketing-agents 연동)
│ ├── decisions/ decisions ledger 조회·시각화
│ ├── stream-bridge/ stream 신호 receiver (2026-05-11)
│ ├── marketing-compose/ 카피·답글 생성 (brand SoT 주입)
│ └── threads-browser-host/ Threads 브라우저 자동화 (별도 stdio MCP)
│
├── scripts/
│ ├── automation/ 6+ 자동화 (LaunchAgent 흡수)
│ ├── marketing/ MCP 도구 구현 + CLI
│ ├── db/ DB 연결 + sync
│ ├── admin/ 운영자 CLI
│ ├── decisions/ decisions 분석
│ └── entity/ 멀티 엔터티 레지스트리
│
├── brand/ ★ 마케팅 identity SoT
│ ├── personas/operator.yaml 화자 정체성
│ └── voice/ 채널별 톤
│ ├── _global.yaml 전 채널 (광고법·금기)
│ ├── threads.yaml
│ ├── instagram.yaml
│ └── fb.yaml
│
├── data/
│ ├── decisions/realchoice.jsonl ★ hash-chained ledger (append-only)
│ ├── reports/YYYY-MM-DD.md
│ ├── pending-monitor-actions/
│ └── pending-user-actions.md
│
├── sql/ 스키마 1,300 LOC (10 파일)
│ ├── 000_schema.sql campaign_metrics_daily, posts, threads_post_snapshots
│ ├── 050_multi_tenant_baseline.sql ★ RLS + tenant_id
│ ├── 060_threads_clone.sql Threads snapshot (사고 회복)
│ ├── 100_views.sql
│ └── 200_personas.sql (nemotron-personas 로 migrate 예정)
│
├── docs/ 핸드오프 + 결정 + 가이드
│ └── incidents/
│ └── 2026-05-14-threads-self-delete.md 사고 리뷰
│
├── crontab ★ 10 자동화
├── docker-compose.yml 3 service (magnet-mcp + magnet-cron + magnet-postgres)
├── CLAUDE.md ★ 자동 로드 컨텍스트
└── SECURITY.md데이터 모델 (multi-tenant + RLS)
| 테이블 | 역할 |
|---|---|
campaign_metrics_daily | 광고 성과 (spend, impr, conv, roas, cac) |
posts_fb / posts_ig / posts_threads | 소셜 게시물 + insights |
threads_post_snapshots | Threads snapshot (사고 회복 자산) |
decisions | 의사결정 audit ledger |
campaign_budgets_proposed | 예산 변경 안 (HMAC 서명, TTL) |
copy_ab_tests | A/B 테스트 메타 |
RLS: POSTGRES_USER=magnet_app (non-superuser, BYPASSRLS=false), connection stage 에서 SET app.tenant_id = <id> 주입. 기본값 tenant_id=1 (realchoice).
Crontab (10 task)
| 주기 | 작업 |
|---|---|
| 매시 정각 | sync_meta_live.py --since 0 + refresh_daily_kpi.py |
| 매시 정각 | sync_threads_live.py |
| 매시 10분 | learning_reset_monitor.py — 광고 학습 reset 회복 |
| 30분 주기 | ingest_decisions.py realchoice |
| 매일 01:00 | naver_searchad_sync.py |
| 매일 03:00 | sync_meta_live.py --since 2 — 어제/그제 재폴링 |
| 매일 09:00 | ad_xray_daily.py — DataLab + keyword sync |
| 15분 주기 | scheduled_carousel_publish.py — 운영자 시간 트리거 |
| 매시 30분 | sync_threads_full.py — 게시물 snapshot |
| 20분 주기 | scheduled_reply_publish.py — 답글 자동 게시 |
| 매일 00:05 | logrotate.sh |
| 09:00 외 | daily_monitor.py, faq_responder.py, roas_safety_net.py, refund_spike_safeguard.py, negative_feedback_alert.py, threads_token_refresh.py |
Decisions Ledger (hash-chained)
data/decisions/realchoice.jsonl 에 모든 의사결정 append-only 저장:
{
"ts": "2026-05-20T15:00:00+09:00",
"actor": "magnet-agent",
"type": "ad_paused",
"context": {"campaign_id": "23845...", "reason": "ROAS < 1.5"},
"prev_hash": "sha256:abc..." // 이전 line 의 SHA-256
}decisions_verify_chain_integrity() 도구로 체인 무결성 검증.
Brand SoT (operator + voice)
모든 카피·답글은 다음 2단계 강제:
marketing_compose_post_prompt({...})— brand SoT 자동 주입marketing_validate_copy(text)— 광고법·금기 키워드·해시태그 정책 검증
brand/personas/operator.yaml:
identity: "F&B 10년차 두 친구, 매장 비하인드"
disclose: false
hide:
- real_name
- exact_store_namebrand/voice/threads.yaml:
tone: "친구 톤, 농담 OK"
no_hashtag: true # Threads 는 해시태그 X
forbidden:
- "최고", "1위", "유일" # 광고법
length_max: 500Threads 사고 (2026-05-14, 영구 기록)
Threads 브라우저 자동화의 selector 결함으로 본 게시물 3개 + 모든 반응 영구 손실.
| 항목 | 값 |
|---|---|
| Root cause | threads_browser.py selector 가 답글 menu 에서 parent thread menu 잡음 |
| Impact | 운영자 carousel 본 게시물 3개 + 모든 반응 |
| Recovery | data/pending-monitor-actions/2026-05-15-carousel-relaunch.md (Graph API 재게시) |
| Guard | MAGNET_THREADS_DANGER_ENABLED=1 환경변수 없으면 delete/unrepost raise |
이후 모든 destructive Threads op (delete, unrepost) manual only, snapshot 매시간 백업으로 회복 자산 확보.
자가발전 에이전트
- 매시간 in-session loop + 매일 04:00 KST deep round
- 산물:
data/reports/YYYY-MM-DD.md,config/thresholds.yaml(자율 조정) - Token cap: 100K/round
[NEEDS_REVIEW]섹션으로 본 세션·사용자 결정 위임
외부 의존성
| 시스템 | 통합 |
|---|---|
| Meta Ads + CAPI | Marketing API v19+ |
| Naver SearchAd | API (HMAC-SHA256) |
| Threads / IG / FB | Graph API + 브라우저 자동화 (호스트 macOS Playwright) |
| stream MCP | 신호 수신 (purchase, inventory_alert) |
| nemotron-personas | 페르소나 조회 (별도 MCP) |
| truvia-marketing-agents | 콘텐츠 발주 인터페이스 |
Tenant ID Mapping (D-magnet-tenant-map-1)
magnet 의 MAGNET_TENANT_ID 는 service-internal RLS tenant_id 이며, AXE Labs platform 의 customer ID (customers.yaml key) 와 별개 namespace 입니다. 본 섹션은 두 ID 의 mapping rule 을 명시합니다.
두 ID 의 분리
| 식별자 | 출처 | 예시 | 변경 책임 |
|---|---|---|---|
| AXE customer ID | customers.yaml 의 top-level key | axe, realchoice | 운영자 (액스코퍼레이션) |
MAGNET_TENANT_SLUG | magnet .env.local 의 customer-name string | realchoice | service operator |
MAGNET_TENANT_ID | magnet DB 의 RLS row identifier (magnet.tenant.id, integer) | 1 (realchoice) | DB migration |
같은 customer 가 두 ID 모두 가질 수 있으나 string ↔ integer 매핑 이 별도 lookup 필요.
SSOT — customers.yaml 의 service_tenant_map (D-magnet-tenant-map-1, 2026-05-23)
신설 필드. 각 customer 블록 안에서 customer 가 사용하는 각 service 의 internal tenant id 를 명시:
customers:
realchoice:
...
service_tenant_map: # NEW field (D-magnet-tenant-map-1)
magnet:
tenant_slug: "realchoice"
tenant_id: 1
stream:
tenant_id: 1 # truvia_ssot (port 5433) 의 tenant row
axe:
...
service_tenant_map:
# axe customer 가 magnet 사용 시 tenant_id=2 부여 (충돌 회피)
magnet:
tenant_slug: "axe"
tenant_id: 2신규 customer 가 magnet 사용 시 — 충돌 회피 절차
- 운영자가
customers.yaml의 신규 customer 블록에service_tenant_map.magnet등재 —tenant_id는 현재 최대값 + 1 - magnet DB migration 또는 admin tool 로
magnet.tenant에 row INSERT (id=신규) - customer macmini 의 magnet
.env.local에MAGNET_TENANT_SLUG/MAGNET_TENANT_ID등재 (axe ship 이 customers.yaml manifest 에서 자동 주입 —B-magnet-tenant-env-injection향후 작업) - RLS 검증:
SET app.tenant_id=<신규>후 cross-tenant read 가 빈 결과 보장 (테스트)
realchoice 측 코드 자산 호환성
~/magnet 의 코드 자산은 변경 없음 — RLS 구조 + tenant_id=1 (realchoice) 유지. 본 신설 mapping 은 운영자 측 manifest 가시성 만 추가 (DB schema 영향 0).
향후 axe 가 magnet 사용 시
axe customer 가 magnet 도입을 결정하면:
customers.yaml > customers.axe.service_tenant_map.magnet.tenant_id = 2등재- magnet DB 에
INSERT INTO magnet.tenant (id, slug) VALUES (2, 'axe') - 양 customer 의 magnet 데이터는 RLS 로 격리, 충돌 0
환경 변수 (140+ 라인 .env.example)
# DB
POSTGRES_DB=magnet
POSTGRES_USER=magnet_app
POSTGRES_PASSWORD=
POSTGRES_HOST=
# Multi-tenant — service-internal namespace (Tenant ID Mapping 섹션 참조)
MAGNET_TENANT_SLUG=realchoice
MAGNET_TENANT_ID=1
# Meta Ads
META_ADS_ACCESS_TOKEN=
META_AD_ACCOUNT_ID=
MAGNET_PROPOSAL_HMAC_KEY=
META_PIXEL_ID=
# Naver
NAVER_AD_CUSTOMER_ID=
NAVER_AD_API_KEY=
NAVER_AD_SECRET_KEY=
MAGNET_NAVER_AD_LIVE=1
# Transport
MAGNET_TRANSPORT=sse
MAGNET_HTTP_HOST=127.0.0.1
MAGNET_HTTP_PORT=8770
# Decisions
MAGNET_DECISIONS_LOG_PATH=/data/decisions/realchoice.jsonl
# Threads guard
MAGNET_THREADS_DANGER_ENABLED= # 미설정이 안전 (delete/unrepost raise)관련 문서
- Stream service — sales/SCM 신호 발신자
- /Users/axe/magnet/docs/ — 핸드오프 + incidents
- /Users/axe/magnet/CLAUDE.md — 자동 로드 컨텍스트