Social Media Marketing (SMM)
The Kapaldo SMM system lets an operator generate AI-written Facebook posts (with branded images), queue them for admin review, and have a local agent post approved content automatically — without ever storing a Facebook password.
Architecture
smm/ agent ayts-api (Cloudflare Workers) ayts-admin
───────────────── ──────────────────────────── ──────────────────
npm run generate-drafts ──► POST /api/smm/posts ──────────────► Admin sees drafts
at /admin/smm
↓ clicks Approve
npm run poll ◄─────────── GET /api/smm/posts/approved ◄──────── status = approved
│ (X-SMM-Key header)
│ Playwright logs into Facebook
│ posts to page / groups
└─► PATCH /api/smm/posts/:id/status (posting → posted / failed)
The agent (smm/ directory) is a Node.js/TypeScript project that runs locally or on a server you control. It never runs in the cloud. Facebook credentials stay on your machine.
Components
1. generate-drafts script
Path: smm/src/generate-drafts.ts
Run: npm run generate-drafts (from smm/)
Uses Google Gemini to write:
- A page text post
- A group text post
- A vendor recruitment card (image + caption)
- Branded media cards (launch / how-it-works / vendor / community)
Uploads images to R2 via the API, then calls POST /api/smm/posts to create drafts in the database. Does not require Facebook login.
2. Admin review UI
Path: ayts-admin/app/admin/smm/page.tsx
URL: admin.kapaldo.com/admin/smm
Admins see all draft posts with image previews, approve or delete them. The page is under "Marketing → Social Media" in the sidebar.
Features:
- Filter tabs: All / Draft / Approved / Posting / Posted / Failed
- Compose modal: write a custom post, choose target (page / groups / both)
- Config modal: edit FB page URL, group URLs per location
3. poll daemon
Path: smm/src/poller.ts
Run: npm run poll (from smm/)
Polls GET /api/smm/posts/approved every 30 seconds. When approved posts are found, it:
- Marks the post
posting - Launches a Playwright browser (headed or headless)
- Logs into Facebook (using saved session cookies — password only prompted if session expired)
- Posts to the page and/or target groups
- Marks the post
postedorfailed
4. API routes
File: ayts-api/src/routes/smm.ts
Base path: /api/smm
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /posts | Admin JWT | List all posts |
POST | /posts | Admin JWT or X-SMM-Key | Create draft |
PATCH | /posts/:id/approve | Admin JWT | Approve a draft |
DELETE | /posts/:id | Admin JWT | Delete draft or failed post |
GET | /posts/approved | X-SMM-Key | Agent polls for approved posts |
PATCH | /posts/:id/status | X-SMM-Key | Agent updates post status |
GET | /config | Admin JWT | Read FB config |
PUT | /config | Admin JWT | Save FB config |
The X-SMM-Key is a shared secret stored as a Cloudflare Workers secret (SMM_API_KEY).
Database Tables
smm_posts
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
content | TEXT | Post body |
image_url | TEXT | Optional image URL |
target | TEXT | page, groups, or page_and_groups |
post_type | TEXT | text or image |
status | TEXT | draft → approved → posting → posted / failed |
error_text | TEXT | Failure reason if failed |
approved_by | UUID | Admin user ID |
approved_at | TIMESTAMPTZ | |
posted_at | TIMESTAMPTZ | |
created_at | TIMESTAMPTZ |
smm_config
Key-value store for runtime configuration (managed via the admin UI):
| Key | Description |
|---|---|
fb_email | Facebook login email |
fb_page_url | URL of the Kapaldo Facebook page |
fb_page_name | Display name for content generation |
lagonglong_groups | Comma-separated group URLs |
salay_groups | Comma-separated group URLs |
claveria_groups | Comma-separated group URLs |
Security
- Facebook password is never stored. The poller prompts for it via a hidden CLI input (
readlinewith raw-mode echo suppression) only when the saved session cookie has expired. - Session cookies are persisted to
smm/session/fb-session.jsonbetween runs. - The agent authenticates to the API with
X-SMM-Key(a long random secret), never with admin credentials. - Only
draftandfailedposts can be deleted;postedposts are permanent audit records.
Setup & Configuration
1. Set the API key in Cloudflare Workers
cd ayts-api
npx wrangler secret put SMM_API_KEY --env production
# Enter any long random string (32+ characters) when prompted
2. Configure smm/.env
FB_EMAIL=your-facebook-email@gmail.com
# FB_PASSWORD is NOT stored here — prompted at runtime when session expires
FB_PAGE_URL=https://www.facebook.com/kapaldo05
LAGONGLONG_TARGET_GROUPS=https://www.facebook.com/groups/...,...
SALAY_TARGET_GROUPS=https://www.facebook.com/groups/...,...
CLAVERIA_TARGET_GROUPS=https://www.facebook.com/groups/...,...
GEMINI_API_KEY=your-gemini-key
KAPALDO_LOCATION=Claveria, Misamis Oriental
KAPALDO_MESSENGER_URL=https://m.me/kapaldo05
KAPALDO_STORE_URL=https://kapaldo.com
SMM_API_URL=https://ayts-api-prod.jerquinbayudo.workers.dev
SMM_API_KEY=same-value-you-set-in-wrangler-above
3. Daily workflow
# Step 1 — Generate AI content and push as drafts (no FB login needed)
cd smm && npm run generate-drafts
# Step 2 — Admin reviews and approves at admin.kapaldo.com/admin/smm
# Step 3 — Run the poller to execute approved posts
cd smm && npm run poll
# Enter FB password if prompted (session expired)
Pending / Known Issues
-
SMM_API_KEYmust be reconfigured — the deploy script generated a random key that was not saved. Runwrangler secret put SMM_API_KEY --env productionwith a known value and add it tosmm/.env. - Stray worker
ayts-production— created accidentally during secret setup. Delete it from the Cloudflare dashboard (Workers & Pages → ayts-production → Delete). - No cron / scheduler — the poller must be started manually. Consider running it as a background process with
pm2or a systemd service on a persistent machine. - Image upload in generate-drafts — the
uploadImage()function insmm/src/api-client.tsneeds the R2 upload endpoint to accept file paths, not just multipart blobs. Verify the endpoint works end-to-end before the first run. - Group posting rate limiting — Facebook may throttle or flag accounts that post to many groups in quick succession. The
ACTION_DELAY_MSenv var (default 3000ms) controls the delay between posts — increase it if the account gets restricted.