Back

Technical reference for XCash — SOL payments tied to X (Twitter) identities, built on Solana.

Overview
What is XCash?
XCash lets anyone send Solana (SOL) to an X username without knowing the recipient's wallet address. Under the hood it links an X identity to a custodial Solana wallet created on first login.
How it works
  1. User authenticates via OAuth 2.0 with X (Twitter). Their X UID is stored and a Solana keypair is derived and persisted server-side.
  2. Sender calls the pay endpoint with a recipient X handle and an amount in lamports. XCash resolves the handle to a wallet address and signs the transfer.
  3. The transaction is submitted to the Solana Mainnet-Beta RPC. The tx signature is returned to the caller.
  4. Recipients can withdraw at any time to an external wallet they own.
Authentication
OAuth 2.0 via X
XCash uses X's OAuth 2.0 PKCE flow. After a successful login the server issues a short-lived session cookie (xcash_session) that is HttpOnly, Secure, and SameSite=Lax. Include it in every authenticated request.
Flow
  1. Redirect the user to GET /xauth/twitter. XCash generates a PKCE challenge and redirects to twitter.com/i/oauth2/authorize.
  2. X redirects back to /xauth/twitter/callback with a code parameter.
  3. Server exchanges the code for tokens, upserts the user record, and sets the session cookie.
  4. All subsequent API calls are authenticated via the session cookie — no Bearer token required from the client.
API Reference
GET
/api/me
Returns the authenticated user's profile: X username, display name, avatar, and linked Solana wallet address.
{
  "username": "satoshi",
  "name": "Satoshi",
  "avatar": "https://pbs.twimg.com/...",
  "wallet": "7xKX...F3Qm"
}
GET
/api/balance
Returns the SOL balance of the authenticated user's custodial wallet in lamports and SOL.
{
  "lamports": 2500000000,
  "sol": 2.5
}
POST
/api/pay
Send SOL to a recipient identified by their X handle. Requires an active session.
// Request
{
  "to": "vitalik",      // X username (without @)
  "lamports": 1000000   // amount in lamports (1 SOL = 1e9)
}

// Response
{
  "signature": "5fG3...aP9z",
  "explorer": "https://solscan.io/tx/5fG3...aP9z"
}
POST
/api/withdraw
Withdraw SOL from the custodial wallet to an external Solana address.
// Request
{
  "to": "ExternalWalletAddress...",
  "lamports": 500000000
}

// Response
{
  "signature": "3rH1...bK7w"
}
GET
/api/history
Returns a paginated list of incoming and outgoing transactions for the authenticated user.
{
  "transactions": [
    {
      "type": "received",
      "from": "elonmusk",
      "lamports": 1000000,
      "signature": "5fG3...aP9z",
      "timestamp": "2025-03-24T10:00:00Z"
    }
  ],
  "cursor": "next_page_token"
}
Error Codes
Code Status Description
UNAUTHENTICATED 401 No valid session cookie present. Redirect to /xauth/twitter.
USER_NOT_FOUND 404 The X handle does not have a registered XCash account.
INSUFFICIENT_FUNDS 400 Sender's balance is too low to cover the transfer and network fee.
INVALID_ADDRESS 400 The provided external wallet address is not a valid Solana public key.
TX_FAILED 502 The Solana RPC accepted but the transaction was not confirmed. Retry with the same parameters.
Security
Custodial key storage
User keypairs are encrypted at rest using AES-256-GCM with a server-side master key managed via environment secrets. The plaintext private key is never exposed to the client.
Rate limiting
Pay and withdraw endpoints are rate-limited to 10 requests / minute per session. Exceeding this returns a 429 Too Many Requests.
CSRF protection
Mutating endpoints (POST) validate a X-CSRF-Token header whose value is injected into the page at render time. Cookie-only requests without this header are rejected with 403.
Tech Stack
Layer Technology
Blockchain Solana Mainnet-Beta · @solana/web3.js
Auth X OAuth 2.0 PKCE
Backend Node.js · TypeScript
Session Signed HTTP-only cookies
Database PostgreSQL — user → wallet mapping
Frontend React/Vite/Tailwindcss