| name | social-post |
| description | Compose and post full-stack social posts to @shorted___ on X — text + X-card link preview + generated infographic. Wraps the Twitter bot, /api/og/twitter PNG endpoint, and /news/[slug] editorial pages. Use when the user says "post a tweet", "tweet today's shorts", "share to X", "social post", "compose a tweet about $TICKER", or "/social-post". |
| allowed-tools | Read, Write, Bash(npx tsx:*), Bash(node:*), Bash(curl:*), Bash(gh:*), Edit, Grep, Glob |
Social Post — Full-Stack X Pipeline
READ FIRST — before drafting any tweet or Take:
- PERSONA.md — the Shorted voice
- CHECKLIST.md — validation gates run before every
--live
The skill's job is not "generate a tweet". The job is "post something
that sounds like Shorted". Most of what makes this work is in those
two docs, not in this CLI reference.
This skill posts data-rich tweets to @shorted___ that include:
- a card preview when the tweet URL is a
shorted.com.au page with og/twitter meta
- a generated PNG infographic (via
/api/og/twitter/[type]) attached as media
- when relevant, a link to a
/news/[slug] "Shorted Take" editorial page so the share-back loop stays on shorted.com.au
Source: scripts/twitter/ (the bot). See scripts/twitter/OPERATIONS.md for the production runbook.
Quick reference
cd scripts/twitter
npx tsx src/index.ts daily-shorts
npx tsx src/index.ts movers
npx tsx src/index.ts stock-of-the-day
npx tsx src/index.ts weekly-digest
npx tsx src/index.ts breaking-news
npx tsx src/index.ts insider-trade --stock=BHP
npx tsx src/index.ts daily-shorts --live
npx tsx src/index.ts daily-shorts --live --no-image
TWITTER_DRY_RUN_DEFAULT=true in .env is the safety net — --live is required to actually post.
When to use this skill
- The user asks for a specific post: "tweet today's top shorts" → run
daily-shorts
- The user wants to share a stock-specific story: "post about $LOT lithium news" → consider
breaking-news (auto-finds the latest) or insider-trade --stock=LOT
- The user asks to compose an ad-hoc tweet: gather the angle, propose copy + destination URL + infographic option, always dry-run first, then ask before
--live
The pipeline (what happens under the hood)
- Text —
templates.ts buildXxxTweet() calls the public shorted.com.au API, formats data into ≤280 chars, includes a canonical URL on its own line so X parses it for the card.
- Card preview — when the URL is
shorted.com.au/<path>, X fetches the page, reads og:image + twitter:card, renders a large image card.
Verified-good destinations (cards work today): /top, /shorts/[code], /shorts/[code]/news, /reports/weekly/[slug], /insider-trading/[code], /news/[slug].
- Infographic (optional) — bot calls
/api/og/twitter/[type] on shorted.com.au, gets a 1200×675 PNG, uploads to X via v2.uploadMedia, attaches as media.
Variants live: top-shorts, stock-of-day. More can be added in web/src/app/api/og/twitter/[type]/route.tsx.
Composing an ad-hoc post
When the user asks for a tweet that isn't one of the canned commands:
- Read PERSONA.md. Even if you've read it before — re-read the
worked examples. You will drift into AI-voice without re-grounding.
- Pick the angle. One headline, one stock, one chart — never more.
- Pick the destination URL — must be a
shorted.com.au page so the card renders:
- Stock-specific →
https://shorted.com.au/shorts/[CODE]
- News →
https://shorted.com.au/shorts/[CODE]/news or /news/[slug] if a Shorted Take exists
- Sector →
https://shorted.com.au/industry/[slug]
- Aggregate →
https://shorted.com.au/top
- Check the destination is live with a quick curl:
curl -sI -A "Mozilla/5.0" "https://shorted.com.au/<path>" | head -1
Should be HTTP/2 200.
- Verify card meta is present:
curl -sL -A "Mozilla/5.0" "https://shorted.com.au/<path>" | grep -oE 'twitter:(card|image)[^>]*' | head -2
twitter:card content="summary_large_image" + twitter:image URL ⇒ card will render.
- Pick or generate an infographic — for ad-hoc posts use an existing PNG variant. If a new variant is needed, edit
web/src/app/api/og/twitter/[type]/route.tsx.
- Draft tweet copy — respect the X 1-cashtag limit: at most one
$TICKER per post. Lists of tickers must be plain JEME / BBAB / LOT without $.
- Run the lint — required before showing the user:
node scripts/twitter/scripts/lint-copy.mjs --text "<the draft>"
- Exits 1 if any HARD gate from CHECKLIST.md fails. Rewrite, re-lint.
- Soft warnings: judgement call. State the warning + your reason for
keeping the line, or rewrite.
- Walk the manual checklist — CHECKLIST.md items the lint can't
enforce: read-aloud test, "did the bot write this" test, variance
from recent posts, emoji discipline.
- Dry-run the post pipeline (text + card + infographic preview):
npx tsx src/index.ts <command>
npx tsx src/index.ts --custom-text "<the draft>"
- Show the user the preview + checklist results and ask before
--live. Never --live without explicit user approval on the
specific draft you just showed.
Authoring a new "Shorted Take" for an ad-hoc post
If the user wants the bot's tweet to link to a fresh editorial article on shorted.com.au (not just a stock page):
- Pick the source headline from
news_articles (price-sensitive, recent).
- Write a 150–250 word editorial body in markdown — analytical commentary, not a rehash of the article.
- Insert into
editorial_takes:
INSERT INTO editorial_takes (slug, headline, stock_code, body_md, sentiment,
source_article_id, source_url, source_name, published_at, model)
VALUES ($slug, $headline, $stock_code, $body_md, $sentiment,
$source_id, $source_url, $source_name, NOW(), 'manual');
Slug is kebab-case stock-code-short-summary (≤80 chars).
- Wait ~30 sec for Vercel cache (revalidate=600) or hit
/api/revalidate?path=/news/$slug.
- Verify:
curl -sI "https://shorted.com.au/news/$slug" | head -1 → 200.
- Tweet linking to that URL via the bot.
Gotchas
Key files
| File | Purpose |
|---|
scripts/twitter/src/index.ts | CLI entry — commands, flags, dispatch |
scripts/twitter/src/templates.ts | Tweet text generators |
scripts/twitter/src/twitter-client.ts | X API wrapper, OAuth 2.0 + 1.0a + DryRun |
scripts/twitter/src/shorted-api.ts | Public API client with 5xx retry |
web/src/app/api/og/twitter/[type]/route.tsx | PNG infographic endpoint |
web/src/app/news/[slug]/page.tsx | Shorted Take editorial page |
web/src/app/news/[slug]/opengraph-image.tsx | Take OG card |
services/migrations/000034_editorial_takes.up.sql | DB schema |
scripts/twitter/PROFILE.md | Brand voice, handle, bio |
scripts/twitter/OPERATIONS.md | Production runbook (cron, secrets) |
docs/plans/twitter-fullstack-social.md | Roadmap for L2/L3 work still pending |
Brand voice (from PROFILE.md)
- Cash, data, transparency. No financial advice ever.
- Cite ASIC + T+4 delay disclaimer in posts that surface short-position data.
- Plain-English, sparse emoji (🔴/🟢/📊/🏆 only), no exclamation marks.
- Stock codes uppercase, no
$ prefix in lists (cashtag limit).
- Always ends with the canonical shorted.com.au URL on its own line.