Welcome to the @ardrive/turbo-sdk! This SDK provides functionality for interacting with the Turbo Upload and Payment Services and is available for both NodeJS and Web environments.
- Table of Contents
- Installation
- Quick Start
- Usage
- APIs
- ArNS Names
- You need a Solana key, not Solana funds
- Two identities, never conflated
- The twelve sponsored actions
- Not covered — these still cost you SOL
- Pricing — quote the total
- The two shapes, if you drive it yourself
- Nonces, retries and refunds
- Listing a wallet's names
- Buying a name with a credit card (fiat / Stripe)
- Signers
- Events
- Logging
- CLI
- Turbo Credit Sharing
- Developers
npm install @ardrive/turbo-sdkor
yarn add @ardrive/turbo-sdkimport { ArweaveSigner, TurboFactory } from '@ardrive/turbo-sdk';
import Arweave from 'arweave';
import fs from 'fs';
import open from 'open';
import path from 'path';
async function uploadWithTurbo() {
const jwk = JSON.parse(fs.readFileSync('./my-jwk.json', 'utf-8'));
const signer = new ArweaveSigner(jwk);
const turbo = TurboFactory.authenticated({ signer });
try {
// upload some simple data - log upload progress events
const { id, owner, dataCaches, fastFinalityIndexes } = await turbo.upload({
data: 'Hello, world!',
events: {
// overall events (includes signing and upload events)
onProgress: ({ totalBytes, processedBytes, step }) => {
console.log('Overall progress:', { totalBytes, processedBytes, step });
},
onError: ({ error, step }) => {
console.log('Overall error:', { error, step });
},
},
});
// upload a file - log signing and upload progress events
const filePath = path.join(__dirname, './my-image.png');
const fileSize = fs.statSync(filePath).size;
const { id, owner, dataCaches, fastFinalityIndexes } =
await turbo.uploadFile({
fileStreamFactory: () => fs.createReadStream(filePath),
fileSizeFactory: () => fileSize,
events: {
// overall events (includes signing and upload events)
onProgress: ({ totalBytes, processedBytes, step }) => {
console.log('Overall progress:', { totalBytes, processedBytes, step });
},
onError: ({ error, step }) => {
console.log('Overall error:', { error, step });
},
// signing events
onSigningProgress: ({ totalBytes, processedBytes }) => {
console.log('Signing progress:', { totalBytes, processedBytes });
},
onSigningError: (error) => {
console.log('Signing error:', { error });
},
onSigningSuccess: () => {
console.log('Signing success!');
},
// upload events
onUploadProgress: ({ totalBytes, processedBytes }) => {
console.log('Upload progress:', { totalBytes, processedBytes });
},
onUploadError: (error) => {
console.log('Upload error:', { error });
},
onUploadSuccess: () => {
console.log('Upload success!');
},
},
});
// upload complete!
console.log('Successfully upload data item!', {
id,
owner,
dataCaches,
fastFinalityIndexes,
});
} catch (error) {
// upload failed
console.error('Failed to upload data item!', error);
}
}The SDK is provided in both CommonJS and ESM formats, and it's compatible with bundlers such as Webpack, Rollup, and ESbuild. Utilize the appropriately named exports provided by this SDK's package.json based on your project's configuration. Refer to the examples directory to see how to use the SDK in various environments.
Warning
Polyfills are not provided by default for bundled web projects (Vite, ESBuild, Webpack, Rollup, etc.) . Depending on your apps bundler configuration and plugins, you will need to provide polyfills for various imports including crypto, process, fs and buffer. Refer to your bundler's documentation for how to provide the necessary polyfills.
import { TurboFactory } from '@ardrive/turbo-sdk/web';
const turbo = TurboFactory.unauthenticated();
const rates = await turbo.getFiatRates();The web bundle is available as a GitHub release artifact. You can reference it directly via jsDelivr CDN:
<script type="module">
import { TurboFactory } from 'https://cdn.jsdelivr.net/gh/ardriveapp/turbo-sdk@latest/bundles/web.bundle.min.js';
const turbo = TurboFactory.unauthenticated();
const rates = await turbo.getFiatRates();
</script>Or download the bundle from GitHub Releases and serve it locally.
Full example available in the examples/typescript/cjs.
import { TurboFactory } from '@ardrive/turbo-sdk';
const turbo = TurboFactory.unauthenticated();
const rates = await turbo.getFiatRates();Full example available in the examples/typescript/esm.
import { TurboFactory } from '@ardrive/turbo-sdk/node';
const turbo = TurboFactory.unauthenticated();
const rates = await turbo.getFiatRates();The SDK provides TypeScript types. When you import the SDK in a TypeScript project:
import { TurboFactory } from '@ardrive/turbo-sdk/<node/web>';Types are exported from ./lib/types/[node/web]/index.d.ts and should be automatically recognized, offering benefits such as type-checking and autocompletion.
Examples are available in the examples directory. To run examples:
yarn example:web- opens up the example web pageyarn example:cjs- runs example CJS node scriptyarn example:esm- runs example ESM node script
Creates an instance of a client that accesses Turbo's unauthenticated services.
const turbo = TurboFactory.unauthenticated();Creates an instance of a client that accesses Turbo's authenticated and unauthenticated services. Requires either a signer, or private key to be provided. See the Signers section for all supported signers and authentication methods.
const signer = new ArweaveSigner(jwk);
const turbo = TurboFactory.authenticated({ signer });For development and testing, you can configure the SDK to use blockchain testnets. This allows you to test your integration with free testnet tokens without spending real cryptocurrency.
Important: The SDK defaults to mainnet. You must explicitly set the gatewayUrl parameter to use a testnet.
// Base Sepolia (recommended for testing)
const turbo = TurboFactory.authenticated({
privateKey: process.env.BASE_SEPOLIA_PRIVATE_KEY,
token: 'base-eth',
gatewayUrl: 'https://sepolia.base.org', // Required for testnet
paymentServiceConfig: {
url: 'https://payment.services.ar-io.dev', // ar.io testnet sandbox
},
uploadServiceConfig: {
url: 'https://upload.services.ar-io.dev', // ar.io testnet sandbox
}
});
// Solana Devnet
const turbo = TurboFactory.authenticated({
privateKey: bs58.encode(secretKey),
token: 'solana',
gatewayUrl: 'https://api.devnet.solana.com',
paymentServiceConfig: {
url: 'https://payment.services.ar-io.dev',
},
uploadServiceConfig: {
url: 'https://upload.services.ar-io.dev',
}
});
// Ethereum Sepolia
const turbo = TurboFactory.authenticated({
privateKey: process.env.SEPOLIA_PRIVATE_KEY,
token: 'ethereum',
gatewayUrl: 'https://sepolia.gateway.tenderly.co',
paymentServiceConfig: {
url: 'https://payment.services.ar-io.dev',
},
uploadServiceConfig: {
url: 'https://upload.services.ar-io.dev',
},
});These endpoints are the ar.io Testnet Sandbox — the full ar.io stack (upload, payment, ArNS,
and gateway) running on testnet, with a faucet so nothing costs real money. Uploaded data is
served from the sandbox gateway at https://ar-io.dev and is ephemeral (purged after ~3
days); it is never posted to mainnet Arweave. See
the ar.io Testnet Sandbox docs.
Supported Testnets:
- ARIO staging (
ario) - Staging ARIO on Solana devnet; fee-free funding, claim from the ar.io faucet - Base Sepolia (
base-eth) - Supports on-demand funding - Solana Devnet (
solana) - Supports on-demand funding - Ethereum Sepolia (
ethereum) - Manual top-up only - Polygon Amoy (
pol) - Manual top-up only
Returns the list of currencies supported by the Turbo Payment Service for topping up a user balance of AR Credits (measured in Winston Credits, or winc).
const currencies = await turbo.getSupportedCurrencies();Returns the list of countries supported by the Turbo Payment Service's top up workflow.
const countries = await turbo.getSupportedCountries();Returns the current raw fiat to AR conversion rate for a specific currency as reported by third-party pricing oracles.
const fiatToAR = await turbo.getFiatToAR({ currency: 'USD' });Returns the current fiat rates for 1 GiB of data for supported currencies, including all top-up adjustments and fees.
const rates = await turbo.getFiatRates();Returns the current amount of Winston Credits including all adjustments for the provided fiat currency.
const { winc, actualPaymentAmount, quotedPaymentAmount, adjustments } =
await turbo.getWincForFiat({
amount: USD(100),
});Returns the current amount of Winston Credits including all adjustments for the provided token amount.
const { winc, actualTokenAmount, equivalentWincTokenAmount } =
await turbo.getWincForToken({
tokenAmount: WinstonToTokenAmount(100_000_000),
});Get the current price from the Turbo Payment Service, denominated in the specified fiat currency, for uploading a specified number of bytes to Turbo.
const turbo = TurboFactory.unauthenticated();
const { amount } = await turbo.getFiatEstimateForBytes({
byteCount: 1024 * 1024 * 1024,
currency: 'usd', // specify the currency for the price
});
console.log(amount); // Estimated usd price for 1 GiBExample Output
{
"byteCount": 1073741824,
"amount": 20.58,
"currency": "usd",
"winc": "2402378997310"
}Get the current price from the Turbo Payment Service, denominated in the specified token, for uploading a specified number of bytes to Turbo.
const turbo = TurboFactory.unauthenticated({ token: 'solana' });
const { tokenPrice } = await turbo.getTokenPriceForBytes({
byteCount: 1024 * 1024 * 100,
});
console.log(tokenPrice); // Estimated SOL Price for 100 MiBReturns the estimated cost in Winston Credits for the provided file sizes, including all upload adjustments and fees.
const [uploadCostForFile] = await turbo.getUploadCosts({ bytes: [1024] });
const { winc, adjustments } = uploadCostForFile;Uploads a signed data item. The provided dataItemStreamFactory should produce a NEW signed data item stream each time is it invoked. The dataItemSizeFactory is a function that returns the size of the file. The signal is an optional AbortSignal that can be used to cancel the upload or timeout the request. The events parameter is an optional object that can be used to listen to upload progress, errors, and success (refer to the Events section for more details).
const filePath = path.join(__dirname, './my-signed-data-item');
const dataItemSize = fs.statSync(filePath).size;
const uploadResponse = await turbo.uploadSignedDataItem({
dataItemStreamFactory: () => fs.createReadStream(filePath),
dataItemSizeFactory: () => dataItemSize,
signal: AbortSignal.timeout(10_000), // cancel the upload after 10 seconds
events: {
// track upload events only
onUploadProgress: ({ totalBytes, processedBytes }) => {
console.log('Upload progress:', { totalBytes, processedBytes });
},
onUploadError: (error) => {
console.log('Upload error:', { error });
},
onUploadSuccess: () => {
console.log('Upload success!');
},
},
});Creates a Stripe checkout session for a Turbo Top Up with the provided amount, currency, owner. The returned URL can be opened in the browser, all payments are processed by Stripe. To leverage promo codes, see TurboAuthenticatedClient.
const { url, winc, paymentAmount, quotedPaymentAmount, adjustments } =
await turbo.createCheckoutSession({
amount: USD(10.0), // $10.00 USD
owner: publicArweaveAddress,
// promo codes require an authenticated client
});
// Open checkout session in a browser
window.open(url, '_blank');const turbo = TurboFactory.unauthenticated({ token: 'ethereum' });
const { url, winc, paymentAmount } = await turbo.createCheckoutSession({
amount: USD(10.0), // $10.00 USD
owner: publicEthereumAddress,
});const turbo = TurboFactory.unauthenticated({ token: 'solana' });
const { url, winc, paymentAmount } = await turbo.createCheckoutSession({
amount: USD(10.0), // $10.00 USD
owner: publicSolanaAddress,
});const turbo = TurboFactory.unauthenticated({ token: 'pol' });
const { url, winc, paymentAmount } = await turbo.createCheckoutSession({
amount: USD(10.0), // $10.00 USD
owner: publicPolygonAddress,
});const turbo = TurboFactory.unauthenticated({ token: 'kyve' });
const { url, winc, paymentAmount } = await turbo.createCheckoutSession({
amount: USD(10.0), // $10.00 USD
owner: publicKyveAddress,
});Submits the transaction ID of a funding transaction to Turbo Payment Service for top up processing. The txId is the transaction ID of the transaction to be submitted.
Note
Use this API if you've already executed your token transfer to the Turbo wallet. Otherwise, consider using topUpWithTokens to execute a new token transfer to the Turbo wallet and submit its resulting transaction ID for top up processing all in one go
const turbo = TurboFactory.unauthenticated(); // defaults to arweave token type
const { status, id, ...fundResult } = await turbo.submitFundTransaction({
txId: 'my-valid-arweave-fund-transaction-id',
});Issues a signed request to get the credit balance of a wallet measured in AR (measured in Winston Credits, or winc).
const { winc: balance } = await turbo.getBalance();Returns the wallet's remaining free-tier upload allowance in bytes as { bytesRemaining }, so you can tell up front whether an upload will be free. bytesRemaining is null for a wallet with an unlimited allowance (an exempt/partner wallet), and 0 when the free tier is disabled on the target Turbo deployment. It is advisory — the authoritative free/charge decision is made at upload time — and is a wallet-side figure (a per-network cap may also apply). Deployment-wide free-tier limits are on the service's /info endpoint.
const { bytesRemaining } = await turbo.getFreeStatus();It is also available on the TurboUnauthenticatedClient for any wallet by address:
const { bytesRemaining } = await turbo.getFreeStatus('a-native-address');Issues a signed request for the signing wallet's own completed top-up (payment) history — both cryptocurrency and fiat top-ups — merged newest-first and keyset-paginated. This is self-scoped: it returns only the signing wallet's rows (the service reads the address from the signature, never a query parameter), so it is available on the TurboAuthenticatedClient only. limit is the page size (1-100, default 50). To page, pass the previous response's cursor while hasMore is true.
Each item is discriminated by type: a 'crypto' item includes wincCredited, tokenType, tokenQuantity, usdEquivalent, senderAddress, transactionId, and blockHeight; a 'fiat' item includes wincCredited, paymentAmount, currencyType, paymentProvider, receiptId, and giftMessage. Every item has an ISO-8601 UTC date.
const { payments, hasMore, cursor } = await turbo.getPaymentHistory({
limit: 25,
});
// Fetch the next page while more results remain
if (hasMore) {
const next = await turbo.getPaymentHistory({ limit: 25, cursor });
}Returns the native address of the connected signer.
const address = await turbo.signer.getNativeAddress();Returns the current amount of Winston Credits including all adjustments for the provided fiat currency, amount, and optional promo codes.
const { winc, paymentAmount, quotedPaymentAmount, adjustments } =
await turbo.getWincForFiat({
amount: USD(100),
promoCodes: ['MY_PROMO_CODE'], // promo codes require an authenticated client
});Creates a Stripe checkout session for a Turbo Top Up with the provided amount, currency, owner, and optional promo codes. The returned URL can be opened in the browser, all payments are processed by Stripe. Promo codes require an authenticated client.
const { url, winc, paymentAmount, quotedPaymentAmount, adjustments } =
await turbo.createCheckoutSession({
amount: USD(10.0), // $10.00 USD
owner: publicArweaveAddress,
promoCodes: ['MY_PROMO_CODE'], // promo codes require an authenticated client
});
// open checkout session in a browser
window.open(url, '_blank');The easiest way to upload data to Turbo. The signal is an optional AbortSignal that can be used to cancel the upload or timeout the request. dataItemOpts is an optional object that can be used to configure tags, target, and anchor for the data item upload.
const uploadResult = await turbo.upload({
data: 'The contents of my file!',
signal: AbortSignal.timeout(10_000), // cancel the upload after 10 seconds
dataItemOpts: {
// optional
},
events: {
// optional
},
});Signs and uploads a raw file. There are two ways to provide the file to the SDK:
- Using a
fileparameter - Using a
fileStreamFactoryandfileSizeFactory
In Web with a file input:
const selectedFile = e.target.files[0];
const uploadResult = await turbo.uploadFile({
file: selectedFile,
dataItemOpts: {
tags: [{ name: 'Content-Type', value: 'text/plain' }],
},
events: {
onUploadProgress: ({ totalBytes, processedBytes }) => {
console.log('Upload progress:', { totalBytes, processedBytes });
},
onUploadError: (error) => {
console.log('Upload error:', { error });
},
onUploadSuccess: () => {
console.log('Upload success!');
},
},
});In NodeJS with a file path:
const filePath = path.join(__dirname, './my-unsigned-file.txt');
const fileSize = fs.stateSync(filePath).size;
const uploadResult = await turbo.uploadFile({
file: filePath,
dataItemOpts: {
tags: [{ name: 'Content-Type', value: 'text/plain' }],
},
});Note: The provided fileStreamFactory should produce a NEW file data stream each time it is invoked. The fileSizeFactory is a function that returns the size of the file. The signal is an optional AbortSignal that can be used to cancel the upload or timeout the request. dataItemOpts is an optional object that can be used to configure tags, target, and anchor for the data item upload.
const filePath = path.join(__dirname, './my-unsigned-file.txt');
const fileSize = fs.stateSync(filePath).size;
const uploadResult = await turbo.uploadFile({
fileStreamFactory: () => fs.createReadStream(filePath),
fileSizeFactory: () => fileSize,
});By default, the Turbo upload methods will split files that are larger than 10 MiB into chunks and send them to the upload service multi-part endpoints. This behavior can be customized with the following inputs:
chunkByteCount: The maximum size in bytes for each chunk. Must be between 5 MiB and 500 MiB. Defaults to 5 MiB.maxChunkConcurrency: The maximum number of chunks to upload concurrently. Defaults to 5. Reducing concurrency will slow down uploads, but reduce memory utilization and serialize network calls. Increasing it will upload faster, but can strain available resources.chunkingMode: The chunking mode to use. Can be 'auto', 'force', or 'disabled'. Defaults to 'auto'. Auto behavior means chunking is enabled if the file would be split into at least three chunks.maxFinalizeMs: The maximum time in milliseconds to wait for the finalization of all chunks after the last chunk is uploaded. Defaults to 1 minute per GiB of the total file size.
// Customize chunking behavior
await turbo.upload({
...params,
chunkByteCount: 1024 * 1024 * 500, // Max chunk size
maxChunkConcurrency: 1, // Minimize concurrency
});// Disable chunking behavior
await turbo.upload({
...params,
chunkingMode: 'disabled',
});// Force chunking behavior
await turbo.upload({
...params,
chunkingMode: 'force',
});With the upload methods, you can choose to Top Up with selected crypto token on demand if the connected wallet does not have enough credits to complete the upload.
This is done by providing the OnDemandFunding class to the fundingMode parameter on upload methods. The maxTokenAmount (optional) is the maximum amount of tokens in the token type's smallest unit value (e.g: Winston for arweave token type) to fund the wallet with. The topUpBufferMultiplier (optional) is the multiplier to apply to the estimated top-up amount to avoid underpayment during on-demand top-ups due to price fluctuations on longer uploads. Defaults to 1.1, meaning a 10% buffer.
Note: On demand API currently only available for $ARIO (ario), $SOL (solana), $ETH on Base Network (base-eth) and $USDC on Base Network (base-usdc) token types.
const turbo = TurboFactory.authenticated({
signer: arweaveSignerWithARIO,
token: 'ario',
});
await turbo.upload({
...params,
fundingMode: new OnDemandFunding({
maxTokenAmount: ARIOToTokenAmount(500), // Max 500 $ARIO
topUpBufferMultiplier: 1.1, // 10% buffer to avoid underpayment
}),
});Another method of uploading files is via the x402 protocol. This method is optimized for agent workflows and allows for direct uploads to Arweave gateways that support the x402 protocol using an EVM wallet and base-usdc token type.
const turbo = TurboFactory.authenticated({
signer: ethereumSignerWithBaseUSDC,
token: 'base-usdc',
});
await turbo.uploadFile({
...params,
fundingMode: new X402Funding({ maxMUSDCAmount: 1_000_000 }), // Max 1 USDC. Opt out if too expensive
});Using the x402 protocol, you can also upload raw data to Turbo without signing a data item. This method is ideal for quick agent workflows where the ownership of the data is not required to be tied to a specific wallet. The eventual data item on chain will be signed by Turbo's x402 EVM signer.
const turbo = TurboFactory.authenticated({
signer: ethereumSignerWithBaseUSDC,
token: 'base-usdc',
});
await turbo.uploadRawX402Data({
data: myRawData,
maxMUSDCAmount: 1_000_000, // Max 1 USDC. Opt out if too expensive
});NOTE: For free uploads under 100 KiB, this method of upload currently does not require a signature and can be used with an unauthenticated client.
// Unsigned free upload of raw data under 100 KiB
const turbo = TurboFactory.unauthenticated({ token: 'base-usdc' });
await turbo.uploadRawX402Data({
data: myRawData,
});uploadFolder({ folderPath, files, dataItemOpts, signal, maxConcurrentUploads, throwOnFailure, manifestOptions, folderIndex, manifestDataItemOpts })
Signs and uploads a folder of files. For NodeJS, the folderPath of the folder to upload is required. For the browser, an array of files is required. The dataItemOpts is an optional object that can be used to configure tags, target, and anchor for the data item upload. The signal is an optional AbortSignal that can be used to cancel the upload or timeout the request. The maxConcurrentUploads is an optional number that can be used to limit the number of concurrent uploads. The throwOnFailure is an optional boolean that can be used to throw an error if any upload fails. The manifestOptions is an optional object that can be used to configure the manifest file, including a custom index file, fallback file, or whether to disable manifests altogether. Manifests are enabled by default. The folderIndex is an optional folder index that skips files already on Arweave. The manifestDataItemOpts is an optional object that configures the manifest data item only, and defaults to dataItemOpts.
const folderPath = path.join(__dirname, './my-folder');
const { manifest, fileResponses, manifestResponse } = await turbo.uploadFolder({
folderPath,
dataItemOpts: {
// optional
tags: [
{
// User defined content type will overwrite file content type
name: 'Content-Type',
value: 'text/plain',
},
{
name: 'My-Custom-Tag',
value: 'my-custom-value',
},
],
// no timeout or AbortSignal provided
},
manifestOptions: {
// optional
indexFile: 'custom-index.html',
fallbackFile: 'custom-fallback.html',
disableManifests: false,
},
});<input type="file" id="folder" name="folder" webkitdirectory />
<script type="module">
const folderInput = document.getElementById('folder');
folderInput.addEventListener('change', async (event) => {
const selectedFiles = folderInput.files;
console.log('Folder selected:', selectedFiles);
const { manifest, fileResponses, manifestResponse } =
await turbo.uploadFolder({
files: Array.from(selectedFiles).map((file) => file),
});
console.log(manifest, fileResponses, manifestResponse);
});
</script>The uploadFolder method supports folder-level and per-file events for tracking upload progress. This is useful for building progress bars or providing feedback to users during folder uploads.
const folderPath = path.join(__dirname, './my-folder');
const { manifest, fileResponses, manifestResponse } = await turbo.uploadFolder({
folderPath,
events: {
// Per-file events
onFileStart: ({ fileName, fileSize, fileIndex, totalFiles }) => {
console.log(
`Starting file ${
fileIndex + 1
}/${totalFiles}: ${fileName} (${fileSize} bytes)`,
);
},
onFileProgress: ({
fileName,
fileIndex,
totalFiles,
fileProcessedBytes,
fileTotalBytes,
step,
}) => {
const percentComplete = (fileProcessedBytes / fileTotalBytes) * 100;
console.log(
`File ${
fileIndex + 1
}/${totalFiles} (${fileName}) ${step}: ${percentComplete.toFixed(2)}%`,
);
},
onFileComplete: ({ fileName, fileIndex, totalFiles, id }) => {
console.log(
`Completed file ${fileIndex + 1}/${totalFiles}: ${fileName} (${id})`,
);
},
onFileError: ({ fileName, fileIndex, totalFiles, error }) => {
console.error(
`Error uploading file ${fileIndex + 1}/${totalFiles}: ${fileName}`,
error,
);
},
// Folder-level aggregate events
onFolderProgress: ({
processedFiles,
totalFiles,
processedBytes,
totalBytes,
currentPhase,
}) => {
const percentComplete = (processedBytes / totalBytes) * 100;
console.log(
`Folder progress (${currentPhase}): ${processedFiles}/${totalFiles} files, ${percentComplete.toFixed(
2,
)}%`,
);
},
onFolderError: (error) => {
console.error('Folder upload error:', error);
},
onFolderSuccess: () => {
console.log('Folder upload complete!');
},
},
});An Arweave upload is permanent, so paying twice for byte identical files buys
nothing. Pass a folderIndex and uploadFolder hashes every file, asks the
index which of those files already have a data item on Arweave, and signs,
uploads and pays for only the rest. The manifest is assembled from the ids that
were already known plus the ids of whatever this run uploaded.
import {
composeFolderIndex,
createChainFolderIndex,
createFileFolderIndex,
} from '@ardrive/turbo-sdk/node';
const folderIndex = composeFolderIndex([
// Fast local cache, kept outside the folder being uploaded.
createFileFolderIndex({ filePath: '.turbo/folder-index.jsonl' }),
// Fallback for a machine that has never deployed before, e.g. a CI runner.
// getPublicKey() is the one form every signer type can produce.
createChainFolderIndex({ owner: await turbo.signer.getPublicKey() }),
]);
const { manifest, manifestResponse, folderIndexSummary } =
await turbo.uploadFolder({
folderPath: path.join(__dirname, './dist'),
folderIndex,
// Deploy varying tags belong on the manifest, which is rewritten every time.
manifestDataItemOpts: {
tags: [{ name: 'Git-Commit', value: process.env.GITHUB_SHA }],
},
});
console.log(folderIndexSummary);
// { totalFiles: 143, uploadedFiles: 2, reusedFiles: 141, ... }An index key is <sha-256 of the bytes>.<sha-256 of the tags>, and both halves
matter. Keying on the bytes alone would reuse a data item whose tags are not the
ones you asked for: an empty a.css and an empty b.js hash identically, and
sharing one item between them would serve JavaScript as text/css, which a
browser refuses to execute. Covering the tags means a reused data item is
always exactly the data item this call would otherwise have created — same
bytes, same Content-Type, same dataItemOpts tags.
Files uploaded with an index carry one extra tag, File-SHA256, holding the
sha-256 of their own bytes. That tag is what createChainFolderIndex filters on.
The corollary is a real cost cliff, so it is worth being blunt about. A per
file tag whose value changes between deploys changes every key, and re-uploads
the whole folder at full price. A commit sha, a build number or a timestamp in
dataItemOpts means you never reuse anything, and the deploy still succeeds, so
nothing about the run looks wrong except the bill.
That is deliberate. The alternative — keying on bytes alone — reuses an item tagged with a previous deploy's commit sha, so the tags on chain quietly stop describing what is on chain. A wrong bill is recoverable; a data item that lies about itself is permanent. So the index errs towards paying again.
To keep the cliff from being silent, uploadFolder logs a warning when a file
it is about to upload has bytes the index already holds under a different set
of tags, which is what a deploy-varying per file tag looks like:
3 of the 3 file(s) this run is about to upload are already on Arweave byte for
byte, under a different set of tags. Their content has not changed but their
tags have, so they are being paid for again. A folder index key covers the tags
on a file as well as its bytes. That is usually a tag in dataItemOpts whose
value changes between deploys -- a commit sha, a build number, a timestamp -- in
which case move it to manifestDataItemOpts rather than paying for these files
again. It can also be a file that kept its content but changed its Content-Type,
through a rename or a new extension, which is expected and costs one upload.
Very little else produces that signal: a folder the index has never seen has
unknown bytes, and a layer that could not be reached reports nothing known, so
neither triggers it. A file that kept its content but changed its Content-Type
through a rename does trigger it, and the message says so. It also fires for one
drifted file among a hundred reused ones, not only when everything misses. A
layer that does not implement the optional knownContentHashes cannot answer
the question and stays quiet. The fix, whenever it is a varying tag, is always
the same: move it to manifestDataItemOpts, since the manifest is rewritten on
every deploy anyway.
| Layer | Where it lives | Survives a fresh checkout |
|---|---|---|
createMemoryFolderIndex(seed?) |
memory | no |
createFileFolderIndex({ filePath }) (NodeJS) |
a JSON lines log | only if the file is kept |
createChainFolderIndex({ owner, appName?, ... }) |
gateway GraphQL | yes |
composeFolderIndex([...]) |
layers the above | -- |
Reads fall through a composed index in order and writes go to every layer that
is not readOnly, so an id recovered from the gateway is cached locally for the
next run. A layer that throws is skipped, not propagated — a full disk under
the file layer must not stop the memory layer from holding ids the run has
already paid for, and an unreachable gateway must not stop the local cache from
answering. Pass a logger as the second argument to composeFolderIndex to see
which layer was skipped and why.
createFileFolderIndex writes an append-only log, one JSON record per line,
compacted when it is next loaded. It appends after every single upload rather
than rewriting at the end of the run, so a deploy killed part way through never
loses a file it has paid for — and appending is constant work per file, where
rewriting the whole file per upload is quadratic and costs minutes and gigabytes
of writes on a first deploy of a few thousand files. It is also the more crash
safe shape: a process killed mid write can only damage the last line, which is
dropped on load, where a torn rewrite loses every id in the file.
An index is a cache. A get or resolve that throws is treated as a miss and
logged — an unreachable gateway costs you a re-upload, it does not fail your
deploy. Anything with get and set is a valid index, so implement
TurboFolderUploadIndex to back one with a database, an object store, or a CI
cache. Treat the keys as opaque.
createChainFolderIndex needs the owner a gateway indexes uploads under,
which is the base64url sha-256 of the signer's public key. Pass
await turbo.signer.getPublicKey() and the SDK derives it, which works for
every signer type.
A bare string is deliberately rejected, because it cannot be disambiguated: a
raw 32 byte ed25519 public key base64urls to exactly 43 characters, the same
shape as an owner address, and guessing wrong means the sweep matches nothing
and the whole folder is re-uploaded with no error at all. Say which one you
have — { publicKey } or { address } — if you are not passing the bytes.
An 0x... Ethereum address or a base58 Solana address is not accepted, because
owners: on a gateway does not match those. (Verified against arweave.net:
owners matches the 43 character address and returns nothing for the raw public
key, so the conversion has to happen client side.)
The sweep is scoped to owners: [your own address], so it can only ever find
items you signed. Within that scope, File-SHA256 is self asserted — it is
a tag your own past uploads wrote, not something a gateway verifies against the
bytes — and the index trusts it. That is safe for uploads this SDK made, since it
only ever writes a hash it computed from the file in front of it.
uploadFolder writes whichever tag the index it is given declares, so setting
hashTagName moves both the tag that is written and the tag the sweep filters
on, and the two cannot drift apart. Every layer in a composeFolderIndex stack
that declares one has to declare the same one, or the call throws: one tag is
written per file, so a stack that disagrees would leave whichever layer lost
matching nothing, for ever, without an error.
It stops being safe if you point hashTagName at a tag you were already using
for something else. Any of your own past items carrying 64 hex characters under
that name would be treated as a candidate, and one whose tag set happens to
match would be reused — putting a manifest path in front of unrelated bytes. Use
a name nothing else of yours writes.
A sweep can examine at most pageSize * maxPages items, 2,000 by default. A
folder with more files than that, or a long enough deployment history, can
therefore reach the page limit with files still unresolved — and those files are
uploaded and paid for again while the summary reports them as ordinary new
files. Pass a logger to createChainFolderIndex and it says so when this
happens, naming how many files were left. Raise maxPages or pageSize, or put
a createFileFolderIndex in front, and the sweep has less to find.
createMemoryFolderIndex, createChainFolderIndex and composeFolderIndex all
work in the browser. createFileFolderIndex is NodeJS only, since there is no
filesystem to write to; persist the map yourself and seed
createMemoryFolderIndex with it, or rely on the chain index.
Note that hashing differs by platform. NodeJS streams each file through a
node:crypto digest, so file size is not a concern. The browser has no streaming
WebCrypto digest, so each File is buffered whole before it is hashed — a very
large File can exhaust the tab.
A gateway indexes an upload minutes after it lands, so two machines deploying the same brand new file at the same moment can each pay for it once. Only the bill is affected, and only for genuinely new bytes -- the manifest is correct either way.
Tops up the connected wallet with Credits by submitting a payment transaction for the token amount to the Turbo wallet and then submitting that transaction id to Turbo Payment Service for top up processing.
- The
tokenAmountis the amount of tokens in the token type's smallest unit value (e.g: Winston for arweave token type) to fund the wallet with. - The
feeMultiplier(optional) is the multiplier to apply to the reward for the transaction to modify its chances of being mined. Credits will be added to the wallet balance after the transaction is confirmed on the given blockchain. Defaults to 1.0, meaning no multiplier. - The
turboCreditDestinationAddress(optional) is the native address to credit the funds to. If not provided, the connected wallet's native address will be used. Note: Not available for KYVE token type.
const turbo = TurboFactory.authenticated({ signer, token: 'arweave' });
const { winc, status, id, ...fundResult } = await turbo.topUpWithTokens({
tokenAmount: WinstonToTokenAmount(100_000_000), // 0.0001 AR
feeMultiplier: 1.1, // 10% increase in reward for improved mining chances
turboCreditDestinationAddress: '0xabc...123', // Any custom EVM / SOL / AR / KYVE native destination address
});const turbo = TurboFactory.authenticated({ signer, token: 'ario' });
const { winc, status, id, ...fundResult } = await turbo.topUpWithTokens({
tokenAmount: ARIOToTokenAmount(100), // 100 $ARIO
});// USDC on Ethereum Mainnet
const { winc, status, id, ...fundResult } = await TurboFactory.authenticated({
signer,
token: 'usdc',
}).topUpWithTokens({
tokenAmount: USDCToTokenAmount(1), // 1 USDC
});
// USDC on Base Network
const { winc, status, id, ...fundResult } = await TurboFactory.authenticated({
signer,
token: 'base-usdc',
}).topUpWithTokens({
tokenAmount: USDCToTokenAmount(1), // 1 USDC
});
// USDC on Polygon Network
const { winc, status, id, ...fundResult } = await TurboFactory.authenticated({
signer,
token: 'polygon-usdc',
}).topUpWithTokens({
tokenAmount: USDCToTokenAmount(1), // 1 USDC
});const turbo = TurboFactory.authenticated({ signer, token: 'ethereum' });
const { winc, status, id, ...fundResult } = await turbo.topUpWithTokens({
tokenAmount: ETHToTokenAmount(0.00001), // 0.00001 ETH
});const turbo = TurboFactory.authenticated({ signer, token: 'pol' });
const { winc, status, id, ...fundResult } = await turbo.topUpWithTokens({
tokenAmount: POLToTokenAmount(0.00001), // 0.00001 POL
});const turbo = TurboFactory.authenticated({ signer, token: 'base-eth' });
const { winc, status, id, ...fundResult } = await turbo.topUpWithTokens({
tokenAmount: ETHToTokenAmount(0.00001), // 0.00001 ETH bridged on Base Network
});const turbo = TurboFactory.authenticated({ signer, token: 'solana' });
const { winc, status, id, ...fundResult } = await turbo.topUpWithTokens({
tokenAmount: SOLToTokenAmount(0.00001), // 0.00001 SOL
});const turbo = TurboFactory.authenticated({ signer, token: 'kyve' });
const { winc, status, id, ...fundResult } = await turbo.topUpWithTokens({
tokenAmount: KYVEToTokenAmount(0.00001), // 0.00001 KYVE
});Shares credits from the connected wallet to the provided native address and approved winc amount. This action will create a signed data item for the approval
const { approvalDataItemId, approvedWincAmount } = await turbo.shareCredits({
approvedAddress: '2cor...VUa',
approvedWincAmount: 800_000_000_000, // 0.8 Credits
expiresBySeconds: 3600, // Credits will expire back to original wallet in 1 hour
});Revokes all credits shared from the connected wallet to the provided native address.
const revokedApprovals = await turbo.revokeCredits({
revokedAddress: '2cor...VUa',
});Returns all given or received credit share approvals for the connected wallet or the provided native address.
const { givenApprovals, receivedApprovals } =
await turbo.getCreditShareApprovals({
userAddress: '2cor...VUa',
});The client can buy and manage ArNS names paid with Turbo Credits, or with a credit card. Either way the bundler performs the on-chain ARIO purchase for you, and sponsors every lamport of Solana fees and rent.
Turbo takes custody of nothing. The ANT that backs your name is minted straight to you. There is no "claim later" or "transfer out" step.
An ANT is a Metaplex Core asset on Solana, so the owner is always a Solana address — even when you pay with Arweave or Ethereum credits. But the owner never pays: Turbo is the fee payer on every sponsored action, so the owner's SOL balance can stay at zero for the life of the name.
Supply the owner as an ArNSOwnerSigner:
import { solanaOwnerSigner } from '@ardrive/turbo-sdk';
// From a secret key (servers, scripts, tests)
const owner = solanaOwnerSigner(bs58SolanaSecretKey);A browser wallet (Phantom, Solflare, or an app's embedded wallet) should implement the interface directly rather than exposing a secret key:
const owner = {
getAddress: () => wallet.publicKey.toBase58(),
signTransaction: async (txBase64) => {
// atob/btoa rather than Buffer: browsers do not provide Buffer unless the
// app polyfills it. The spread is safe here because a Solana transaction
// is capped at 1232 bytes.
const tx = VersionedTransaction.deserialize(
Uint8Array.from(atob(txBase64), (c) => c.charCodeAt(0)),
);
const signed = await wallet.signTransaction(tx);
return btoa(String.fromCharCode(...signed.serialize()));
},
signMessage: (message) => wallet.signMessage(message),
};| Who | How it travels | |
|---|---|---|
| Payer | the Turbo identity holding credits — Arweave, Ethereum or Solana | the client's signer |
| ANT owner | always a Solana address | the owner parameter |
They are allowed to be different wallets, and routinely are: one account pays while another owns.
const turbo = TurboFactory.authenticated({ privateKey: jwk });
// Buy — the ONE signature in the whole lifecycle. Grants Turbo controller
// rights in this SAME transaction, which is why everything below needs no
// signature of its own until you revoke it.
const { antId, messageId } = await turbo.buyArNSName({
name: 'my-name',
owner,
type: 'lease', // or 'permabuy'
years: 1, // leases only
onNonce: (nonce) => persist(nonce), // fires BEFORE the wallet prompt
});
// Lifecycle — no signature at all, spends ARIO.
await turbo.extendArNSLease({ name: 'my-name', years: 2 });
await turbo.upgradeArNSName({ name: 'my-name' });
await turbo.increaseArNSUndernameLimit({ name: 'my-name', increaseQty: 5 });
// Records — a small flat/derived credits margin recovers the sponsored SOL
// rent. Handled whichever shape the server picks.
await turbo.setArNSRecord({
antId,
owner,
transactionId,
undername: '@',
ttlSeconds: 900,
});
await turbo.removeArNSRecord({ antId, owner, undername: 'docs' });
// Record metadata — display name, logo, description, keywords. Same margin,
// same shape rules as setArNSRecord. `null` clears a field; omit to leave it.
await turbo.setArNSRecordMetadata({
antId,
owner,
undername: '@',
displayName: 'My Docs',
recordDescription: null, // clear it
});
await turbo.removeArNSRecordMetadata({ antId, owner, undername: 'docs' });
// Hand ONE record to another address — distinct from transferring the ANT.
await turbo.transferArNSRecord({
antId,
owner,
undername: 'docs',
target: newOwnerAddress,
});
// Controllers and transfer — owner-signed, same flat/derived margin.
// addArNSController is for RE-granting after a revoke, or granting some
// OTHER address — Turbo already has it from the buy above.
await turbo.addArNSController({ antId, owner }); // omit target => Turbo
await turbo.removeArNSController({ antId, owner }); // the revoke
await turbo.transferArNSAnt({ antId, owner, target: newOwnerAddress });| Action | Costs credits | Owner signature |
|---|---|---|
buyArNSName |
yes — ARIO purchase + ANT spawn rent | always, once |
extendArNSLease / upgradeArNSName / increaseArNSUndernameLimit |
yes — ARIO purchase | no |
setArNSRecord / removeArNSRecord / setArNSRecordMetadata / removeArNSRecordMetadata / transferArNSRecord |
yes — small flat/derived margin | only after you revoke Turbo |
addArNSController / removeArNSController / transferArNSAnt |
yes — small flat/derived margin | yes |
Every action costs credits — gas sponsorship was never meant to be free
sponsorship. The four purchase actions charge the ARIO cost (plus, for
buyArNSName, a rent-derived surcharge for the ANT it mints); the other eight
charge a small margin that recovers the Solana rent/fees Turbo fronts on your
behalf, computed the same max(rent-derived, flat floor) way as the ANT spawn
surcharge. Preview it before you pay:
const { wincQty } = await turbo.getArNSActionPrice('remove-controller');getArNSActionPrice covers the eight non-purchase actions, by their route
name (set-record, remove-record, set-record-metadata,
remove-record-metadata, transfer-record, add-controller,
remove-controller, transfer) — use getArNSPriceForName for the four
purchase actions instead, since their cost is dominated by the ARIO purchase,
not this margin.
buyArNSName grants Turbo controller rights inside the same transaction you
sign — the add-controller(Turbo) instruction rides along with the mint, so
there is no separate step. That's why setArNSRecord and the rest complete in
a single call immediately after buying, with no signature of their own.
addArNSController is for re-granting after a revoke, or adding a different
controller — not something you call after a fresh buy. Revoking is always
available — but, like every other action here, not free of credits.
Sponsorship covers the twelve actions above and nothing else. Everything
else in the ArNS, ANT and core programs stays on the direct-signer path via
@ar.io/sdk and costs the user SOL —
notably buying a returned name (auctions, deliberately excluded: the
premium is unbounded), claiming a reserved name, the primary-name flow
(which lives in the ario core program), release/reassign, and ANT-level
metadata.
Note ANT-level metadata (the ANT's own name/ticker/description/keywords/logo)
is distinct from RECORD-level metadata, which setArNSRecordMetadata does
sponsor. Don't tell users they can "manage a name forever without SOL" — scope
the claim to the twelve actions above.
const price = await turbo.getArNSPriceForName({
intent: 'Buy-Name',
name: 'my-name',
type: 'lease',
years: 1,
});
price.wincTotal; // <- charge or display THIS
price.winc; // the name only, EXCLUDING the ANT spawn surchargeBuying mints a fresh ANT, and Turbo fronts that account's Solana rent. A flat
cost-recovery surcharge covers it, and in a real response the surcharge can
exceed the name's own price — so reading winc under-quotes every purchase.
wincTotal is added by the SDK precisely so the correct field is the obvious
one. Never hardcode the surcharge: it is config-driven and derived from live
rates.
Every action returns one of two shapes, and the server picks which:
let res = await turbo.createArNSAction('buy-name', { name, ownerAddress });
if (res.status === 'awaiting-signature') {
res = await turbo.signArNSAction(
res.nonce,
await owner.signTransaction(res.transaction),
);
}
// res.status === 'completed'; res.messageId is the on-chain writeBranch on status, never on which action you called: setArNSRecord completes
alone while Turbo is a controller and flips to awaiting-signature the moment
you revoke Turbo. It degrades instead of breaking.
Sign the exact bytes returned. Turbo has already signed as fee payer; rebuilding the transaction invalidates that signature.
Credits are debited when the action is created, not when it is signed. So:
- Persist the nonce before prompting for a signature — use
onNonce. - Never re-create an action to retry. That debits again. Poll instead:
await turbo.getArNSActionStatus(nonce). - An abandoned action refunds itself — don't build a refund flow.
- Replaying
signArNSActionon a completed action returns{ alreadyCompleted: true }rather than buying twice.
InsufficientCreditsError (HTTP 402) is thrown when the balance is short;
prompt a top-up, then create a fresh action.
const { names } = await turbo.getArNSNames(); // defaults to the signer's addressReceipt history, not a live ownership check: a name transferred away still
appears. Verify present control on chain using the returned antId.
getArNSFiatPurchaseQuote prices a purchase in fiat and returns a Stripe
payment session, so a user can buy a name without holding credits first.
const quote = await turbo.getArNSFiatPurchaseQuote({
name: 'my-name',
intent: 'Buy-Name',
type: 'lease',
years: 1,
currency: 'usd',
});Its paymentAmount is the real charge and already includes the ANT spawn
surcharge. (On the getArNSPriceForName fiat estimate the split is the other
way round: fiatEstimate.paymentAmount is the base and
fiatEstimate.paymentAmountWithAntSpawn is the total.) Throws
FiatPaymentsDisabledError when the service has Stripe switched off.
The SDK supports multiple wallet types and signing methods across different blockchains. You can authenticate using either a signer instance or a private key depending on your use case.
const jwk = await arweave.crypto.generateJWK();
const turbo = TurboFactory.authenticated({ privateKey: jwk });const signer = new ArweaveSigner(jwk);
const turbo = TurboFactory.authenticated({ signer });const signer = new ArconnectSigner(window.arweaveWallet);
const turbo = TurboFactory.authenticated({ signer });const signer = new EthereumSigner(privateKey);
const turbo = TurboFactory.authenticated({ signer });const turbo = TurboFactory.authenticated({
privateKey: ethHexadecimalPrivateKey,
token: 'ethereum',
});const turbo = TurboFactory.authenticated({
privateKey: ethHexadecimalPrivateKey,
token: 'pol',
});const turbo = TurboFactory.authenticated({
privateKey: ethHexadecimalPrivateKey,
token: 'base-eth',
});const turbo = TurboFactory.authenticated({
privateKey: ethHexadecimalPrivateKey,
token: 'base-usdc',
});const signer = new HexSolanaSigner(bs58.encode(secretKey));
const turbo = TurboFactory.authenticated({ signer });const turbo = TurboFactory.authenticated({
walletAdapter: window.solana,
token: 'solana',
});const turbo = TurboFactory.authenticated({
privateKey: bs58.encode(secretKey),
token: 'solana',
});const turbo = TurboFactory.authenticated({
privateKey: kyveHexadecimalPrivateKey,
token: 'kyve',
});import { privateKeyFromKyveMnemonic } from '@ardrive/turbo-sdk';
const turbo = TurboFactory.authenticated({
privateKey: privateKeyFromKyveMnemonic(mnemonic),
token: 'kyve',
});The SDK provides events for tracking the state signing and uploading data to Turbo. You can listen to these events by providing a callback function to the events parameter of the upload, uploadFile, uploadFolder, and uploadSignedDataItem methods.
These events are available for upload, uploadFile, and uploadSignedDataItem methods:
onProgress- emitted when the overall progress changes (includes both upload and signing). Each event consists of the total bytes, processed bytes, and the step (upload or signing)onError- emitted when the overall upload or signing fails (includes both upload and signing)onSuccess- emitted when the overall upload or signing succeeds (includes both upload and signing) - this is the last event emitted for the upload or signing processonSigningProgress- emitted when the signing progress changes.onSigningError- emitted when the signing fails.onSigningSuccess- emitted when the signing succeedsonUploadProgress- emitted when the upload progress changesonUploadError- emitted when the upload failsonUploadSuccess- emitted when the upload succeeds
const uploadResult = await turbo.uploadFile({
fileStreamFactory: () => fs.createReadStream(filePath),
fileSizeFactory: () => fileSize,
events: {
// overall events (includes signing and upload events)
onProgress: ({ totalBytes, processedBytes, step }) => {
console.log('Overall progress:', { totalBytes, processedBytes, step });
},
onError: ({ error, step }) => {
console.log('Overall error:', { error, step });
},
onSuccess: () => {
console.log('Overall success!');
},
// signing events
onSigningProgress: ({ totalBytes, processedBytes }) => {
console.log('Signing progress:', { totalBytes, processedBytes });
},
onSigningError: (error) => {
console.log('Signing error:', { error });
},
onSigningSuccess: () => {
console.log('Signing success!');
},
// upload events
onUploadProgress: ({ totalBytes, processedBytes }) => {
console.log('Upload progress:', { totalBytes, processedBytes });
},
onUploadError: (error) => {
console.log('Upload error:', { error });
},
onUploadSuccess: () => {
console.log('Upload success!');
},
},
});These events are available for the uploadFolder method:
onFileStart- emitted when a file in the folder starts uploading. Includes the file name, file size, file index, and total number of filesonFileProgress- emitted when a file's upload or signing progress changes. Includes the file name, file index, total files, processed bytes for the file, total bytes for the file, and the current step (signing or upload)onFileComplete- emitted when a file successfully completes uploading. Includes the file name, file index, total files, and the data item IDonFileError- emitted when a file upload fails. Includes the file name, file index, total files, and the erroronFolderProgress- emitted when the overall folder upload progress changes. Includes the number of processed files, total files, processed bytes across all files, total bytes across all files, and the current phase (files or manifest)onFolderError- emitted when the overall folder upload failsonFolderSuccess- emitted when the folder upload successfully completes (including manifest generation) - this is the last event emitted for the folder upload process
const uploadResult = await turbo.upload({
data: 'The contents of my file!',
signal: AbortSignal.timeout(10_000), // cancel the upload after 10 seconds
dataItemOpts: {
// optional
},
events: {
// overall events (includes signing and upload events)
onProgress: ({ totalBytes, processedBytes, step }) => {
const percentComplete = (processedBytes / totalBytes) * 100;
console.log('Overall progress:', {
totalBytes,
processedBytes,
step,
percentComplete: percentComplete.toFixed(2) + '%', // eg 50.68%
});
},
onError: (error) => {
console.log('Overall error:', { error });
},
onSuccess: () => {
console.log('Signed and upload data item!');
},
// upload events
onUploadProgress: ({ totalBytes, processedBytes }) => {
console.log('Upload progress:', { totalBytes, processedBytes });
},
onUploadError: (error) => {
console.log('Upload error:', { error });
},
onUploadSuccess: () => {
console.log('Upload success!');
},
// signing events
onSigningProgress: ({ totalBytes, processedBytes }) => {
console.log('Signing progress:', { totalBytes, processedBytes });
},
onSigningError: (error) => {
console.log('Signing error:', { error });
},
onSigningSuccess: () => {
console.log('Signing success!');
},
},
});The SDK uses winston for logging. You can set the log level using the setLogLevel method.
TurboFactory.setLogLevel('debug');Global installation:
npm install -g @ardrive/turbo-sdkor
yarn global add @ardrive/turbo-sdkor install locally as a dev dependency:
npm install --save-dev @ardrive/turbo-sdkor
yarn add -D @ardrive/turbo-sdkturbo --helpor from local installation:
yarn turbo --helpnpx turbo --helpGlobal options:
-V, --version- output the version number-h, --help- display help for command--dev- Enable development endpoints (default: false)-g, --gateway <url>- Set a custom crypto gateway URL--upload-url <url>- Set a custom upload service URL--payment-url <url>- Set a custom payment service URL--cu-url <url>- Set a custom AO compute unit URL--process-id <id>- Set a custom target process ID for AO action-t, --token <token>- Token type for the command or connected wallet (default: "arweave")
Wallet options:
-w, --wallet-file <filePath>- Wallet file to use with the action. Formats accepted: JWK.json, KYVE, ETH, or POL private key as a string, or SOL Secret Key as a Uint8Array-m, --mnemonic <phrase>- Mnemonic to use with the action (KYVE only)-p, --private-key <key>- Private key to use with the action
Upload options:
--paid-by <paidBy...>- A list of native addresses to pay for the upload.--ignore-approvals- When no paid by is provided, the CLI will look for and use any received credit share approvals to pay for the upload. This flag will ignore any approvals and only use the connected wallet's balance for upload payment. Default: false--use-signer-balance-first- Use the connected wallet's balance before using any credit share approvals for the upload. Default: false
Get the balance of a connected wallet or native address in Turbo Credits.
Command Options:
-a, --address <nativeAddress>- Native address to get the balance of
e.g:
turbo balance --address 'crypto-wallet-public-native-address' --token solanaturbo balance --wallet-file '../path/to/my/wallet.json' --token arweaveGet the remaining free-tier upload allowance (in bytes) for a connected wallet or native address. Prints unlimited for an exempt/partner wallet.
Command Options:
-a, --address <nativeAddress>- Native address to check the free-tier allowance of
e.g:
turbo free-status --address 'crypto-wallet-public-native-address' --token arweaveturbo free-status --wallet-file '../path/to/my/wallet.json' --token arweaveGet the signing wallet's own top-up (payment) history — both crypto and fiat top-ups, newest first. Requires a wallet (it is signature-scoped to that wallet). Prints a JSON page of { payments, hasMore, cursor }; when hasMore is true, pass the printed cursor back with --cursor to fetch the next page.
Command Options:
--limit <limit>- Max number of rows to return (1-100, default 50)--cursor <cursor>- Opaque pagination cursor from a prior response
e.g:
turbo payment-history --wallet-file '../path/to/my/wallet.json' --token arweave --limit 25# Fetch the next page using the cursor printed by the previous call
turbo payment-history --wallet-file '../path/to/my/wallet.json' --token arweave --cursor '<cursor-from-previous-page>'Top up a connected wallet or native address with Turbo Credits using a supported fiat currency. This command will create a Stripe checkout session for the top-up amount and open the URL in the default browser.
Command Options:
-a, --address <nativeAddress>- Native address to top up-c, --currency <currency>- Currency to top up with-v, --value <value>- Value of fiat currency for top up. e.g: 10.50 for $10.50 USD
e.g:
# Open Stripe hosted checkout session in browser to top up for 10.00 USD worth of Turbo Credits
turbo top-up --address 'crypto-wallet-public-native-address' --token ethereum --currency USD --value 10Fund a wallet with Turbo Credits by submitting a payment transaction for the crypto amount to the Turbo wallet and then submitting that transaction id to Turbo Payment Service for top up processing. Alternatively, submit a transaction ID of an existing funding transaction to Turbo Payment Service for top up processing.
Command Options:
-v, --value <value>- Value of crypto token for fund. e.g: 0.0001 for 0.0001 KYVE-i, --tx-id <txId>- Transaction ID of an existing funding transaction-a, --address <nativeAddress>- Optional native address to send the Turbo credits to
e.g:
# Fund any valid destination wallet with 10 USDC worth of Turbo Credits on Base Network
turbo crypto-fund --value 10 --token base-usdc --private-key '0xabc...123' --address 'any-valid-evm-sol-ar-kyve-native-address'turbo crypto-fund --value 0.0001 --token kyve --private-key 'b27...45c'turbo crypto-fund --tx-id 'my-valid-arweave-fund-transaction-id' --token arweaveturbo crypto-fund --value 100 --token ario --wallet-file ../path/to/arweave/wallet/with/ario.json# Use a custom AO process ID and compute unit:
turbo crypto-fund --value 100 --token ario --process-id agYcCFJtrMG6cqMuZfskIkFTGvUPddICmtQSBIoPdiA --cu-url https://cu.ao-testnet.xyz# Send to custom destination address
turbo crypto-fund --value 100 --token ario --wallet-file ../path/to/arweave/wallet/with/ario.json --address 'Any-Valid-AR-EVM-SOL-KYVE-Native-Address'Upload a folder of files and create and upload a manifest file for the folder upload to the Turbo Upload Service.
Command Options:
-f, --folder-path <folderPath>- Path to the folder to upload--index-file <indexFile>- File to use for the "index" path in the resulting manifest--fallback-file <fallbackFile>- File to use for the "fallback" path in the resulting manifest--no-manifest- Disable manifest creation--max-concurrency <maxConcurrency>- Maximum number of concurrent uploads--on-demand- Enable on-demand top up if the connected wallet does not have enough credits to complete the upload (only available for $ARIO, $SOL, and $ETH on Base Network token types)--max-crypto-top-up-value <maxCryptoTopUpValue>- Maximum value of crypto token for on-demand top up. e.g: 100 for 100 $ARIO. NOTE: This is a value in the token's standard crypto unit, not the smallest unit. e.g: 100 for 100 $ARIO, NOT 100000000 for 100 $ARIO--top-up-buffer-multiplier <topUpBufferMultiplier>- Multiplier to apply to the estimated top-up amount to avoid underpayment during on-demand top-ups due to price fluctuations. Default: 1.1 (10% buffer)
e.g:
turbo upload-folder --folder-path '../path/to/my/folder' --token solana --wallet-file ../path/to/sol/sec/key.jsonUpload a file to the Turbo Upload Service.
Command Options:
-f, --file-path <filePath>- Path to the file to upload--on-demand- Enable on-demand top up if the connected wallet does not have enough credits to complete the upload (only available for $ARIO, $SOL, and $ETH on Base Network token types)--max-crypto-top-up-value <maxCryptoTopUpValue>- Maximum value of crypto token for on-demand top up. e.g: 100 for 100 $ARIO. NOTE: This is a value in the token's standard crypto unit, not the smallest unit. e.g: 100 for 100 $ARIO, NOT 100000000 for 100 $ARIO--top-up-buffer-multiplier <topUpBufferMultiplier>- Multiplier to apply to the estimated top-up amount to avoid underpayment during on-demand top-ups due to price fluctuations. Default: 1.1 (10% buffer)
e.g:
turbo upload-file --file-path '../path/to/my/file.txt' --token ethereum --wallet-file ../path/to/eth/private/key.txt --paid-by '0x...first-payer-address' '0x...second-payer-address' '0x...third-payer-address' 'etc...'Get the current credit price estimate from Turbo Payment Service for a given value and price type.
Command Options:
--value <value>- Value to get the price for. e.g: 10.50 for $10.50 USD, 1024 for 1 KiB, 1.1 for 1.1 AR--type <type>- Type of price to get. e.g: 'bytes', 'arweave', 'usd', 'kyve'. Default: 'bytes'--currency <currency>- Currency unit of the reported price (e.g: 'usd', 'eur', 'gbp').
e.g:
turbo price --value 10.50 --type usdturbo price --value 1024 --type bytesturbo price --value 1.1 --type arweaveGet the current fiat estimation from the Turbo Payment Service, denominated in the specified fiat currency, for uploading a specified number of bytes to Turbo.
Command Options:
--byte-count <byteCount>- Byte count of data to get the fiat estimate for--currency <currency>- Currency unit of the reported price (e.g: 'usd', 'eur', 'gbp')
e.g:
turbo fiat-estimate --byte-count 102400 --currency usdGet the current price from the Turbo Payment Service, denominated in the specified token, for uploading a specified number of bytes to Turbo.
Command Options:
--byte-count <byteCount>- Byte count of data to get the token price for
e.g:
turbo token-price --byte-count 102400 --token solanaShares credits from the connected wallet to the provided native address and approved winc amount.
Command Options:
-a, --address <nativeAddress>- Native address to that will receive the Credits-v, --value <value>- Value of winc to share to the target address-e, --expires-by-seconds <seconds>- Expiry time in seconds for the credit share approval
e.g:
turbo share-credits --address 2cor...VUa --value 0.083155650320 --wallet-file ../path/to/my/wallet --expires-by-seconds 3600Revoke all credits shared from the connected wallet to the provided native address.
Command Options:
-a, --address <nativeAddress>- Native address to revoke credit share approvals for
e.g:
turbo revoke-credits --wallet-file ../path/to/my/walletList all given and received credit share approvals from the connected wallet or the provided native address.
Command Options:
-a, --address <nativeAddress>- Native address to list credit share approvals for
e.g:
turbo list-shares --address 2cor...VUa --wallet-file ../path/to/my/walletBuy and manage ArNS names by paying with Turbo Credits. Purchases resolve on-chain asynchronously: buy/extend/upgrade commands return a nonce you can poll with arns-action-status. (arns-purchase-status reads a separate namespace, the one a fiat quote lands in.)
All ArNS commands accept the global --payment-url <url> option to target a specific bundler/payment service (e.g. a local or devnet bundler at http://localhost:4001), and --token <token> (e.g. arweave, solana, ethereum) to select the wallet/identity type. Every write command requires a wallet (--wallet-file, --private-key, or --mnemonic) to pay; the ANT-scoped ones (transfer-arns-ant, set-arns-record, remove-arns-record, set-arns-record-metadata, remove-arns-record-metadata, transfer-arns-record, add-arns-controller, remove-arns-controller) also require --owner-key for the owner proof. The read-only commands (arns-price, arns-action-price, arns-purchase-status, arns-fiat-quote) need neither. arns-action-status reads nothing on-chain either, but takes a wallet because getArNSActionStatus lives on the authenticated client.
When a purchase is rejected for lack of Turbo Credits (HTTP 402), the command prints a clear "insufficient credits — top up your balance and retry" message and exits non-zero.
Every write command debits credits now — see the pricing table for what each one charges. Preview the eight non-purchase commands' cost with arns-action-price before running them.
Quote an ArNS purchase paid by credit card (Stripe) instead of Turbo Credits. Records a quote and returns a Stripe session to complete elsewhere — nothing is charged by this command.
turbo arns-fiat-quote --name my-name --type lease --years 1 \
--address <destination-address> --currency usd# hosted Stripe Checkout, with promo codes
turbo arns-fiat-quote --name my-name --type permabuy \
--address <destination-address> --currency eur \
--method checkout-session --promo-code LAUNCH FRIENDSPrints the quote's nonce (poll it with arns-purchase-status), the Stripe paymentSessionId, and whichever of clientSecret / checkoutUrl applies to the chosen --method. Exits non-zero with a clear message when the payment service has fiat disabled.
Get the Turbo Credit price (in winc + mARIO, plus the equivalent Credits) to buy, extend, increase undernames on, or upgrade an ArNS name. The intent is inferred from the flags you pass:
--type <lease|permabuy>→ Buy-Name (a lease also needs--years)--increase-qty <qty>→ Increase-Undername-Limit--years <years>(without--type) → Extend-Lease- only
--name→ Upgrade-Name
Command Options:
--name <name>- ArNS name to price--type <lease|permabuy>- Purchase type for a Buy-Name price--years <years>- Lease duration in years (Buy-Name lease / Extend-Lease)--increase-qty <qty>- Number of additional undernames to price
e.g:
# Price a 1-year lease against a local bundler
turbo arns-price --name my-name --type lease --years 1 --payment-url http://localhost:4001# Price a permabuy
turbo arns-price --name my-name --type permabuy# Price extending an existing lease by 2 years
turbo arns-price --name my-name --years 2Buy an ArNS name (lease or permabuy) paying with Turbo Credits. Prints the purchase receipt and a nonce to track the on-chain write.
Command Options:
--name <name>- ArNS name to buy--type <lease|permabuy>- Purchase type--years <years>- Lease duration in years (required forlease)--owner-key <base58SolanaSecretKey>- Solana secret key that will OWN the ANT and signs for it. Separate from the wallet paying in Turbo Credits; it needs a key to sign with, not SOL.--paid-by <paidBy...>- Optional delegated payer address(es) whose credits cover the purchase
e.g:
# Lease for 1 year. The ANT is minted to --owner-key, which needs NO SOL:
# Turbo pays every fee and rent. The paying wallet is separate.
turbo buy-arns-name --name my-name --type lease --years 1 \
--owner-key <base58SolanaSecretKey> \
--wallet-file ../path/to/my/wallet.json --payment-url http://localhost:4001# Permabuy, paying with a Solana wallet. Payer and ANT owner may still differ.
turbo buy-arns-name --name my-name --type permabuy \
--owner-key <base58SolanaSecretKey> \
--token solana --wallet-file ../path/to/sol/secret-key.jsonExtend an existing ArNS name lease with Turbo Credits.
Command Options:
--name <name>- ArNS name whose lease to extend--years <years>- Number of years to extend by--paid-by <paidBy...>- Optional delegated payer address(es)
e.g:
turbo extend-arns-lease --name my-name --years 2 --wallet-file ../path/to/my/wallet.jsonIncrease the undername limit of an ArNS name with Turbo Credits.
Command Options:
--name <name>- ArNS name to modify--increase-qty <qty>- Number of additional undernames--paid-by <paidBy...>- Optional delegated payer address(es)
e.g:
turbo increase-arns-undernames --name my-name --increase-qty 10 --wallet-file ../path/to/my/wallet.jsonUpgrade an ArNS leased name to a permanent (permabuy) name with Turbo Credits.
Command Options:
--name <name>- ArNS name to upgrade--paid-by <paidBy...>- Optional delegated payer address(es)
e.g:
turbo upgrade-arns-name --name my-name --wallet-file ../path/to/my/wallet.jsonGet the status of an ArNS purchase by its nonce (returned by the buy/extend/upgrade commands). The response includes a state of pending, success, or failed.
Command Options:
--nonce <nonce>- The purchase nonce to look up
e.g:
turbo arns-purchase-status --nonce 3f8c...e21 --payment-url http://localhost:4001Status of a credit-paid ArNS action by its nonce: the four purchase actions and the eight non-purchase ones. This is the command the buy/extend/upgrade output points at.
arns-purchase-status is a different namespace (/arns/purchase/), which is where a fiat quote's nonce lands. Passing an action nonce to it returns "Purchase status not found".
Command Options:
--nonce <nonce>- ArNS action nonce to look up
e.g:
turbo arns-action-status --nonce 3f8c...e21 \
--wallet-file ../path/to/my/wallet.jsonSelf-custody exit: transfer a Turbo-custodied ANT to a Solana public key you control. Authenticated with an action-bound, single-use signature.
Command Options:
--owner-key <base58SolanaSecretKey>- Solana secret key that OWNS the ANT and signs for it. Separate from the wallet paying in Turbo Credits; it needs a key to sign with, not SOL.--ant-id <antId>- ANT (Metaplex Core asset) ID to transfer--target <address>- Target Solana pubkey to transfer the ANT to
e.g:
turbo transfer-arns-ant --ant-id ant-123 --target 7xKX...gAsU \
--owner-key <base58SolanaSecretKey> --wallet-file ../path/to/my/wallet.jsonSet a resolution record on a Turbo-custodied ANT.
Command Options:
--owner-key <base58SolanaSecretKey>- Solana secret key that OWNS the ANT and signs for it. Separate from the wallet paying in Turbo Credits; it needs a key to sign with, not SOL.--ant-id <antId>- ANT ID to set a record on--undername <undername>- Undername record to set (defaults to@, the apex record)--transaction-id <transactionId>- Arweave transaction ID the record resolves to--ttl-seconds <ttlSeconds>- TTL in seconds for the record
e.g:
turbo set-arns-record --ant-id ant-123 --undername docs \
--transaction-id A1b2...Xyz --ttl-seconds 900 \
--owner-key <base58SolanaSecretKey> --wallet-file ../path/to/my/wallet.jsonRemove a resolution record (undername) from a Turbo-custodied ANT.
Command Options:
--owner-key <base58SolanaSecretKey>- Solana secret key that OWNS the ANT and signs for it. Separate from the wallet paying in Turbo Credits; it needs a key to sign with, not SOL.--ant-id <antId>- ANT ID to remove a record from--undername <undername>- Undername record to remove
e.g:
turbo remove-arns-record --ant-id ant-123 --undername docs \
--owner-key <base58SolanaSecretKey> --wallet-file ../path/to/my/wallet.jsonSet a record's display name, logo, description, or keywords on a Turbo-custodied ANT. This is RECORD-level metadata — distinct from the ANT's own name/ticker/description/keywords/logo, which is not sponsored. Fields are tri-state: pass --display-name/--record-logo/--record-description/--record-keywords to set a field, --clear-* to explicitly clear it, or omit both to leave it unchanged.
Command Options:
--owner-key <base58SolanaSecretKey>- Solana secret key that OWNS the ANT and signs for it. Separate from the wallet paying in Turbo Credits; it needs a key to sign with, not SOL.--ant-id <antId>- ANT ID to set record metadata on--undername <undername>- Undername record to set metadata on (defaults to@, the apex record)--display-name <displayName>/--clear-display-name--record-logo <transactionId>/--clear-record-logo--record-description <description>/--clear-record-description--record-keywords <keywords...>/--clear-record-keywords
e.g:
turbo set-arns-record-metadata --ant-id ant-123 --undername docs \
--display-name "My Docs" --record-keywords arweave permaweb \
--owner-key <base58SolanaSecretKey> --wallet-file ../path/to/my/wallet.json# Clear the description, leave everything else unchanged
turbo set-arns-record-metadata --ant-id ant-123 --undername docs \
--clear-record-description \
--owner-key <base58SolanaSecretKey> --wallet-file ../path/to/my/wallet.jsonClear all of a record's metadata on a Turbo-custodied ANT.
Command Options:
--owner-key <base58SolanaSecretKey>- Solana secret key that OWNS the ANT and signs for it. Separate from the wallet paying in Turbo Credits; it needs a key to sign with, not SOL.--ant-id <antId>- ANT ID to remove record metadata from--undername <undername>- Undername record whose metadata to clear
e.g:
turbo remove-arns-record-metadata --ant-id ant-123 --undername docs \
--owner-key <base58SolanaSecretKey> --wallet-file ../path/to/my/wallet.jsonHand ONE record to another address — distinct from transfer-arns-ant, which hands over the whole ANT and every record on it.
Command Options:
--owner-key <base58SolanaSecretKey>- Solana secret key that OWNS the ANT and signs for it. Separate from the wallet paying in Turbo Credits; it needs a key to sign with, not SOL.--ant-id <antId>- ANT ID whose record to transfer--undername <undername>- Undername record to transfer--target <address>- Target Solana pubkey to transfer the record to
e.g:
turbo transfer-arns-record --ant-id ant-123 --undername docs --target 7xKX...gAsU \
--owner-key <base58SolanaSecretKey> --wallet-file ../path/to/my/wallet.jsonGrant controller rights on a Turbo-custodied ANT. Owner-signed — changing an ANT's access control is an owner-only instruction. Not needed after a fresh buy-arns-name: the grant already rides in that same signed transaction. Use this to re-grant after a revoke, or to add a different address as controller.
Command Options:
--owner-key <base58SolanaSecretKey>- Solana secret key that OWNS the ANT and signs for it. Separate from the wallet paying in Turbo Credits; it needs a key to sign with, not SOL.--ant-id <antId>- ANT ID to add a controller to--target <address>- Solana pubkey to grant controller rights to (omit for Turbo itself, which is what makesset-arns-recorda single call)
e.g:
turbo add-arns-controller --ant-id ant-123 \
--owner-key <base58SolanaSecretKey> --wallet-file ../path/to/my/wallet.jsonRevoke controller rights on a Turbo-custodied ANT — the escape hatch that keeps "Turbo is not a custodian" honest. Always available, but not free of credits: after revoking, set-arns-record keeps working, it just starts requiring the owner's signature.
Command Options:
--owner-key <base58SolanaSecretKey>- Solana secret key that OWNS the ANT and signs for it. Separate from the wallet paying in Turbo Credits; it needs a key to sign with, not SOL.--ant-id <antId>- ANT ID to remove a controller from--target <address>- Solana pubkey to revoke (omit to revoke Turbo)
e.g:
turbo remove-arns-controller --ant-id ant-123 \
--owner-key <base58SolanaSecretKey> --wallet-file ../path/to/my/wallet.jsonPreview the Turbo Credit price of one of the eight non-purchase actions, without creating it. Rejects the four ARIO-purchase actions (buy-name, extend-lease, upgrade-name, increase-undername-limit) — use arns-price for those instead, since their cost is dominated by the ARIO purchase, not this flat/derived margin. Needs no wallet.
Command Options:
--action <action>- One ofset-record,remove-record,set-record-metadata,remove-record-metadata,transfer-record,add-controller,remove-controller,transfer
e.g:
turbo arns-action-price --action remove-controller --payment-url http://localhost:4001Users can share their purchased Credits with other users' wallets by creating Credit Share Approvals. These approvals are created by uploading a signed data item with tags indicating the recipient's wallet address, the amount of Credits to share, and an optional amount of seconds that the approval will expire in. The recipient can then use the shared Credits to pay for their own uploads to Turbo.
Shared Credits cannot be re-shared by the recipient to other recipients. Only the original owner of the Credits can share or revoke Credit Share Approvals. Credits that are shared to other wallets may not be used by the original owner of the Credits for sharing or uploading unless the Credit Share Approval is revoked or expired.
Approvals can be revoked at any time by similarly uploading a signed data item with tags indicating the recipient's wallet address. This will remove all approvals and prevent the recipient from using the shared Credits. All unused Credits from expired or revoked approvals are returned to the original owner of the Credits.
To use the shared Credits, recipient users must provide the wallet address of the user who shared the Credits with them in the x-paid-by HTTP header when uploading data. This tells Turbo services to look for and use Credit Share Approvals to pay for the upload before using the signer's balance.
For user convenience, during upload the Turbo CLI will use any available Credit Share Approvals found for the connected wallet before using the signing wallet's balance. To instead ignore all Credit shares and only use the signer's balance, use the --ignore-approvals flag. To use the signer's balance first before using Credit shares, use the --use-signer-balance-first flag. In contrast, the Turbo SDK layer does not provide this functionality and will only use approvals when paidBy is provided.
The Turbo SDK provides the following methods to manage Credit Share Approvals:
shareCredits: Creates a Credit Share Approval for the specified wallet address and amount of Credits.revokeCredits: Revokes all Credit Share Approvals for the specified wallet address.listShares: Lists all Credit Share Approvals for the specified wallet address or connected wallet.dataItemOpts: { ...opts, paidBy: string[] }: Upload methods now accept 'paidBy', an array of wallet addresses that have provided credit share approvals to the user from which to pay, in the order provided and as necessary, for the upload.
The Turbo CLI provides the following commands to manage Credit Share Approvals:
share-credits: Creates a Credit Share Approval for the specified wallet address and amount of Credits.revoke-credits: Revokes all Credit Share Approvals for the specified wallet address.list-shares: Lists all Credit Share Approvals for the specified wallet address or connected wallet.paidBy: --paid-by <paidBy...>: Upload commands now accept '--paid-by', an array of wallet addresses that have provided credit share approvals to the user from which to pay, in the order provided and as necessary, for the upload.--ignore-approvals: Ignore all Credit Share Approvals and only use the signer's balance.--use-signer-balance-first: Use the signer's balance first before using Credit Share Approvals.
nvmnode(>= 18)yarn
yarn install- installs dependenciesyarn build- builds web/node/bundled outputs
yarn test- runs integration tests using the configured environment (localhost by default); setPAYMENT_SERVICE_URLandUPLOAD_SERVICE_URLto target the ar.io Testnet Sandboxyarn test:docker- runs integration tests against locally running docker containers (recommended)yarn example:web- opens up the example web pageyarn example:cjs- runs example CJS node scriptyarn example:esm- runs example ESM node script
yarn lint:check- checks for linting errorsyarn lint:fix- fixes linting errorsyarn format:check- checks for formatting errorsyarn format:fix- fixes formatting errors
- Code to interfaces.
- Prefer type safety over runtime safety.
- Prefer composition over inheritance.
- Prefer integration tests over unit tests.
For more information on how to contribute, please see CONTRIBUTING.md.