# Scallop Portfolio Analytics

> Aggregate user positions and protocol TVL via the Scallop TS SDK portfolio query. Use when user says "portfolio", "TVL", "total value locked", "lendings", "user dashboard", "all obligations", "net worth", "aggregate positions", "user portfolio", "protocol stats", or asks for a single view across supplies + obligations + veSCA.

- Skill: `scallop-io/scallop-portfolio-analytics` (Agent Skill)
- Install (CLI): `npx skillmds@latest add scallop-io/scallop-portfolio-analytics`
- Raw SKILL.md: https://api.skillmd.com/api/skills/scallop-io/scallop-portfolio-analytics/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Data & Analytics
- License: MIT
- Author: scallop-io (https://skillmd.com/u/scallop-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/scallop-io/scallop-portfolio-analytics

---


# 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](../../../sui-scallop-sdk/src/services/query/portfolioCalculations.ts); the I/O entry points are on [src/models/scallopQuery/index.ts](../../../sui-scallop-sdk/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

```typescript
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

```typescript
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:

```typescript
// 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

```typescript
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](../scallop-market-limits/SKILL.md) |
| Per-obligation health, liquidation thresholds | [obligation-manager](../scallop-obligation-manager/SKILL.md) |
| Find liquidatable positions in the returned obligations list | [liquidation-helper](../scallop-liquidation-helper/SKILL.md) |
| veSCA detail (rewards, lock extensions) | [vesca](../scallop-vesca/SKILL.md) |

## 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 (`getLendings` then per-obligation reads) when you only need part of the data.
- For server-side monitors, prefer a single `getUserPortfolio` per tick over many discrete calls — fewer RPC round-trips.
- Pass `{ indexer: true }` to `getUserPortfolio` / `getTvl` to take the indexer fast-path; it falls back to on-chain reads automatically if the indexer is unavailable.

## Error Handling

- The `veScas` array is empty when the user holds no veSCA — not an error.
- Empty positions are filtered out (`lendings` only includes entries with positive withdrawable amounts; `borrowings` only 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](../scallop-query-data/SKILL.md) - Lower-level reads
- [obligation-manager Skill](../scallop-obligation-manager/SKILL.md) - Per-obligation drilldown
- [market-limits Skill](../scallop-market-limits/SKILL.md) - Per-asset caps
- [supported-coins.md](../../references/supported-coins.md) - Asset list

