Authentication
Clutch Hub API uses wallet-based JWT authentication. There is no username/password registration. Your blockchain public key (address) is your identity.
How it works
- Build the canonical proof-of-key-ownership challenge
clutch-auth:{publicKey}:{timestamp}, wherepublicKeyis the exact string you will pass to the mutation andtimestampis the current Unix time in seconds. - Sign the challenge with the wallet's private key (see Challenge signing).
- Call the public
generateToken(publicKey, timestamp, signature)GraphQL mutation. - The API verifies the timestamp is within ±120 seconds of server time and that the signature recovers to
publicKey; only then does it return a JWT signed with HS256 containing apkclaim (your public key) andexp. - Include the token on protected HTTP requests and WebSocket connections.
The SDK does all of this automatically via ensureAuth() when you use authenticated methods — provide the wallet's private key to the ClutchHubSdk constructor (or setPrivateKey).
generateToken
mutation GenerateToken($publicKey: String!, $timestamp: Int!, $signature: AuthSignatureInput!) {
generateToken(publicKey: $publicKey, timestamp: $timestamp, signature: $signature) {
token
expiresAt
}
}
publicKey— wallet address (0x+ 40 hex) or uncompressed public key (130 hex).timestamp— Unix seconds; rejected if more than ±120s from server time (stateless replay window).signature—{ r, s, v }: recoverable secp256k1 signature over the challenge (r/s32-byte hex,0xoptional;v27 or 28).
expiresAt is a Unix timestamp in seconds.
Challenge signing
The signature follows the same convention as transaction signing:
message = "clutch-auth:" + publicKey + ":" + timestamp(publicKeybyte-for-byte as sent in the mutation).hashHex = hex(keccak256(utf8(message)))— 64-char lowercase hex, no0x.- Sign
keccak256(utf8(hashHex))— Keccak-256 over the UTF-8 bytes of the hex string — with recoverable secp256k1;v = recoveryId + 27.
In the SDK this is exposed as signAuthChallenge(publicKey, timestamp, privateKey) (with helpers buildAuthChallengeMessage and authChallengeHashHex).
HTTP requests
For protected GraphQL operations, send:
Authorization: Bearer <token>
Example with curl:
curl -X POST http://localhost:3000/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_JWT" \
-d '{"query":"mutation { sendRawTransaction(rawTransaction: \"0x...\") }"}'
WebSocket subscriptions
GraphQL subscriptions use GET /graphql/ws with the graphql-transport-ws protocol.
Send the JWT in the connection_init payload:
{
"Authorization": "Bearer YOUR_JWT"
}
Public list subscriptions work without a token. The SDK sends a token when available but still connects if token generation fails.
Auth requirements by operation
| Operation | Auth required |
|---|---|
generateToken | No JWT (requires a signed proof-of-key-ownership challenge instead) |
listRideRequests, listRideOffers, listActiveTrips, listCompletedTrips, listRecentTrips | No |
rideRequestsUpdated, rideOffersUpdated, activeTripsUpdated, completedTripsUpdated, recentTripsUpdated | No |
accountBalance, accountBalanceUpdated | Yes |
All createUnsigned* mutations | Yes |
sendRawTransaction | Yes |
userRideRequest, rideRequest | Yes / No (stubs — do not use) |
POST /faucet does not require a JWT. It is gated by server config (faucet_enabled) instead.