@gaian-network/sdk
v2.0.0
Published
TypeScript SDK for the Gaian Network v2 API — cross-border crypto-to-fiat payments
Maintainers
Readme
@gaian-network/sdk
TypeScript SDK for the Gaian Network v2 API — a single signed gateway for user onboarding, KYC, wallets, and crypto-to-fiat payments across Vietnam, Thailand, and other supported markets.
Installation
npm install @gaian-network/sdkQuick start
import { GaianClient } from '@gaian-network/sdk';
const client = new GaianClient({
apiKey: process.env.GAIAN_API_KEY!,
secretKey: process.env.GAIAN_API_SECRET!,
});
const resp = await client.createUser();
const userId = resp.data!.userId;Get your API key and secret from the Client Admin Portal. Both are shown only once at creation — store them securely, server-side only.
Every method hangs directly off the client — there are no per-resource sub-clients (client.createUser, client.quote, client.placeOrder, ...), not client.users.register(...).
Environments
| Environment | Base URL |
|---|---|
| Sandbox (default) | https://sandbox.gaian.network |
| Production | https://api.gaian.network |
const client = new GaianClient({
apiKey: process.env.GAIAN_API_KEY!,
secretKey: process.env.GAIAN_API_SECRET!,
environment: 'production', // defaults to 'sandbox'
});Authentication
Every request is HMAC-signed — there's no static API-key header. Each key has a public key (apiKey) and a secret (secretKey); the client signs every outgoing request automatically:
message = apiKey + method + path + query + canonicalBody + timestamp
signature = base64url( HMAC_SHA256(secretKey, message) )where canonicalBody is the JSON body with top-level keys sorted alphabetically and compact-serialized (empty string for GET/no body). This is implemented in signer.ts — you never need to call it directly, GaianClient handles signing for you.
Never hardcode the API key/secret or expose them in client-side code. Signatures must be generated on your backend.
Payment flows
Standard flow (on-chain signing required)
quote (or parseQr first) → placeOrder → sign & broadcast the returned transaction
→ verifyOrder → getOrderStatus (poll until terminal)Prefunded flow (no on-chain signing, settles from tenant balance)
quotePrefund → placePrefundedOrder → getOrderStatus (poll)Direct bank-transfer flow (production only, no QR code)
listBanks → verifyAccount (recommended) → quoteDirect (or quoteDirectPrefund)
→ placeOrder (or placePrefundedOrder) → verifyOrder (on-chain variant only) → getOrderStatusA quoteDirect/quote quote must be consumed by placeOrder; a quoteDirectPrefund/quotePrefund quote must be consumed by placePrefundedOrder — cross-use is rejected by the API. Quotes are single-use with a short TTL (~120s).
Usage
Users
Create a user
const resp = await client.createUser();
const userId = resp.data!.userId;No wallet or KYC data is attached at creation time — link a wallet and submit KYC afterward using the returned userId.
Get a user by ID or wallet
const resp = await client.getUserById({ userId });
const resp2 = await client.getUserByWallet({ walletAddress: '0xWalletAddress' });Check market access
const resp = await client.getMarkets({ userId });
if (resp.data!.markets['VN']?.status !== 'APPROVED') {
console.log('Vietnam market not available');
}| MarketStatus | Description |
|---|---|
| MarketApproved | Approved for payments in this market |
| MarketPending | KYC/verification in progress |
| MarketNotStarted | Nothing submitted yet for this market |
| MarketRejected | Rejected — check rejectReason (populated for PH/BR only) |
List / get orders
const resp = await client.listUserOrders({
userId,
page: 1,
pageSize: 20,
status: 'completed', // optional filter
});
for (const o of resp.data) {
console.log(o.orderId, o.status, o.fiatAmount, o.fiatCurrency);
}
console.log('total:', resp.pagination.totalCount);
const order = await client.getUserOrder({ userId, orderId });Wallets
const wallet = await client.createWallet({
userId,
address: '0xWalletAddress',
chain: 'base',
});
// the first wallet added for a user becomes primary automatically
const resp = await client.listWallets({ userId });
for (const w of resp.data ?? []) {
console.log(w.address, w.chain, w.isPrimary);
}KYC
Hosted KYC link
The returned URL is time-limited. Generate it on demand — do not cache.
const resp = await client.generateKycLink({
userId,
callbackUrl: 'https://myapp.com/kyc/callback', // optional
});
// Redirect the user to resp.data!.urlSubmit KYC information directly
const resp = await client.submitKyc({
userId,
firstName: 'Nguyen',
lastName: 'Van A',
email: '[email protected]',
gender: 'male',
dateOfBirth: '1990-01-15',
nationality: 'VN',
nationalId: '012345678901',
type: 'ID_CARD',
expiryDate: '2025-01-01',
addressLine1: '123 Le Loi',
city: 'Ho Chi Minh City',
countryOfResidence: 'VN',
occupation: 'OCC9',
phoneCountryCode: '+84',
phoneNumber: '912345678',
frontIdImage: '<base64>',
backIdImage: '<base64>',
holdIdImage: '<base64>',
});
console.log(resp.data!.kycStatus);frontIdImage/backIdImage/holdIdImage are optional — omit them entirely if the flow doesn't require ID images at submission time.
Update KYC information
All fields are optional — only the fields you set are sent/updated.
const resp = await client.updateKyc({ userId, email: '[email protected]' });Organization
const resp = await client.getOrganizationBalance();
console.log(`${resp.data!.availableBalance} ${resp.data!.currency} available`);Restricted to tenants on the prefund billing model.
Payments
Parse a QR code
const resp = await client.parseQr({ qrString });
// country is an optional ISO-3166-1 alpha-2 hint:
const resp2 = await client.parseQr({ qrString, country: 'VN' });
console.log(resp.data!.bankBin, resp.data!.accountNumber);Create a quote
const resp = await client.quote({
qrString,
amount: 500000, // fiat amount
chainId: ChainSolana,
settlementCurrency: 'USDC',
userId,
});
const quoteId = resp.data!.quoteId;| ChainID | Chain |
|---|---|
| ChainEthereum | Ethereum (1) |
| ChainOptimism | Optimism (10) |
| ChainBSC | BSC (56) |
| ChainPolygon | Polygon (137) |
| ChainBase | Base (8453) |
| ChainArbitrum | Arbitrum (42161) |
| ChainSolana | Solana (101) |
Prefunded quote (no on-chain step, no chainId):
const resp = await client.quotePrefund({
qrString,
amount: 500000,
settlementCurrency: 'USDC',
userId,
});Direct bank-transfer quote (no QR code, production only — see listBanks/verifyAccount below):
const resp = await client.quoteDirect({
accountNumber: '0123456789',
code: bankCode, // from listBanks
amount: 500000,
country: 'VN',
chainId: ChainSolana,
settlementCurrency: 'USDC',
userId, // exactly one of userId/walletAddress/email
});
// or, prefunded:
const resp2 = await client.quoteDirectPrefund({ /* same, minus chainId */ });Place an order
const resp = await client.placeOrder({ quoteId });
// resp.data!.encodedTransaction is what you sign and broadcast on-chain.
const txHash = await broadcastTransaction(resp.data!.encodedTransaction);Prefunded order (no on-chain step — go straight to polling getOrderStatus):
const resp = await client.placePrefundedOrder({ quoteId });Verify the on-chain transaction
const resp = await client.verifyOrder({ orderId, transactionHash: txHash });The API returns HTTP 200 even when verification did not succeed. A resolved promise does not mean the payment is confirmed — always branch on
resp.data!.status:
switch (resp.data!.status) {
case VerifyVerified:
// confirmed — poll getOrderStatus for settlement
break;
case VerifyPending:
// transaction hasn't propagated on-chain yet — resubmit the same transactionHash shortly
break;
case VerifyFailed:
console.error('verification failed:', resp.data!.message);
break;
}Poll order status
for (;;) {
const resp = await client.getOrderStatus({ orderId });
if (isTerminal(resp.data!.status)) {
console.log('final status:', resp.data!.statusLabel);
break;
}
await new Promise((r) => setTimeout(r, 3000));
}| OrderStatus | Label | Terminal? |
|---|---|---|
| OrderPending (0) | pending | no |
| OrderAwaitingDeposit (1) | awaiting_deposit | no |
| OrderPaymentReceived (2) | payment_received | no |
| OrderQueued (3) | queued | no |
| OrderProcessing (4) | processing | no |
| OrderCompleted (10) | completed | yes |
| OrderFailed (20) | failed | yes |
| OrderCancelled (21) | cancelled | yes |
| OrderExpired (22) | expired | yes |
| OrderUnknown (99) | unknown | no |
VerifyStatus (returned by verifyOrder) is a separate type from OrderStatus — their integer values overlap (both have a 1) but mean different things. Don't compare across the two.
Banks (direct bank transfer)
Production only — not available in sandbox.
const resp = await client.listBanks({ country: 'VN' });
for (const b of resp.data!.banks) {
console.log(b.code, b.name); // b.code is what quoteDirect/verifyAccount expect
}
const verify = await client.verifyAccount({
accountNumber: '0123456789',
code: bankCode,
country: 'VN',
});A failed verification (bad account number) is not an error — it returns HTTP 200 with
valid: falseandaccountName: null. Checkresp.data!.valid, not just whether the call threw.
Response envelopes
Every method returns one of two generic wrappers (response.ts):
UserResponse<T>—{data, requestId}— used by User/KYC/Wallet/Organization endpoints.PaymentResponse<T>—{success, requestId, data}— used by Payment endpoints (parseQr,quote*,placeOrder*,verifyOrder,getOrderStatus,listBanks,verifyAccount).
listUserOrders is the one exception — its response has data/pagination/requestId as siblings at the top level, so it returns ListUserOrdersResponse directly instead of a wrapped envelope.
Check each feature file's doc comment if you're unsure which shape applies.
Error handling
All errors thrown are instances of GaianError:
import { GaianError } from '@gaian-network/sdk';
try {
await client.getUserById({ userId: 'does-not-exist' });
} catch (err) {
if (err instanceof GaianError) {
console.log(err.code); // 'NOT_FOUND' | 'UNAUTHORIZED' | 'BAD_REQUEST' | ...
console.log(err.statusCode); // 404
console.log(err.message); // API error message
console.log(err.details); // raw response body
}
}v2 has two distinct error body shapes depending on which backing service handled the request — the SDK handles both transparently:
- User/KYC/Wallet/Organization endpoints: flat
{message}. - Payment endpoints:
{success:false, error:{code,message,details}}.
| code | HTTP status | When |
|--------|------------|------|
| UNAUTHORIZED | 401 | Missing or invalid signature/API key |
| FORBIDDEN | 403 | Valid key, insufficient permissions |
| NOT_FOUND | 404 | Resource does not exist |
| BAD_REQUEST | 400 | Invalid request parameters |
| CONFLICT | 409 | Duplicate resource or conflicting state, e.g. an already-consumed quote |
| SERVER_ERROR | 5xx | Gaian server error — retry with backoff |
| NETWORK_ERROR | — | Network timeout or connection failure |
Configuration
const client = new GaianClient({
apiKey: process.env.GAIAN_API_KEY!,
secretKey: process.env.GAIAN_API_SECRET!,
environment: 'sandbox', // or 'production'
baseUrl: 'http://localhost:4000', // override, e.g. for local proxies or tests
fetch: myFetchImpl, // custom fetch — how you inject timeouts/proxies/mocks
logger: myLogger, // optional: implement { info, error, debug }
debug: true, // log request/response via the logger
});Requirements
- Node.js ≥ 18 (native
fetch,crypto.randomUUID,crypto.createHmac) - TypeScript ≥ 5.0 (for
exactOptionalPropertyTypes)
License
MIT
