Deploy your own X (Twitter) OAuth service in one click. It holds your X OAuth client id and secret server-side and does the authorization-URL build, the code exchange, and the token refresh on behalf of your CLI tools — so those tools never handle a client secret, and every user authenticates under your X app, quota, and consent screen.
It is a thin HTTP wrapper over aux4/oauth, deployed as an api-type machine on aux4.cloud. The package itself contains no secrets — your credentials are supplied as machine environment variables.
aux4 aux4 cloud deploy x-oauth-app --package aux4/x-oauth-app --api true). You get a URL like https://<your-scope>.on.aux4.cloud/x-oauth-app.X_CLIENT_ID.Either way the app's Callback URI must be exactly http://localhost:9876/callback and the type/website filled in.
aux4 aux4 cloud x-oauth-app env set X_CLIENT_ID=... X_CLIENT_SECRET=... # secret only for a Web App
Values are encrypted at rest per scope and applied to the live machine immediately.
export X_AUTH_BROKER="https://<your-scope>.on.aux4.cloud/x-oauth-app/api"
The client never sees the secret. Expired tokens are refreshed through the app automatically (X returns a refresh token when offline.access is requested).
You do not normally install this package locally — you deploy it. To inspect or run it locally:
aux4 aux4 pkger install aux4/x-oauth-app
The service exposes a liveness check plus three X endpoints, served under the /api prefix (for example GET <machine-url>/api/health):
GET /health — returns {"status":"ok"}.GET /x/authorize-url — returns the X authorization URL, a PKCE codeVerifier, and the state.POST /x/exchange — takes the authorization code, the codeVerifier, and the redirectUri, and returns the tokens plus the resolved profile.POST /x/refresh — takes a refreshToken and returns a renewed set of tokens.X specifics (baked in so you don't have to rediscover them):
x.com / api.x.com, never twitter.com (which redirects and lowercases the query string, breaking client_id / code_challenge).tweet.read users.read offline.access — users.read is required for the /2/users/me profile lookup done during the exchange, and offline.access for a refresh token.clientSecretIn: basic); public clients simply have no secret.| Environment variable | Required | Description | |----------------------|----------|-------------| | X_CLIENT_ID | Yes | X OAuth client id | | X_CLIENT_SECRET | For a Web App (confidential) client | X OAuth client secret. Omit for a Native App (public) client. | | X_SCOPES | No | Default scopes (space or comma separated) used when a request does not specify any. Resolution is request → X_SCOPES → the bundled default (tweet.read users.read offline.access). |
Until X_CLIENT_ID is set, the endpoints return an error.
GET /x/authorize-urlQuery parameters: redirectUri (loopback, e.g. http://localhost:9876/callback), scopes (space separated; defaults to X_SCOPES then the bundled default), state.
curl "https://<your-scope>.on.aux4.cloud/x-oauth-app/api/x/authorize-url?redirectUri=http://localhost:9876/callback"
{
"url": "https://x.com/i/oauth2/authorize?response_type=code&client_id=...&code_challenge=...",
"codeVerifier": "b7f3...",
"state": "..."
}
POST /x/exchangeJSON body: code, codeVerifier, redirectUri.
curl -X POST "https://<your-scope>.on.aux4.cloud/x-oauth-app/api/x/exchange" \
-H "Content-Type: application/json" \
-d '{"code":"...","codeVerifier":"b7f3...","redirectUri":"http://localhost:9876/callback"}'
{
"accessToken": "...",
"refreshToken": "...",
"expiresIn": 7200,
"tokenType": "bearer",
"principal": { "data": { "id": "12", "name": "...", "username": "..." }, "provider": "x" }
}
POST /x/refreshJSON body: refreshToken.
curl -X POST "https://<your-scope>.on.aux4.cloud/x-oauth-app/api/x/refresh" \
-H "Content-Type: application/json" \
-d '{"refreshToken":"..."}'
These endpoints are unauthenticated: anything that can reach the URL can start an OAuth flow with your X app. That is by design for a loopback CLI login, but treat the deployment URL as semi-sensitive, register only the callback URI your clients use, and keep X_SCOPES scoped to what you need. A scope allowlist is a planned addition.