Part 1: Authentication Hell: Getting AWS Lambda Talking to X
How a simple idea turned into a multi-day battle with APIs, OAuth, and enough authentication errors to fill a spellbook.
The Idea
Every project starts with a simple question. For me, it was: Could I build an autonomous Wizarding World content engine that generates its own content, creates its own artwork, and publishes directly to X without human involvement? The concept seemed straightforward. The vision was:
- AI generates quote โ
- AI generates artwork โ
- AWS stores content โ
- X publishes post
In theory, this looked like a weekend project. In reality, it became an education in authentication systems, API permissions, OAuth flows, and the many ways technology can tell you "no".
Building the Foundation
I decided to build everything on AWS. The initial stack looked like this:
- AWS Lambda
- Amazon DynamoDB
- Amazon Bedrock
- Amazon S3
- EventBridge
- X API
The first objective was intentionally small: Post a single tweet from AWS Lambda. Not an AI-generated tweet. Not an image. Just a tweet. How hard could that be?
The First Lambda
The first Lambda function was incredibly simple:
- Create Lambda
- Install
twitter-api-v2 - Add environment variables
- Call tweet endpoint
Five minutes later everything was deployed. I clicked Test. It failed. Welcome to OAuth.
The Authentication Rabbit Hole
If you've worked with X's API before, you'll know there isn't just one way to authenticate. There are several. At first glance they all appear to do similar things:
- Bearer Token
- OAuth 2.0
- OAuth 2.0 User Context
- OAuth 1.0a
The challenge is understanding which one works with which endpoint. I made what seemed like the obvious choice. I used the Bearer Token. The result?
403 Forbidden
Not very helpful. After digging through the logs, I eventually found the real message:
Unsupported Authentication
Authenticating with OAuth 2.0 Application-Only is forbidden for this endpoint.
This was my first major lesson. Just because you're authenticated doesn't mean you're authorised.
Understanding the Difference
The problem wasn't my code. The problem was the type of identity I was presenting to X.
- A Bearer Token represents: The application
- A User Context token represents: The user
Posting a tweet requires a user. Not just an application. In other words:
- Application: "Hello, I am WizardThoughts."
- X: "Great."
- Application: "I would like to tweet."
- X: "No."
Chasing the Right Token
I spent hours moving between:
- AWS Lambda
- CloudWatch Logs
- X Developer Portal
Testing. Deploying. Testing again. Every change seemed to produce a different error. Sometimes:
401 Unauthorized
Other times:
403 Forbidden
And occasionally:
Unsupported Authentication
At one point I genuinely believed I had broken the entire account configuration. The reality was much simpler: I was using the wrong type of token.
The First Real Breakthrough
Rather than trying to post tweets, I decided to verify authentication first. Instead of:
client.v2.tweet(...)
I switched to:
client.v2.me()
This endpoint simply returns information about the currently authenticated user. If that worked, I would know the authentication was correct. If it failed, the token was wrong.
Suddenly I received:
{
"username": "WizardThoughts"
}
That was the first real victory. For the first time, AWS Lambda had successfully authenticated with X. No guessing. No assumptions. Proof.
The OAuth 2 Dead End
With authentication seemingly solved, I moved on to images. That's when another problem appeared. The basic tweet APIs worked. Media uploads did not. The logs revealed a new pattern. Every media upload attempt failed with:
403 Forbidden
After more debugging, I discovered another important distinction:
- Text posting could work using: OAuth 2 User Context
- Media uploads were much happier with: OAuth 1.0a User Context
Which meant I needed four credentials:
CONSUMER_KEYCONSUMER_SECRETACCESS_TOKENACCESS_TOKEN_SECRET
Suddenly the full picture started making sense.
The Moment Everything Clicked
The final authentication test looked like this:
const client = new TwitterApi({
appKey: process.env.CONSUMER_KEY,
appSecret: process.env.CONSUMER_SECRET,
accessToken: process.env.ACCESS_TOKEN,
accessSecret: process.env.ACCESS_TOKEN_SECRET
});
const me = await client.v2.me();
Response:
{
"username": "WizardThoughts"
}
Success. Not a partial success. Not a maybe. A genuine, repeatable success. The authentication layer was finally solved.
The First Automated Tweet
Once the OAuth issues were resolved, posting a tweet became almost trivial. The same infrastructure that had spent days refusing to cooperate suddenly worked exactly as intended. The first tweet appeared. Not created manually. Not posted from a browser. Automatically. Directly from AWS Lambda. That single post represented far more than a tweet. It proved the entire foundation was viable.
Lessons Learned
Three lessons stood out above everything else.
Authentication Is a Product Feature - Most developers treat authentication as setup. It isn't. It's part of the application. Understanding identities, scopes, permissions, and token types is essential.
Trust Error Messages - Most of my mistakes came from ignoring what the logs were telling me. Eventually every answer was in CloudWatch. The challenge was learning how to read it.
Prove Identity First - Before attempting any complex API action:
- Authenticate โ
- Verify user โ
- Then perform action
Testing with v2.me() saved hours of debugging.
What's Next?
At this point the bot could successfully:
- โ Authenticate with X
- โ Run in AWS Lambda
- โ Post automated tweets
But it still had one major limitation. Every tweet had to be written manually. In Part 2, we'll teach the bot how to think for itself using Amazon Bedrock, prompt engineering, and growth-focused content generation. Because a bot that can post is useful. A bot that can create its own content is where the magic begins.
โก๐ช Next: Part 2 - Prompt Engineering for Growth: Creating Viral Wizarding Content
Comments
No comments yet. Start the discussion.