Skip to main content

SDK Usage

Installation​

npm install clutch-hub-sdk-js

Create an SDK instance​

import { ClutchHubSdk } from 'clutch-hub-sdk-js';

const API_URL = 'http://localhost:3000';
const publicKey = '0xYourPublicKey';
const privateKey = '0xYourPrivateKey';
const CHAIN_ID = 2077; // from your own app config, never from the hub

const sdk = new ClutchHubSdk(API_URL, publicKey, privateKey, CHAIN_ID);

The constructor requires the API URL and the wallet public key. Pass the private key as the third argument (or later via sdk.setPrivateKey(privateKey)) if you will call authenticated methods: the SDK automatically obtains a JWT via generateToken, which requires signing a proof-of-key-ownership challenge (clutch-auth:{chainId}:{publicKey}:{timestamp}) with the private key. The key is used locally for signing only and is never sent to the API. Read-only usage (public list* queries and subscriptions) works without it.

In a browser app, do not hold a key at all: pass a signer for the user's wallet instead of the key (see Use a wallet).

The fourth argument, chainId, is new in this SDK version. Pin it to the network your app targets — it's used both to build the auth challenge above and, optionally, to verify a hub-returned transaction before you sign it (see Verify before you sign).

Basic transaction flow​

Every on-chain action follows the same pattern:

  1. Create unsigned tx — API builds payload and fetches nonce
  2. Sign client-side — private key never leaves your app
  3. Submit — send raw RLP hex to the network
const unsigned = await sdk.createUnsignedRideRequest({
pickup: { latitude: 35.7, longitude: 51.4 },
dropoff: { latitude: 35.8, longitude: 51.5 },
fare: 5_000_000n, // $5.00 — amounts are bigint, at 1 USD = 1,000,000 CLT
});

const signed = await sdk.signTransaction(unsigned, privateKey);
const result = await sdk.submitTransaction(signed.rawTransaction);

console.log('Submitted:', signed.txHash, result);
Amounts are bigint

Every fare/amount field is a bigint, not a number — write 5_000_000n, not 5000000. A number silently loses precision above 2^53, which is a reachable fare at this peg, not just a theoretical ceiling. Use formatUsd to render one for display.

Verify before you sign​

signTransaction accepts an optional third argument describing what you expected to build. When present, the SDK checks the hub-returned unsigned transaction against it — type, fare/amount, references, from, and chain_id (checked against the value pinned in the constructor, never against anything the hub itself reports) — and throws before signing if anything doesn't match:

const unsigned = await sdk.createUnsignedRideRequest({
pickup: { latitude: 35.7, longitude: 51.4 },
dropoff: { latitude: 35.8, longitude: 51.5 },
fare: 5_000_000n,
});

const signed = await sdk.signTransaction(unsigned, privateKey, {
type: 'RideRequest',
fare: 5_000_000n,
});

Previously the SDK signed whatever the hub returned, unseen — client-side signing kept the private key off the wire but didn't confirm the hub built the transaction you actually asked for. This closes that gap for everything except the referrer field, which the hub injects server-side and which the SDK surfaces on the result for display rather than verifying (there's no signed-quote flow yet that would let a client confirm it). Omitting the third argument skips verification entirely, matching pre-v3 behavior.

Burn (redeem CLT)​

const unsigned = await sdk.createUnsignedBurn({
amount: 2_000_000n, // $2.00
redemptionRef: myRedemptionIntentHash, // optional — omit for a plain burn
});

const signed = await sdk.signTransaction(unsigned, privateKey, {
type: 'Burn',
amount: 2_000_000n,
redemptionRef: myRedemptionIntentHash,
});

await sdk.submitTransaction(signed.rawTransaction);

Burning is permissionless — any wallet may destroy its own CLT. redemptionRef is optional: supply it (a hex-encoded hash of an off-chain redemption intent) when an operator's payout process needs to match this burn to a withdrawal request; omit it for a burn with no off-chain counterpart. There is no corresponding createUnsignedMint — minting is restricted to the chain's mint_authority and isn't exposed through this SDK. See CLT Economics for why mint/burn are the only two operations that change total supply.

Get CLT​

There is no SDK call that hands out CLT — requestFaucet() was removed along with the faucet it called. CLT is issued against a USDT deposit instead: send USDT (TRC-20) to your permanent deposit address, and the treasury mints the matching CLT to the wallet that owns that address. Those endpoints are plain REST on payment-orchestrator and accept the same Hub-issued JWT this SDK already holds, so there is no second login. See Deposits.

Full ride lifecycle (passenger + driver)​

See the Ride Lifecycle guide for a complete walkthrough.

Summary of SDK calls:

StepRoleSDK method
Request ridePassengercreateUnsignedRideRequest
Cancel requestPassengercreateUnsignedRideRequestCancel
Submit offerDrivercreateUnsignedRideOffer
Accept offerPassengercreateUnsignedRideAcceptance
Pay farePassengercreateUnsignedRidePay
Cancel tripEithercreateUnsignedRideCancel

Each write operation: signTransaction → submitTransaction.

Query chain state​

const requests = await sdk.listRideRequests();
const offers = await sdk.listRideOffers(requestTxHash);
const active = await sdk.listActiveTrips({ passengerAddress: publicKey });
const balance = await sdk.getAccountBalance(); // bigint

The SDK has no chain-info method. Query the hub's chainInfo GraphQL field directly if you need genesis-committed parameters, and keep getting the constructor's chainId from your own app config — never from this response:

const res = await fetch(`${API_URL}/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: '{ chainInfo { chainId isTestnet txFee totalSupply mintAuthority } }' }),
});
const { data } = await res.json();
console.log(`chain ${data.chainInfo.chainId}, tx_fee ${data.chainInfo.txFee}`);

Real-time updates​

const dispose = sdk.subscribeRideRequests(null, {
onData: (requests) => console.log('Requests:', requests),
onError: (err) => console.error(err),
});

// When done:
dispose();

See SDK Subscriptions for all subscription methods.

Coordinates​

Both formats work for pickup/dropoff:

{ latitude: 35.7, longitude: 51.4 }
{ lat: 35.7, lng: 51.4 }

A browser app should not hold a private key. The user's wallet keeps it and signs after the user approves. Clutch accounts are Ethereum-type accounts (secp256k1, address = last 20 bytes of Keccak-256 of the public key), so the address of a MetaMask or Trust Wallet account is a valid Clutch address. So is a TronLink account: TRON uses the same kind of key and writes the same 20 bytes in base58 (T…), and the SDK works with the 0x form. The wallet is only a signer: it is not asked to add Clutch as a network.

import { ClutchHubSdk, discoverInjectedWallets, connectWallet } from 'clutch-hub-sdk-js';

// 1. Find wallets (EIP-6963 and TIP-6963 announcements, then window.ethereum and window.tron).
// Show a button per wallet. wallet.kind is 'evm' (MetaMask, Trust Wallet) or 'tron' (TronLink).
const wallets = await discoverInjectedWallets();

// 2. The wallet asks the user to share an account.
const signer = await connectWallet(wallets[0]);

// 3. Use the signer where you used a key. The address is in lower case.
const sdk = new ClutchHubSdk(API_URL, signer.address, signer, CHAIN_ID);

const unsigned = await sdk.createUnsignedRideRequest({ pickup, dropoff, fare: 5_000_000n });
const signed = await sdk.signTransaction(unsigned, signer, { type: 'RideRequest', fare: 5_000_000n });
await sdk.submitTransaction(signed.rawTransaction);

What the user sees, and what to do about it:

  • A prompt for each signature. The first authenticated call after a page load asks the wallet to sign in (clutch-auth:…; the token lives 6 hours, in memory), then signTransaction asks for the transaction (clutch-tx:{chainId}:{hash}). Tell the user what each prompt is for before it opens: a wallet shows the text it signs, not the ride.
  • No prompts from timers. The SDK never asks a wallet to sign in the background; its subscriptions go without a token. If your app polls an authenticated endpoint, check sdk.hasValidToken() first.
  • The user can say no. MetaMask and Trust Wallet reject with code: 4001. A second request while a prompt is open gives -32002. TronLink rejects a signature with an error whose message is user rejected request, and no code. If the user switched accounts in the wallet, the SDK refuses the signature and says which account to switch back to.
  • Accounts change. Call watchWalletAccounts(wallet, listener). It calls listener(account) when the user switches, and listener(null) when the wallet stops sharing the page. Build a new signer for the new account with createSignerFor(wallet, account).
  • Connect again without a prompt. On the next visit sharedWalletAccount(wallet) gives the account the wallet already shares, or null, and never opens a prompt.
  • TronLink. The signer is createTronLinkSigner, and connectWallet and createSignerFor choose it for a 'tron' wallet. TronLink signs the same texts with signMessageV2, so the user sees the same clutch-… text. The SDK checks every signature before it returns it.

The node and the Hub API accept the wallet's signature next to the signature of a key; see Signing and encoding.

Creating a key (scripts and servers)​

The SDK does not generate keys — it signs with a key you supply. For a script or a server (not a browser app, which uses a wallet), derive one with @noble/secp256k1, the same library and derivation the node and Hub API use:

import * as secp from '@noble/secp256k1';
import { keccak_256 } from '@noble/hashes/sha3';
import { bytesToHex } from '@noble/hashes/utils';

const privateKeyBytes = secp.utils.randomPrivateKey();
const privateKey = '0x' + bytesToHex(privateKeyBytes);

// Uncompressed public key minus its 0x04 prefix, keccak-256'd; address is the last 20 bytes
const publicKeyBytes = secp.getPublicKey(privateKeyBytes, false);
const address = '0x' + bytesToHex(keccak_256(publicKeyBytes.slice(1)).slice(12, 32));

Fund the new address by depositing USDT before submitting transactions.

Never reuse a key from this repo

Config files in clutch-node and clutch-hub-api ship real private keys so the local stack runs out of the box — including validator author_secret_key values. Those keys are public, so anyone can sign as those accounts. Generate your own for anything beyond a throwaway local chain, and never store a production key in browser localStorage.

Security​

  • Private keys are never sent to the server, and a browser app holds none: the user's wallet signs
  • Signing uses @noble/secp256k1 and Keccak-256 (a wallet signs with personal_sign, TronLink with signMessageV2)
  • Transaction encoding uses RLP matching the node
  • See Signing and Encoding for the full spec