X (Twitter) Commerce
Sell products, receive tips, and earn affiliate revenue directly from X (Twitter). These endpoints power the merchant X Commerce dashboard: linking an X handle, connection state, tip and social-sale aggregates, affiliate revenue, and buy-button ("payment post") generation.
All X Commerce endpoints except the public creator profile (GET /x/pay/:username) and the public
payment-link generator (POST /x/pay/:username/link) require a valid JWT sent as
Authorization: Bearer <token>. The base URL for all requests is https://gullypay.xyz/api. Tips and
social sales are persisted as real graph_edges rows and aggregated per creator from that data.
Connect with X runs a real X OAuth 2.0 authorization-code flow with PKCE. The handle is read from
X itself (GET /2/users/me) after the token exchange — never from client input — so a connection
proves the user controls that X account. Handles linked this way carry verified: true. A handle
already linked to another account is rejected with 409.
POST /x/link remains as an unverified self-service fallback for when OAuth is not configured: it
attaches a handle to the authenticated user's own account (owner id/email come from the JWT, never the
body), marked verified: false. Prefer the OAuth flow.
The OAuth endpoints require X_CLIENT_ID and X_CLIENT_SECRET (an X developer OAuth 2.0 confidential
app) plus X_CALLBACK_URL (must match a registered redirect URI —
https://gullypay.xyz/api/x/oauth/callback in production). When these are absent, GET /x/oauth/start
and the callback fail cleanly with 400 "X OAuth is not configured" — the platform never fakes a
successful link. Requested scopes: users.read tweet.read offline.access.
Endpoints
Connection & Onboarding
/x/oauth/startBegin the real X OAuth 2.0 (PKCE) flow; returns the X authorize URL (auth required)/x/oauth/callbackX OAuth redirect target; verifies the handle and links it, then redirects to the dashboard (no auth — user resolved from state)/x/linkUnverified self-service link fallback (auth required)/x/me/connectionCurrent X connection state incl. verified flag + real public-metric stats (auth required)/x/me/stats/refreshRefresh the linked handle's public follower/post counts from the X API (auth required)/x/me/disconnectUnlink the X handle from the authenticated user (auth required)/x/me/affiliateAffiliate revenue summary for the authenticated user (auth required)Tips & Aggregates
/x/tipSend a tip to a creator by X username (auth required)/x/tips/:username/statsTip count + total + recent tips received by a creator (auth required)/x/tips/:usernameRecent tips received by a creator (auth required)/x/creators/:username/top-tippersTop tippers for a creator, aggregated from tip edges (auth required)/x/creators/:username/social-salesRecent X-attributed social sales for a creator (auth required)/x/creators/:username/activityDaily tips + sales + affiliate time-series (auth required)Buy Buttons & Payment Links
/x/buy-buttonGenerate a buy button / payment post for a product (auth required)/x/pay/:usernamePublic creator pay profile/x/pay/:username/linkGenerate a public payment link for a creatorConnect with X (OAuth 2.0 + PKCE)
/x/oauth/startBegin the OAuth flowCall with the user's JWT. The response contains the X authorize URL; redirect the browser to it. The
server generates and persists a CSRF state and a PKCE verifier/challenge against the user's record.
/x/oauth/callbackOAuth redirect target (X → GullyPay)X redirects the browser here with ?code=…&state=…. The server resolves the initiating user from
state, exchanges the code at https://api.twitter.com/2/oauth2/token (PKCE + Basic auth), reads the
verified handle and public_metrics from GET /2/users/me, links that handle to the user
(verified: true), then 302-redirects the browser back to …/x-commerce?x_connected=1&handle=<handle>.
On failure it redirects with ?x_error=<message>. Access tokens are used transiently and never
persisted. A handle already owned by another account is rejected with 409.
On a successful OAuth connect the response captures a real snapshot of followers, following,
and posts (public_metrics.tweet_count) from X. POST /x/me/stats/refresh re-pulls these live
using the app-only X_BEARER_TOKEN (when configured; otherwise returns available: false).
Engagement rate and link clicks are not exposed by the X API and are intentionally left
unavailable (—) rather than fabricated.
Link an X Account (unverified fallback)
/x/linkLink an X handle to the authenticated user| Field | Type | Required | Description |
|---|---|---|---|
xUsername | string | Yes | The X handle to link (a leading @ is stripped) |
xUserId | string | No | The numeric X user id, if known |
The owning user id and email come from the JWT, not the body. Linking is idempotent: it ensures the user has a wallet, an affiliate code, and a commerce-graph node so inbound tips can be aggregated.
Connection State
/x/me/connectionCurrent X connection stateverified is true only when the handle was connected via the OAuth flow. stats is a real snapshot
from the X API public_metrics, or null until a real fetch has succeeded (the UI shows —, never
fabricated numbers). When no handle is linked, connected is false and the string fields are null.
Refresh Public Stats
/x/me/stats/refreshPull the linked handle's public metrics from the X APIGated by X_BEARER_TOKEN (app-only bearer). Returns the refreshed snapshot, or an honest
{ "available": false, "reason": "not_configured" | "not_connected" | "x_api_error" | "no_metrics", "stats": null }
when live stats can't be sourced.
Affiliate Summary
/x/me/affiliateAffiliate revenue summaryReal figures from the affiliates ledger plus a referral count. Returns a zeroed summary when the
user has no affiliate record.
Tips
/x/tipSend a tip to a creator| Field | Type | Required | Description |
|---|---|---|---|
toUsername | string | Yes | Recipient creator's X handle |
amount | number | Yes | Tip amount in minor units (cents) |
currency | string | Yes | ISO currency code |
Each tip is persisted as a real graph_edges row (edgeType = tip, weight = amount) targeting the
recipient's graph node, so all tip aggregates below are computed from real data.
Top Tippers
/x/creators/:username/top-tippersTop tippers for a creatorAggregates tip edges grouped by sender, resolved back to a display handle, ordered by total tipped.
Optional ?limit= (default 5). Returns [] until real tips exist.
Social Sales
/x/creators/:username/social-salesRecent X-attributed social salesReads X-attributed sales persisted as x_sale graph edges. This is a real query that returns an
honest empty list ([]) until sales are attributed to X — the platform does not fabricate rows.
Activity Time-Series
/x/creators/:username/activityDaily tips + sales + affiliate seriesOptional ?days= (1–90, default 7). Every series is a real DB aggregation; missing days are
zero-filled so a chart can render a continuous axis.
Buy Buttons
/x/buy-buttonGenerate a buy button / payment post for a productThe merchant dashboard's "+ Create Payment Post" action calls this endpoint with a product picked from the merchant's real catalog and shows the returned direct link and embed code.
Next Steps
- Commerce Graph -- The graph that stores tip and sale edges
- CRM & Loyalty -- The affiliate/referral network behind affiliate revenue
- Merchants -- Create the merchant that owns buy buttons