Portfolio Analytics
Aggregate every Scallop position a user holds — supplied assets, all obligations (with debts + collaterals + USD values), spool stakes, veSCA — into a single view. Plus protocol-level TVL.
Overview
ScallopQuery exposes three composite read methods that fan out across all of a user's positions, plus a protocol-wide TVL view. The assembly logic lives in buildUserPortfolio in src/services/query/portfolioCalculations.ts; the I/O entry points are on src/models/scallopQuery/index.ts — call them on the query instance, not via internal subpaths.
ScallopQuery method |
Returns |
|---|---|
getUserPortfolio({ walletAddress?, indexer? }) |
Lendings + obligation accounts (as borrowings) + pending rewards + veSCAs, all priced |
getLendings(poolCoinNames?, ownerAddress?, args?) |
Just the supplied positions (cheaper) |
getTvl({ indexer? }) |
Protocol-wide TVL (supplyValue, borrowValue, totalValue, plus the lending/collateral split) |
The Python SDK (sui-scallop-sdk >= 0.3.0a1) wraps the same views: query.get_user_portfolio(address), query.get_lendings(address), query.get_tvl() — the examples below are TypeScript, but you are not locked to it. All TS shapes on this page were verified against @scallop-io/sui-scallop-sdk v4.3.0.
Full User Portfolio
import { Scallop } from '@scallop-io/sui-scallop-sdk';
const scallopSDK = new Scallop({ networkType: 'mainnet' });
await scallopSDK.init();
const query = scallopSDK.client.query;
const portfolio = await query.getUserPortfolio({ walletAddress: '0xUSER' });
// Real shape (abridged — see buildUserPortfolio in
// src/services/query/portfolioCalculations.ts for the full structure):
// {
// ...totals,
// lendings: [
// {
// coinName, symbol, coinType, coinPrice, coinDecimals,
// suppliedCoin, suppliedValue, stakedCoin,
// supplyApr, supplyApy, incentiveApr,
// }
// ],
// borrowings: [ // note: NOT "obligations"
// {
// obligationId,
// totalDebtsInUsd, totalCollateralInUsd, riskLevel,
// availableCollateralInUsd, totalUnhealthyCollateralInUsd,
// collaterals: [ { coinName, depositedCoin, depositedValueInUsd, ... } ],
// borrowedPools: [ { coinName, borrowedCoin, borrowedValueInUsd,
// borrowApr, borrowApy, incentiveInfos, ... } ],
// }
// ],
// pendingRewards: { lendings: [...], borrowIncentives: [...] },
// veScas: [ { veScaKey, lockedScaInCoin, lockedScaInUsd, currentVeScaBalance,
// remainingLockPeriodInDays, unlockAt /* epoch ms */ } ],
// }
Render a Dashboard
console.log('=== Lending Positions ===');
for (const p of portfolio.lendings) {
if (!p.suppliedCoin) continue;
console.log(
`${p.symbol.padEnd(8)} supplied=${p.suppliedCoin}` +
` ($${p.suppliedValue.toFixed(2)}) APY=${(p.supplyApy * 100).toFixed(2)}%`
);
}
console.log('\n=== Obligations ===');
for (const ob of portfolio.borrowings) {
const flag = ob.riskLevel >= 1 ? 'LIQUIDATABLE'
: ob.riskLevel >= 0.8 ? 'AT RISK'
: 'OK';
console.log(
`${ob.obligationId.slice(0, 10)}... risk=${ob.riskLevel.toFixed(3)} ` +
`debt=$${ob.totalDebtsInUsd.toFixed(2)} ` +
`coll=$${ob.totalCollateralInUsd.toFixed(2)} ${flag}`
);
}
for (const v of portfolio.veScas ?? []) {
console.log('\n=== veSCA ===');
console.log(`Locked: ${v.lockedScaInCoin} SCA`);
console.log(`Voting power: ${v.currentVeScaBalance}`);
// unlockAt is already in milliseconds — do not multiply by 1000
console.log(`Unlocks: ${new Date(v.unlockAt).toISOString()}`);
}
Lightweight: Lendings Only
When you don't need obligation/veSCA data (e.g. a yield aggregator), skip the expensive parts:
// First positional arg is poolCoinNames (omit for "all"), second is owner address.
const lendings = await query.getLendings(undefined, '0xUSER');
const totalSupplied = Object.values(lendings)
.filter((l): l is NonNullable<typeof l> => !!l)
.reduce((sum, l) => sum + l.suppliedValue, 0);
console.log(`Total supplied: $${totalSupplied.toFixed(2)}`);
Protocol TVL
const tvl = await query.getTvl();
console.log(`Supply (lending + collateral): $${tvl.supplyValue}`);
console.log(` - lending: $${tvl.supplyLendingValue}`);
console.log(` - collateral: $${tvl.supplyCollateralValue}`);
console.log(`Borrow: $${tvl.borrowValue}`);
console.log(`TVL: $${tvl.totalValue}`);
When you pass { indexer: true } the response is augmented with *ChangeRatio fields (e.g. supplyValueChangeRatio) sourced from the Scallop indexer. Useful as a denominator when comparing a user's exposure against protocol size, or for an indexer that snapshots TVL on a schedule.
Composition with Other Skills
| Need | Skill |
|---|---|
| Borrow/supply caps per asset | market-limits |
| Per-obligation health, liquidation thresholds | obligation-manager |
| Find liquidatable positions in the returned obligations list | liquidation-helper |
| veSCA detail (rewards, lock extensions) | vesca |
Performance Notes
getUserPortfolio is a fan-out: market data + all owned obligations + all market coins + veSCA, each priced. Expect several seconds on a cold call. For dashboards:
- Cache the response per address with a TTL (30–60s for read-only UIs).
- Stream individual sections (
getLendingsthen per-obligation reads) when you only need part of the data. - For server-side monitors, prefer a single
getUserPortfolioper tick over many discrete calls — fewer RPC round-trips. - Pass
{ indexer: true }togetUserPortfolio/getTvlto take the indexer fast-path; it falls back to on-chain reads automatically if the indexer is unavailable.
Error Handling
- The
veScasarray is empty when the user holds no veSCA — not an error. - Empty positions are filtered out (
lendingsonly includes entries with positive withdrawable amounts;borrowingsonly includes obligations with debt or collateral). - RPC failures bubble up as the underlying error; wrap in a retry with exponential backoff for keeper code.
References
- query-data Skill - Lower-level reads
- obligation-manager Skill - Per-obligation drilldown
- market-limits Skill - Per-asset caps
- supported-coins.md - Asset list