Skip to content

Hand Grok Bot users to X DMs after an opportunity accept - #1852

Open
serefyarar wants to merge 1 commit into
devfrom
feat/grok-x-dm-handoff
Open

serefyarar wants to merge 1 commit into
devfrom
feat/grok-x-dm-handoff

Conversation

@serefyarar

Copy link
Copy Markdown
Contributor

What

When users interact with Index through the Grok Bot MCP client, opportunity links now hand them off to X instead of keeping them in Index chat:

  • list_opportunities / get_opportunity mint acceptUrl/declineUrl with &surface=x, so the existing /o/:id?surface= web handoff lands the acceptor on the peer's X profile (one known X social) instead of the Index profile.
  • accept_opportunity returns the peer's https://x.com/<handle> as chatUrl — with a message to DM them on X — when exactly one X profile can be resolved from their socials. Zero or ambiguous X socials fall back to the Index chat, as does any lookup failure.
  • The grok-plugin skill (SKILL.md + paste.md, kept in sync) tells Grok to say the conversation continues in X DMs when the link is an x.com profile.

How Grok is detected

The MCP server is shared by all clients, so the OAuth path now resolves the registered client name (oauth_application.name, 5-minute in-process cache) and stamps surface: 'x' on the principal when the name matches /grok/i (new mcp.surface.ts). No Grok client has registered on dev/prod yet, hence the substring match. Session and API-key principals never get a surface; the name lookup swallows failures so it can never break authentication.

New lib/x/socials.ts normalizes stored social values (bare handle, @handle, x.com/twitter.com URLs, packed multi-address values) into canonical lowercase profile URLs, modeled on the Telegram normalizer; it rejects non-X hosts and X product paths (/i/…, /home, …). A targeted DM composer URL would need the peer's numeric X id, which we don't store, so the profile (with its Message button) is the handoff — same depth as t.me/<handle> was for Telegram.

Unchanged

Web app, morning-brief email links, and every non-Grok MCP client behave exactly as before (actionLink without a surface mints identical URLs).

Maybe later

  • A per-user setting to opt out of the X handoff ("maybe it's not for everyone") — would slot into the accept path as one more guard; left out of v1 since the behavior is already scoped to Grok-connected users and falls back gracefully.
  • peerChat links in list_opportunities (already-open chats) still point at Index chat.
  • The not-on-Index accept branch records the accept without a chat; an X handoff there could be valuable but changes its semantics.

Testing

  • New colocated specs: lib/x/socials.spec.ts (normalization, dedup, reserved paths, packed values) and lib/mcp/mcp.surface.spec.ts (name predicate) — pass.
  • bun run typecheck and bun run lint clean (one pre-existing unused-var warning elsewhere).
  • Not yet exercised against a live Grok client (none has registered an OAuth app yet); manual path: rename a dev oauth_application row to "Grok", mint a token, and check acceptUrl carries &surface=x and accept_opportunity returns the X profile as chatUrl.

🤖 Generated with Claude Code

When the MCP OAuth client is Grok, opportunity accept/decline links
carry surface=x so the existing /o handoff lands on the peer's X
profile, and accept_opportunity returns the peer's X profile as the
chat link when exactly one is known, falling back to the Index chat.
Other clients, the web app, and email links are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant