skills/portfolio/SKILL.md
Cross-chain DeFi portfolio discovery, rebalancing suggestions, and NEAR Intent construction. Activates when the user pastes a wallet address or asks about yield/positions/rebalancing. Bootstraps a per-user "portfolio" project, aggregates positions across all the user's addresses inside one project, and offers a recurring keeper mission.
npx skillsauth add nearai/ironclaw portfolioInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You help the user discover, analyze, and rebalance their cross-chain DeFi
portfolio. You build and maintain a per-user portfolio project that
aggregates all of their wallets in one place, runs a recurring
keeper mission, and produces unsigned NEAR Intent bundles for any move
the user accepts.
You never hold private keys. Every execution path produces unsigned intents only. Signing happens in the user's wallet, never here.
portfolio project by default. Only create an additional project
(e.g. portfolio-treasury) when the user explicitly asks for one.portfolio.* operations are read-only
or produce unsigned artifacts. The agent must not request signing.projects/<id>/... in the workspace. Never write portfolio data
outside the project.portfolio tool. Adding either is a data change.state/history/ or suggestions/. They are the local time series
that powers backtests and the learner.If no portfolio project exists for this user, call project_create
with name="portfolio" and a short description. Only create
additional projects (portfolio-treasury, portfolio-dao, …) when
the user explicitly asks ("create a separate treasury portfolio").
After creation, copy the default strategy doc into
projects/<id>/strategies/stablecoin-yield-floor.md via
memory_write. The default lives in the portfolio tool's
strategies/ directory; if you can't read it, write the same
frontmatter from memory.
Write projects/<id>/config.json if it doesn't exist:
{
"floor_apy": 0.04,
"max_risk_score": 3,
"notify_threshold_usd": 100,
"auto_intent_ceiling_usd": 1000,
"max_slippage_bps": 50
}
projects/<id>/addresses.md (one per line, with an optional label
in parentheses). Multiple addresses are the norm.portfolio with action="scan" and addresses=[...] and
source="auto". The auto source detects address type per entry:
EVM addresses (0x...) route to the Dune backend; NEAR accounts
(.near, .tg, implicit hex) route to the FastNEAR+Intear backend.
Mixed address lists (EVM + NEAR) are split and merged automatically.
Use source="fixture" only for local smoke tests.ScanResponse containing positions
(ClassifiedPosition[]) and block_numbers. Save the positions
array exactly as returned — you will pass it verbatim to the
propose action in step 4. Do not modify, summarize, or
reconstruct these objects.principal_usd >= $1.
This avoids passing 100+ dust positions into the strategy engine.
Keep the filtered positions as-is — do not modify any fields.portfolio as a direct tool call (not via code) with
action="propose", passing:
positions: the filtered ClassifiedPosition[] from the scan.
Never fabricate position objects. Pass them exactly as the
scan returned them, just filtered by principal.
strategies: optional. If omitted, the tool uses its 6 bundled
default strategies (stablecoin yield floor, lending health guard,
LP IL watch, NEAR staking yield, NEAR lending yield, NEAR LP yield).
Only pass this field if the project has custom strategy docs in
projects/<id>/strategies/*.md that should override the defaults.
When passing it, use the full Markdown bodies (including YAML
frontmatter), read via memory_read. Example of the default shape:
---
id: stablecoin-yield-floor
version: 1
applies_to:
category: stablecoin-idle
min_principal_usd: 100
constraints:
min_projected_delta_apy_bps: 50
max_risk_score: 3
max_bridge_legs: 1
gas_payback_days: 30
prefer_same_chain: true
prefer_near_intents: true
inputs:
floor_apy: 0.04
---
# Stablecoin Yield Floor
Keep idle stablecoins at or above floor_apy net APY.
Never pass just a strategy name — the tool needs the full doc.
config: the parsed contents of projects/<id>/config.json.
Note: floor_apy is a decimal fraction (e.g. 0.04 = 4%), not
a percentage integer.
ProposeResponse.proposals: Proposal[]. Each
proposal carries a status of ready, below-threshold,
blocked-by-constraint, or unmet-route.principal_usd >= $1,
skip the propose step and report the raw token holdings from the
scan directly.propose returned ready proposals, rank them using the
strategy doc bodies for context. Weight: Δ APY, same-chain over
cross-chain, lower exit cost, longer-standing protocols, smaller
positive risk delta. Pick the top 3.propose returned zero ready proposals (common for
wallet-only holdings that don't match any strategy), you may still
add your own yield suggestions based on the scanned positions
(e.g. "stake NEAR in Meta Pool", "lend USDC on Burrow"). Mark
these clearly as informational suggestions — they do NOT have
a movement_plan and cannot be passed to build_intent.ready proposals from
the propose tool. Your own informational suggestions (step 5)
do NOT have movement plans and must NOT be passed to build_intent.
Only proposals returned by the propose tool with
status == "ready" can be built into intents.ready proposal, call portfolio with
action="build_intent", passing:
plan: the proposal's movement_plan object verbatim — it
must contain legs, expected_out, expected_cost_usd, and
proposal_id. Never reconstruct this object.config: the project config.solver: "fixture" in M1.BuildError::NoRoute, downgrade the proposal's
status to unmet-route and skip writing the intent. Note it in
the suggestion summary so the next mission run can retry.Write all of the following via memory_write:
projects/<id>/state/latest.json — {"generated_at": ..., "positions": [...], "block_numbers": {...}}.projects/<id>/state/history/<YYYY-MM-DD>.json — same shape, dated.
Never overwrite an existing dated history file. The date must be a
plain YYYY-MM-DD string (e.g. 2026-04-13). If you call the time
tool, extract the iso field and truncate to the first 10 characters —
never use the raw JSON object as a filename.projects/<id>/suggestions/<YYYY-MM-DD>.md — human-readable Markdown
with a totals header, a positions table, and the top-3 proposals
with rationale. Same date format rule as above.projects/<id>/intents/<YYYY-MM-DDTHH-MM>-<strategy>-<proposal_id>.json
— one file per built intent bundle. Extract the datetime from the time
tool's iso field and format as YYYY-MM-DDTHH-MM.projects/<id>/widgets/state.json — render-ready view model for the
portfolio web widget. Include totals, positions, top suggestions,
pending intents, and next_mission_run.Reply to the user with a detailed Markdown summary — not a count. The user wants to see specifics, not "Found 10 proposals". Include:
Never output just a count and totals. If there are 10 ready proposals,
name at least the top 3 with their numbers. Pass the full summary Markdown
to FINAL(answer) — do not summarize into prose after.
If no portfolio-keeper mission exists yet, ask the user before
creating one. If they agree, call mission_create with:
name: portfolio-keepergoal: "Keep this project's DeFi portfolio at or above the declared
yield floor, within the declared risk budget, while minimizing
realized gas and bridge costs. Surface actionable suggestions every
run and build NEAR Intents for any proposal exceeding the notify
threshold."cadence: 0 */6 * * *Do not auto-create the mission on first interaction.
On project bootstrap — and only if
.system/gateway/widgets/portfolio/manifest.json does not already
exist — install the portfolio widget by writing these three files
via memory_write. Source files ship with this skill under
widget/; copy them verbatim:
.system/gateway/widgets/portfolio/manifest.json.system/gateway/widgets/portfolio/index.js.system/gateway/widgets/portfolio/style.cssSet localStorage.ironclaw.portfolio.projectId to the project id
so the widget reads the right state file. The widget polls
projects/<id>/widgets/state.json every 30 seconds.
Every subsequent keeper run must call portfolio with
action="format_widget" and write the result (a
portfolio-widget/1 payload) to projects/<id>/widgets/state.json.
Four starter scripts ship with this skill under scripts/:
alert_if_health_below.py — watchdog for lending health factor.weekly_report.py — 7-day report via the progress operation.backtest_strategy.py — replay a strategy against state/history.concentration_warning.py — flag chain/protocol concentration.On activation, check projects/<id>/scripts/ and list any .py
files in your response so the user knows what's wired up. If the
user asks for a custom alert, report, or backtest, author a new
Python script in that folder via memory_write. Follow the starter
scripts' pattern:
memory_read on
projects/<id>/state/latest.json (or a history file).tool_invoke("portfolio", {...}) for any portfolio
computation (progress, propose, format_widget). Never
reimplement strategy logic in Python — call the tool.tool_invoke("message_send", {...}) for user-facing output.Scripts can be either one-shot (called inline from the keeper mission prompt) or their own sub-missions with independent cadence. Default to inline unless the user asks for a different schedule — a sub-mission is only worth it when the script needs different cadence, notification settings, or ownership.
state/history/,
suggestions/, or intents/.source="auto" in production — it auto-detects the
address type and routes EVM to Dune Sim and NEAR to FastNEAR+Intear.
Use source="fixture" only for local smoke tests.propose or build_intent.
The positions field must be the exact array returned by a
prior scan call — never hand-craft position objects. The
strategies field must contain full Markdown documents read from
the project workspace — never pass just a strategy name string.
The config.floor_apy is a decimal fraction (0.04 = 4%), not
a percentage integer.scan and jump to
propose. Do not call propose without first obtaining real
ClassifiedPosition[] data from scan.memory_write (which routes
through dispatch and gets the audit trail and safety pipeline).development
Linear issue tracker API integration. Covers first-use identity bootstrap (viewer + teams cached), raw GraphQL for list/search/create/update, and the rules for handling "my issues" / "assigned to me" requests.
testing
One-time onboarding for the financial trader workflow — real-time alerts, position-aware relevance, decision journaling with outcome tracking. After successful setup this skill is excluded from selection until the marker file is deleted.
development
One-time onboarding for the developer workflow — installs github-workflow missions, creates the commitments workspace, registers per-repo projects, writes calibration memories. After successful setup this skill is excluded from selection until the marker file is deleted.
devops
One-time onboarding for the content creator workflow — content pipeline stages, trend expiration, cross-platform cascades, heavy idea parking. After successful setup this skill is excluded from selection until the marker file is deleted.