# Twitter Personality: npm instructions, a bun build script, and a paywall wired but switched off

> Twitter Personality profiles a handle by pulling about 15 recent posts through the paid X API and streaming a model response into the page. Its own files are the informative part: a build script that calls bun while the setup steps say npm, four Stripe variables for a paywall that is off by default, and database columns that keep an older product's name on purpose.

**wordware-ai/twitter** — AI Agent for Twitter Personality Analysis

- Repository: https://github.com/wordware-ai/twitter
- Website: https://twitter.wordware.ai
- Stars: 1,450 · Forks: 228
- Language: TypeScript
- License: not declared
- Published: 2026-09-10 · Updated: 2026-09-10 · Language: en
- Canonical page: https://hysenlabs.com/projects/wordware-ai-twitter

## The build script calls bun while the setup steps call npm

The setup instructions walk through cloning the repository, running `npm install`, and starting with `npm run dev`. The manifest disagrees at build time. The build script is `bun db:migrate && next build`, and there is a second script named `build:old` whose entire content is `next build`, which is to say a variant that skips the migration step. Anyone who followed the README exactly and then ran a production build would reach a bun invocation the README never mentioned, and the fallback script that would have worked is named in a way that invites skipping it. The migration itself runs through `tsx migrate.ts`, with a companion `db:generate` script wired to the migration toolkit's generator.

## Four Stripe variables ship for a paywall that is off

The example environment file reserves a complete billing block: a secret key, a webhook secret, a price identifier and a product identifier. The comment above them calls it paywall plumbing and states that the paywall is disabled by default through a config file in the source tree. The README repeats the same thing, pointing readers at that config file rather than describing a live paywall.

The plumbing is not decorative. The manifest carries a script that runs the Stripe CLI in listener mode and forwards events to the webhook route on port 3000, so the path from a checkout to a database row is already built and can be switched on by changing a flag. What is missing is any statement of what gets gated when it is enabled, since neither document says which analyses the paywall would cover or whether the cached 2024 results would remain readable. For a self hoster that is the question to answer before flipping it.

## The model is one variable, and its default is written twice

Generation runs through the Vercel AI Gateway, and the choice of model is a single environment variable holding any Gateway model string. The default is named in two places, the setup steps and the comment beside the variable in the example file, and both name the same xai model with a non reasoning suffix. Two other examples are offered in the setup steps, one from Z.ai and one from OpenAI, and the README states plainly that swapping models means changing that one value.

The Gateway key is the other half of that arrangement, and its absence is deliberate in one environment. The example file says to create a key in the Vercel dashboard, then notes that on Vercel deployments OIDC authentication is automatic and the variable can be omitted. That single line is the difference between a free preview and a self hosted instance that needs a billing relationship with the platform, and it is the reason the model variable is described as a Gateway string rather than a provider specific name.

## Data comes from a metered X token with a scraper behind it

The analysis starts with a handle and takes the profile plus roughly 15 recent posts. The primary source is the official X API v2, authenticated with a bearer token that the example file describes as pay per use, with credits bought in the X developer console. Behind it sits a second key for a third party scraping service, named explicitly as the fallback.

Two inputs are possible at the start, one handle or two, and the second is described as a compatibility check. That maps onto the two streaming endpoints the architecture notes name, one for a single analysis and one for a pair, so the compatibility path is a real code path rather than a UI flourish.

The cost shape follows directly. Every generation spends X API budget, and a paired run spends it twice, while the results themselves are cached in a hosted Postgres database. A deployment that is read far more often than it is written pays nothing per view, which is the design reason the caching layer is described in as much detail as the prompts.

## The cached rows decide the schema, not the types

The prompts live in one file and the shapes in another, and the relationship between them is explicitly a compatibility contract. The prompt enumerates the exact output keys, and a comment records that structured output mode is deliberately avoided because constrained decoding flattens the voice of the responses. That is an unusual choice for a feature that streams JSON, and it explains why the client needs its own partial parser.

The second file documents the shapes, and the note attached says they mirror the cached JSONB analyses key for key, followed by an instruction not to change keys casually. The same pattern appears in the database columns: the user and pair rows carry status flags in columns whose names start with an older product's name, and those historical names are kept for data compatibility.

So the constraint on a refactor is not the TypeScript. It is every row already written, which is also why previously generated analyses from the original viral version keep rendering after the relaunch.

## The endpoint streams raw JSON text and the browser parses it

Both analysis endpoints stream raw JSON text rather than server rendered events, and the client renders partial JSON as it arrives using a parser kept in the source tree for that purpose. Nothing on the server waits for the model to finish before sending bytes, which is what lets the personality text appear progressively instead of after a full generation.

The same result is then made shareable in two other ways. It is rendered into dynamic OG images, so a link preview is generated per analysis rather than being a static card, and it is cached, which is what allows a shared link to keep working after the model response is no longer reproducible. The share links themselves are derived from a base URL variable, with the example file giving a local address as the development case.

One dependency in the manifest is a hardened JSON parser, and the streaming design is the reason it is there: partial text arriving from a model is exactly the input that a plain parse should not be trusted with.

## No license file, no releases, and a product name from a previous one

Three facts about the repository itself are worth recording. The license is not declared in the metadata and there is no license file in the root listing, so nothing in the repository states the terms for reuse. There are no releases at all, so there is no version to deploy other than the default branch. The most recent push is dated 2026-08-02 and the repository is not archived.

The manifest tells a third story about identity. The package is named `twitter-personality` at version 0.1.0, marked private, which does not match the repository name, and the same older product name survives inside the database column prefixes described earlier. Branding belongs to a third product again: the design tokens for it are defined in the Tailwind config and the global stylesheet, with the logo files kept in a brand directory under the public folder.

Taken together, three product names occupy one codebase, and the one that persists in the database is the one that cannot be changed without a migration.

## Conclusion

This is a small, readable Next application that is cheaper to run than to fork, and the honest reason to read it is the caching and dedupe design rather than the personality output. Before running it, budget three paid dependencies at once, because the X token is metered per call and the Gateway key is required the moment you deploy anywhere other than Vercel, where OIDC removes it. Read the prompt file before you touch anything, since the output keys are mirrored key for key into cached JSONB rows and renaming one silently breaks every stored analysis. And note that no license file is present, so the default position on reuse is no reuse.

## FAQ

### What does Twitter Personality do with a handle?

It fetches the profile and about 15 recent posts through the official X API v2, with a third party scraping service as fallback, then streams a model analysis covering a roast, strengths, love life, a spirit animal and pickup lines. The result is cached in Postgres and rendered into a dynamic share image.

### Does running Twitter Personality myself need an X API token?

Yes. The example environment file asks for an X API v2 bearer token described as pay per use, with credits bought in the X developer console, and a separate key for the fallback scraper. Every generation spends from that budget, and a two handle comparison spends twice.

### Can I change the model Twitter Personality uses?

Yes, through one variable that accepts any Vercel AI Gateway model string. The default is an xai model with a non reasoning suffix, and the setup steps give a Z.ai and an OpenAI model as alternatives. The Gateway key can be omitted on Vercel deployments, where OIDC authentication is automatic.

### Is the Twitter Personality paywall active?

No, it is off by default through a flag in the source config file. The example environment file still reserves a Stripe secret key, webhook secret, price and product identifiers, and the manifest carries a script that forwards Stripe webhooks to the local API route.

### Why do Twitter Personality database columns keep an older product name?

The user and pair rows carry status flags in columns prefixed with the previous product's name, and those names are kept for data compatibility. The row shapes mirror the cached JSONB analyses key for key, which is why previously generated results keep rendering.

## Sources

- [Issues](https://github.com/wordware-ai/twitter/issues)
- [Project website](https://twitter.wordware.ai)
- [README](https://github.com/wordware-ai/twitter/blob/main/README.md)
- [wordware-ai/twitter on GitHub](https://github.com/wordware-ai/twitter)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/wordware-ai-twitter
