Scallop portfolio analytics
Skill scallop-io/scallop-skills/skills/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.From its SKILL.md
npx -y skills add scallop-io/scallop-skills --skill scallop-portfolio-analyticsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its file declares
Copied from the file, not written here
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
7.2 KB, as published. Nobody here has run it
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