> ## Documentation Index
> Fetch the complete documentation index at: https://docs.renaiss.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# TypeScript SDK

> Use the Renaiss TypeScript SDK to browse gacha machines, prepare wallets, pull gacha, and submit buybacks.

The Renaiss TypeScript SDK gives you typed helpers for public gacha discovery and authenticated gacha flows.

Use a public client for read-only data. Use an authenticated client when a user needs to sign in or read account data. Add an approved [builder API key](/builder-program) when that client will prepare wallets, pull gacha, or accept buyback offers.

<Note>
  The SDK is in alpha. APIs may change before the first stable release.
</Note>

## Quickstart

<Steps>
  <Step title="Install the package">
    Install the client package and `viem` if you want to use the built-in viem signer adapters.

    <CodeGroup>
      ```bash pnpm theme={null}
      pnpm add @renaiss-protocol/client@alpha viem
      ```

      ```bash npm theme={null}
      npm install @renaiss-protocol/client@alpha viem
      ```

      ```bash yarn theme={null}
      yarn add @renaiss-protocol/client@alpha viem
      ```
    </CodeGroup>
  </Step>

  <Step title="Create a public client">
    Create a read-only client for public gacha discovery.

    ```ts theme={null}
    import { createPublicClient } from "@renaiss-protocol/client";

    const publicClient = createPublicClient();
    ```
  </Step>

  <Step title="Fetch gacha machines">
    Fetch a page of active gacha machines.

    ```ts theme={null}
    import {
      GachaMachineStage,
      getError,
      getValue,
      isFailed,
    } from "@renaiss-protocol/client";

    const machinesResult = await publicClient
      .listGachaMachines({
        pageSize: 5,
        stage: GachaMachineStage.Active,
      })
      .firstPage();

    if (isFailed(machinesResult)) {
      throw new Error(getError(machinesResult).detail);
    }

    for (const machine of getValue(machinesResult).items) {
      console.log(machine.slug, machine.name);
    }
    ```
  </Step>

  <Step title="Create an authenticated client">
    Sign in with SIWE, create a user API key, then pass that key and your approved builder API key to `createSecureClient` for write workflows.

    ```ts theme={null}
    import {
      createSecureClient,
      getError,
      getValue,
      isFailed,
    } from "@renaiss-protocol/client";
    import { privateKey } from "@renaiss-protocol/client/viem";

    const signer = privateKey(process.env.PRIVATE_KEY);

    const apiKeyResult = await publicClient.createApiKeyWithSiwe({
      name: "My Renaiss integration",
      prefix: "my-app",
      signer,
    });

    if (isFailed(apiKeyResult)) {
      throw new Error(getError(apiKeyResult).detail);
    }

    const builderApiKey = process.env.RENAISS_BUILDER_API_KEY;

    if (builderApiKey === undefined) {
      throw new Error("Missing RENAISS_BUILDER_API_KEY.");
    }

    const secureClient = createSecureClient({
      apiKey: getValue(apiKeyResult).key,
      builderApiKey,
      signer,
    });
    ```
  </Step>

  <Step title="Prepare the wallet and pull gacha">
    Make the user's Safe wallet ready, then pull from a machine and update your UI through draw resolution. Token release continues asynchronously.

    ```ts theme={null}
    import { GachaQuantity, getError, getValue, isFailed } from "@renaiss-protocol/client";

    const readinessResult = await secureClient.ensureSafeWalletReady();

    if (isFailed(readinessResult)) {
      throw new Error(getError(readinessResult).detail);
    }

    const pullResult = await secureClient.pullGacha({
      machineSlug: "example-machine",
      quantity: GachaQuantity.Single,
      onEvent(event) {
        console.log(event.status, event.data.action);
      },
    });

    if (isFailed(pullResult)) {
      throw new Error(getError(pullResult).detail);
    }

    const pull = getValue(pullResult);

    for (const draw of pull.draws) {
      console.log(draw.collectible.name, draw.settlementId);
    }
    ```
  </Step>
</Steps>

The client targets Node.js `24` or later. By default, requests go to `https://api.renaiss.xyz`.

Set `RENAISS_API_URL` or pass `baseUrl` to point at another environment.

```ts theme={null}
const publicClient = createPublicClient({
  baseUrl: process.env.RENAISS_API_URL,
});
```

Builder-authorized write workflows also need a pre-provisioned approved builder API key. Set it in your app configuration and pass it as `builderApiKey` only on secure clients that call wallet, pull, or buyback write methods.

To attribute pulls on-chain, also set a builder code and pass it per pull as `builderCode`. See [Builder code attribution](#builder-code-attribution).

## SDK patterns

The SDK uses the same patterns across public and authenticated workflows: paginated list methods, `Result` values for expected failures, and action-specific error guard utilities.

### Pagination

List methods return a lazy paginator. Use `firstPage()` when you only need one page.

```ts theme={null}
const machines = publicClient.listGachaMachines({
  pageSize: 10,
});

const firstPageResult = await machines.firstPage();

if (isFailed(firstPageResult)) {
  throw new Error(getError(firstPageResult).detail);
}

for (const machine of getValue(firstPageResult).items) {
  console.log(machine.slug);
}
```

Use `for await` to iterate through pages. Each page is still a `Result`, so handle failures inside the loop.

```ts theme={null}
for await (const pageResult of publicClient.listGachaMachines({ pageSize: 20 })) {
  if (isFailed(pageResult)) {
    console.error(getError(pageResult));
    break;
  }

  for (const machine of getValue(pageResult).items) {
    console.log(machine.slug);
  }
}
```

You can resume from a cursor returned by a previous page.

```ts theme={null}
const firstPage = getValue(firstPageResult);

if (firstPage.nextCursor !== undefined) {
  for await (const pageResult of machines.from(firstPage.nextCursor)) {
    if (isFailed(pageResult)) {
      throw new Error(getError(pageResult).detail);
    }

    console.log(getValue(pageResult).items);
  }
}
```

### Error handling

The SDK returns expected failures as `Result` values. It does not throw for request validation errors, API errors, schema validation errors, or handled signing failures.

Use `isFailed()` and `getError()` for general handling. Use action-specific error guard utilities when you want exhaustive handling for one action.

```ts theme={null}
import {
  GachaQuantity,
  exhaustive,
  getError,
  isFailed,
  isPullGachaError,
} from "@renaiss-protocol/client";

const result = await secureClient.pullGacha({
  machineSlug: "example-machine",
  quantity: GachaQuantity.Single,
});

if (isFailed(result)) {
  const error = getError(result);

  if (!isPullGachaError(error)) {
    throw new Error(error.detail);
  }

  switch (error.code) {
    case "INSUFFICIENT_ALLOWANCE":
    case "INSUFFICIENT_USDT_AMOUNT":
      // Ask the user to deposit more USDT or prepare their wallet again.
      break;
    case "GACHA_SIGNING_FAILED":
      // The wallet rejected the signature or could not sign.
      break;
    case "GACHA_STREAM_FAILED":
    case "GACHA_V3_STREAM_TIMEOUT":
      // Recover resolved draws with fetchGachaDrawStatuses when possible.
      break;
    case "SAFE_ACCOUNT_NOT_FOUND":
      // Run ensureSafeWalletReady before retrying the pull.
      break;
    case "CARD_PACK_NOT_FOUND":
    case "CARD_PACK_NOT_ACTIVE":
      // Refresh machine discovery and ask the user to select an active machine.
      break;
    case "UNAUTHORIZED":
      // Create a new API key or ask the user to sign in again.
      break;
    case "WRONG_REQUEST_PARAMS":
      // Check request fields, including builderApiKey for write workflows.
      break;
    case "RATE_LIMITED":
      // Back off and retry after lowering request volume.
      break;
    case "GACHA_V3_PRICE_MISMATCH":
    case "CARD_PACKS_QUERY_FAILED":
    case "GACHA_V3_VENDING_MACHINE_ADDRESS_NOT_FOUND":
    case "GACHA_V3_ON_CHAIN_PACK_ID_NOT_SET":
    case "FUNCTION_ERROR":
    case "OPEN_PACK_VRF_FAILED":
    case "INVALID_SCHEMA":
    case "UNKNOWN_ERROR":
      // Log details and show a generic retry state.
      break;
    default:
      exhaustive(error.code);
  }
}
```

Available guard utilities include:

* `isListGachaMachinesError`
* `isPullGachaError`
* `isListGachaBuybackOffersError`
* `isFetchGachaDrawStatusesError`
* `isBuybackGachaError`
* `isIsSafeWalletDeployedError`
* `isDeploySafeWalletError`
* `isIsPermit2UsdtApprovedError`
* `isApprovePermit2UsdtError`
* `isEnsureSafeWalletReadyError`

## Public client

Public clients can call unauthenticated endpoints such as gacha machine discovery, machine detail, and machine contents.

```ts theme={null}
import {
  createPublicClient,
  getError,
  getValue,
  isFailed,
} from "@renaiss-protocol/client";

const publicClient = createPublicClient();

const result = await publicClient
  .listGachaMachines({
    pageSize: 10,
  })
  .firstPage();

if (isFailed(result)) {
  console.error(getError(result));
} else {
  const machines = getValue(result).items;

  for (const machine of machines) {
    console.log(machine.slug, machine.name);
  }
}
```

## Authenticated client

Authenticated clients need a user API key. The SDK can create one by asking the user's wallet to sign a SIWE message, then exchanging the session for an API key.

`createSecureClient({ apiKey })` is enough for authenticated reads:

* `fetchAuthenticatedUser`
* `listGachaBuybackOffers`
* `listUserActivities`
* `fetchGachaDrawStatuses`

Builder-authorized write workflows also need `builderApiKey`:

* `ensureSafeWalletReady`
* `isSafeWalletDeployed`
* `deploySafeWallet`
* `isPermit2UsdtApproved`
* `approvePermit2Usdt`
* `pullGacha`
* `buybackGacha`

Use a pre-provisioned approved builder API key. The SDK sends `x-builder-api-key` only for write-authorized requests, not for SIWE API key creation or authenticated reads.

### Wallet integrations

The signer must implement the Renaiss signer interface. The viem adapter can use a private key, an existing viem wallet client, or an injected browser provider.

<Warning>
  Never expose private keys in browser code. For browser wallets, adapt the user's injected provider instead of using `privateKey`.
</Warning>

<CodeGroup>
  ```ts Private key theme={null}
  import { createSecureClient, getError, getValue, isFailed } from "@renaiss-protocol/client";
  import { privateKey } from "@renaiss-protocol/client/viem";

  const signer = privateKey(process.env.PRIVATE_KEY);
  const builderApiKey = process.env.RENAISS_BUILDER_API_KEY;

  if (builderApiKey === undefined) {
    throw new Error("Missing RENAISS_BUILDER_API_KEY.");
  }

  const apiKeyResult = await publicClient.createApiKeyWithSiwe({
    name: "Server integration",
    prefix: "server",
    signer,
  });

  if (isFailed(apiKeyResult)) {
    throw new Error(getError(apiKeyResult).detail);
  }

  const secureClient = createSecureClient({
    apiKey: getValue(apiKeyResult).key,
    builderApiKey,
    signer,
  });
  ```

  ```ts Browser wallet theme={null}
  import { createSecureClient, getError, getValue, isFailed } from "@renaiss-protocol/client";
  import { signerFrom } from "@renaiss-protocol/client/viem";
  import { createWalletClient, custom, getAddress } from "viem";

  if (window.ethereum === undefined) {
    throw new Error("No injected wallet provider found.");
  }

  const accounts = (await window.ethereum.request({
    method: "eth_requestAccounts",
  })) as string[];

  const [account] = accounts;

  if (account === undefined) {
    throw new Error("No wallet account selected.");
  }

  const signer = signerFrom(
    createWalletClient({
      account: getAddress(account),
      transport: custom(window.ethereum),
    }),
  );

  const builderApiKey = "<approved-builder-api-key>";

  const apiKeyResult = await publicClient.createApiKeyWithSiwe({
    name: "Browser integration",
    prefix: "browser",
    signer,
  });

  if (isFailed(apiKeyResult)) {
    throw new Error(getError(apiKeyResult).detail);
  }

  const secureClient = createSecureClient({
    apiKey: getValue(apiKeyResult).key,
    builderApiKey,
    signer,
  });
  ```
</CodeGroup>

### Ensure wallet ready

Gacha write flows use the authenticated user's deterministic Safe wallet and Permit2 USDT approval. They require both the user API key and the secure client's `builderApiKey`.

Call `ensureSafeWalletReady()` before letting a user pull gacha or accept buyback offers. It deploys the Safe if needed and approves Permit2 USDT if needed. `isSafeWalletDeployed()` and `isPermit2UsdtApproved()` also need `builderApiKey` because they call prepare routes for sponsored write operations.

```ts theme={null}
import { getError, getValue, isFailed } from "@renaiss-protocol/client";

const readinessResult = await secureClient.ensureSafeWalletReady();

if (isFailed(readinessResult)) {
  throw new Error(getError(readinessResult).detail);
}

const readiness = getValue(readinessResult);

console.log("Safe wallet:", readiness.safe.safeWalletAddress);
console.log("Deployment:", readiness.deployment.status);
console.log("Permit2 USDT:", readiness.permit2UsdtApproval.status);
```

### Check wallet deployed

Use `isSafeWalletDeployed()` when you only need to check whether the authenticated user's Safe already exists.

```ts theme={null}
const deployedResult = await secureClient.isSafeWalletDeployed();

if (isFailed(deployedResult)) {
  throw new Error(getError(deployedResult).detail);
}

if (getValue(deployedResult)) {
  console.log("Safe wallet is deployed.");
} else {
  console.log("Safe wallet still needs deployment.");
}
```

### Deploy wallet

Use `deploySafeWallet()` when you want wallet deployment as a separate step.

```ts theme={null}
const deploymentResult = await secureClient.deploySafeWallet();

if (isFailed(deploymentResult)) {
  throw new Error(getError(deploymentResult).detail);
}

const deployment = getValue(deploymentResult);

console.log(deployment.status);
console.log(deployment.transactionHash);
console.log(deployment.userOperationHash);
```

### Approve Permit2 for custom flows

Most integrations should call `ensureSafeWalletReady()`. Use `approvePermit2Usdt()` explicitly when your flow handles deployment and token approval in separate screens.

```ts theme={null}
const approvalResult = await secureClient.approvePermit2Usdt();

if (isFailed(approvalResult)) {
  throw new Error(getError(approvalResult).detail);
}

const approval = getValue(approvalResult);

console.log(approval.status);
console.log(approval.transactionHash);
```

You can also check approval status without submitting an approval transaction.

```ts theme={null}
const approvedResult = await secureClient.isPermit2UsdtApproved();

if (isFailed(approvedResult)) {
  throw new Error(getError(approvedResult).detail);
}

console.log("Permit2 USDT approved:", getValue(approvedResult));
```

## Discovery

Use discovery methods to browse gacha machines, machine contents, buyback offers, and user activity feeds. List methods return paginated `Result` values, so use the same pagination and error handling patterns from [SDK patterns](#sdk-patterns).

<CodeGroup>
  ```ts listGachaMachines theme={null}
  import { GachaMachineStage, getError, getValue, isFailed } from "@renaiss-protocol/client";

  const machinesResult = await publicClient
    .listGachaMachines({
      pageSize: 20,
      stage: GachaMachineStage.Active,
    })
    .firstPage();

  if (isFailed(machinesResult)) {
    throw new Error(getError(machinesResult).detail);
  }

  for (const machine of getValue(machinesResult).items) {
    console.log(machine.slug, machine.name);
  }
  ```

  ```ts listGachaMachineContents theme={null}
  const contentsResult = await publicClient
    .listGachaMachineContents({
      slug: "example-machine",
      pageSize: 50,
    })
    .firstPage();

  if (isFailed(contentsResult)) {
    throw new Error(getError(contentsResult).detail);
  }

  for (const card of getValue(contentsResult).items) {
    console.log(card.name, card.tier);
  }
  ```

  ```ts listGachaBuybackOffers theme={null}
  const offersResult = await secureClient
    .listGachaBuybackOffers({
      pageSize: 10,
      search: "dragon",
    })
    .firstPage();

  if (isFailed(offersResult)) {
    throw new Error(getError(offersResult).detail);
  }

  for (const offer of getValue(offersResult).items) {
    console.log(offer.cardName, offer.buybackAmountInUsdt);
  }
  ```

  ```ts listUserActivities theme={null}
  import { UserActivityFilter } from "@renaiss-protocol/client";

  const activitiesResult = await secureClient
    .listUserActivities({ pageSize: 20 })
    .firstPage();

  if (isFailed(activitiesResult)) {
    throw new Error(getError(activitiesResult).detail);
  }

  for (const activity of getValue(activitiesResult).items) {
    if (activity.type === UserActivityFilter.GachaV3Pull) {
      console.log(activity.packId, activity.token, activity.checkoutIds);
    } else if (activity.type === UserActivityFilter.GachaV3Buyback) {
      console.log(activity.buybackIds, activity.nftTokenId);
    } else if (activity.type === UserActivityFilter.GachaV3ReleaseToken) {
      console.log(activity.releaseTokenId, activity.nftTokenId);
    }
  }
  ```
</CodeGroup>

## Gacha machine

Use gacha machine methods to fetch one machine, inspect its contents, pull from it, stream pull progress, and submit buybacks for eligible cards.

### Fetch a gacha machine

Use `fetchGachaMachine()` when you already have a machine slug and need the full machine object.

```ts theme={null}
const machineResult = await publicClient.fetchGachaMachine({
  slug: "example-machine",
});

if (isFailed(machineResult)) {
  throw new Error(getError(machineResult).detail);
}

const machine = getValue(machineResult);

console.log(machine.slug, machine.name, machine.priceInUsdt);
```

### Pull gacha

Use `pullGacha()` from a secure client after the user's wallet is ready. The secure client must include `builderApiKey`. The SDK prepares the pull, asks the signer for the Safe typed-data signature, submits the pull, and streams progress through aggregated draw resolution. Token release continues asynchronously after the stream closes.

```ts theme={null}
import { GachaQuantity, getError, getValue, isFailed } from "@renaiss-protocol/client";

const readinessResult = await secureClient.ensureSafeWalletReady();

if (isFailed(readinessResult)) {
  throw new Error(getError(readinessResult).detail);
}

const pullResult = await secureClient.pullGacha({
  builderCode: process.env.RENAISS_BUILDER_CODE,
  machineSlug: "example-machine",
  quantity: GachaQuantity.Single,
  onEvent(event) {
    console.log(event.status, event.data.action);
  },
});

if (isFailed(pullResult)) {
  throw new Error(getError(pullResult).detail);
}

const pull = getValue(pullResult);

console.log("Transaction hashes:", pull.txHashes);

for (const draw of pull.draws) {
  console.log(draw.collectible.name, draw.settlementId);
}
```

### Builder code attribution

`pullGacha()` accepts an optional `builderCode` for on-chain attribution. Pass a bytes32 value: `0x` followed by 64 hex characters.

```ts theme={null}
const pullResult = await secureClient.pullGacha({
  builderCode:
    "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
  machineSlug: "example-machine",
  quantity: GachaQuantity.Single,
});
```

The code is set per pull, not on the client. Pass a different value on each call when one integration attributes pulls to more than one campaign.

The API resolves the code into the Permit2 witness that the user signs, and the SDK submits the same resolved value with the pull. When you omit `builderCode`, the API signs and submits the zero bytes32 value.

<Info>
  `builderCode` is public on-chain data, not a secret. It is separate from `builderApiKey`, which authorizes the write and must stay server-side. A pull still needs `builderApiKey` on the secure client.
</Info>

If `builderCode` is not a valid bytes32 value, the SDK returns a `WRONG_REQUEST_PARAMS` result before preparing the pull.

### Pull gacha SSE stages

`pullGacha()` accepts `onEvent`, which receives validated SSE events in arrival order. Use these events to show progress until every selected draw resolves.

The final `pullGacha()` result still returns a `Result`. If the stream emits an `error` event or closes before every selected draw appears in a completed `GACHA_V3_DRAW_RESOLVED` event, the SDK returns a failed result. A successful result contains `draws`, `events`, and `txHashes`. It does not wait for token release.

| Action                   | What is happening                                                | UI handling                                                                                                                                                                   |
| ------------------------ | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GACHA_V3_OPEN_PACK`     | The pack open transaction is being submitted and confirmed.      | Show "Opening machine..." and link any completed `txHashes`.                                                                                                                  |
| `GACHA_V3_DRAW_RESOLVED` | The random draws are being resolved and matched to collectibles. | Show "Resolving draw\..." until the complete event contains every selected draw. Then show the resolved collectibles and explain that token release continues asynchronously. |

Each action can arrive with these statuses:

| Status     | Meaning                     | Fields                                                                                                           |
| ---------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `start`    | The stage started.          | `id`, `data.action`, `data.timestamp`                                                                            |
| `progress` | The stage is still running. | `id`, `data.action`, `data.timestamp`, optional `data.message`                                                   |
| `complete` | The stage finished.         | `id`, `data.action`, `data.timestamp`, `data.txHashes`, optional `data.draws`                                    |
| `error`    | The stage failed.           | `id`, `data.action`, `data.timestamp`, optional `data.code`, optional `data.details`, optional `data.statusCode` |

```ts theme={null}
import {
  GachaQuantity,
  GachaStreamAction,
  GachaStreamEventStatus,
  type GachaStreamEvent,
  getError,
  getValue,
  isFailed,
} from "@renaiss-protocol/client";

function getPullMessage(event: GachaStreamEvent): string {
  if (event.status === GachaStreamEventStatus.Error) {
    return event.data.details ?? "Pull failed.";
  }

  switch (event.data.action) {
    case GachaStreamAction.OpenPack:
      return event.status === GachaStreamEventStatus.Complete
        ? "Pack opened. Waiting for draw..."
        : "Opening machine on-chain...";
    case GachaStreamAction.DrawResolved:
      return event.status === GachaStreamEventStatus.Complete
        ? "Draw resolved. Token release will continue asynchronously."
        : "Resolving random draw...";
  }
}

const pullResult = await secureClient.pullGacha({
  machineSlug: "example-machine",
  quantity: GachaQuantity.Single,
  onEvent(event) {
    console.log(getPullMessage(event));

    if (
      event.status === GachaStreamEventStatus.Complete &&
      event.data.action === GachaStreamAction.DrawResolved
    ) {
      for (const draw of event.data.draws ?? []) {
        console.log("Resolved collectible:", draw.collectible.name);
      }
    }
  },
});

if (isFailed(pullResult)) {
  throw new Error(getError(pullResult).detail);
}

const pull = getValue(pullResult);

for (const draw of pull.draws) {
  console.log(draw.collectible.name, draw.settlementId);
}
```

### Track asynchronous settlement

Use `fetchGachaDrawStatuses()` to recover resolved draws after an interrupted or timed-out stream. You can also use it to check whether each token was assigned, released, or bought back. This authenticated read needs the user's API key. It does not need `builderApiKey`.

```ts theme={null}
async function fetchDrawStatuses(
  machineId: string,
  pullTransactionHash: `0x${string}`,
) {
  const statusesResult = await secureClient.fetchGachaDrawStatuses({
    machineId,
    pullTransactionHash,
  });

  if (isFailed(statusesResult)) {
    throw new Error(getError(statusesResult).detail);
  }

  for (const draw of getValue(statusesResult)) {
    console.log(
      draw.collectible.name,
      draw.status,
      draw.releaseTxHash,
      draw.buybackTxHash,
    );
  }
}
```

`machineId` is the gacha machine's internal UUID, not its slug. `pullTransactionHash` is the transaction hash from the completed `GACHA_V3_OPEN_PACK` event.

<Warning>
  The SDK's public gacha machine models currently expose the machine slug, but not its internal UUID. Call `fetchGachaDrawStatuses()` only if your integration already has that UUID.
</Warning>

### Buyback

Use `listGachaBuybackOffers()` to show available offers, then pass one or more compatible offers to `buybackGacha()`. Listing offers is an authenticated read; submitting the buyback requires `builderApiKey`. Offers in one request must share a pack, vending machine, and token. The SDK combines up to 10 unique settlement IDs and aggregates their authorized amount before signing.

```ts theme={null}
const offersResult = await secureClient
  .listGachaBuybackOffers({
    pageSize: 10,
  })
  .firstPage();

if (isFailed(offersResult)) {
  throw new Error(getError(offersResult).detail);
}

const [offer] = getValue(offersResult).items;

if (offer === undefined) {
  console.log("No buyback offers are available.");
} else {
  const buybackResult = await secureClient.buybackGacha({
    offers: [offer],
  });

  if (isFailed(buybackResult)) {
    throw new Error(getError(buybackResult).detail);
  }

  const buyback = getValue(buybackResult);

  console.log("Buyback transaction:", buyback.txHash);
  console.log("Total USDT:", buyback.totalAmountInUsdt);
}
```
