x-bookmarks
Read the user's latest X (Twitter) bookmarks as typed data (text, author, metrics, media, links, quoted posts, articles) with a TypeScript SDK that calls X's Bookmarks GraphQL API via fetch inside the
SKILL.mdskills/x-bookmarks/SKILL.md
GitHubX bookmarks
sdk.ts in this folder reads bookmarks with network calls only. It runs
fetch() inside an x.com page via page.evaluate, so the user's cookies
apply. No clicks, no scrolling, no DOM scraping. Do not copy cookies or
tokens out of the browser.
Preconditions: Chrome has the playwriter extension, and the user is logged
into x.com. Read playwriter skill in full first if you have not used it.
If playwriter session new fails with extension_not_connected, show the
user the printed install steps and stop. Do not try other workarounds.
Get the latest bookmarks
playwriter session new --tab-group x
playwriter -s <id> --timeout 60000 -e "$(cat <<'EOF'
const { getLatestBookmarks } = await import(require('node:os').homedir() + '/.config/opencode/skills/x-bookmarks/sdk.ts')
const bookmarks = await getLatestBookmarks({ context, limit: 50 })
require('node:fs').writeFileSync('/tmp/x-bookmarks.json', JSON.stringify(bookmarks, null, 2))
console.log(bookmarks.length, bookmarks.map((b) => `@${b.author.screenName}: ${b.text.slice(0, 80)}`).join('\n'))
EOF
)"
The playwriter sandbox runs Node 24, so import() of a .ts file works
(type stripping). Write big results to /tmp instead of printing them;
console output gets truncated.
getLatestBookmarks returns XTweet[], newest bookmark first. See the
interfaces at the top of sdk.ts for all fields. It opens its own tab and
always closes it (also on error), so no x.com tab is left open.
Manual pagination
Only use this when you need cursors. You own the tab, so close it in
finally. Never leave the page open after the export.
const sdk = await import(require('node:os').homedir() + '/.config/opencode/skills/x-bookmarks/sdk.ts')
const page = await context.newPage()
try {
const query = await sdk.discoverBookmarksQuery({ page })
const first = await sdk.fetchBookmarksPage({ page, query, count: 100 })
const second = await sdk.fetchBookmarksPage({ page, query, cursor: first.nextCursor })
} finally {
await page.close()
}
Only discoverBookmarksQuery navigates.
How it works
discoverBookmarksQueryopenshttps://x.com/i/bookmarksand waits for the web app's ownGET /i/api/graphql/<queryId>/Bookmarksrequest. It copiesqueryId, thefeaturesparam and the bearer header from it.fetchBookmarksPagereplays that GraphQL call withfetch()in the page, addingx-csrf-tokenfrom thect0cookie.- The response is parsed into
XTweetobjects. Thecursor-bottom-*entry givesnextCursor.
Gotchas
- Never hardcode the queryId or features. They change with X deploys. Always discover them from the live request.
- The queryId is not in
main.js. It is in a lazy chunk. Do not waste time scanning bundles. x-client-transaction-idis not required for this read endpoint.- The recording started from History (
/i/history), which shows the same Bookmarks tab. Theflow/timeline.json,live_pipelineandviewer_contextcalls in the recording are noise. UsersByRestIds HTTP-403page errors in playwriter output come from x.com itself. Ignore them.- Long posts: full text is in
note_tweet; the SDK handles this. - On
401/403from Bookmarks, ormissing ct0 cookie, ask the user to log into x.com in Chrome.