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

# Integrate the Fixed-Rate Vault

> Integrate deposits through VedaRouter, withdrawal requests through AtomicQueue, and fixed-rate reads using network-specific contract addresses.

**VedaVault** is SuperEarn's fixed-APR vault stack built on Veda BoringVault. `VAULT` on this page is **BoringVault and its ERC-20 share token**. Users **deposit through VedaRouter** and **submit withdrawal requests directly to AtomicQueue**. BoringVault does not expose ERC-4626 `deposit/redeem/previewRedeem` entrypoints.

The deposit, withdrawal, and rate-query flow is the same on each network. Select the matching network and contract addresses below. `ASSET` is the underlying token for that deployment; on Kairos, it is mockUSDT.

For the architecture and APR model, start with [fixed-rate vault architecture and rate updates](/en/protocol/fixed-rate-vault.md). The default operating target is a regular update **every 24 hours for USDT (6 decimals) and every hour for JPYC (18 decimals)**. This page focuses on integration calls; coordinate updates separately with the team on Kairos, which has no automated keeper.

## 1. Networks and contract addresses

Mainnet contract addresses will be added after the production deployment. The mainnet column is intentionally blank.

| Network | Use |
| - | - |
| Kaia mainnet | Production integration; addresses pending |
| Kaia Kairos (chain ID 1001) | Integration testing with mockUSDT; see [Test on Kairos](#7-test-on-kairos) |

Use the addresses for your selected network. Router, Keeper, Gateway, CustomVault, and the rate Controller use Transparent proxies; **use their proxy addresses in the table**.

| Contract | Kaia mainnet address | Kairos testnet address |
| - | - | - |
| Underlying token (`ASSET`; mockUSDT on Kairos) | | [`0xFD9a576384e9872fb6f7b3ed24c9be915E809C78`](https://kairos.kaiascan.io/address/0xFD9a576384e9872fb6f7b3ed24c9be915E809C78) |
| VedaRouter — user deposit proxy | | [`0xCF5A814F8D5fa5E51C7D25d00Af1fa930EDc51Bf`](https://kairos.kaiascan.io/address/0xCF5A814F8D5fa5E51C7D25d00Af1fa930EDc51Bf) |
| BoringVault — VedaVault share token, 18 decimals | | [`0xada4481a03c26d378554e0F24247DaFc8Afa542F`](https://kairos.kaiascan.io/address/0xada4481a03c26d378554e0F24247DaFc8Afa542F) |
| Accountant — checkpointed rate | | [`0xc9de16B4E7Ab99c5466c3bA4F958C113faF20eef`](https://kairos.kaiascan.io/address/0xc9de16B4E7Ab99c5466c3bA4F958C113faF20eef) |
| AtomicQueue — user withdrawal requests | | [`0x1Cb256edad979cfc25622b35fD54d848Cdce8DD5`](https://kairos.kaiascan.io/address/0x1Cb256edad979cfc25622b35fD54d848Cdce8DD5) |
| FixedAprRateController — APR and projected rate | | [`0xA3914f4685838359b2a8403E8acea85f2cc4cB97`](https://kairos.kaiascan.io/address/0xA3914f4685838359b2a8403E8acea85f2cc4cB97) |
| VedaKeeper — keeper operations proxy | | [`0xC819088522bedD6f4AC78bE42B98B384F5c5795b`](https://kairos.kaiascan.io/address/0xC819088522bedD6f4AC78bE42B98B384F5c5795b) |
| Teller — internal minting/redemption | | [`0xEbD6156c992000D78035663995d3e3896B29d2c3`](https://kairos.kaiascan.io/address/0xEbD6156c992000D78035663995d3e3896B29d2c3) |
| VedaCustomVaultGateway proxy | | [`0xcd6C5b0c39039990f3F9341037c4a8Ad14a85827`](https://kairos.kaiascan.io/address/0xcd6C5b0c39039990f3F9341037c4a8Ad14a85827) |
| CustomVault proxy | | [`0x89837c5e0715551c31e7a52d75aC6465645Ea5c3`](https://kairos.kaiascan.io/address/0x89837c5e0715551c31e7a52d75aC6465645Ea5c3) |
| Withdrawal request transfer-lock hook | | [`0xf97B8B74F4293a460246a23bD7A6fB6B7a7806e3`](https://kairos.kaiascan.io/address/0xf97B8B74F4293a460246a23bD7A6fB6B7a7806e3) |
| AtomicSolverV4 | | [`0xFe1C618980A58dD2fDA4d8E77A0D6C916B03BCf6`](https://kairos.kaiascan.io/address/0xFe1C618980A58dD2fDA4d8E77A0D6C916B03BCf6) |
| ManagerWithMerkleVerification | | [`0x59fF09424EC310eb614abC063a43E97949355F7e`](https://kairos.kaiascan.io/address/0x59fF09424EC310eb614abC063a43E97949355F7e) |
| BaseDecoderAndSanitizer | | [`0xd5e955EBFDbBb2EB2D547dCC97152687b9BEB6B5`](https://kairos.kaiascan.io/address/0xd5e955EBFDbBb2EB2D547dCC97152687b9BEB6B5) |
| RolesAuthority | | [`0x2734eE3104fA6CcdaAaABA68e4E33D3EE38B08EC`](https://kairos.kaiascan.io/address/0x2734eE3104fA6CcdaAaABA68e4E33D3EE38B08EC) |
| ProxyAdmin | | [`0xC29491B25823a44467a9bcA422B56a789eF86b7E`](https://kairos.kaiascan.io/address/0xC29491B25823a44467a9bcA422B56a789eF86b7E) |

Deposits enter BoringVault. The keeper can move liquidity through Gateway to CustomVault and return it for withdrawals. Gateway holds CustomVault shares and temporarily holds underlying during deposits; normal CustomVault withdrawals pay BoringVault directly.

## 2. Integration paths and units

| Action | User approval target | User call | Result |
| - | - | - | - |
| Deposit | **Approve ASSET to VedaRouter** | `Router.deposit(assets, minimumShares, receiver)` | BoringVault shares minted to receiver |
| Request withdrawal | **Approve BoringVault shares to AtomicQueue** | `Queue.safeUpdateAtomicRequest(VAULT, ASSET, request, ACCOUNTANT, 0)` | Request recorded; no funds moved yet |
| Fulfill withdrawal | No additional user approval | **Keeper** settles through VedaKeeper | ASSET sent to requester; no separate claim |
| Cancel | None | Update the same pair with `offerAmount = 0` | Request cleared; Queue allowance can then be revoked |

Router approves BoringVault internally and calls Teller `bulkDeposit`. **Users do not approve underlying to Teller or BoringVault.** Direct Teller `deposit`, `depositWithPermit`, and `bulkDeposit` are not public. Use `approve` followed by `deposit`.

* Underlying token: read `ASSET.decimals()`. With 6 decimals, `1 token = 1_000_000` base units.
* BoringVault shares: **18 decimals**. `1 share = 10^18`.
* `R = Accountant.getRate()` is **ASSET base units per 1e18 share base units (1 share)**. ASSET is Accountant's base asset in this integration. **A JPYC-based rate has 18 decimals; a USDT-based rate has 6 decimals**, while shares have 18 decimals in both cases. At 1 share = 1 asset, the JPYC rate is `10^18` and the USDT rate is `10^6`. Read the current value from Accountant.
* Deposit quote: `shares = floor(assets × 10^18 / R)`.
* Value at the checkpointed rate: `assets = floor(shares × R / 10^18)`.
* Actual request payout: `floor(offerAmount × atomicPrice / 10^18)`.

Use `bigint` for all amounts, never JavaScript `number`. The example applies **10 bps (0.1%)** tolerance to minimum deposit shares. This is an example policy; adjust it for your application. Do not set `minimumShares` to zero. **Do not apply this deposit margin to a withdrawal's `atomicPrice`.** Lowering the withdrawal price reduces the actual payout.

### Approval targets and amounts

Call `approve` on the **token contract** and pass the spending contract as `spender`. Approval alone creates neither a deposit nor a withdrawal request.

| Purpose | Transaction target (`to`) | spender | Allowance |
| - | - | - | - |
| Deposit | `ASSET` token | **VedaRouter proxy** | Only this deposit's `assets`, in ASSET base units |
| Withdrawal request | **BoringVault share token** | **AtomicQueue** | Only this request's `offerAmount`, in 18-decimal share units |

For example, depositing 100 tokens with 6 decimals uses `ASSET.approve(Router, 100_000_000)`; withdrawing 10 shares uses `VAULT.approve(Queue, 10 × 10^18)`. **Do not approve ASSET to Queue to withdraw.** Settlement does not charge additional underlying or a shortfall to the user's wallet.

> [!WARNING]
>
> **Do not use unlimited approvals or approve more than the current deposit/request amount.** An exact finite approval is appropriate when depositing or withdrawing the full balance. If an existing allowance exceeds the required amount, confirm `approve(spender, 0)` before approving only the required amount. Allowances have no expiry and are independent of request deadlines. A remaining allowance also applies to tokens received later. Minimal approvals limit exposure to an incorrect spender, contract flaws, and changes in permissions or implementations; they do not remove every risk.

* Check the **chain ID, token address, spender, and amount** in the wallet. Names and symbols are insufficient. Use the published addresses for that network. While mainnet addresses are blank, do not substitute arbitrary or QA addresses.
* This integration does not require approving tokens to Teller, Solver, VedaKeeper, Gateway, or an operator EOA, or approving BoringVault shares to Router. An EOA approved as spender can call `transferFrom` directly. If you approve the wrong spender, **set that actual spender's allowance to zero on the affected token**. The helpers below only clean up the configured Router/Queue allowances.
* Do not send tokens with a plain `transfer` to Router, Vault, Queue, or Keeper. That does not perform a deposit or withdrawal, mint shares, or guarantee an automatic refund. Use `deposit` for deposits, `safeUpdateAtomicRequest` for new withdrawal requests, and `updateAtomicRequest` for cancellation.
* A permit signature also grants spending rights. If your app separately supports permit, verify its token, chain, spender, amount, and signature deadline. The permit deadline limits **signature submission**, not the lifetime of the resulting allowance. This example uses `approve`.

### Asynchronous requests and atomic settlement

A deposit uses **an approval transaction → a Router deposit transaction**. Within `deposit`, the asset transfer and share mint succeed or revert together. A confirmed approval does not revert when the later deposit fails or its signature is rejected.

A withdrawal uses **share approval → request submission → later keeper settlement**. Shares remain in the wallet after submission. An authorized operator calls VedaKeeper, and **AtomicQueue, as spender, calls `transferFrom` for the requested shares**. The operator EOA does not directly withdraw from the user's wallet. Share redemption and ASSET payment happen atomically **within one settlement transaction**; payment failure reverts the share transfer too. Each request fills in full, without partial fills.

A valid request with sufficient share balance and allowance can settle **before its deadline without another user signature**. Do not label a successful request transaction as a completed withdrawal. Distinguish request confirmation, pending settlement, and payment; verify payment with `AtomicRequestFulfilled` and ASSET transfers. Keeper permissions and transfer locks do not justify leaving unnecessary allowances.

## 3. Runnable wallet integration

Run `npm install viem` and use the following file in a TypeScript browser app. **Pass a `FixedRateDeployment` containing the chain and its contract addresses to `connectFixedRate`.** The same methods handle deposits, withdrawal requests, cancellation, and reads on either network. The included `kairosDeployment` is ready for testing. Configure mainnet once its addresses are published.

Minimal ABIs are included. Asset decimals are read from the selected token. Amount inputs are decimal strings; excess precision is rejected. The helper resets an existing larger allowance to zero before approving only the exact required amount. Each operation waits for its transaction receipt, so approvals confirm before dependent calls. Reconnect when the wallet account or network changes. The example mints shares to the connected user; Router also accepts a different `receiver`.

When constructing a configuration from `deployment.json`, map its address keys explicitly: `asset`, `accountant`, `controller`, and `teller` keep their names; use `router ← vedaRouter`, `vault ← boringVault`, `queue ← atomicQueue`, and `keeper ← vedaKeeper`. Set `chain` to the viem Chain configuration matching the wallet and RPC. The included `kairosDeployment` already applies this mapping.

```typescript theme={null}
import {
  createPublicClient, createWalletClient, custom, defineChain, http,
  erc20Abi, parseAbi, parseUnits, type EIP1193Provider, type Hash, type Address, type Chain,
} from 'viem';

export const kairosAddresses = {
  "asset": "0xFD9a576384e9872fb6f7b3ed24c9be915E809C78",
  "router": "0xCF5A814F8D5fa5E51C7D25d00Af1fa930EDc51Bf",
  "vault": "0xada4481a03c26d378554e0F24247DaFc8Afa542F",
  "accountant": "0xc9de16B4E7Ab99c5466c3bA4F958C113faF20eef",
  "queue": "0x1Cb256edad979cfc25622b35fD54d848Cdce8DD5",
  "controller": "0xA3914f4685838359b2a8403E8acea85f2cc4cB97",
  "keeper": "0xC819088522bedD6f4AC78bE42B98B384F5c5795b",
  "teller": "0xEbD6156c992000D78035663995d3e3896B29d2c3"
} as const;
export const kairos = defineChain({
  id: 1001, name: 'Kaia Kairos',
  nativeCurrency: { name: 'KAIA', symbol: 'KAIA', decimals: 18 },
  rpcUrls: { default: { http: ['https://public-en-kairos.node.kaia.io'] } },
  blockExplorers: { default: { name: 'KaiaScan', url: 'https://kairos.kaiascan.io' } },
});
export type FixedRateDeployment = {
  chain: Chain;
  addresses: Record<keyof typeof kairosAddresses, Address>;
};
export const kairosDeployment: FixedRateDeployment = {
  chain: kairos, addresses: kairosAddresses,
};
export const mockAbi = parseAbi(['function mint(address receiver, uint256 amount)']);
export const routerAbi = parseAbi([
  'function deposit(uint256 assets, uint256 minimumShares, address receiver) returns (uint256 shares)',
  'function paused() view returns (bool)',
  'function supplyCap() view returns (uint256)',
  'function whitelistEnabled() view returns (bool)',
  'event Deposited(address indexed payer, address indexed receiver, uint256 assets, uint256 shares)',
]);
export const accountantAbi = parseAbi([
  'function getRate() view returns (uint256)',
  'function getRateSafe() view returns (uint256)',
  'function accountantState() view returns (address payoutAddress, uint96 highwaterMark, uint128 feesOwedInBase, uint128 totalSharesLastUpdate, uint96 exchangeRate, uint16 allowedExchangeRateChangeUpper, uint16 allowedExchangeRateChangeLower, uint64 lastUpdateTimestamp, bool isPaused, uint24 minimumUpdateDelayInSeconds, uint16 managementFee, uint16 performanceFee)',
  'event ExchangeRateUpdated(uint96 oldRate, uint96 newRate, uint64 currentTime)',
]);
export const queueAbi = parseAbi([
  'struct AtomicRequest { uint64 deadline; uint88 atomicPrice; uint96 offerAmount; bool inSolve; }',
  'function updateAtomicRequest(address offer, address want, AtomicRequest request)',
  'function safeUpdateAtomicRequest(address offer, address want, AtomicRequest request, address accountant, uint256 discount)',
  'function getUserAtomicRequest(address user, address offer, address want) view returns (AtomicRequest)',
  'function isPaused() view returns (bool)',
  'event AtomicRequestUpdated(address indexed user, address indexed offerToken, address indexed wantToken, uint256 amount, uint256 deadline, uint256 minPrice, uint256 timestamp)',
  'event AtomicRequestFulfilled(address indexed user, address indexed offerToken, address indexed wantToken, uint256 offerAmountSpent, uint256 wantAmountReceived, uint256 timestamp)',
]);
export const controllerAbi = parseAbi([
  'function aprBps() view returns (uint256)',
  'function lastPokeAt() view returns (uint256)',
  'function minInterval() view returns (uint256)',
  'function previewNextRate() view returns (uint256 newRate, uint256 dt)',
]);
const pauseAbi = parseAbi(['function isPaused() view returns (bool)']);
const keeperAbi = parseAbi(['function paused() view returns (bool)']);

// Invoke from a wallet-connect button. Reconnect after accountsChanged/chainChanged.
export async function connectFixedRate(
  provider: EIP1193Provider,
  deployment: FixedRateDeployment,
  rpcUrl = deployment.chain.rpcUrls.default.http[0],
) {
  const { chain, addresses: a } = deployment;
  const wallet = createWalletClient({ chain, transport: custom(provider) });
  const [account] = await wallet.requestAddresses();
  if (!account) throw new Error('Connect a wallet');
  if (await wallet.getChainId() !== chain.id) throw new Error(`Switch wallet to ${chain.name} (${chain.id})`);
  const client = createPublicClient({ chain, transport: http(rpcUrl) });
  if (await client.getChainId() !== chain.id) throw new Error(`RPC must use chain ${chain.id}`);
  const assetDecimals = await client.readContract({ address: a.asset, abi: erc20Abi,
    functionName: 'decimals' });
  const wait = async (hash: Hash) => {
    const receipt = await client.waitForTransactionReceipt({ hash });
    if (receipt.status !== 'success') throw new Error(`Transaction reverted: ${hash}`);
    return receipt;
  };
  async function approveExact(token: `0x${string}`, spender: `0x${string}`, amount: bigint) {
    const allowance = await client.readContract({ address: token, abi: erc20Abi,
      functionName: 'allowance', args: [account, spender] });
    if (allowance === amount) return;
    if (allowance !== 0n) await wait(await wallet.writeContract({ account,
      address: token, abi: erc20Abi, functionName: 'approve', args: [spender, 0n] }));
    await wait(await wallet.writeContract({ account, address: token, abi: erc20Abi,
      functionName: 'approve', args: [spender, amount] }));
  }
  // Revocation calls the token directly; it also works while Queue is paused.
  async function revokeAllowance(token: Address, spender: Address) {
    const remaining = await client.readContract({ address: token, abi: erc20Abi,
      functionName: 'allowance', args: [account, spender] });
    if (remaining === 0n) return; // Already revoked.
    return wait(await wallet.writeContract({ account, address: token, abi: erc20Abi,
      functionName: 'approve', args: [spender, 0n] }));
  }
  const revokeDepositAllowance = () => revokeAllowance(a.asset, a.router);
  const revokeQueueAllowance = () => revokeAllowance(a.vault, a.queue);
  function parseAmount(value: string, decimals: number) {
    if (!/^(0|[1-9]\d*)(\.\d+)?$/.test(value)
      || (value.split('.')[1]?.length ?? 0) > decimals) {
      throw new Error('Use a positive decimal string within token precision');
    }
    return parseUnits(value, decimals);
  }
  // Test token helper: available only for the published Kairos mockUSDT.
  async function mintMockUSDT(amount = '1000') {
    if (chain.id !== kairos.id || a.asset.toLowerCase() !== kairosAddresses.asset.toLowerCase()) {
      throw new Error('mockUSDT mint is only available for the Kairos test deployment');
    }
    const units = parseAmount(amount, assetDecimals);
    if (units <= 0n) throw new Error('Amount must be positive');
    const { request } = await client.simulateContract({ account, address: a.asset,
      abi: mockAbi, functionName: 'mint', args: [account, units] });
    return wait(await wallet.writeContract(request));
  }
  async function deposit(amount: string, slippageBps = 10n) {
    const assets = parseAmount(amount, assetDecimals);
    if (assets <= 0n || slippageBps < 0n || slippageBps >= 10000n) throw new Error('Invalid amount/slippage');
    await approveExact(a.asset, a.router, assets);
    const rate = await client.readContract({ address: a.accountant, abi: accountantAbi,
      functionName: 'getRateSafe' });
    const minimumShares = (assets * 10n ** 18n / rate) * (10000n - slippageBps) / 10000n;
    if (minimumShares === 0n) throw new Error('Deposit too small');
    const { request } = await client.simulateContract({ account, address: a.router,
      abi: routerAbi, functionName: 'deposit', args: [assets, minimumShares, account] });
    return wait(await wallet.writeContract(request));
  }
  async function requestWithdraw(shareAmount: string) {
    const shares = parseAmount(shareAmount, 18);
    if (shares <= 0n || shares > (1n << 96n) - 1n) throw new Error('Invalid share amount');
    const old = await client.readContract({ address: a.queue, abi: queueAbi,
      functionName: 'getUserAtomicRequest', args: [account, a.vault, a.asset] });
    // Clear even expired stored requests before granting a new allowance.
    if (old.offerAmount > 0n) throw new Error('Cancel stored request before approving a new one, including expired requests');
    const balance = await client.readContract({ address: a.vault, abi: erc20Abi,
      functionName: 'balanceOf', args: [account] });
    if (shares > balance) throw new Error('Insufficient shares');
    await approveExact(a.vault, a.queue, shares);
    const rate = await client.readContract({ address: a.accountant, abi: accountantAbi,
      functionName: 'getRateSafe' });
    if (rate <= 0n || rate > (1n << 88n) - 1n) throw new Error('Invalid atomic price');
    if (shares * rate / 10n ** 18n === 0n) throw new Error('Withdrawal rounds to zero');
    const block = await client.getBlock();
    const atomicRequest = { deadline: block.timestamp + 7n * 86400n,
      // Ignored by safeUpdateAtomicRequest: price is read from Accountant at execution.
      atomicPrice: 0n, offerAmount: shares, inSolve: false };
    const { request } = await client.simulateContract({ account, address: a.queue,
      abi: queueAbi, functionName: 'safeUpdateAtomicRequest',
      args: [a.vault, a.asset, atomicRequest, a.accountant, 0n] });
    return wait(await wallet.writeContract(request));
  }
  async function cancelWithdraw() {
    // The safe entrypoint rejects zero amounts; use the regular entrypoint to cancel.
    const { request } = await client.simulateContract({ account, address: a.queue,
      abi: queueAbi, functionName: 'updateAtomicRequest', args: [a.vault, a.asset,
        { deadline: 0n, atomicPrice: 0n, offerAmount: 0n, inSolve: false }] });
    // Return cancellation success independently of allowance cleanup.
    return wait(await wallet.writeContract(request));
  }
  async function getStatus() {
    const block = await client.getBlock();
    const blockNumber = block.number;
    const [assets, shares, rate, request, state, aprBps, lastPokeAt, preview,
      routerPaused, tellerPaused, queuePaused, keeperPaused, allowance, depositAllowance] = await Promise.all([
      client.readContract({ address: a.asset, abi: erc20Abi, functionName: 'balanceOf', args: [account], blockNumber }),
      client.readContract({ address: a.vault, abi: erc20Abi, functionName: 'balanceOf', args: [account], blockNumber }),
      client.readContract({ address: a.accountant, abi: accountantAbi, functionName: 'getRate', blockNumber }),
      client.readContract({ address: a.queue, abi: queueAbi, functionName: 'getUserAtomicRequest', args: [account, a.vault, a.asset], blockNumber }),
      client.readContract({ address: a.accountant, abi: accountantAbi, functionName: 'accountantState', blockNumber }),
      client.readContract({ address: a.controller, abi: controllerAbi, functionName: 'aprBps', blockNumber }),
      client.readContract({ address: a.controller, abi: controllerAbi, functionName: 'lastPokeAt', blockNumber }),
      client.readContract({ address: a.controller, abi: controllerAbi, functionName: 'previewNextRate', blockNumber }),
      client.readContract({ address: a.router, abi: routerAbi, functionName: 'paused', blockNumber }),
      client.readContract({ address: a.teller, abi: pauseAbi, functionName: 'isPaused', blockNumber }),
      client.readContract({ address: a.queue, abi: queueAbi, functionName: 'isPaused', blockNumber }),
      client.readContract({ address: a.keeper, abi: keeperAbi, functionName: 'paused', blockNumber }),
      client.readContract({ address: a.vault, abi: erc20Abi, functionName: 'allowance', args: [account, a.queue], blockNumber }),
      client.readContract({ address: a.asset, abi: erc20Abi, functionName: 'allowance', args: [account, a.router], blockNumber }),
    ]);
    const requestState = request.offerAmount === 0n ? 'none'
      : request.deadline < block.timestamp ? 'expired' : 'open';
    return { account, blockNumber, assets, shares, rate, value: shares * rate / 10n ** 18n,
      aprBps, lastPokeAt, previewRate: preview[0], request, requestState,
      requestPayout: request.offerAmount * request.atomicPrice / 10n ** 18n,
      sharesLocked: requestState === 'open', allowance, depositAllowance,
      accountantPaused: state[8], routerPaused, tellerPaused, queuePaused, keeperPaused };
  }
  return { account, client, mintMockUSDT, deposit, requestWithdraw, cancelWithdraw,
    revokeDepositAllowance, revokeQueueAllowance, getStatus };
}
```

Pass your connected wallet's EIP-1193 provider as `provider` (for example, your wallet SDK provider or an injected wallet's `window.ethereum`). `deployment` is the selected network configuration. Obtain its underlying token before depositing; for test tokens, follow [Test on Kairos](#7-test-on-kairos).

```typescript theme={null}
import { connectFixedRate, type FixedRateDeployment } from './client';

async function integrate(provider: Parameters<typeof connectFixedRate>[0], deployment: FixedRateDeployment) {
  const api = await connectFixedRate(provider, deployment);
  await api.deposit('100');          // Deposit 100 underlying tokens and receive shares
  const before = await api.getStatus();
  const requestReceipt = await api.requestWithdraw('10');
  const pending = await api.getStatus();
  return { before, pending, requestReceipt };
}
```

## 4. Exact withdrawal semantics

```solidity theme={null}
struct AtomicRequest {
    uint64 deadline;     // Unix seconds; expired when block.timestamp > deadline
    uint88 atomicPrice;  // ASSET base units per 1e18 shares
    uint96 offerAmount;  // 18-decimal share amount
    bool inSolve;       // Set false; Queue manages this during settlement
}
```

### atomicPrice: use the safe entrypoint to store the execution-time rate

> [!WARNING]
>
> **`atomicPrice` is the fixed price that determines the actual payout, not a minimum acceptable price.** Create requests with **`safeUpdateAtomicRequest(VAULT, ASSET, request, ACCOUNTANT, 0)`**. The final argument, `discount=0`, stores the Accountant rate when the request transaction executes, without a discount. Later rate increases do not reprice an existing request.

1. Approve **AtomicQueue for exactly the requested BoringVault shares** and wait for confirmation.
2. Read **`Accountant.getRateSafe()` at the official deployment address** for a preview. Stop if paused or the read fails. Display shares, price, and estimated payout using `floor(offerAmount × rate / 10^18)`. Both the price and rounded payout must be positive.
3. Set `request.atomicPrice` to a **`0n` placeholder** and call the safe entrypoint. It ignores this field and overwrites the price using the supplied Accountant's **`getRateInQuoteSafe(ASSET)`** within the transaction. **Use the network's official `ACCOUNTANT` address and `discount=0n`**. Do not allow users to select an arbitrary Accountant: checking its `vault()` does not authenticate it as the official Accountant.
4. After confirmation, check the stored price and payout with `getUserAtomicRequest` or `getStatus()`. A poke between the preview and execution can change the registered price. If settlement has already cleared the request, inspect `AtomicRequestUpdated.minPrice` in the registration receipt and the `AtomicRequestFulfilled` event. Despite the name `minPrice`, this event field is the actual fixed settlement price.

The example's `requestWithdraw()` uses this safe path. The `atomicPrice: 0n` input is ignored **only by the safe entrypoint**. Never pass a zero price with a nonzero amount to ordinary `updateAtomicRequest`: it can permit shares to be given away for nothing.

The safe entrypoint checks share balance and allowance, deadline, a positive amount, the Accountant's vault, the discount limit, and the `uint88` price range. Queue or Accountant pause makes it revert. It does **not poke automatically, guarantee future payment, reprice existing requests, or reject a payout that rounds to zero**. `discount` uses a **1,000,000 denominator**, not basis points; its limit is 10,000 (1%). Always use zero in this integration. With ASSET as the Accountant base asset, `getRateInQuoteSafe(ASSET)` and `getRateSafe()` return the same value and units. For another quote asset, use the former rate getter, but do not change this withdrawal integration's supported pair arbitrarily.

Ordinary `updateAtomicRequest` remains public for cancellation. It also permits nonzero requests and **stores the supplied price unchanged**, so use of the safe path is not enforced. For example, with a current USDT rate of `1_000_000` and `offerAmount = 100 × 10^18`:

| Registration method | Stored price | Payout on successful settlement |
| - | - | - |
| Safe entrypoint, official Accountant, discount 0 | `1_000_000` if that is the rate at execution | **100 USDT** |
| Ordinary update, supplied atomicPrice `500_000` | `500_000` | **50 USDT** |

The second request still settles for **50 USDT** even if the rate later becomes `1_010_000`. Follow the cancellation and revocation steps below for an incorrect request, then resubmit through the safe path. A settlement that confirms first cannot be undone.

### Request state and share locks

* There is **one request per `(user, VAULT, ASSET)`**. Updating the same pair replaces it rather than adding to it. The example requires clearing any stored request, including an expired one, before reapproving. This integration supports only the `(VAULT, ASSET)` pair; do not use arbitrary offer/want tokens.
* `updateAtomicRequest` does not validate every balance, allowance, and price condition. Check sufficient shares and Queue allowance, positive `atomicPrice`, and a nonzero rounded payout. **A zero-price request can give away shares for nothing; never submit one.**
* Shares remain in the wallet until settlement. An active request for this `(VAULT, ASSET)` pair blocks **all outbound share `transfer` and `transferFrom` calls from that wallet**, not just the requested amount. Queue settlement transfers are exempt from this additional lock; Teller deny-list checks still apply.
* Cancellation, fulfillment, or `block.timestamp > deadline` removes this request lock. Expiry does not delete storage or settle the request. Separate Teller restrictions still apply.
* The example uses **7 days** until expiry. This is neither a mandatory wait nor a fulfillment guarantee. Resubmit expired requests if you still want to withdraw.
* Queue pause **also blocks cancellation**. Ask the team to unpause. Revoking share allowance blocks settlement but leaves the active request lock until cancellation or expiry. If cancellation races settlement, whichever confirms first takes effect.
* `offerAmount == 0` alone does not distinguish never-created, canceled, and fulfilled requests. Use `AtomicRequestFulfilled` and token balances to confirm payment. The recipient is always the requester; there is no withdrawal receiver argument. A wrapper contract submitting a request must hold/approve the shares and receives the payout itself.

### Canceling requests and revoking approvals

**Cancel with `updateAtomicRequest` and `offerAmount=0`. The safe entrypoint rejects zero amounts and cannot be used to cancel.**

These are separate transactions. `cancelWithdraw()` only clears the request and returns its cancellation receipt. `revokeQueueAllowance()` only sets BoringVault's allowance to Queue to zero.

```typescript theme={null}
const cancellationReceipt = await api.cancelWithdraw();
// Cancellation is confirmed here, even if the next signature is rejected.
await api.revokeQueueAllowance();
const status = await api.getStatus(); // Check request.offerAmount === 0n and allowance === 0n
```

| Situation | Required action |
| - | - |
| Deposit fails or is abandoned after approval | Use `revokeDepositAllowance()` to revoke ASSET → Router approval |
| Request fails or is abandoned after share approval | Read the request state, then use `revokeQueueAllowance()` to revoke approval |
| Cancellation succeeds but revocation fails or is rejected | Display cancellation success separately from incomplete approval cleanup; retry only revocation |
| Request expires | It cannot settle, but storage and allowance remain. Clear the stored request and revoke allowance |
| Request is fulfilled | Its amount becomes zero. An exact finite share allowance is consumed by the transfer; still check and revoke any remainder |
| Queue is paused or an open request has an unwanted price | Call `revokeQueueAllowance()` directly to revoke settlement permission. Also cancel the request when Queue is unpaused |

**Revocation does not delete the request or release its share transfer lock.** The lock remains until cancellation or expiry. Reapproving before expiry can make the stored request executable again at its old price. Read and cancel unwanted requests before reapproving. The example also requires explicitly clearing expired stored requests before granting a new approval.

Token `approve(Queue, 0)` is a separate call even when Queue is paused. Revocation and cancellation take effect **only after on-chain confirmation** and cannot undo settlement confirmed first. For pending or timed-out transactions, inspect the transaction hash, request, allowance, and payment events before retrying. Run one request/approval workflow per account at a time. Stop and reconnect if the wallet account or network changes.

## 5. Rates and operational state

`getRate()` returns the **last on-chain checkpoint**. Time passing or calling this view does not accrue interest in storage. An authorized Controller `poke` applies a fixed curve anchored to absolute time:

```text theme={null}
newRate = floor(anchorRate × ratePerSecond^(now - anchorTime))
```

`previewNextRate()` returns the projected `(newRate, dt)` as a view. It is **not the rate currently used for deposits/redemptions or a guaranteed payout**; pause and update bounds can make the actual poke fail. The rate depends only on absolute elapsed time, so **poke cadence does not change the realized APY** — a late or irregular poke lands on the same curve, with no drift. `aprBps()` returns the effective APY in bps (2%). Integer rounding only shaves sub-unit amounts at the floor of the rate.

Rate updates and withdrawal settlement are keeper operations. Users cannot call Controller `poke`, Keeper `poke/redeemSolve`, or Queue `solve` with ordinary permissions. See the Kairos testing section for manual test coordination.

| Check | Read / effect |
| - | - |
| Checkpointed rate | `Accountant.getRate()`; readable while paused |
| Transaction rate | `Accountant.getRateSafe()`; reverts if Accountant is paused |
| Accountant pause | `accountantState().isPaused` (**zero-based output index 8**); blocks deposits and redemption |
| Router pause / policies | `paused`, `supplyCap`, `whitelistEnabled`, `whitelisted(account)`; deposit policies |
| Teller pause / restrictions | `isPaused`, `shareLockPeriod`, `fromDenyList`, `toDenyList`, `operatorDenyList`; Router requires zero shareLockPeriod |
| Queue pause | `isPaused()`; blocks request creation, changes, cancellation, and fulfillment |
| Keeper pause | `paused()`; restricts team settlement operations, separate from user requests |
| Rate freshness | Controller `lastPokeAt()`, `aprBps()`, `previewNextRate()` |

The example's `getStatus()` reads balances, rates, requests, and pauses at one block. `sharesLocked` indicates **only the withdrawal request lock for this `(VAULT, ASSET)` pair**, not whether every transfer restriction passes. Successful reads do not guarantee a write will pass cap, whitelist, deny-list, liquidity, or other checks. Simulate writes and verify receipts as shown.

### Default rate-update cadence

**The regular keeper operating target is an update every 24 hours for USDT (6 decimals) and every hour for JPYC (18 decimals).** JPYC's higher rate precision makes integer rounding at short intervals much smaller. In both cases, `getRate()` stays unchanged between successful checkpoints. Deposits, withdrawal requests, and reads do not automatically poke; withdrawal settlement is a separate operation. These intervals are not withdrawal waiting periods or completion guarantees.

See the architecture page's [default keeper operating rules](/en/protocol/fixed-rate-vault.md#default-keeper-operating-rules) for asset-specific cadences, execution checks, delay/failure handling, and rounding comparisons. Read `lastPokeAt()` and the rate to confirm updates; do not show projected rates or scheduled times as confirmed values. The cadence is off-chain operational configuration, not automatic execution selected by contract decimals. Both Controller's `minInterval` and Accountant's `minimumUpdateDelayInSeconds` must permit it, and APR changes can create an additional checkpoint. Coordinate manual rate-update and settlement tests with the team on Kairos, which has no automated keeper.

## 6. Events and failure handling

The full ABI JSON includes events and custom errors. Index events **at the specified contract address** with `getLogs` or `watchContractEvent`. Choose an indexing start block for the selected deployment. For Kairos integration tests, use `verifiedBlock` from the downloadable deployment JSON.

| Purpose | Contract / event |
| - | - |
| User deposits | **Router `Deposited(payer, receiver, assets, shares)`** |
| Share mint / burn | BoringVault `Enter`, `Exit`, ERC-20 `Transfer` |
| Rate changes | Accountant `ExchangeRateUpdated`; Controller `Poked` |
| Request creation / update / cancellation | Queue `AtomicRequestUpdated`; `amount = 0` means cancellation |
| Completed payment | Queue `AtomicRequestFulfilled`; `wantAmountReceived` is the actual payment |

Router uses Teller `bulkDeposit`, so the Teller **`Deposit` event is not emitted**. Distinguish Teller `BulkDeposit` from Router `Deposited`.

| Symptom | Check / action |
| - | - |
| Transaction will not submit | Match the wallet and RPC to the selected network and obtain its gas token |
| Deposit allowance missing | The underlying spender must be the **Router proxy** |
| `MinimumMintNotMet` / deposit simulation failure | Re-read the rate; check minimum shares, balance, cap, whitelist, deny lists, and pauses |
| Share transfer failure | Cancel an active request or check expiry; also check Teller restrictions |
| Request stays pending | Check request expiry and settlement status; for Kairos, coordinate manual settlement with the team |
| Settlement failure | Coordinate deadline, share balance, Queue allowance, request price, pauses, and liquidity with the team |
| Request amount is zero but payment is unclear | Check Fulfilled events and ASSET transfers in that transaction |

[EarnUSDT integration](/en/developers/integrate-earn-usdt.md) uses the separate SuperEarnRouter/CooldownVault product. Do not mix those interfaces with this VedaRouter/AtomicQueue flow.

## 7. Test on Kairos

On **Kairos testnet (chain ID 1001)**, use **mockUSDT (`mUSDT`, 6 decimals)** as `ASSET`. mockUSDT and testnet vault shares have no economic value. The test deployment connects mockUSDT to a single-asset CustomVault.

* RPC: `https://public-en-kairos.node.kaia.io` ([official Kaia RPC reference](https://docs.kaia.io/references/public-en/)).
* Get test KAIA for gas from the [Kaia Faucet](https://www.kaia.io/faucet). Obtain mockUSDT separately by calling `mint` below.
* Verified **2026-10-14**, block **230316651**. Initial APR **200 bps = 2%**, unlimited supply cap, whitelist off, Teller share-lock period zero. Governance may change settings; read them when integrating.

Download the **Kairos** address set, full ABIs, and the same TypeScript module used above. The ABI bundle is keyed by `asset`, `vedaRouter`, `boringVault`, `accountant`, `atomicQueue`, and the other deployment JSON keys.

Select Kairos in your wallet, then call `mockUSDT.mint(receiver, amount)` or use the helper:

```typescript theme={null}
import { connectFixedRate, kairosDeployment } from './client';

const api = await connectFixedRate(provider, kairosDeployment);
await api.mintMockUSDT('1000');     // Anyone can mint 1,000 mUSDT to their wallet
await api.deposit('100');
const requestReceipt = await api.requestWithdraw('10');
// Send requestReceipt.transactionHash to the team to arrange settlement.
```

`mint` has no role restriction, daily limit, or approval requirement. You can obtain test tokens without team approval. mockUSDT does not support EIP-2612 permit; use `approve` followed by `deposit`. The helper performs the approval for you. Test minting is available only for the published Kairos mockUSDT.

> [!NOTE]
>
> **There is no automated keeper on testnet.** You can mint, deposit, submit/cancel withdrawal requests, and read state yourself. **To test interest updates (`poke`) or completed redemptions, contact the SuperEarn team to coordinate an integration test.** Email [contact@superearn.io](mailto:contact@superearn.io) with your Kairos wallet address, request transaction hash if available, amount, and preferred time. Waiting alone does not update the rate or fulfill a withdrawal.

To test a redemption including accrued interest, use **team poke → read updated rate → submit fresh request → team settlement**. Cancel an existing request before replacing it. At the initial rate of `1_000_000` and a 2% target APY, the rate first increases by one unit about **1,593 seconds (26 minutes 33 seconds)** after the anchor. The rate is a function of elapsed time since the anchor, so earlier pokes do not delay this; they can advance `lastPokeAt` without changing the rate.

mockUSDT generates no yield. The team prepares mock liquidity for interest payouts and confirms the settlement schedule.


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