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

# Wallet integration

> Display lending positions, select a market, and prepare a supply transaction for a browser or mobile wallet.

Use the API to read the data your interface needs. Use the SDK to build an
unsigned transaction when the user selects an action. Soroban RPC prepares
that transaction, and your wallet signs it.

Each lending account has a position NFT. Display its supplied assets and debt
as a position, rather than a receipt token in the wallet balance list.
Transferring the NFT transfers control of the whole account, including its debt.

<Info>
  The v1 routes are available on mainnet and testnet. These examples read mainnet.
  For testnet transactions, select testnet and use its API, RPC, contracts,
  and token addresses together.
</Info>

## 1. Read positions and supported assets

Call the API from any language. These responses are arrays.

```bash theme={"system"}
curl 'https://api.xoxno.com/stellar-lending/v1/assets'
curl 'https://api.xoxno.com/stellar-lending/v1/assets?usage=borrow'
curl 'https://api.xoxno.com/stellar-lending/v1/users/GAVWFZK5BGCGBWH4O2CXAHXVRIYVAMGCTZJ24IPLVNLT2WQ2LJBDEEBP/positions'
```

The first call lists collateral assets; the second lists borrow assets.
The third returns one item per indexed position NFT owned by the address.
Each position includes balances, prices, names, logos, and risk estimates.
You do not need a separate token catalog or client-side balance calculations.

For direct HTTP reads, follow `Link: <...>; rel="next"` until the header is
absent. Resolve relative links against the URL of the page you just read.
Keep requests on the same endpoint, reject cycles, and check HTTP errors.
An empty page can still have a next link.

The optional SDK read client handles pagination. It has no Stellar SDK runtime
dependency. Public response types are available from the same entry.

```bash theme={"system"}
npm install @xoxno/sdk-js@1.0.228
```

For React Native, the optional reader needs standards-compatible `URL` and
`URLSearchParams`, plus `fetch` and cancellation support. Your wallet runtime
may already provide them. If compatible URL support is missing, install
`react-native-url-polyfill@2.0.0`.
Load `import 'react-native-url-polyfill/auto'` first in the app entry point,
before SDK and app imports. Direct HTTP reads do not require the SDK reader.

```ts theme={"system"}
import {
  createStellarLendingReadClient,
  type LendingPosition,
} from '@xoxno/sdk-js/stellar-lending/read'

const network = 'mainnet' as const
const apiUrl = {
  mainnet: 'https://api.xoxno.com',
  testnet: 'https://testnet-api.xoxno.com',
}[network]
const read = createStellarLendingReadClient({ baseUrl: apiUrl })

export async function portfolio(owner: string) {
  const [positions, assets] = await Promise.all([
    read.positions(owner),
    read.assets(),
  ])
  return { positions, assets }
}
```

Use `read.assets({ usage: 'borrow' })` for a borrowing screen. Pass an
`AbortSignal` when a screen closes or the selected wallet changes.
See the [integrator API reference](/docs/stellar-lending/dev/integrator-api) for
fields, pagination, and freshness rules.

## 2. Select a market

Let the user choose an asset and one of its `spokes`. Preserve `hubId`,
`spokeId`, and `sac` together. The same token can appear in several hubs or spokes.
A symbol such as `USDC` is a label, not a transaction identifier.

For new supply, check `canOpenAccount`, `canSupply`, and
`supplyCapacity.amountRaw`. For a top-up, select an existing account and use its
spoke. These fields help your interface explain whether supply appears available.
Simulation and ledger execution enforce the current contract rules.

Keep API data, RPC, contract addresses, market selection, and signing passphrase
on the same network. Use [addresses](/docs/stellar-lending/dev/addresses) to verify
contract wiring. Reading the API does not require an API key.

## 3. Prepare a supply transaction for the wallet

The caller needs a funded Stellar account, XLM for fees, and the selected tokens.
XLM supply uses its Stellar Asset Contract address, not the string `XLM`. Keep
enough spendable XLM for fees and account reserves.

Builders are synchronous and make no RPC requests. Their `xdr` is unsigned and
unprepared. Fetch the current source sequence, build the transaction, then call
`prepareStellarTxXdr` before signing. Preparation adds the Soroban footprint,
authorization entries, and resource fees.

The following functions continue from the initialization above. Supply amounts
are decimal integer strings in **token base units**. For a 7-decimal token,
`"1.25"` becomes `"12500000"`. Use `parseStellarTokenAmount` for decimal input.
It checks precision, positive amounts, and the i128 range through the SDK.

```bash theme={"system"}
npm install @xoxno/sdk-js@1.0.228 @stellar/stellar-sdk@16.3.0
```

Install one copy of the Stellar SDK so the builder and RPC helper use the same
transaction classes. This example continues from the network selected above.

For React Native transaction imports, initialize `TextEncoder` and `TextDecoder`
first. Use your wallet integration's Stellar SDK and Metro runtime setup.
Verify preparation, signing, submission, and recovery on the target device.

```ts theme={"system"}
import { rpc } from '@stellar/stellar-sdk'
import {
  assertStellarPreparedTxAuth,
  buildStellarSupplyTx,
  getStellarDeployment,
  prepareStellarTxXdr,
  parseStellarTokenAmount,
  type StellarSupplyArgs,
} from '@xoxno/sdk-js/stellar-lending'

const deployment = getStellarDeployment(network)
const server = new rpc.Server(deployment.sorobanRpcUrl)

export async function prepareSupply(caller: string, selection: StellarSupplyArgs) {
  const source = await server.getAccount(caller)
  const built = buildStellarSupplyTx({
    network,
    caller,
    sourceSequence: source.sequenceNumber(),
    controllerAddress: deployment.lendingController,
    timeoutSeconds: 300,
  }, selection)
  const preparedXdr = await prepareStellarTxXdr(server, built.xdr, {
    network,
    invokedContractId: deployment.lendingController,
  })
  assertStellarPreparedTxAuth(preparedXdr, {
    network, caller, builtXdr: built.xdr,
  })
  return preparedXdr
}
```

Pass `asset: selectedAsset.sac`, `hubId`, `spokeId`, and a positive raw amount
string as `amount`. Use the SDK helper to convert decimal input:

```ts theme={"system"}
const amount = parseStellarTokenAmount(inputAmount, selectedAsset.decimals)
```

The helper accepts decimal strings, never JavaScript numbers. It rejects
excess precision instead of rounding and stops when token decimals are missing.
Show its validation error before signing.
Pass `accountNonce: '0'` to create an account. For a top-up, use the selected
position's `accountId` and its spoke.

In an owner wallet flow, recheck `owner_of(accountId)` on
`deployment.positionNftContract` before preparation. Indexed ownership can lag
an NFT transfer. Other supply permissions are covered in
[Supply](/docs/stellar-lending/dev/supply).

The authorization check compares the prepared invocation with the builder
output and limits caller token transfers to the requested supply. If the check
fails, do not send the XDR to the wallet.

The result of `prepareSupply` is unsigned XDR ready for the wallet to sign.
Show the network, token, amount, account, spoke, hub, and estimated fee first.
Keep the prepared authorization entries; they cover the controller invocation
and nested token transfer. See [position NFT](/docs/stellar-lending/dev/position-nft)
for its ownership view.

`sourceSequence` is the Stellar source account sequence. `accountNonce` is the
lending account id. They are unrelated. A wallet can own several accounts on
one spoke. Never infer its account id from a spoke, token, or first position.

## 4. Sign, submit and confirm

Connect the prepared XDR to your wallet provider. Sign with
`deployment.passphrase` and the selected caller. Keep private keys in the wallet.
The XOXNO builder and preparation helper do not sign or broadcast.

This example receives your wallet callback and durable pending-transaction
storage. Store the network with the hash and signed XDR before submission.
XDR does not contain a network passphrase. Recovery must use the recorded
network and its RPC, even if the user has since switched networks.

Run browser signing in the client. A native wallet uses the same prepared
XDR and signing network in its own signing flow.

```ts theme={"system"}
import { Transaction, TransactionBuilder } from '@stellar/stellar-sdk'

export async function signAndSubmit(
  preparedXdr: string,
  sign: (xdr: string, networkPassphrase: string) => Promise<string>,
  persistPending: (hash: string, signedXdr: string, network: 'mainnet' | 'testnet') => Promise<void>,
) {
  const prepared = TransactionBuilder.fromXDR(preparedXdr, deployment.passphrase)
  const signedXdr = await sign(preparedXdr, deployment.passphrase)
  const signed = TransactionBuilder.fromXDR(signedXdr, deployment.passphrase)
  if (!(signed instanceof Transaction) || signed.signatures.length === 0
      || !signed.hash().equals(prepared.hash())) {
    throw new Error('Wallet must sign the prepared transaction unchanged')
  }
  const hash = signed.hash().toString('hex')
  await persistPending(hash, signedXdr, network) // Must survive reload/app switching.

  let sent: Awaited<ReturnType<typeof server.sendTransaction>>
  try {
    sent = await server.sendTransaction(signed)
  } catch {
    return { status: 'UNKNOWN' as const, hash, signedXdr }
  }
  if (sent.status === 'ERROR') {
    return { status: 'REJECTED' as const, hash, signedXdr, result: sent }
  }
  try {
    const result = await server.pollTransaction(hash, { attempts: 30 })
    if (result.status === 'SUCCESS' || result.status === 'FAILED') {
      return { status: result.status, hash, result }
    }
  } catch {
    // A polling error cannot prove whether the transaction landed.
  }
  return { status: 'UNKNOWN' as const, hash, signedXdr }
}
```

Your signing callback returns a signed XDR string. Keep the prepared
transaction's sequence, operations, fees, authorizations, and timebounds unchanged.

If preparation fails, show the error and stop before signing. Never pass
unprepared builder output to the signer.

| Result | Application behavior |
| - | - |
| `PENDING` or `DUPLICATE` from send | Show confirming; poll the original transaction hash. |
| `TRY_AGAIN_LATER` from send | Keep the envelope and query the original hash; submission is uncertain. |
| `ERROR` on the first send | Show RPC rejection and retain diagnostics. |
| `FAILED` from transaction lookup | Show confirmed ledger failure. |
| `SUCCESS` from transaction lookup | Show success, then refresh positions, indexes, and wallet balances. |
| Transport error, polling timeout, or `NOT_FOUND` | Show pending/unknown; keep the original hash and signed XDR. |

For `UNKNOWN`, query `server.getTransaction(hash)` again. If you retry submission,
send the same signed XDR and retain its hash. A later rejection does not prove
that the original failed. Do not sign a replacement while the original envelope
remains valid. After its timebound expires, resolve the original hash before
deciding whether a replacement is needed.

Expiry alone does not prove failure. If lookup remains `NOT_FOUND`, check the
original network's retained transaction history and reconcile ledger and account
state. If the available history cannot prove the outcome, keep the operation
unresolved and do not automatically repeat it.

Serialize actions from the same Stellar source account to avoid sequence races.

After `SUCCESS`, store a new lending account id from `result.returnValue`,
decoded with `scValToNative`. Reconcile it with the indexed positions and NFT
ownership.

Mark the pending record terminal on `SUCCESS`, `FAILED`, or a definitive
first-send rejection. An indexer delay means the interface is refreshing,
not that supply should be repeated.

## 5. Display the wallet's positions

Use `accountId` as the position id within its `network` and `nftContract`.
Show `spokeName`, then display `supplied` and `borrow` as separate lists.
Use each leg's `hubId` and `sac` together as its key.

| Display | Field and behavior |
| - | - |
| Token | `name`, `symbol`, and `logoUrl`. Use a default icon for missing or failed images. |
| Balance | `amount` is a decimal string; `amountRaw` is the exact integer in token base units. |
| Yield or cost | `apy` is a fraction. Multiply by 100 for a percentage. |
| USD value | `valueUsd`; the price per whole token is `priceUsd`. |
| Position image | `nftImage` is a dynamic SVG URL. It is not a balance or ownership authority. |
| Health factor | Show "No debt" when `hasDebt` is `false`. Otherwise show the value or "Unavailable". |
| Borrowing | `borrowLimitUsd` and `availableBorrowUsd` are account estimates. Market constraints still apply. |
| Net yield | `netApy` is estimated annual supply income minus debt cost, divided by positive net equity. |
| Missing data | Show `null` as "Unavailable", never as zero. |

Keep `amountRaw` and `accountId` as strings. Do not convert them to JavaScript
numbers. The API applies interest indexes; do not apply legacy RAY conversion
again. USD values and risk remain indexed estimates.

This small formatter works in a browser or React Native application:

```ts theme={"system"}
export function positionSummary(position: LendingPosition) {
  const percent = (value: number | null) =>
    value === null ? 'Unavailable' : `${(value * 100).toFixed(2)}%`
  return {
    title: position.spokeName ?? `Account ${position.accountId}`,
    health: position.hasDebt === false ? 'No debt'
      : position.healthFactor === null ? 'Unavailable'
      : position.healthFactor.toFixed(2),
    netApy: percent(position.netApy),
    supplied: position.supplied.map(leg => ({
      key: `${leg.hubId}:${leg.sac}`,
      label: `${leg.amount ?? 'Unavailable'} ${leg.symbol}`,
      apy: percent(leg.apy),
      logoUrl: leg.logoUrl,
    })),
    borrow: position.borrow.map(leg => ({
      key: `${leg.hubId}:${leg.sac}`,
      label: `${leg.amount ?? 'Unavailable'} ${leg.symbol}`,
      apy: percent(leg.apy),
      logoUrl: leg.logoUrl,
    })),
  }
}
```

Render `nftImage` with an HTML image in a browser. In React Native, use your
SVG renderer; native `Image` does not provide portable SVG support.
For example, `SvgUri` from `react-native-svg` accepts a URL. Use native `Image`
for supported raster token logos and an SVG renderer for SVG logos.

Use the public HTTPS API from a mobile device. For local development, configure
a reachable server URL: a physical device's `localhost` refers to the device,
not your computer.

Show loading, empty, error, and incomplete-data states separately. A failed
request is not an empty portfolio. Discard an old wallet result when selection
changes. Check each field even when another field makes the item incomplete.

Read spendable wallet balances through Horizon or the token contract's `balance`
view. Before receiving a classic issued asset, check its code/issuer trustline.
This includes borrow, withdraw, and possible refunds. XLM and pure Soroban tokens
need no classic trustline. A SAC address alone cannot identify the code/issuer.

## Other lending actions

Use the same build → prepare → sign → submit → confirm lifecycle for every action.

| Action | SDK builder | Key input |
| - | - | - |
| Top up supply | `buildStellarSupplyTx` | Existing `accountNonce` and its spoke |
| Withdraw | `buildStellarWithdrawTx` | Existing account; `amount: '0'` withdraws all available supply |
| Borrow | `buildStellarBorrowTx` | Existing account and token base-unit amount |
| Repay | `buildStellarRepayTx` | Existing account and positive token base-unit payment |

Repay has no repay-all sentinel. To close debt, use the ceiled debt with an accrual buffer; excess is refunded. Withdrawal and borrowing must satisfy the account's current risk checks and pool liquidity. See [withdraw](/docs/stellar-lending/dev/withdraw), [borrow](/docs/stellar-lending/dev/borrow), and [repay](/docs/stellar-lending/dev/repay).

## Agent instructions and integration checks

Agents should follow the same explicit user intent and wallet signing boundary as a human integration. Use the skills already maintained in [XOXNO/rs-lending-xlm](https://github.com/XOXNO/rs-lending-xlm/tree/main/skills):

| Skill | Use it for |
| - | - |
| [xoxno-lending](https://github.com/XOXNO/rs-lending-xlm/blob/main/skills/xoxno-lending/SKILL.md) | Routing the task; identifiers, addresses and units |
| [xoxno-lending-sdk](https://github.com/XOXNO/rs-lending-xlm/blob/main/skills/xoxno-lending-sdk/SKILL.md) | Reads, positions, transaction lifecycle and wallet frontend integration |
| [xoxno-lending-data](https://github.com/XOXNO/rs-lending-xlm/blob/main/skills/xoxno-lending-data/SKILL.md) | REST endpoint inventory, caching and indexed data semantics |
| [xoxno-lending-troubleshooting](https://github.com/XOXNO/rs-lending-xlm/blob/main/skills/xoxno-lending-troubleshooting/SKILL.md) | Simulation, RPC and contract errors |
| [xoxno-swap-aggregator](https://github.com/XOXNO/rs-lending-xlm/blob/main/skills/xoxno-swap-aggregator/SKILL.md) | Optional swaps and composed lending strategies |

The SDK skill includes `reads.md`, `positions.md`, `transactions.md`, and `frontend.md`; the data skill includes `api.md`. Check the installed SDK exports and deployed routes before copying an example.

Before shipping, verify:

* The selected network matches API, metadata, contracts, RPC, and the signing passphrase.
* The user selected an explicit reserve tuple and, for an existing position, an explicit account id with current NFT ownership.
* Amounts use integer base units; v1 positions need no RAY conversion; missing values remain unavailable.
* Only prepared XDR reaches the signer; the signed envelope and hash survive app reloads.
* Success requires ledger `SUCCESS`; uncertain submission cannot trigger a second supply.
* Position rows, prices, balances, and ownership reconcile after confirmation, including when the indexer lags.
* Testnet covers new supply, existing-account top-up, withdrawal, wallet rejection, metadata/price unavailability, and recovery after a submission timeout.


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