fix(recipes/x-to-brain): use /users/by/username for app-only bearer health check

Takeover of #2343. /users/me requires user-context OAuth and always fails
under the app-only bearer the recipe collects. Health check + setup curls
now use /users/by/username/$X_HANDLE, with X_HANDLE declared in secrets
so the installer prompts for it. Recipe version 0.8.1 -> 0.8.2.

Co-authored-by: ethanbeard <ethanbeard@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-07-21 14:43:44 -07:00
co-authored by ethanbeard Claude Fable 5
parent 0612b0daa8
commit 5df66c3855
2 changed files with 42 additions and 8 deletions
+13 -8
View File
@@ -1,7 +1,7 @@
---
id: x-to-brain
name: X-to-Brain
version: 0.8.1
version: 0.8.2
description: Twitter timeline, mentions, and keyword monitoring flow into brain pages. Tracks deletions, engagement velocity, OCR on images, and real-time alerts.
category: sense
requires: []
@@ -9,9 +9,12 @@ secrets:
- name: X_BEARER_TOKEN
description: X API v2 Bearer token (Basic tier minimum, $200/mo for full archive search)
where: https://developer.x.com/en/portal/dashboard — create a project + app, copy the Bearer Token from "Keys and tokens"
- name: X_HANDLE
description: Your X username without the @ (used for the app-only health check — /users/me requires user-context OAuth, which app-only bearer tokens don't have)
where: Your X profile — the handle in your profile URL, e.g. x.com/yourhandle → yourhandle
health_checks:
- type: http
url: "https://api.x.com/2/users/me"
url: "https://api.x.com/2/users/by/username/$X_HANDLE"
auth: bearer
auth_token: "$X_BEARER_TOKEN"
label: "X API"
@@ -110,15 +113,17 @@ Tell the user:
4. Inside the project, create a new App
5. Go to the app's 'Keys and tokens' tab
6. Under 'Bearer Token', click 'Generate' (or 'Regenerate')
7. Copy the Bearer Token and paste it to me
7. Copy the Bearer Token and paste it to me, along with your X handle (without the @)
Note: Free tier gives read-only access with low limits. Basic tier ($200/mo)
gives search/recent endpoint and higher limits. Pro tier gets full archive search."
Validate immediately:
Set both `X_BEARER_TOKEN` and `X_HANDLE` in the environment. Validate immediately
(app-only bearer tokens cannot call `/users/me` — that endpoint requires
user-context OAuth — so validation uses the by-username lookup):
```bash
curl -sf -H "Authorization: Bearer $X_BEARER_TOKEN" \
"https://api.x.com/2/users/me" \
"https://api.x.com/2/users/by/username/$X_HANDLE" \
&& echo "PASS: X API connected" \
|| echo "FAIL: X API token invalid"
```
@@ -134,10 +139,10 @@ starting with 'AAA...', (3) if you just created the app, the token is valid imme
```bash
# Look up the user's X user ID from their handle
curl -sf -H "Authorization: Bearer $X_BEARER_TOKEN" \
"https://api.x.com/2/users/by/username/USERNAME" | grep -o '"id":"[^"]*"'
"https://api.x.com/2/users/by/username/$X_HANDLE" | grep -o '"id":"[^"]*"'
```
Ask the user for their X handle (e.g., @yourhandle). Look up their user ID.
Look up the user ID from the handle collected in Step 1.
Save it — the collector needs the numeric ID, not the handle.
### Step 3: Configure the Collector
@@ -205,7 +210,7 @@ The agent should review collected data 2-3x daily and run enrichment.
```bash
mkdir -p ~/.gbrain/integrations/x-to-brain
echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","event":"setup_complete","source_version":"0.8.1","status":"ok","details":{"user_id":"X_USER_ID"}}' >> ~/.gbrain/integrations/x-to-brain/heartbeat.jsonl
echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","event":"setup_complete","source_version":"0.8.2","status":"ok","details":{"user_id":"X_USER_ID"}}' >> ~/.gbrain/integrations/x-to-brain/heartbeat.jsonl
```
## Production Patterns (v0.8.1)
+29
View File
@@ -263,6 +263,35 @@ describe('twilio-voice-brain recipe', () => {
});
});
describe('x-to-brain recipe', () => {
test('health check works with an app-only bearer token (#2343)', () => {
const { readFileSync } = require('fs');
const content = readFileSync(
new URL('../recipes/x-to-brain.md', import.meta.url),
'utf-8'
);
const recipe = parseRecipe(content, 'x-to-brain.md');
expect(recipe).not.toBeNull();
const httpChecks = recipe!.frontmatter.health_checks
.filter((c: any) => typeof c === 'object' && c.type === 'http');
expect(httpChecks.length).toBeGreaterThan(0);
const secretNames = new Set(recipe!.frontmatter.secrets.map((s: any) => s.name));
for (const check of httpChecks as any[]) {
// /users/me requires user-context OAuth; the recipe only collects an
// app-only bearer token, so probing it always fails.
expect(check.url).not.toContain('/users/me');
// Every $VAR the check expands must be declared in secrets, or the
// installer never prompts for it and the check fails for everyone.
const vars = [check.url, check.auth_token, check.auth_user, check.auth_pass]
.filter((v: unknown): v is string => typeof v === 'string')
.flatMap((v: string) => v.match(/\$[A-Z_][A-Z0-9_]*/g) ?? [])
.map((v: string) => v.slice(1));
expect(vars.length).toBeGreaterThan(0);
for (const name of vars) expect(secretNames.has(name)).toBe(true);
}
});
});
// --- All recipes parse without error ---
describe('all recipes', () => {