A Twitter bot that counts down from a very large number, posting tweets with the current count in word form. Inspired by Count Von Count from Sesame Street!
- Automatic countdown posts on X a few times a day, with random phrases and tags
- Web interface showing the current count, a self-hosted feed of recent posts, and the Count's Ledger of silly statistics
- Badge endpoint for displaying countdown in README files
- Health check endpoint for monitoring
- Secure with rate limiting, helmet.js, and input validation
- Comprehensive error handling and retry logic
- Node.js 20+
- AWS Account with DynamoDB access
- X (Twitter) developer account with API v2 access and a prepaid credit balance. X moved to pay-per-use pricing in 2026; a plain post costs about $0.015, so a few dollars covers months at the default cadence. With no credits every post fails with HTTP 402
credits depleted.
- Clone the repository:
git clone https://github.com/brianfunk/voncountdown.git
cd voncountdown- Install dependencies:
npm install- Copy
.env.exampleto.env:
cp .env.example .env- Configure environment variables in
.env:
# AWS Configuration
AWS_ACCESS_KEY_ID=your_aws_access_key_id
AWS_SECRET_ACCESS_KEY=your_aws_secret_access_key
AWS_REGION=us-east-1
DYNAMODB_TABLE=voncountdown
# Application Configuration
PORT=8080
NODE_ENV=development
# Twitter API Configuration
TWITTER_API_KEY=your_twitter_api_key
TWITTER_API_SECRET=your_twitter_api_secret
TWITTER_ACCESS_TOKEN=your_twitter_access_token
TWITTER_ACCESS_TOKEN_SECRET=your_twitter_access_token_secret
-
Create DynamoDB table:
- Table name:
voncountdown(or setDYNAMODB_TABLEenv var) - Partition key:
number(Number) - Sort key:
datetime(String)
- Table name:
-
Check the X credentials and credit balance:
npm run check:x- Start the application:
npm startSet DRY_RUN=1 to run everything (DynamoDB scan, website, scheduler) without posting to X or writing to DynamoDB. BOOT_DELAY_MS overrides the 5 minute pause before the first post after startup.
For development with auto-reload:
npm run devHome page displaying the current countdown state.
Returns a badge image showing the current countdown number.
- Returns 503 if service is initializing
- Returns 500 on error
Health check endpoint returning JSON. status is degraded whenever the last post attempt failed, and bot says why and when the next attempt is:
{
"status": "ok",
"current_number": 1111373357578,
"current_string": "One trillion one hundred eleven billion...",
"current_comma": "1,111,373,357,578",
"bot": {
"last_post_at": "2026-10-02T01:00:00.000Z",
"last_post_number": 1111373357578,
"next_post_at": "2026-10-02T06:12:00.000Z",
"last_error": null,
"consecutive_failures": 0,
"dry_run": false
},
"uptime": 1234.56,
"timestamp": "2026-10-02T01:30:00.000Z"
}curl https://voncountdown.com/healthand look atbot.last_error.code: 402means the X account is out of API credits. Top up at https://developer.x.com, the bot retries on its own every 6 hours.code: 401or403means the keys were rejected. Regenerate them in the X developer portal (app permissions must be Read and Write) and update the App Runner environment variables.- Anything else: check CloudWatch logs for the
COUNTDOWN TICK FAILEDentry. The bot backs off exponentially (1 minute up to 1 hour) and never skips a number on failure. - Run
npm run check:xlocally to confirm credentials and credits before redeploying.
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage report
npm run test:coverageSee tests/README.md for testing guide.
The application uses simple console logging (stdout/stderr) for compatibility with AWS App Runner and CloudWatch Logs.
Log Levels:
[INFO]- General information and flow tracking[DEBUG]- Detailed debugging information[WARN]- Warnings and non-critical issues[ERROR]- Errors and exceptions
Log Format:
[INFO] [2026-01-13T16:30:00.000Z] Message {"key":"value"}
Features:
- Automatic sanitization of sensitive data (credentials, tokens, keys)
- Timestamped logs for easy debugging
- Structured JSON data for parsing
- Extensive logging throughout countdown flow, Twitter API calls, and DynamoDB operations
Viewing Logs:
- Local: Logs appear in console when running
npm startornpm run dev - AWS App Runner: View logs in CloudWatch Logs console
The application consists of:
- Express.js web server
- DynamoDB for persistent storage of countdown state
- Twitter API v2 for posting tweets
- Console logging (stdout/stderr) for CloudWatch compatibility
- Node-cache for in-memory caching
- Handlebars for server-side templating
- On startup, scans DynamoDB (all pages) for the lowest number, which is the last post
- Waits 5 minutes, then computes the next number (current minus one) as words and a comma string
- Randomly adds a phrase and tag (1 in 5 chance)
- Posts via the X API v2
- Only after a successful post: updates in-memory state and writes the record to DynamoDB
- Schedules the next post with a random delay of 2 to 8 hours (about 4 to 6 posts a day)
- No credits or bad credentials (402, 401, 403): logs an
ACTION NEEDEDline and retries in 6 hours - X rate limits (429): waits for the reset time X reports
- DynamoDB throttling: the SDK retries with adaptive backoff (up to 5 attempts)
- Other errors: exponential backoff from 1 minute, capped at 1 hour
- Failures never decrement: the number only moves after a post succeeds
- Zero: countdown stops gracefully
| Variable | Description | Default |
|---|---|---|
AWS_ACCESS_KEY_ID |
AWS access key | Required |
AWS_SECRET_ACCESS_KEY |
AWS secret key | Required |
AWS_REGION |
AWS region | us-east-1 |
DYNAMODB_TABLE |
DynamoDB table name | voncountdown |
PORT |
Server port | 8080 |
NODE_ENV |
Environment | development |
DRY_RUN |
1 to log instead of posting to X or writing DynamoDB |
unset |
BOOT_DELAY_MS |
Pause before the first post after startup | 300000 |
TWITTER_API_KEY |
Twitter API key | Required |
TWITTER_API_SECRET |
Twitter API secret | Required |
TWITTER_ACCESS_TOKEN |
Twitter access token | Required |
TWITTER_ACCESS_TOKEN_SECRET |
Twitter access token secret | Required |
- Helmet.js for security headers with Content Security Policy (CSP)
- CSP limited to the page's own assets, Google Fonts, shields.io badge and the YouTube embed
- Rate limiting (100 req/15min per IP, 60 req/min for health endpoint)
- Input validation and sanitization
- XSS protection in templates
- Environment variable validation
- Request timeout handling (30 seconds)
- Log sanitization to prevent credential leakage
- Create App Runner service
- Connect to GitHub repository
- Set environment variables in App Runner console:
AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_REGIONDYNAMODB_TABLETWITTER_API_KEYTWITTER_API_SECRETTWITTER_ACCESS_TOKENTWITTER_ACCESS_TOKEN_SECRETNODE_ENV=productionPORT=8080
- The live service builds from the
masterbranch with auto-deploy off and runtime Node 22. To release: mergedevintomaster, then start a deployment from the App Runner console (oraws apprunner start-deployment). - App Runner streams logs to CloudWatch Logs and handles scaling and health checks.
Logging: All logs go to stdout/stderr and are automatically captured by CloudWatch Logs. Use AWS Console to view logs.
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 8080
CMD ["npm", "start"]- Fork the repository
- Create a feature branch
- Make your changes
- Run tests:
npm test - Submit a pull request
Code and documentation copyright 2016 Brian Funk. Code released under the MIT license.
