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

# Developer Quickstart

Mainnet integration guide draft · English v1.0 · 7 October 2026

[Docs Home](/index) · [Developer Guide](/resources/developer-guide) · [Smart Contracts and References](/resources/smart-contracts-and-references)

Partners enter the official deployment materials and ABIs for the selected product, then prepare deposits and redemptions with the user's EVM wallet. The TypeScript examples below connect quotes, simulation, submission, successful receipts, and actual token receipt for the public deposit and redemption interfaces. Check the product's eligibility requirements and supported features, then apply the examples to the relevant release version.

## Inputs to prepare

The values and distribution paths for the official mainnet manifest, ABI files, and partner API specification will be finalized at release. The following is an **integration input template** to map into a partner's local configuration, rather than a finalized official manifest schema. `null` and empty arrays indicate values that have not been entered. Do not invent addresses or a `chainId`, or use public verification environment values as mainnet values.

```json theme={"system"}
{
  "productId": null,
  "releaseVersion": null,
  "chainId": null,
  "rpcUrl": null,
  "nativeCurrency": null,
  "confirmations": null,
  "paymentToken": null,
  "productToken": null,
  "depositVault": null,
  "redemptionVault": null,
  "abis": { "deposit": null, "redemption": null },
  "enabledPaths": [],
  "pricePolicyRef": null,
  "eligibilityPolicyRef": null
}
```

`paymentToken`, `productToken`, `depositVault`, and `redemptionVault` are the official user-facing call addresses. Enter the full ABI arrays matching the deployment in `abis.deposit` and `abis.redemption`. Compare implementation addresses, source versions, ABI file integrity, and supported routes against the release materials described in [Smart Contracts and References](/resources/smart-contracts-and-references). Obtain `confirmations` from the network and product's confirmation policy.

Read and validate the official inputs from a local configuration file, then pass them to `connectIntegration(config, abis, provider, recordSubmission)`. `provider` is the user's connected EIP1193 wallet. This example uses the calling conventions of viem `2.53.1`. Saving the code or connecting a wallet does not submit a transaction. Call deposit or redemption functions when the user confirms the displayed amount, recipient, and route. `recordSubmission` is the partner's function for storing the transaction hash, chain, product, and call immediately after submission. If a replacement transaction is confirmed, also store its new hash, the original hash, and the replacement reason. If the connection is lost while waiting for a receipt, use the stored hash to query the transaction again.

## Connect the wallet and contracts

```typescript theme={"system"}
import {
  createPublicClient, createWalletClient, custom, defineChain, http,
  decodeEventLog, erc20Abi, getAddress, isAddress, parseUnits, zeroAddress,
  type Abi, type Address, type EIP1193Provider, type Hash,
} from "viem";

type ReleasedInputs = {
  productId: string;
  releaseVersion: string;
  chainId: number;
  nativeCurrency: { name: string; symbol: string; decimals: number };
  rpcUrl: string;
  confirmations: number;
  paymentToken: Address;
  productToken: Address;
  depositVault: Address;
  redemptionVault: Address;
};
type ContractAbis = { deposit: Abi; redemption: Abi };
type Submission = {
  chainId: number; productId: string; contract: Address;
  functionName: string; hash: Hash; replacesHash?: Hash; reason?: string;
};

export async function connectIntegration(
  c: ReleasedInputs, abis: ContractAbis, provider: EIP1193Provider,
  recordSubmission: (entry: Submission) => void,
) {
  if (!c.productId || !c.releaseVersion ||
      !Number.isInteger(c.chainId) || c.chainId <= 0 ||
      !Number.isInteger(c.confirmations) || c.confirmations < 1) {
    throw new Error("Enter the release specification and confirmation criteria");
  }
  for (const address of [
    c.paymentToken, c.productToken, c.depositVault, c.redemptionVault,
  ]) {
    if (!isAddress(address) || getAddress(address) === zeroAddress) {
      throw new Error("Enter the official call addresses");
    }
  }
  const chain = defineChain({
    id: c.chainId, name: "Configured ARCO network",
    nativeCurrency: c.nativeCurrency,
    rpcUrls: { default: { http: [c.rpcUrl] } },
  });
  const publicClient = createPublicClient({ chain, transport: http(c.rpcUrl) });
  const walletClient = createWalletClient({ chain, transport: custom(provider) });
  const [account] = await walletClient.requestAddresses();
  if (!account) throw new Error("No connected account");

  async function checkNetwork() {
    const [current] = await walletClient.getAddresses();
    if (!current || getAddress(current) !== getAddress(account)) {
      throw new Error("The account has changed. Prepare the integration state again");
    }
    if (await publicClient.getChainId() !== c.chainId ||
        await walletClient.getChainId() !== c.chainId) {
      throw new Error("Check the selected product's network");
    }
  }
  await checkNetwork();

  async function read<T>(
    address: Address, abi: Abi, functionName: string, args: readonly unknown[] = [],
  ): Promise<T> {
    return await publicClient.readContract({ address, abi, functionName, args }) as T;
  }
  async function submit(
    address: Address, abi: Abi, functionName: string, args: readonly unknown[],
  ) {
    await checkNetwork();
    const { request } = await publicClient.simulateContract({
      address, abi, functionName, args, account,
    });
    const hash = await walletClient.writeContract(request);
    try {
      recordSubmission({ chainId: c.chainId, productId: c.productId,
        contract: address, functionName, hash });
      const receipt = await publicClient.waitForTransactionReceipt({
        hash, confirmations: c.confirmations,
        onReplaced(replacement) {
          recordSubmission({ chainId: c.chainId, productId: c.productId,
            contract: address, functionName, hash: replacement.transactionReceipt.transactionHash,
            replacesHash: hash, reason: replacement.reason });
        },
      });
      if (receipt.status !== "success") throw new Error("Transaction failed");
      return receipt;
    } catch (cause) {
      throw new Error("Original transaction lookup required: " + hash, { cause });
    }
  }
  type Receipt = Awaited<ReturnType<typeof submit>>;
  function eventArgs(receipt: Receipt, address: Address, abi: Abi, name: string) {
    for (const log of receipt.logs) {
      if (getAddress(log.address) !== getAddress(address)) continue;
      try {
        const event = decodeEventLog({ abi, data: log.data, topics: log.topics });
        if (event.eventName === name && event.args && !Array.isArray(event.args)) {
          return event.args as unknown as Record<string, unknown>;
        }
      } catch { /* Skip logs that do not belong to this ABI. */ }
    }
    throw new Error("Expected event not found. Query the original transaction");
  }
  function hasTransfer(
    receipt: Receipt, token: Address, from: Address, to: Address, value: bigint,
  ) {
    return receipt.logs.some((log) => {
      if (getAddress(log.address) !== getAddress(token)) return false;
      try {
        const event = decodeEventLog({ abi: erc20Abi, data: log.data, topics: log.topics });
        return event.eventName === "Transfer" &&
          getAddress(event.args.from) === getAddress(from) &&
          getAddress(event.args.to) === getAddress(to) && event.args.value === value;
      } catch { return false; }
    });
  }
  return { c, abis, account, publicClient, read, submit, eventArgs, hasTransfer };
}
type Integration = Awaited<ReturnType<typeof connectIntegration>>;
function exactUnits(amount: string, decimals: number) {
  if (!/^(0|[1-9][0-9]*)([.][0-9]+)?$/.test(amount) ||
      (amount.split(".")[1]?.length ?? 0) > decimals) {
    throw new Error("Check the amount format and decimal precision");
  }
  return parseUnits(amount, decimals);
}
```

## Display the quote and set the minimum receipt

On the deposit screen, convert the amount using the payment asset's `decimals`, then query `quoteDeposit`. On the redemption screen, use the ProductToken's `decimals` and `quoteRedeem`. The user reviews the quote, recipient, network, and permitted price movement. Pass `minSharesOut` and `minAssetsOut` as integers in the smallest units after this confirmation. Do not use `0` to disable price protection. Calculate amounts with `bigint` and display them in the relevant token's units.

For example, when permitted price movement is entered as an integer `bps`, the minimum receipt can be calculated as `quote * (10_000n - bps) / 10_000n`. Validate the range of `bps` and apply the product's pricing and cost policies. Show the amount to the user again after obtaining a new quote. Do not insert an estimated fee into a quote that does not include fees.

## Submit a deposit and verify receipt

The function below rechecks the quote after approving the required payment asset and then simulates the deposit. After submission, it checks the deposit event's product contract, user, recipient, and quantities against the ProductToken mint log. The returned `balance` is the holding at the confirmed receipt's block. The quantity received in this transaction is `sharesOut`.

```typescript theme={"system"}
export async function depositExample(
  x: Integration, amount: string, minSharesOut: bigint, recipient: Address,
) {
  const { c, abis, account, read, submit } = x;
  const decimals = await read<number>(c.paymentToken, erc20Abi, "decimals");
  const paymentAssets = exactUnits(amount, decimals);
  if (paymentAssets <= 0n) throw new Error("Check the deposit amount");
  if (await read<bigint>(c.paymentToken, erc20Abi, "balanceOf", [account]) < paymentAssets) {
    throw new Error("Insufficient payment asset balance");
  }
  const allowance = await read<bigint>(
    c.paymentToken, erc20Abi, "allowance", [account, c.depositVault],
  );
  if (allowance < paymentAssets) {
    // Also handle payment tokens that require resetting an existing allowance first.
    if (allowance > 0n) {
      await submit(c.paymentToken, erc20Abi, "approve", [c.depositVault, 0n]);
    }
    await submit(c.paymentToken, erc20Abi, "approve", [c.depositVault, paymentAssets]);
  }
  const quote = await read<bigint>(c.depositVault, abis.deposit, "quoteDeposit", [paymentAssets]);
  if (minSharesOut <= 0n || quote < minSharesOut) throw new Error("Recheck the quote and minimum receipt");
  // minSharesOut is the value confirmed by the user from the displayed quote and permitted price movement.
  const receipt = await submit(
    c.depositVault, abis.deposit, "deposit", [paymentAssets, minSharesOut, recipient],
  );
  const event = x.eventArgs(receipt, c.depositVault, abis.deposit, "ProductDeposit");
  const sharesOut = event.sharesOut as bigint;
  if (getAddress(event.sender as Address) !== getAddress(account) ||
      getAddress(event.recipient as Address) !== getAddress(recipient) ||
      event.paymentAssets !== paymentAssets || typeof sharesOut !== "bigint" ||
      sharesOut < minSharesOut ||
      !x.hasTransfer(receipt, c.productToken, zeroAddress, recipient, sharesOut)) {
    throw new Error("Deposit result under verification. Query the original transaction before depositing again");
  }
  const balance = await x.publicClient.readContract({
    address: c.productToken, abi: erc20Abi, functionName: "balanceOf",
    args: [recipient], blockNumber: receipt.blockNumber,
  });
  return { txHash: receipt.transactionHash, blockNumber: receipt.blockNumber, sharesOut, balance };
}
```

## Internal direct redemption and request acceptance

This example is limited to the internal direct redemption and request-and-claim flow in the public [ArcoRedemptionVault](https://github.com/LBMike/arco-giwa/blob/8160f354b55dfe9a525565e30ddc007f51448f3d/packages/contracts/contracts/vaults/ArcoRedemptionVault.sol). The public redemption implementation burns user tokens through the vault's permissions, so it does not require a separate ProductToken `approve`. Products whose release ABI uses a different transfer or approval mechanism follow that specification.

```typescript theme={"system"}
export async function redeemExample(
  x: Integration, amount: string, minAssetsOut: bigint,
  recipient: Address, mode: "internal-instant" | "request",
) {
  const { c, abis, account, read, submit } = x;
  const decimals = await read<number>(c.productToken, erc20Abi, "decimals");
  const sharesIn = exactUnits(amount, decimals);
  if (sharesIn <= 0n ||
      await read<bigint>(c.productToken, erc20Abi, "balanceOf", [account]) < sharesIn) {
    throw new Error("Check the redemption amount and token balance");
  }
  const quote = await read<bigint>(c.redemptionVault, abis.redemption, "quoteRedeem", [sharesIn]);
  if (minAssetsOut <= 0n || quote < minAssetsOut) throw new Error("Recheck the quote and minimum receipt");
  const receipt = await submit(
    c.redemptionVault, abis.redemption,
    mode === "internal-instant" ? "redeemInstant" : "requestRedeem",
    [sharesIn, minAssetsOut, recipient],
  );
  const event = x.eventArgs(
    receipt, c.redemptionVault, abis.redemption,
    mode === "internal-instant" ? "ProductRedeemInstant" : "ProductRedeemRequested",
  );
  if (getAddress(event.sender as Address) !== getAddress(account) ||
      getAddress(event.recipient as Address) !== getAddress(recipient) ||
      event.sharesIn !== sharesIn ||
      !x.hasTransfer(receipt, c.productToken, account, zeroAddress, sharesIn)) {
    throw new Error("Redemption result under verification. Query the original transaction");
  }
  if (mode === "request") {
    if (typeof event.requestId !== "bigint" || typeof event.assetsOut !== "bigint" ||
        event.assetsOut < minAssetsOut) throw new Error("Check the request record");
    return {
      state: "requested", chainId: c.chainId, redemptionVaultAddress: c.redemptionVault,
      requestId: String(event.requestId), requestedTxHash: receipt.transactionHash,
      lockedAssetsOut: String(event.assetsOut),
    };
  }
  const assetsOut = event.assetsOut as bigint;
  if (typeof assetsOut !== "bigint" || assetsOut < minAssetsOut ||
      !x.hasTransfer(receipt, c.paymentToken, c.redemptionVault, recipient, assetsOut)) {
    throw new Error("Payment result under verification. Query the original transaction");
  }
  const balance = await x.publicClient.readContract({
    address: c.paymentToken, abi: erc20Abi, functionName: "balanceOf",
    args: [recipient], blockNumber: receipt.blockNumber,
  });
  return { state: "received", txHash: receipt.transactionHash, assetsOut, balance };
}
```

For `internal-instant`, show receipt only after verifying the successful transaction receipt, the user's token burn, and the payment asset transfer logs. For `request`, the acceptance transaction burns the tokens and fixes the expected payment amount at the price recorded at that time; it returns `requested`. Store the `chainId`, vault address, request ID, and original transaction together. Do not show acceptance as payment completion.

## Verify payment after a request

In the public request-and-claim implementation, query `redemptionRequests(requestId)` to check `recipient`, `assetsOut`, `fulfilled`, and `claimed`. `ProductRedeemFulfilled` indicates that payment assets have been reserved and the claim is ready; it does not mean payment is complete. When `fulfilled=true` and `claimed=false`, the designated recipient simulates and signs `claimRedeem(requestId)`. Show payment as complete only after checking `ProductRedeemClaimed` in the successful receipt and the actual payment asset transfer.

If a released product uses direct operator payment, NAV fixed at settlement, or a separate settlement route, use that payment transaction and the release's state specification. OTC quote acceptance, orders, execution, and settlement are not replaced by this direct redemption function. Do not record an acquisition-based token transfer as a burn.

## Interruptions and recovery

Simulation failure, user rejection, and an unknown result after submission are distinct states. Once a submission hash is available, query the chain, original transaction, account nonce, and relevant events before resubmitting. Do not treat an RPC timeout as transaction failure or create a new request because a query returns no result. When the API lags, distinguish the onchain outcome from a pending display update. Follow the recovery tables in the [Developer Guide](/resources/developer-guide) for specific event and error responses.

## Scope

These examples reference the ProductToken, ArcoDepositVault, and ArcoRedemptionVault interfaces in [public source version 8160f354](https://github.com/LBMike/arco-giwa/tree/8160f354b55dfe9a525565e30ddc007f51448f3d). ArcoUSD's multiple assets and 1:1 exchanges, Savings return connections, Funds burning and treasury settlement, and OTC orders and execution connect through separate release interfaces and feature matrices. For an actual product integration, verify official addresses, full ABIs, eligibility conditions, pricing policies, and supported routes.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.