# TurboAuthenticatedClient (/(apis)/turboauthenticatedclient) #### getBalance() Issues a signed request to get the credit balance of a wallet measured in AR (measured in Winston Credits, or winc). ```typescript const { winc: balance } = await turbo.getBalance(); ``` #### getFreeStatus() 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. ```typescript const { bytesRemaining } = await turbo.getFreeStatus(); ``` It is also available on the `TurboUnauthenticatedClient` for any wallet by address: ```typescript const { bytesRemaining } = await turbo.getFreeStatus('a-native-address'); ``` #### getPaymentHistory() 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`. ```typescript 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 }); } ``` #### signer.getNativeAddress() Returns the [native address][docs/native-address] of the connected signer. ```typescript const address = await turbo.signer.getNativeAddress(); ``` #### getWincForFiat() Returns the current amount of Winston Credits including all adjustments for the provided fiat currency, amount, and optional promo codes. ```typescript const { winc, paymentAmount, quotedPaymentAmount, adjustments } = await turbo.getWincForFiat({ amount: USD(100), promoCodes: ['MY_PROMO_CODE'], // promo codes require an authenticated client }); ``` #### createCheckoutSession() 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. ```typescript 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'); ``` #### upload() 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. ```typescript 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 }, }); ``` #### uploadFile() Signs and uploads a raw file. There are two ways to provide the file to the SDK: 1. Using a `file` parameter 2. Using a `fileStreamFactory` and `fileSizeFactory` ##### Using file` In Web with a file input: ```typescript 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: ```typescript 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' }], }, }); ``` ##### Using fileStreamFactory` and `fileSizeFactory` Note: The provided `fileStreamFactory` should produce a NEW file data stream each time it is invoked. The SDK calls it again for every retry, and a stream can be read only once. In the browser, pass `() => file.stream()` rather than a stream you created ahead of time; a factory that returns one stream instance fails on the first retry. 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. ```typescript 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, }); ``` ##### Customize Multi-Part Upload Behavior 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. ```typescript // Customize chunking behavior await turbo.upload({ ...params, chunkByteCount: 1024 * 1024 * 500, // Max chunk size maxChunkConcurrency: 1, // Minimize concurrency }); ``` ```typescript // Disable chunking behavior await turbo.upload({ ...params, chunkingMode: 'disabled', }); ``` ```typescript // Force chunking behavior await turbo.upload({ ...params, chunkingMode: 'force', }); ``` #### On Demand Uploads 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`), $USDC on Base Network (`base-usdc`) and $USDC on Solana (`solana-usdc`) token types. ```typescript const turbo = TurboFactory.authenticated({ privateKey: bs58.encode(secretKey), // a Solana key holding $ARIO token: 'ario', }); await turbo.upload({ ...params, fundingMode: new OnDemandFunding({ maxTokenAmount: ARIOToTokenAmount(500), // Max 500 $ARIO topUpBufferMultiplier: 1.1, // 10% buffer to avoid underpayment }), }); ``` #### x402 Uploads 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. > **Note:** x402 uploads need the optional peer dependency `x402-fetch`, which keeps its dependency tree out of installs that pay with credits: > > ```shell > npm install x402-fetch > ``` > > Without it, credit-paid uploads and the x402 price routes work as usual, and an x402 upload fails before signing with a message naming this install. Browser consumers of the prebuilt bundle resolve `x402-fetch` themselves, through their bundler or an import map, because the bundle leaves it external. ```typescript 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 }); ``` Large items are uploaded in chunks and paid for when the upload is created, so the payload is never sent just to discover its price. Smaller items go in a single request, which is buffered in memory so its length can be declared — the service prices an x402 upload from `Content-Length`, and a streamed body has none. The service URL must be HTTPS: an x402 authorization is a bearer credential, so the SDK refuses to send one over cleartext. Loopback is the exception — `localhost`, `127.0.0.1` and `::1` are allowed over plain HTTP, so local development against a bundler on your own machine still works. #### Pricing an x402 Upload Before Sending It `getX402PriceForDataItem` prices a signed data item from its byte count, so you can learn the cost without transmitting the payload. Without it the only way to get a price is to POST the data and read the 402 challenge. ```typescript const turbo = TurboFactory.unauthenticated({ token: 'base-usdc' }); const quote = await turbo.getX402PriceForDataItem({ byteCount: signedDataItemByteCount, // the SIGNED item, not the payload inside it }); console.log(quote.usdcAmount); // amount to pay, in USDC's smallest unit ``` `getX402PriceForRawData` prices raw data that Turbo will wrap into a data item itself, and reports the wrapping overhead — a data item is larger than its payload by its header, signature and tags, which a caller cannot compute. ```typescript const quote = await turbo.getX402PriceForRawData({ byteCount: myRawData.byteLength, tagCount: 3, // tags you intend to attach; they change the overhead contentType: 'image/png', }); console.log(quote.overhead, quote.estimatedDataItemSize); ``` Both take an optional `network`, defaulting to `base`. **This is the x402 network, not the SDK token type**: the route builds its token as `usdc-{network}`, so `base-usdc` is accepted on mainnet only because the network there is literally `base`. Against a testnet service, pass `network: 'base-sepolia'`. #### Raw x402 Data Uploads 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. ```typescript 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 105 KiB, this method of upload currently does not require a signature and can be used with an unauthenticated client. ```ts // Unsigned free upload of raw data under 105 KiB const turbo = TurboFactory.unauthenticated({ token: 'base-usdc' }); await turbo.uploadRawX402Data({ data: myRawData, }); ``` #### uploadFolder() 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](#incremental-folder-uploads) that skips files already on Arweave. The manifestDataItemOpts` is an optional object that configures the manifest data item only, and defaults to `dataItemOpts`. ##### NodeJS Upload Folder ```typescript 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, }, }); ``` ##### Browser Upload Folder ```html 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); }); ``` ##### Upload Folder with Progress Events 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. ```typescript 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!'); }, }, }); ``` ##### Incremental Folder Uploads A runnable version of everything below is in [`examples/folder-index`](https://github.com/ardriveapp/turbo-sdk/blob/alpha/examples/folder-index/index.mjs): it deploys the same folder three times and prints what each run uploaded and reused. 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. ```typescript 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, ... } ``` ###### What a reused file is matched on An index key is `\.\`, 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 trade-off this buys, and how you find out 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. ###### Index layers | 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. ###### Telling a gateway whose uploads to sweep `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.) ###### Trust model 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. ###### When the sweep runs out of pages 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. ###### In the browser `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. ###### Known limitation 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. #### topUpWithTokens() 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 `tokenAmount` is 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. ##### Arweave (AR) Crypto Top Up ```typescript 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 native destination address }); ``` ##### AR.IO Network (ARIO) Crypto Top Up $ARIO is an SPL token on Solana, so pay with a Solana key. Without a `turboCreditDestinationAddress`, the credits go to the account of that key's base58 public key: the same account `getBalance()` reads and uploads signed by that key pay from. ```typescript const turbo = TurboFactory.authenticated({ privateKey: bs58.encode(secretKey), token: 'ario', }); const { winc, status, id, ...fundResult } = await turbo.topUpWithTokens({ tokenAmount: ARIOToTokenAmount(100), // 100 $ARIO }); ``` ##### USDC Crypto Top Up ```typescript // 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 }); // USDC (SPL) on Solana — signed with a SOLANA wallet, not an EVM one. The // payment is an SPL transfer into Turbo's associated token account, so the // payer just needs USDC plus a little SOL for fees. const { winc, status, id, ...fundResult } = await TurboFactory.authenticated({ privateKey: bs58.encode(secretKey), token: 'solana-usdc', }).topUpWithTokens({ tokenAmount: USDCToTokenAmount(1), // 1 USDC }); ``` The mint for `solana-usdc` is chosen from the RPC you point at: a `gatewayUrl` whose host contains `devnet` uses Circle's devnet USDC, otherwise mainnet USDC. That is a heuristic — if you use a **private or paid devnet RPC** whose hostname does not say `devnet`, name the mint explicitly, or you will sign a transfer of the wrong mint: ```typescript const gatewayUrl = 'https://my-rpc.example.com/\'; const turbo = TurboFactory.authenticated({ privateKey: bs58.encode(secretKey), token: 'solana-usdc', gatewayUrl, tokenTools: new SolanaUsdcToken({ gatewayUrl, mintAddress: '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU', // devnet USDC }), }); ``` ##### Ethereum (ETH) Crypto Top Up ```typescript const turbo = TurboFactory.authenticated({ signer, token: 'ethereum' }); const { winc, status, id, ...fundResult } = await turbo.topUpWithTokens({ tokenAmount: ETHToTokenAmount(0.00001), // 0.00001 ETH }); ``` ##### Polygon (POL / MATIC) Crypto Top Up ```typescript const turbo = TurboFactory.authenticated({ signer, token: 'pol' }); const { winc, status, id, ...fundResult } = await turbo.topUpWithTokens({ tokenAmount: POLToTokenAmount(0.00001), // 0.00001 POL }); ``` ##### Eth on Base Network Crypto Top Up ```typescript 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 }); ``` ##### Solana (SOL) Crypto Top Up ```typescript const turbo = TurboFactory.authenticated({ signer, token: 'solana' }); const { winc, status, id, ...fundResult } = await turbo.topUpWithTokens({ tokenAmount: SOLToTokenAmount(0.00001), // 0.00001 SOL }); ``` #### shareCredits() 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 ```typescript 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 }); ``` #### revokeCredits() Revokes all credits shared from the connected wallet to the provided native address. ```typescript const revokedApprovals = await turbo.revokeCredits({ revokedAddress: '2cor...VUa', }); ``` #### getCreditShareApprovals() Returns all given or received credit share approvals for the connected wallet or the provided native address. ```typescript const { givenApprovals, receivedApprovals } = await turbo.getCreditShareApprovals({ userAddress: '2cor...VUa', }); ``` # TurboFactory (/(apis)/turbofactory) #### unauthenticated() Creates an instance of a client that accesses Turbo's unauthenticated services. ```typescript const turbo = TurboFactory.unauthenticated(); ``` #### authenticated() 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. ```typescript const signer = new ArweaveSigner(jwk); const turbo = TurboFactory.authenticated({ signer }); ``` #### Testnet Configuration 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. ```typescript // 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](https://docs.ar.io/build/testnet). **Supported Testnets**: - **ARIO staging** (`ario`) - Staging ARIO on Solana devnet; fee-free funding, claim from the [ar.io faucet](https://faucet.services.ar-io.dev) - **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 # TurboUnauthenticatedClient (/(apis)/turbounauthenticatedclient) #### getSupportedCurrencies() 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). ```typescript const currencies = await turbo.getSupportedCurrencies(); ``` #### getSupportedCountries() Returns the list of countries supported by the Turbo Payment Service's top up workflow. ```typescript const countries = await turbo.getSupportedCountries(); ``` #### getFiatToAR() Returns the current raw fiat to AR conversion rate for a specific currency as reported by third-party pricing oracles. ```typescript const fiatToAR = await turbo.getFiatToAR({ currency: 'USD' }); ``` #### getFiatRates() Returns the current fiat rates for 1 GiB of data for supported currencies, including all top-up adjustments and fees. ```typescript const rates = await turbo.getFiatRates(); ``` #### getWincForFiat() Returns the current amount of Winston Credits including all adjustments for the provided fiat currency. ```typescript const { winc, actualPaymentAmount, quotedPaymentAmount, adjustments } = await turbo.getWincForFiat({ amount: USD(100), }); ``` #### getWincForToken() Returns the current amount of Winston Credits including all adjustments for the provided token amount. ```typescript const { winc, actualTokenAmount, equivalentWincTokenAmount } = await turbo.getWincForToken({ tokenAmount: WinstonToTokenAmount(100_000_000), }); ``` #### getFiatEstimateForBytes() Get the current price from the Turbo Payment Service, denominated in the specified fiat currency, for uploading a specified number of bytes to Turbo. ```typescript 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 GiB ``` **Output:** ```json { "byteCount": 1073741824, "amount": 20.58, "currency": "usd", "winc": "2402378997310" } ``` #### getTokenPriceForBytes() Get the current price from the Turbo Payment Service, denominated in the specified token, for uploading a specified number of bytes to Turbo. ```typescript const turbo = TurboFactory.unauthenticated({ token: 'solana' }); const { tokenPrice } = await turbo.getTokenPriceForBytes({ byteCount: 1024 * 1024 * 100, }); console.log(tokenPrice); // Estimated SOL Price for 100 MiB ``` #### getUploadCosts() Returns the estimated cost in Winston Credits for the provided file sizes, including all upload adjustments and fees. ```typescript const [uploadCostForFile] = await turbo.getUploadCosts({ bytes: [1024] }); const { winc, adjustments } = uploadCostForFile; ``` #### uploadSignedDataItem() 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). ```typescript 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!'); }, }, }); ``` #### createCheckoutSession() 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]. ##### Arweave (AR) Fiat Top Up ```typescript 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'); ``` ##### Ethereum (ETH) Fiat Top Up ```typescript const turbo = TurboFactory.unauthenticated({ token: 'ethereum' }); const { url, winc, paymentAmount } = await turbo.createCheckoutSession({ amount: USD(10.0), // $10.00 USD owner: publicEthereumAddress, }); ``` ##### Solana (SOL) Fiat Top Up ```typescript const turbo = TurboFactory.unauthenticated({ token: 'solana' }); const { url, winc, paymentAmount } = await turbo.createCheckoutSession({ amount: USD(10.0), // $10.00 USD owner: publicSolanaAddress, }); ``` ##### Polygon (POL / MATIC) Fiat Top Up ```typescript const turbo = TurboFactory.unauthenticated({ token: 'pol' }); const { url, winc, paymentAmount } = await turbo.createCheckoutSession({ amount: USD(10.0), // $10.00 USD owner: publicPolygonAddress, }); ``` #### submitFundTransaction() 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. 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 ```typescript const turbo = TurboFactory.unauthenticated(); // defaults to arweave token type const { status, id, ...fundResult } = await turbo.submitFundTransaction({ txId: 'my-valid-arweave-fund-transaction-id', }); ``` # Buying a name with a credit card (fiat / Stripe) (/(arns-names)/buying-a-name-with-a-credit-card-fiat-stripe) `getArNSFiatPurchaseQuote` prices a purchase in fiat and returns a Stripe payment session, so a user can buy a name without holding credits first. ```typescript 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. # Listing a wallet's names (/(arns-names)/listing-a-wallet-s-names) ```typescript const { names } = await turbo.getArNSNames(); // defaults to the signer's address ``` Receipt history, not a live ownership check: a name transferred away still appears. Verify present control on chain using the returned `antId`. # Nonces, retries and refunds (/(arns-names)/nonces-retries-and-refunds) 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 `signArNSAction` on 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. **The owner has about 30 seconds to sign.** Solana accepts the transaction Turbo builds for only about 30 seconds after the action is created, so prompt the owner straight away. `expiresAt` is not that deadline: it is the ~15-minute point at which an uncompleted action is refunded. `signArNSAction` throws `ArNSActionExpiredError` (a `FailedRequestError`, with `nonce`, `status` and `creditsReleased`) when the signature arrived too late: a 409, or a 400 reading `Action \ expired...`. `creditsReleased` is `true` when the service has already returned the credits, and `false` when they are held until the ~15-minute refund. Either way, create a **new** action if the change is still wanted. A 503 `Blockhash not found` is different: the service could not prove the transaction expired. Re-post the **same** signed bytes to `signArNSAction` rather than creating a new action; a resubmission is idempotent. `/sign` needs no payer signature: the owner's signature inside the transaction authorises it, and the credits were debited at create. So `signArNSAction` sends no payer headers by default. Its optional third argument, `headers`, is for a service that requires them. # Not covered — these still cost you SOL (/(arns-names)/not-covered-these-still-cost-you-sol) 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`](https://github.com/ar-io/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. # Pricing — quote the total (/(arns-names)/pricing-quote-the-total) ```typescript 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 surcharge ``` Buying 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. # The twelve sponsored actions (/(arns-names)/the-twelve-sponsored-actions) ```typescript 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 | #### Point the name at your content while you buy it Pass `antState` and the ANT's opening record is written by the `ario_ant::initialize` that runs inside the transaction you already sign — free and atomic. No second action, no second signature, no second debit. Without it a fresh name resolves to the AR.IO logo, which is the on-chain default. ```typescript await turbo.buyArNSName({ name: 'my-name', owner, type: 'permabuy', antState: { transactionId: '\', // the root `@` target targetProtocol: 0, // 0 = Arweave (default), 1 = IPFS CID ticker: 'MYSITE', }, }); ``` `antState` is accepted on `buyArNSName` only — the service rejects it on every other action. Note it is nested: a **top-level** `transactionId` on a buy is a 400 by design, because that spelling means the set-record target and silently accepting it would point the name at the logo while the caller believed otherwise. **Mind the size budget.** The sponsored mint is ONE Solana transaction against the 1232-byte packet limit, shared with Turbo's fee-payer transfer and an `add_controller` grant. At a worst-case 51-character name only ~71 bytes are spare: | Fields | Cost | Fits? | | ------------------------------------------- | --------- | ------ | | `transactionId` + `targetProtocol` | ~1 byte | always | | `ticker` (16) + `logo` (43) | ~65 bytes | yes | | `description` (512), or a full keyword list | — | **no** | The budget is dynamic — a shorter name buys headroom — so this SDK imposes no client-side cap. The server measures the real transaction and returns a 400 naming Solana's 1232-byte limit _before_ you are handed anything to sign, with the credit debit refunded inline. That error is deterministic: do not retry it, and surface the server's message rather than replacing it, because it names which fields to drop. Field limits, all rejected at the service edge before any debit: `description` ≤ 512 characters, `keywords` ≤ 16 entries, and `logo` (plus `transactionId` when `targetProtocol` is 0 or unset) must be 43-character Arweave ids. When `targetProtocol` is 1 the target is an IPFS CID and is not shape-checked as an Arweave id. See [`ARNS_ACTIONS_API.md#buy-name-takes-the-ants-opening-state](https://github.com/ar-io/ar-io-bundler/blob/main/docs/architecture/ARNS_ACTIONS_API.md#buy-name-takes-the-ants-opening-state) in `ar-io/ar-io-bundler` for the measured byte table. 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: ```typescript 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. # The two shapes, if you drive it yourself (/(arns-names)/the-two-shapes-if-you-drive-it-yourself) Every action returns one of two shapes, and **the server picks which**: ```typescript 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 write ``` Branch 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. # Two identities, never conflated (/(arns-names)/two-identities-never-conflated) | | 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. # You need a Solana key, not Solana funds (/(arns-names)/you-need-a-solana-key-not-solana-funds) 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`: ```typescript // 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: ```typescript 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), }; ``` # File Upload Events (/(events)/file-upload-events) 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 process - `onSigningProgress` - emitted when the signing progress changes. - `onSigningError` - emitted when the signing fails. - `onSigningSuccess` - emitted when the signing succeeds - `onUploadProgress` - emitted when the upload progress changes - `onUploadError` - emitted when the upload fails - `onUploadSuccess` - emitted when the upload succeeds ```typescript 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!'); }, }, }); ``` # Folder Upload Events (/(events)/folder-upload-events) 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 files - `onFileProgress` - 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 ID - `onFileError` - emitted when a file upload fails. Includes the file name, file index, total files, and the error - `onFolderProgress` - 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 fails - `onFolderSuccess` - emitted when the folder upload successfully completes (including manifest generation) - this is the last event emitted for the folder upload process ```typescript 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!'); }, }, }); ``` # Arweave (/(signers)/arweave) #### Arweave JWK ```typescript const jwk = await arweave.crypto.generateJWK(); const turbo = TurboFactory.authenticated({ privateKey: jwk }); ``` #### ArweaveSigner ```typescript const signer = new ArweaveSigner(jwk); const turbo = TurboFactory.authenticated({ signer }); ``` #### ArconnectSigner ```typescript const signer = new ArconnectSigner(window.arweaveWallet); const turbo = TurboFactory.authenticated({ signer }); ``` # Base (/(signers)/base) #### Base ETH Private Key ```typescript const turbo = TurboFactory.authenticated({ privateKey: ethHexadecimalPrivateKey, token: 'base-eth', }); ``` #### Base USDC Private Key ```typescript const turbo = TurboFactory.authenticated({ privateKey: ethHexadecimalPrivateKey, token: 'base-usdc', }); ``` # Ethereum (/(signers)/ethereum) #### EthereumSigner ```typescript const signer = new EthereumSigner(privateKey); const turbo = TurboFactory.authenticated({ signer }); ``` #### Ethereum Private Key ```typescript const turbo = TurboFactory.authenticated({ privateKey: ethHexadecimalPrivateKey, token: 'ethereum', }); ``` #### POL (MATIC) Private Key ```typescript const turbo = TurboFactory.authenticated({ privateKey: ethHexadecimalPrivateKey, token: 'pol', }); ``` # Solana (/(signers)/solana) #### HexSolanaSigner ```typescript const signer = new HexSolanaSigner(bs58.encode(secretKey)); const turbo = TurboFactory.authenticated({ signer }); ``` #### Solana Web Wallet Adapter ```typescript const turbo = TurboFactory.authenticated({ walletAdapter: window.solana, token: 'solana', }); ``` #### Solana Secret Key ```typescript const turbo = TurboFactory.authenticated({ privateKey: bs58.encode(secretKey), token: 'solana', }); ``` # Dependency advisories (/dependency-advisories) A clean install reports advisories from transitive dependencies, none from this package's own code. The list moves as those dependencies publish, so run `npm audit` for the current one. Three critical advisories come from `elliptic`, which reaches the tree through `@dha-team/arbundles` and its ethers v5 dependencies, along with a high advisory in `secp256k1`. Both clear with a package manager override, measured as three criticals to zero: ```json { "overrides": { "elliptic": "6.6.1", "secp256k1": "5.0.1" } } ``` Yarn reads the same pinning under `resolutions`. Two notes on what the remaining advisories mean here: - The one `ws` copy inside an advisory range, `7.4.6`, sits under `@ethersproject/providers`. Its WebSocket provider is never instantiated by this SDK. - `npm audit fix --force` offers to downgrade this package to 1.13.0. That is npm giving up inside the version ranges, not a fix. # Turbo SDK (/index) **For AI and LLM users**: Access the complete Turbo SDK documentation in plain text format at{" "} llm.txt for easy consumption by AI agents and language models. See [AI Agents & LLMs](/build/agents) for the full agent toolkit. The Turbo SDK provides a high-level interface for uploading data to Arweave through Turbo's optimized infrastructure. Built with TypeScript, it offers seamless integration with built-in error handling, automatic retries, and transparent pricing. ## Quick Start ### Install the SDK ```npm npm install @ardrive/turbo-sdk ``` ### Use the SDK ```javascript // Create an authenticated client const turbo = TurboFactory.authenticated({ privateKey: yourPrivateKey }); // Upload data with automatic payment const result = await turbo.uploadFile({ fileStreamFactory: () => fs.createReadStream('./my-file.pdf'), fileSizeFactory: () => fs.statSync('./my-file.pdf').size, }); console.log('Upload successful:', result); ``` ### Install the SDK ```npm npm install @ardrive/turbo-sdk ``` ### Install polyfills (required for web environments) Polyfills are required for React web environments due to the use of `crypto`, `buffer` and `process` types in the SDK's dependencies. ```npm npm install --save-dev vite-plugin-node-polyfills ``` ```js // vite.config.js plugins: [ nodePolyfills({ globals: { Buffer: true, global: true, process: true, }, }), ], }); ``` Configure your bundler (Webpack, Vite, Rollup, etc.) to provide polyfills for `crypto`, `process`, and `buffer`. Refer to your bundler's documentation for polyfill configuration. ### Use the SDK ```javascript // Create an authenticated client const turbo = TurboFactory.authenticated({ privateKey: yourPrivateKey }); // Upload data with automatic payment const fileInput = document.querySelector('input[type="file"]'); const file = fileInput.files[0]; const result = await turbo.uploadFile({ fileStreamFactory: () => file.stream(), fileSizeFactory: () => file.size, }); console.log('Upload successful:', result); ``` ```html Turbo SDK Upload Example // Polyfills are included in the minimized web bundle, so not necessary to import directly // Function to handle file upload async function uploadFile() { const fileInput = document.getElementById('fileInput'); const file = fileInput.files[0]; const privateKeyInput = document.getElementById('privateKey'); if (!file) { alert('Please select a file'); return; } if (!privateKeyInput.value) { alert('Please enter your private key'); return; } try { // Show loading state document.getElementById('status').textContent = 'Uploading...'; // Create authenticated client const turbo = TurboFactory.authenticated({ privateKey: privateKeyInput.value }); // Upload file const result = await turbo.uploadFile({ fileStreamFactory: () => file.stream(), fileSizeFactory: () => file.size, }); // Show success document.getElementById('status').innerHTML = ` Upload successful! Transaction ID: ${result.id} Data Item ID: ${result.dataItemId} `; } catch (error) { document.getElementById('status').innerHTML = ` Error: ${error.message} `; } } // Add click handler window.addEventListener('load', () => { document.getElementById('uploadBtn').addEventListener('click', uploadFile); }); Turbo SDK Upload Example Private Key (JWK): Select File: Upload to Arweave ``` ## API Reference & Documentation } title="API Reference" description="Complete API documentation for all Turbo client methods" href="/apis/turbo" /> } title="SDK Details" description="Advanced features, events, logging, and credit sharing" href="/sdks/turbo-sdk" /> ## Core Features } title="Upload Management" description="Authenticated and unauthenticated upload clients with retry logic" href="/sdks/turbo-sdk/turboauthenticatedclient" /> } title="Events & Monitoring" description="Monitor upload progress and handle events in real-time" href="/sdks/turbo-sdk/file-upload-events" /> } title="Credit Sharing" description="Manage shared credit pools for streamlined billing" href="/sdks/turbo-sdk/turbo-credit-sharing" /> } title="Logging & Configuration" description="Configure logging for debugging and monitoring uploads" href="/sdks/turbo-sdk/logging" /> # Logging (/logging) The SDK uses winston for logging. You can set the log level using the `setLogLevel` method. ```typescript TurboFactory.setLogLevel('debug'); ``` # Turbo Credit Sharing (/turbo-credit-sharing) Users 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. - `getCreditShareApprovals`: Lists all Credit Share Approvals given or received by the specified wallet address or the 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 `: 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.