/
Docs
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
- User authenticates via OAuth 2.0 with X (Twitter). Their X UID is stored and a Solana keypair is derived and persisted server-side.
- 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.
- The transaction is submitted to the Solana Mainnet-Beta RPC. The tx signature is returned to the caller.
- 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
- Redirect the user to
GET /xauth/twitter. XCash generates a PKCE challenge and redirects totwitter.com/i/oauth2/authorize. - X redirects back to
/xauth/twitter/callbackwith acodeparameter. - Server exchanges the code for tokens, upserts the user record, and sets the session cookie.
- 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 |