@kody/x
README.md
197 lines · 8.5 KB · Markdown@kody/x
Official X logo (black rounded square, white glyph) from the X brand toolkit. Paths are unmodified. X and the X logo are trademarks of X Corp.
Intent
Give any Kody account reusable, headless X API v2 helpers for user lookup, tweets, recent search, and guarded posting. Auth is the caller’s saved x / x-* OAuth integration (optional app-only bearer for public reads). Identity always comes from the connected account — never a hard-coded profile.
This listing is meant to be forked. After you fork, connect your X app and run ./smoke-test on your copy. Do not treat the live @kody/x integration as yours. Share https://kody.codes/@kody/x.
When To Use
- Verify a saved
xOAuth integration and read the connected user - Look up a user by username, read a tweet, list a user’s recent tweets, or search recent posts
- Call uncommon X API v2 routes with
./request - Preview a post or social action with
dryRun: true, then mutate only afterconfirm: true
Do not use this package as a 24/7 bot, a scraping client, or a place to paste access tokens. It does not include encrypted X Chat, Fly sidecars, or any owner-specific infrastructure.
Agent setup
X is bring-your-own OAuth. There is no built-in Kody X app. Tokens come from the hosted connect page. Never paste a client secret or access token into chat.
- Open X Developer Portal → create a Project and App with User authentication (OAuth 2.0).
- App type: Native App (public / PKCE) or a confidential client with PKCE enabled.
- Callback / redirect URI — register exactly
https://kody.codes/connect/oauth(the connect page shows the same value with a copy button). - Website URL can be
https://kody.codes. - Open this prefilled connect URL while signed in to Kody:
https://kody.codes/connect/oauth?provider=xFirst-time setup also needs X’s authorize and token URLs. Use this complete prefilled URL when the connection does not exist yet:
https://kody.codes/connect/oauth?provider=x&authorizeUrl=https%3A%2F%2Fx.com%2Fi%2Foauth2%2Fauthorize&tokenUrl=https%3A%2F%2Fapi.x.com%2F2%2Foauth2%2Ftoken&apiBaseUrl=https%3A%2F%2Fapi.x.com%2F2&flow=pkce&allowedHosts=api.x.com%2Cupload.x.com&dashboardUrl=https%3A%2F%2Fdeveloper.x.com%2Fen%2Fportal%2Fdashboard&scopes=tweet.read%20tweet.write%20users.read%20offline.access%20like.read%20like.write%20follows.read%20follows.write%20bookmark.read%20bookmark.writeDecoded:
- Redirect / callback:
https://kody.codes/connect/oauth - Authorize:
https://x.com/i/oauth2/authorize - Token:
https://api.x.com/2/oauth2/token - API base:
https://api.x.com/2 - Flow:
pkce(addflow=confidential&pkce=trueif the X app has a client secret) - Hosts:
api.x.com,upload.x.com - Scopes:
tweet.read,tweet.write,users.read,offline.access,like.read,like.write,follows.read,follows.write,bookmark.read,bookmark.write
- Paste the Client ID (and Client Secret only for confidential apps) into the Kody form, continue to X, and approve.
- Run Smoke test on the forked package.
Reconnect the same connection with https://kody.codes/connect/oauth?provider=x. Kody reuses the saved authorize/token endpoints.
Optional app-only bearer
Public reads can use an app-only bearer token instead of user context. Save it as xBearerToken (never paste the value into chat) and pass authMode: 'bearer' on read helpers:
https://kody.codes/account/secrets/new?name=xBearerToken&description=X%20API%20v2%20app-only%20bearer%20token%20for%20optional%20public%20reads&allowedHosts=api.x.com&scope=userOAuth remains the default. Writes always use OAuth.
Multi-account
OAuth helpers accept optional account / integration:
| Call | Resolves to |
|---|---|
| omit both | integration x |
account: 'work' | integration x-work |
integration: 'x-work' | that exact integration |
Connect more accounts at https://kody.codes/connect/oauth?provider=x-work (same first-time query params, different provider). ./accounts lists whatever x / x-* integrations the signed-in user has connected.
Scopes
| Scope | Used by |
|---|---|
tweet.read | tweet lookup, user tweets, search |
tweet.write | create / delete tweet, repost |
users.read | ./get-me, user lookup, follow |
offline.access | refresh tokens |
like.read / like.write | like / unlike |
follows.read / follows.write | follow / unfollow |
bookmark.read / bookmark.write | bookmark helpers |
./request uses whatever scopes the saved token already has.
Smoke test
After connect or reconnect, import the published package (not packages.invoke):
import smokeTest from 'kody:@kody/x/smoke-test'
export default async function main() {
return await smokeTest()
}Success looks like { ok: true, integration: 'x', user: { id, username, name } } and does not post. Missing credentials return { ok: false, setup } with the prefilled connect URLs. HTTP 429 counts as auth plumbing verified.
Low-level equivalent (execute exploration only):
import { createAuthenticatedFetch } from 'kody:runtime'
export default async function main() {
const xFetch = await createAuthenticatedFetch('x')
const response = await xFetch('https://api.x.com/2/users/me')
if (!response.ok) {
throw new Error('X users/me failed: ' + response.status)
}
const data = (await response.json()) as { data?: { id?: string; username?: string } }
return { ok: true, hasUser: Boolean(data.data?.id) }
}dryRun and posting
Mutating helpers never write to X when dryRun: true, and they refuse a live write unless confirm: true after the user approved the exact payload.
import createTweet from 'kody:@kody/x/create-tweet'
export default async function main() {
return await createTweet({ text: 'Draft text', dryRun: true })
}Do not infer confirmation from a successful dry-run. Require a fresh confirm: true for the exact text or target.
Exports
| Export | Description |
|---|---|
. | Package overview, connect URLs, and export map |
./accounts | List connected x / x-* integrations |
./smoke-test | Read-only OAuth /users/me check |
./request | Generic X API v2 helper; writes need dryRun / confirm |
./refresh-oauth-token | Rotate tokens through the host refresh helper |
./get-me | Authenticated user profile |
./get-user-by-username | Public user lookup |
./get-tweet | Read one tweet by id |
./search-recent | Search recent public posts |
./get-user-tweets | Recent tweets for a user id |
./create-tweet | Preview or create a post |
./delete-tweet | Preview or delete a post |
./like-tweet / ./unlike-tweet | Preview or like / unlike |
./repost-tweet / ./unrepost-tweet | Preview or repost / unrepost |
./bookmark-tweet / ./remove-bookmark | Preview or bookmark |
./follow-user / ./unfollow-user | Preview or follow / unfollow |
./media-upload-guide | Static media upload notes |
Examples
import listXAccounts from 'kody:@kody/x/accounts'
import getMe from 'kody:@kody/x/get-me'
import createTweet from 'kody:@kody/x/create-tweet'
export default async function main() {
const { accounts } = await listXAccounts()
const me = await getMe()
const draft = await createTweet({
text: 'Hello from Kody',
dryRun: true,
})
return { accounts, me: me.data, draft }
}Named account:
import getMe from 'kody:@kody/x/get-me'
export default async function main() {
return await getMe({ account: 'work' })
}Troubleshooting
- Connect page asks for authorize/token URLs: use the complete first-time URL in Agent setup.
- HTTP 401 / expired token: reconnect at
https://kody.codes/connect/oauth?provider=x, or call./refresh-oauth-token. - HTTP 403 on writes: the X app needs the matching write scopes. Reconnect after adding them.
- HTTP 429: X rate-limited the call. Treat as plumbing-verified for smoke; wait before retrying.
redirect_urimismatch: the X app callback must be exactlyhttps://kody.codes/connect/oauth.