@carbonx3/cx3.js
The official Node.js SDK (CX3js) for CarbonX3 Green Chain.
CX3js connects server-side applications to CarbonX3 Green Chain through the CX3Api gateway. It signs in as an OAuth2 application, signs transactions locally with secp256k1, binds contract ABIs to plain JavaScript methods and deploys the Green Chain token standards. Requests are REST + JSON with named parameters, not binary calldata.
Network status: CarbonX3 Green Chain currently runs as a development network (devnet;
provider.getStatus()returns its current chain id). Devnet CX3, tokens and contracts are test data with no monetary value and may be cleared when the devnet is reset.
Features
- OAuth2 application access: client credentials from CX3WebUI; the access token is requested, cached and renewed for you.
- Local signing: private keys stay in your process, and every write carries the wallet's secp256k1 signature.
- ABI-bound contracts:
token.balanceOf(owner)andtoken.transfer(to, amount), with the ABI loaded from the chain. - Token deploys: CX30, CX31, CX300, CX315 and CX3192, plus the green standards CX337–CX341.
- Private tokens: deploy them, and check authorized wallets before anything is signed.
- Public chain reads: chain status, wallet tokens and activity, CarbonFi pools and CX3WASM interfaces (WIT).
- TypeScript declarations included; works with both
requireandimport.
Requirements
- Node.js 20.19 or later (native
fetch). - An application (client ID and client secret) created in CX3WebUI, as described below.
Installation
npm install @carbonx3/cx3.js
Create an application
- Open CX3WebUI → Settings → API → Applications and connect the wallet the application acts for.
- Name the application, choose its scopes and click Create App. The wallet signs the request.
- Store the client ID (
cx3app_…) and the client secret (cx3sec_…, shown only once) in your server's environment.
| Scope | Grants |
|---|---|
read | Balances, nonces, contracts, ABIs, events, transfers, transactions and read-only calls |
write | Wallet-signed contract sends and native CX3 transfers |
deploy | Contract deploys, plus pause, unpause and delete (owner-signed) |
A wallet can hold up to five applications. Revoking an application on the same page stops its access tokens within a minute.
Quick start
const { Cx3Provider, Cx3Wallet } = require("@carbonx3/cx3.js");
const provider = new Cx3Provider("https://cx3api.carbonx3.com", {
clientId: process.env.CX3_CLIENT_ID, // cx3app_…
clientSecret: process.env.CX3_CLIENT_SECRET // cx3sec_…
});
const wallet = new Cx3Wallet(process.env.CX3_PRIVATE_KEY, provider);
async function main() {
// Native CX3 balance
console.log(await provider.getBalance(wallet.address));
// Token contract; its ABI is loaded from the chain
const token = await provider.getContract(process.env.CONTRACT_ADDRESS, null, wallet);
console.log(await token.balanceOf(wallet.address));
// Wallet-signed token transfer (BigInt or decimal string)
const tx = await token.transfer("0xRecipient…", 10n ** 18n);
await tx.wait();
// Native CX3 transfer
await wallet.transferNative("0xRecipient…", 1.5);
}
main().catch((err) => {
console.error(err.code || err.name, err.message);
process.exitCode = 1;
});
ES modules work too: import { Cx3Provider, Cx3Wallet } from "@carbonx3/cx3.js";. The remaining examples assume an async context.
Authentication
OAuth2 application (recommended). The provider exchanges clientId and clientSecret at POST /oauth/token (client credentials grant) for a one-hour access token and sends it as Authorization: Bearer …. It renews the token a minute before it expires and, if the gateway answers invalid_token, fetches a fresh one and retries once. The secret is sent only to the token endpoint. scope: ["read"] requests fewer scopes than the application has. When the options are omitted, the provider reads CX3_CLIENT_ID and CX3_CLIENT_SECRET.
The access token identifies the application; every write is still signed by the wallet. A route outside the application's scopes answers 403 insufficient_scope, and routes reserved for CarbonX3's own services answer 403 ROUTE_NOT_ALLOWED. Each application has its own rate limit (300 requests per minute by default, 429 RATE_LIMITED).
provider.bindApiKey(address) (or wallet.bindApiKey()) confirms that a wallet is the one the application belongs to: { ok: true }, otherwise wallet_mismatch.
Wallet API key (legacy). A cx3wk_… key from the same settings page still works while applications move to OAuth2. Pass it as apiKey (or set CX3_API_KEY); it is sent as X-Api-Key and carries every scope of its wallet.
const provider = new Cx3Provider("https://cx3api.carbonx3.com", { apiKey: process.env.CX3_API_KEY });
Development node. new Cx3Provider(devNodeUrl, { devNode: true }) talks to a development node that runs without authentication (its /api/v1 routes).
Credentials are required. Without an application, an API key or devNode: true, the constructor throws CREDENTIALS_REQUIRED.
No HTTP Basic. HTTP Basic is the internal login between Green Chain nodes and CarbonX3 services, not an application credential, so the SDK never sends it: the auth option throws BASIC_AUTH_UNSUPPORTED, and headers cannot carry Authorization or X-Api-Key.
Reading chain data
Public chain data, served by the gateway to every application (a development node answers DAPP_ONLY):
const status = await provider.getStatus(); // { height, hash, timestamp, chainId, genesisHash }
const { tokens } = await provider.getTokens(wallet.address); // every token with this wallet's balance
const page = await provider.getActivity(wallet.address, { limit: 20 }); // newest first, no block rewards
const older = await provider.getActivity(wallet.address, { before: page.nextBefore });
const { pairs } = await provider.getDexPairs(); // CarbonFi pools
const lp = await provider.getLpBalance(pairs[0].id, wallet.address); // LP balance in one pool
const { wit } = await provider.getWit(contractAddress); // CX3WASM interface (WIT)
With the read scope:
| Method | Returns |
|---|---|
getBalance(address), getBalanceAndNonce(address), getNonce(address), getPendingNonce(address) | Native CX3 balance and nonces |
getContractInfo(address), getAbi(address), listContracts({ limit, offset, owner, standard, type, live }) | Contract registry entries and ABIs |
call(address, method, params) | A read-only call; the result is in value |
getEvents(address, { type, limit, offset }), getTransfers(address, { limit, offset }), getHolders(address, { limit, offset }) | Contract history |
getAddressTransactions(address, limit), getTxStatus(txHash) | Transactions |
getRequest(requestId, from) | What became of a signed contract send (public; see Waiting for a transaction) |
Contracts
const token = await provider.getContract(address, null, wallet); // ABI from the chain
await token.balanceOf(owner); // view → read-only call
await token.call("balanceOf", { address: owner }); // named parameters (CX30)
const tx = await token.transfer(to, amount); // transaction → wallet-signed send
await tx.wait();
Supported ABI shapes:
- Standard interface JSON:
{ methods: { balanceOf: { type: "view", inputs: [...] } } } - Compiler flat ABI:
[{ type: "function", name, inputs, stateMutability }] - CX3WASM compiler ABI:
{ functions: [{ name, params, view }] }
provider.getContract(address) loads the ABI the chain calls the contract with: shape 2 for contracts compiled to CX3JS, shape 3 for CX3WASM. Built-in chain standards (ABI inputs ["params"]) take one named object: await lp.transfer({ to, amount }). A contract handle created with a provider instead of a wallet can only read; its writes throw NO_SIGNER.
Transactions and fees
Gas is paid in CX3 to the validator that produces the block: gas used × gas price (devnet default 0.0001 CX3 per gas).
- A native transfer uses 21,000 gas: 2.1 CX3. The sender needs the amount plus the fee.
- A contract send or deploy through the gateway is relayed by the chain's own deploy account: the call still runs as your wallet (the chain checks your signature again), and your wallet pays no gas for it. The call's gas limit (100,000 by default) bounds the node's trial run when the call is sent (a call that needs more is refused); in the block the VM runs the call under its own per-call limit.
Too little balance for a native transfer fails with INSUFFICIENT_BALANCE. A native transfer's gas limit and gas price are 0 (the chain's defaults: 21,000 and 0.0001) or 21,000–100,000 and at least the chain floor of 0.0001 (GAS_LIMIT_TOO_LOW, GAS_LIMIT_TOO_HIGH, GAS_PRICE_TOO_LOW), and its amount is greater than 0 with at most 6 decimal places, because CX3 has 6. transferNative checks the amount before signing (INVALID_AMOUNT, INVALID_AMOUNT_PRECISION).
Waiting for a transaction
A contract send waits for its block by default (the node waits up to 25 seconds); with { wait: 0 } it returns at once. tx.wait() polls the transaction status until it is confirmed or failed for good:
- Pending is not failed.
tx.statusis"pending"for{ wait: 0 }, for a queued send (HTTP 202queued: true, codeTX_TIMEOUT: not in a block within the node's wait) and for a dropped send.tx.successstaystrue, andtx.wait()follows the transaction. A queued or dropped deploy isstatus: "pending"too: the token factory givesresult.contractonly afterresult.wait()sees it confirmed. - Dropped sends. A send the node trimmed from its block comes back as HTTP 202 with
tx.dropped === trueandtx.finalAfter(ms epoch, about 12 minutes after the signed timestamp). Until then another validator may still include it, so by defaulttx.wait()keeps waiting untilfinalAfterplus 30 seconds. After that the status saysfailed, andtx.wait()rejects withTX_FAILED. - Only a final failure rejects with
TX_FAILED.TX_TRACK_TIMEOUT(your owntimeoutMs, otherwise 60 seconds or untilfinalAfterfor a dropped send) means the transaction is still pending: itsreceipthasstatus: "pending",droppedandfinalAfter. A network error, a 429 or a 5xx answer while polling does not end the wait. - Never sign the same call again while it can still run. A new signature is a second call. Every send answer carries
tx.requestId, andprovider.getRequest(tx.requestId, wallet.address)tells what became of it:executed,pending,submittedorunknown.
const tx = await token.transfer(to, amount);
if (tx.dropped) console.log("Not in a block yet; final after", new Date(tx.finalAfter));
const receipt = await tx.wait(); // confirmed, or throws TX_FAILED / TX_TRACK_TIMEOUT
Deploying tokens
const { Cx3TokenFactory } = require("@carbonx3/cx3.js");
const factory = new Cx3TokenFactory(wallet); // the application needs the deploy scope
const result = await factory.deployCX30({ name: "My Token", symbol: "MTK", initialSupply: "0" });
console.log(result.contractAddress); // result.contract is a ready Contract handle
| Helper | Standard |
|---|---|
deployCX30 | Fungible token |
deployCX31 | Unit / accounting token |
deployCX300 | Real-world asset |
deployCX315 | Loyalty |
deployCX3192 | Soulbound identity |
deployCX337 … deployCX341 | Green standards: CBAM proof, sustainability report, supply chain, materiality assessment, green claims |
A deploy the node trimmed from its block answers HTTP 202 with result.dropped === true, status: "pending", finalAfter and, when known, its contractAddress (otherwise null): it is not deployed yet, so result.contract is not set. await result.wait() follows it as tx.wait() follows a send: once it is confirmed, status becomes "confirmed" and the factory adds result.contract (and result.authorized for a private token); after finalAfter it rejects with TX_FAILED. Do not deploy the same contract again before then. wallet.deployContract() results have the same dropped, finalAfter and wait().
The helpers deploy to CX3WASM by default. The chain renders CX30 and CX337–CX341 from the fields; CX31, CX300, CX315 and CX3192 take their CX3Script source as sourceCode (otherwise the chain answers STANDARD_NOT_WASM_DEPLOYABLE).
Token symbols are capital letters A-Z and digits 0-9 only. The SDK checks this before signing (INVALID_TOKEN_SYMBOL; see isValidTokenSymbol), and the chain keeps one contract per symbol (409 DUPLICATE_TOKEN_SYMBOL).
Private tokens
A private token is sent, received and seen only by its owner and its authorized wallets. The token contract enforces this on the chain, and the SDK follows the same rules:
- Lists:
getTokensshows a private token only to its owner, its authorized wallets and its holders, and only with that wallet's own signed proof (headerX-CX3-Wallet-Proof, since 2026-10-19; CX3Wallet sends it for its own lists), so an application gets the public tokens;listContracts()shows it only when the application's wallet is its owner or an authorized wallet. Its holders and raw state answer403 PRIVATE_TOKENto every application except the owner's. - Sends: before a
transfer,transferFromormintof a private token, the wallet asks the token whether both ends are authorized. If one is not, it throwsPRIVATE_TOKEN_SENDERorPRIVATE_TOKEN_RECIPIENTinstead of signing a send the block would refuse.{ privateCheck: false }skips the check. - Checks:
provider.isPrivateToken(token)andprovider.isTokenAuthorized(token, wallet), which returnstruefor the owner or a listed wallet,falseotherwise andnullwhen the node does not answer.
const res = await factory.deployCX30({
name: "Team Credit",
symbol: "TEAMC",
initialSupply: "1000000",
visibility: "private",
authorizedWallets: ["0x…", "0x…"] // up to 200; one signed addToWhitelist each after the deploy
});
// res.authorized → [{ wallet, success, txHash }]
await res.contract.addToWhitelist("0xNew…", ""); // later, owner only; removeFromWhitelist removes one
A private CX31, CX300, CX315 or CX3192 takes its CX3WebUI source as sourceCode; the green standards have no private variant. Private keeps a token out of other wallets' lists and hands, but it does not make it secret: blocks, transfers and contract calls stay readable on the chain, as on CX3Scan.
Signing
Digests are UTF-8 strings, hashed with SHA-256 and signed with secp256k1, byte-identical to what the Green Chain node verifies. Each kind starts with its own prefix (signing domains), so a signature of one kind never verifies as another; chainId and genesisHash are the network's (provider.getChainIdentity(), fetched by the wallet methods themselves) and addresses are lower-case 0x + 40 hex:
- Deploy:
CX3_DEPLOY_V1|+JSON.stringify([chainId, genesisHash, owner, standard, name, symbol, decimals, initialSupply, timestamp]) - Contract send:
CX3_CALL_V1|+JSON.stringify([chainId, genesisHash, contractAddress, method, params, fromAddress, timestamp]). The chain checks this signature again inside the relayed transaction: a call runs as your wallet only with it. - Owner action:
CX3_OWNER_V1|+JSON.stringify([chainId, genesisHash, action, address, owner, timestamp]) - Site message:
0x19+"CX3 Signed Message:\n"+ UTF-8 length + message (signPersonalMessage), the way the CX3Wallet extension and app signcx3_sign_message - Native transfer:
JSON.stringify({ type, fromAddress, toAddress, amount, nonce, timestamp, data, gasLimit, gasPrice, chainId, genesisHash }), with 40-hex addresses and no0x;chainIdandgenesisHash(0x+ 64 hex) are the network's, fromprovider.getChainIdentity()
BigInt values in params are converted to decimal strings before signing, and exactly those params are sent. Deploy, contract-send and owner signatures carry a timestamp and are accepted within ±5 minutes of the gateway's clock. A native transfer is admitted only within 60 seconds after its timestamp (the pool drops it after 60 seconds), and a block takes it only within 10 minutes of its timestamp. The genesis hash ties a native transfer to one network and one genesis: after a chain reset, a transfer signed before it no longer verifies. An address is 0x + the last 20 bytes of sha256(uncompressedPublicKey).
The same building blocks are exported: buildDeployDigest, buildSendDigest, buildOwnerActionDigest (each takes the identity as its last argument), buildTransferPayload, personalMessageBytes, signMessage (signs a digest as given), signPersonalMessage, privateKeyToAddress and createPrivateKey.
Error handling
Every error is a Cx3Error with a code. Cx3HttpError adds the HTTP status and the response body (with requestId for a contract send); Cx3TxError adds the transaction receipt. A send or native transfer that the node refuses with HTTP 200 returns tx.success === false with the code in tx.error, and tx.wait() rejects with a Cx3TxError of that code.
const { Cx3HttpError } = require("@carbonx3/cx3.js");
try {
await token.transfer(to, amount);
} catch (err) {
if (err instanceof Cx3HttpError && err.status === 403) console.error("Not allowed:", err.code);
else throw err;
}
| Code | Meaning |
|---|---|
CREDENTIALS_REQUIRED | No application, API key or devNode was given |
INVALID_CLIENT_CREDENTIALS | The client ID or secret is not in the cx3app_… / cx3sec_… format |
invalid_client | The token endpoint refused the application (wrong secret, or revoked) |
insufficient_scope, ROUTE_NOT_ALLOWED | The route needs another scope, or is not open to applications (403) |
RATE_LIMITED | The application's rate limit was reached (429) |
INSUFFICIENT_BALANCE, GAS_PRICE_TOO_LOW | A native transfer's sender cannot pay the amount plus gas, or the gas price is below the chain floor |
INVALID_AMOUNT, INVALID_AMOUNT_PRECISION | A native amount is not greater than 0, or has more than 6 decimal places (CX3 has 6); transferNative checks both before signing |
GAS_LIMIT_TOO_LOW, GAS_LIMIT_TOO_HIGH | A native transfer's gas limit is outside 21,000–100,000 |
GAS_LIMIT_EXCEEDED | A contract send's gasLimit is above the chain's maximum (400, max in the body) |
INVALID_NONCE_ORDER | A native transfer's nonce is not the next one (expectedNonce in the answer) |
DATA_TOO_LARGE, DATA_TOO_DEEP | A transaction's data is too large or nested too deeply |
INVALID_TOKEN_SYMBOL, DUPLICATE_TOKEN_SYMBOL | The symbol is not A-Z / 0-9, or is already in use (409) |
PRIVATE_TOKEN_SENDER, PRIVATE_TOKEN_RECIPIENT, PRIVATE_TOKEN | Private token rules (see above) |
TX_TIMEOUT | The node's 25-second wait ended before a block: queued, not final. A send answer with this code is pending and tx.wait() follows it; without a tx hash the outcome is open (look up its requestId) |
TX_FAILED | The transaction failed for good (a dropped send only after its finalAfter) |
TX_TRACK_TIMEOUT | tx.wait() stopped while the transaction was still pending: not a failure, do not sign it again |
REPLAYED_REQUEST | This signed request was already received or already ran (409); the answer does not say how it ended. Look it up with provider.getRequest(err.body.requestId, wallet.address) and never sign it again |
REQUEST_LOOKUP_UNAVAILABLE | getRequest could not ask the chain (503): try again later, do not sign again |
LEDGER_UNAVAILABLE, LEDGER_FULL | The gateway's /dapp/v1 send could not record the request (503): it was not sent, and the same signed body may be sent again later, without a new signature |
BASIC_AUTH_UNSUPPORTED | The auth option was used |
NO_CHAIN_IDENTITY | The network's chain id or genesis hash is unavailable; a native transfer is not signed without them |
Browser applications
This package is for Node.js servers. Web pages use the CX3 IIFE (window.cx3) together with the CX3Wallet extension: @carbonx3/cx3-iife on npm and jsDelivr, or download it; see the CX3 IIFE guide. Keep the application's credentials on your server behind a same-origin proxy, and never put a client secret, an API key or a private key in browser JavaScript.
Environment variables
| Variable | Used for |
|---|---|
CX3_CLIENT_ID, CX3_CLIENT_SECRET | Application credentials when the options are omitted |
CX3_API_KEY | Legacy wallet API key |
CX3_NODE_URL | Gateway URL for getDefaultProvider() |
TypeScript
Type declarations (index.d.ts) ship with the package.
Upgrading from 0.2.x
Version 0.3.0 signs native transfers for the network's new signing format (0.3.1 to 0.3.4 change only this README, the file headers and comments, not the code). In 0.3.5, bindApiKey with a wallet API key asks the gateway's /wallet/api-key/bind (before, it asked /whoami, which the gateway refuses, so it always failed). In 0.3.6, wait() follows a send or deploy the gateway reports as not final (dropped) until its finalAfter instead of failing after 60 s, a TX_TIMEOUT answer with a hash is pending, getRequest() looks up a signed request, and transferNative refuses more than 6 decimals. 0.3.7 changes only this README: a native transfer's gas price floor is 0.0001 (the 0.3.6 README said 0.000001). 0.3.8 changes only this README too: the CX3 IIFE download link names cx3-0.2.9.min.js.
- The genesis hash is signed.
transferNativeasks the network for its chain id and genesis hash (provider.getChainIdentity()) unless both are given asopts.chainIdandopts.genesisHash. 0.2.x transfers no longer verify on the chain. - No fallback chain id.
buildTransferPayloadthrowsNO_CHAIN_IDENTITYwithoutchainIdandgenesisHash; theCX3_CHAIN_IDvariable and thechainIdBindoption are gone. - Typed digests. Contract sends, deploys and owner actions sign
CX3_CALL_V1/CX3_DEPLOY_V1/CX3_OWNER_V1digests bound to the network (see Signing); the|-joined digests of 0.2.x are refused.signSend,signDeployandsignOwnerActiontake an optional{ chainId, genesisHash }, else they ask the provider (kept for a minute). Thebuild*Digesthelpers need the identity as their last argument. - Site messages.
signPersonalMessagesigns the way the wallets signcx3_sign_message(with the0x19prefix);signMessagestill signs a digest as it is.
Upgrading from 0.1.x
CX3js is in beta (0.x), so a minor version may contain breaking changes. Version 0.2.0:
- Credentials are required. The provider talks to the CX3Api gateway as an OAuth2 application (or with a wallet API key) and throws
CREDENTIALS_REQUIREDwithout one. A development node without authentication needsdevNode: true; 0.1.x used node routes under/api/v1by default. - HTTP Basic was removed. The
authoption throwsBASIC_AUTH_UNSUPPORTED, andheaderscannot carryAuthorizationorX-Api-Key. - Node.js 20.19 or later is required (
@noble/secp256k13.x). - Fixes: BigInt params are signed and sent as decimal strings, and
transferNativesends the gas fields it signs. - New: public chain reads (
getStatus,getTokens,getActivity,getDexPairs,getLpBalance,getWit), private tokens andisValidTokenSymbol.
Documentation
- CX3js SDK reference
- Node.js after deploy
- CX3 IIFE for browsers
- CX3Script developer site
- CarbonX3 Green Chain
License
UNLICENSED: proprietary software of Mersolution Technology Ltd. Use is subject to the CX3Script Terms of Use (item 03): you may install it and ship it unmodified with applications, including commercial ones, that connect to CarbonX3 Green Chain; you may not modify it, sell or republish it on its own, or use it with other networks.