@xpr-agents/openclaw 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -5
- package/openclaw.plugin.json +15 -1
- package/package.json +7 -4
- package/skills/code-sandbox/SKILL.md +30 -0
- package/skills/code-sandbox/skill.json +13 -0
- package/skills/code-sandbox/src/index.ts +212 -0
- package/skills/creative/SKILL.md +32 -0
- package/skills/creative/skill.json +13 -0
- package/skills/creative/src/index.ts +679 -0
- package/skills/defi/SKILL.md +123 -0
- package/skills/defi/dist/index.js +1 -0
- package/skills/defi/skill.json +44 -0
- package/skills/defi/src/index.ts +1788 -0
- package/skills/defi/test-read.mjs +281 -0
- package/skills/governance/SKILL.md +69 -0
- package/skills/governance/dist/index.js +632 -0
- package/skills/governance/skill.json +21 -0
- package/skills/governance/src/index.ts +656 -0
- package/skills/governance/test-read.mjs +176 -0
- package/skills/lending/SKILL.md +63 -0
- package/skills/lending/dist/index.js +1039 -0
- package/skills/lending/skill.json +29 -0
- package/skills/lending/src/index.ts +1105 -0
- package/skills/lending/test-read.mjs +156 -0
- package/skills/nft/SKILL.md +95 -0
- package/skills/nft/dist/index.js +4 -10
- package/skills/nft/skill.json +37 -0
- package/skills/nft/src/index.ts +1539 -0
- package/skills/shellbook/SKILL.md +59 -0
- package/skills/shellbook/skill.json +29 -0
- package/skills/shellbook/src/index.ts +391 -0
- package/skills/shellbook/tsconfig.json +14 -0
- package/skills/smart-contracts/SKILL.md +128 -0
- package/skills/smart-contracts/skill.json +25 -0
- package/skills/smart-contracts/src/index.ts +1327 -0
- package/skills/smart-contracts/tsconfig.json +14 -0
- package/skills/structured-data/SKILL.md +36 -0
- package/skills/structured-data/dist/index.js +501 -0
- package/skills/structured-data/skill.json +13 -0
- package/skills/structured-data/src/index.ts +597 -0
- package/skills/tax/SKILL.md +109 -0
- package/skills/tax/dist/index.js +216 -32
- package/skills/tax/skill.json +20 -0
- package/skills/tax/src/index.ts +1985 -0
- package/skills/web-scraping/SKILL.md +29 -0
- package/skills/web-scraping/dist/index.js +311 -0
- package/skills/web-scraping/skill.json +13 -0
- package/skills/web-scraping/src/index.ts +371 -0
- package/skills/xmd/SKILL.md +52 -0
- package/skills/xmd/dist/index.js +596 -0
- package/skills/xmd/skill.json +22 -0
- package/skills/xmd/src/index.ts +635 -0
- package/skills/xmd/test-read.mjs +178 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tax
|
|
3
|
+
description: Crypto tax reporting for XPR Network with regional support
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Crypto Tax Reporting
|
|
7
|
+
|
|
8
|
+
You have tools to generate crypto tax reports from on-chain XPR Network activity. Supports **New Zealand** (NZ) and **United States** (US).
|
|
9
|
+
|
|
10
|
+
### Key Facts
|
|
11
|
+
|
|
12
|
+
- **NZ tax year:** April 1 – March 31 (e.g. "2025" = Apr 2024 – Mar 2025)
|
|
13
|
+
- **US tax year:** January 1 – December 31 (calendar year, e.g. "2024" = Jan 2024 – Dec 2024)
|
|
14
|
+
- **NZ has NO capital gains tax** — all crypto gains are taxed as **income** if you're a regular trader
|
|
15
|
+
- **US HAS capital gains tax** — short-term (<1 year) taxed as ordinary income, long-term at lower rates. Uses 2024 Single filer federal brackets. Does not include state taxes or NIIT.
|
|
16
|
+
- **Cost basis methods:** FIFO (first-in-first-out) or Average Cost
|
|
17
|
+
- **All tools are read-only** — they query APIs and calculate, never transact
|
|
18
|
+
- **Default region is NZ** — pass `region: "US"` for US tax reports
|
|
19
|
+
|
|
20
|
+
### Typical Workflow
|
|
21
|
+
|
|
22
|
+
For a full tax report, the recommended sequence is:
|
|
23
|
+
|
|
24
|
+
1. `tax_get_balances` — opening balances (start of tax year) and closing balances (end of tax year)
|
|
25
|
+
2. `tax_get_dex_trades` — all Metal X DEX trading history for the period
|
|
26
|
+
3. `tax_get_transfers` — on-chain transfers, auto-categorized (staking rewards, lending, swaps, NFT sales, etc.)
|
|
27
|
+
4. `tax_get_rates` — local currency conversion rates for each token
|
|
28
|
+
5. `tax_calculate_gains` — compute taxable gains/losses using FIFO or Average Cost
|
|
29
|
+
6. `tax_generate_report` — full report with tax brackets and estimated tax
|
|
30
|
+
|
|
31
|
+
Or use `tax_generate_report` directly for a one-shot report that orchestrates all steps automatically.
|
|
32
|
+
|
|
33
|
+
### Data Sources (Mainnet Only)
|
|
34
|
+
|
|
35
|
+
- **Saltant API** — historical balance snapshots (liquid, staked, lending, yield farm)
|
|
36
|
+
- **Metal X API** — DEX trade history in CSV format (only filled trades)
|
|
37
|
+
- **Hyperion API** — raw on-chain transfer/action history
|
|
38
|
+
- **CoinGecko API** — historical and current crypto prices (set `COINGECKO_API_KEY` in .env for full historical access)
|
|
39
|
+
|
|
40
|
+
### Transfer Categories
|
|
41
|
+
|
|
42
|
+
Transfers are auto-categorized by sender/receiver:
|
|
43
|
+
|
|
44
|
+
| Category | Detection |
|
|
45
|
+
|----------|-----------|
|
|
46
|
+
| `staking_reward` | from `eosio` or `eosio.vpay` |
|
|
47
|
+
| `lending_deposit` | to `lending.loan` |
|
|
48
|
+
| `lending_withdrawal` | from `lending.loan` |
|
|
49
|
+
| `lending_interest` | from `lending.loan` with interest memo |
|
|
50
|
+
| `swap_deposit` | to `proton.swaps` |
|
|
51
|
+
| `swap_withdrawal` | from `proton.swaps` |
|
|
52
|
+
| `long_stake` | to `longstaking` (XPR long staking) |
|
|
53
|
+
| `long_unstake` | from `longstaking` |
|
|
54
|
+
| `loan_stake` | to `lock.token` or `yield.farms` (LOAN/SLOAN staking) |
|
|
55
|
+
| `loan_unstake` | from `lock.token` or `yield.farms` |
|
|
56
|
+
| `dex_deposit` | to `dex` or `metalx` |
|
|
57
|
+
| `dex_withdrawal` | from `dex` or `metalx` |
|
|
58
|
+
| `nft_sale` | from `atomicmarket` |
|
|
59
|
+
| `nft_purchase` | to `atomicmarket` |
|
|
60
|
+
| `burn` | to `eosio.null` (token burn = realized loss) |
|
|
61
|
+
| `escrow` | to/from `agentescrow` |
|
|
62
|
+
| `transfer` | everything else |
|
|
63
|
+
|
|
64
|
+
### Staking Income Rules
|
|
65
|
+
|
|
66
|
+
- **Block producer rewards** (`staking_reward`): Full amount is income at time of receipt
|
|
67
|
+
- **Long staking** (XPR via `longstaking`): Only the **excess** over the staked amount is income. E.g. stake 100 XPR, unstake 150 XPR → income of 50 XPR
|
|
68
|
+
- **LOAN staking** (via `lock.token`/`yield.farms`): Same excess-only rule as long staking
|
|
69
|
+
- **Lending interest**: Full amount from `lending.loan` with interest memo is income
|
|
70
|
+
|
|
71
|
+
### Stablecoin Handling
|
|
72
|
+
|
|
73
|
+
XUSDC and XMD are pegged to USD — their local currency value uses forex rates (USD/NZD) directly, without CoinGecko. This is more accurate than market-based pricing for stablecoins.
|
|
74
|
+
|
|
75
|
+
### Rate Sources (Priority Order)
|
|
76
|
+
|
|
77
|
+
1. **DEX trades** — derives token prices from TOKEN/XMD trade ratios (most accurate, no API limits)
|
|
78
|
+
2. **Forward-fill** — gaps between DEX trade dates use nearest prior known rate
|
|
79
|
+
3. **CoinGecko** — fallback for dates with no DEX data. Without API key: limited to 365 days. With `COINGECKO_API_KEY`: unlimited history
|
|
80
|
+
4. **Forex** — stablecoins use USD→NZD conversion rate
|
|
81
|
+
|
|
82
|
+
### Delivering the Report
|
|
83
|
+
|
|
84
|
+
`tax_generate_report` returns a `report_markdown` field — a pre-formatted Markdown document with balance sheets, trading summary, income breakdown, tax brackets, and disclaimer. To deliver it:
|
|
85
|
+
|
|
86
|
+
1. Upload `report_markdown` via `store_deliverable` with `content_type: "application/pdf"` — this is the primary deliverable
|
|
87
|
+
2. Upload `csv_exports.disposals` via `store_deliverable` with `content_type: "text/csv"` — disposals CSV
|
|
88
|
+
3. Upload `csv_exports.income` via `store_deliverable` with `content_type: "text/csv"` — income events CSV
|
|
89
|
+
4. Call `xpr_deliver_job` with ALL URLs comma-separated (PDF first): `"https://ipfs.io/ipfs/QmPDF...,https://ipfs.io/ipfs/QmDisposals...,https://ipfs.io/ipfs/QmIncome..."`
|
|
90
|
+
|
|
91
|
+
**IMPORTANT:** You MUST complete ALL steps (upload + deliver) in a single run. Do NOT stop after uploading the PDF — you must also upload the CSVs and call `xpr_deliver_job`. The job is not complete until `xpr_deliver_job` is called.
|
|
92
|
+
|
|
93
|
+
The frontend displays the primary file (PDF) prominently and lists additional files as download links.
|
|
94
|
+
|
|
95
|
+
### Known Limitations
|
|
96
|
+
|
|
97
|
+
- Only **filled DEX trades** are included (not pending orders)
|
|
98
|
+
- NFT: only buy/sell supported (not auctions)
|
|
99
|
+
- Liquidations on Metal Lending are not supported
|
|
100
|
+
- Escrow payments are tracked but not fully categorized
|
|
101
|
+
- Historical pricing accuracy depends on DEX trade activity and CoinGecko data availability
|
|
102
|
+
|
|
103
|
+
### Important Notes
|
|
104
|
+
|
|
105
|
+
- Always include the **disclaimer** from the report — this is not tax advice
|
|
106
|
+
- Suggest users **save CSV exports** for the IRD 7-year record requirement
|
|
107
|
+
- The `region` parameter defaults to `"NZ"` on all tools — pass a different region code when other regions are added
|
|
108
|
+
- Set `COINGECKO_API_KEY` in .env for best historical pricing (free Demo key removes 365-day limit)
|
|
109
|
+
- For tokens not on CoinGecko, the tool derives prices from Metal X DEX trade ratios
|
package/skills/tax/dist/index.js
CHANGED
|
@@ -6,6 +6,39 @@
|
|
|
6
6
|
* Region system: pass `region` param (default "NZ") to any tool.
|
|
7
7
|
* Adding a new region = adding an entry to REGIONS.
|
|
8
8
|
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
9
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
43
|
exports.default = taxSkill;
|
|
11
44
|
const REGIONS = {
|
|
@@ -25,6 +58,24 @@ const REGIONS = {
|
|
|
25
58
|
],
|
|
26
59
|
disclaimer: 'Estimate only. Consult a NZ tax professional. IRD requires 7 years of records.',
|
|
27
60
|
},
|
|
61
|
+
US: {
|
|
62
|
+
name: 'United States',
|
|
63
|
+
code: 'US',
|
|
64
|
+
currency: 'USD',
|
|
65
|
+
tax_year: { start_month: 1, start_day: 1 },
|
|
66
|
+
cost_basis_methods: ['fifo', 'average'],
|
|
67
|
+
has_capital_gains: true,
|
|
68
|
+
brackets: [
|
|
69
|
+
{ limit: 11600, rate: 0.10 },
|
|
70
|
+
{ limit: 47150, rate: 0.12 },
|
|
71
|
+
{ limit: 100525, rate: 0.22 },
|
|
72
|
+
{ limit: 191950, rate: 0.24 },
|
|
73
|
+
{ limit: 243725, rate: 0.32 },
|
|
74
|
+
{ limit: 609350, rate: 0.35 },
|
|
75
|
+
{ limit: Infinity, rate: 0.37 },
|
|
76
|
+
],
|
|
77
|
+
disclaimer: 'Estimate only — uses 2024 Single filer federal brackets. Does not include state taxes, NIIT (3.8%), or long-term capital gains rates. Short-term gains (<1 year hold) are taxed as ordinary income. Consult a US CPA or tax professional. IRS requires records for 3+ years.',
|
|
78
|
+
},
|
|
28
79
|
};
|
|
29
80
|
function getRegion(code) {
|
|
30
81
|
const key = (code || 'NZ').toUpperCase();
|
|
@@ -117,8 +168,53 @@ async function cgFetch(path) {
|
|
|
117
168
|
const cg = getCoinGeckoConfig();
|
|
118
169
|
return httpGetJson(`${cg.baseUrl}${path}`, cg.headers);
|
|
119
170
|
}
|
|
120
|
-
//
|
|
171
|
+
// Persistent rate cache: "SYMBOL:YYYY-MM-DD" → rate
|
|
172
|
+
// Historical prices are immutable — once fetched they never change.
|
|
173
|
+
// Stored as JSON file so rates survive container restarts.
|
|
174
|
+
const fs = __importStar(require("fs"));
|
|
175
|
+
const path = __importStar(require("path"));
|
|
176
|
+
const RATE_CACHE_FILE = process.env.RATE_CACHE_PATH || path.join(process.cwd(), 'data', 'rate-cache.json');
|
|
121
177
|
const rateCache = new Map();
|
|
178
|
+
let rateCacheDirty = false;
|
|
179
|
+
function loadRateCache() {
|
|
180
|
+
try {
|
|
181
|
+
if (fs.existsSync(RATE_CACHE_FILE)) {
|
|
182
|
+
const data = JSON.parse(fs.readFileSync(RATE_CACHE_FILE, 'utf-8'));
|
|
183
|
+
for (const [k, v] of Object.entries(data)) {
|
|
184
|
+
if (typeof v === 'number' && v > 0)
|
|
185
|
+
rateCache.set(k, v);
|
|
186
|
+
}
|
|
187
|
+
console.log(`[tax] Loaded ${rateCache.size} cached rates from ${RATE_CACHE_FILE}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch { /* start fresh */ }
|
|
191
|
+
}
|
|
192
|
+
function saveRateCache() {
|
|
193
|
+
if (!rateCacheDirty)
|
|
194
|
+
return;
|
|
195
|
+
try {
|
|
196
|
+
const dir = path.dirname(RATE_CACHE_FILE);
|
|
197
|
+
if (!fs.existsSync(dir))
|
|
198
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
199
|
+
const obj = {};
|
|
200
|
+
for (const [k, v] of rateCache)
|
|
201
|
+
obj[k] = v;
|
|
202
|
+
fs.writeFileSync(RATE_CACHE_FILE, JSON.stringify(obj));
|
|
203
|
+
rateCacheDirty = false;
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
console.error(`[tax] Failed to save rate cache:`, err);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function cacheRate(key, rate) {
|
|
210
|
+
// Only cache historical date rates (not "current")
|
|
211
|
+
if (key.includes(':current') || rate <= 0)
|
|
212
|
+
return;
|
|
213
|
+
rateCache.set(key, rate);
|
|
214
|
+
rateCacheDirty = true;
|
|
215
|
+
}
|
|
216
|
+
// Load on startup
|
|
217
|
+
loadRateCache();
|
|
122
218
|
// ── CSV Parser ───────────────────────────────────
|
|
123
219
|
function parseCSV(csv) {
|
|
124
220
|
const lines = csv.trim().split('\n');
|
|
@@ -140,8 +236,8 @@ function categorizeTransfer(account, from, to, amount, symbol, memo) {
|
|
|
140
236
|
const memoLower = (memo || '').toLowerCase();
|
|
141
237
|
const isIncoming = to === account;
|
|
142
238
|
const counterparty = isIncoming ? from : to;
|
|
143
|
-
// Staking rewards
|
|
144
|
-
if (isIncoming && (from === 'eosio' || from === 'eosio.vpay' || from === 'eosio.bpay')) {
|
|
239
|
+
// Staking rewards (block production + community fund)
|
|
240
|
+
if (isIncoming && (from === 'eosio' || from === 'eosio.vpay' || from === 'eosio.bpay' || from === 'cfund.proton')) {
|
|
145
241
|
return 'staking_reward';
|
|
146
242
|
}
|
|
147
243
|
// Lending (lending.loan)
|
|
@@ -784,7 +880,7 @@ function taxSkill(api) {
|
|
|
784
880
|
// ════════════════════════════════════════════════
|
|
785
881
|
api.registerTool({
|
|
786
882
|
name: 'tax_get_transfers',
|
|
787
|
-
description: 'Get on-chain transfer history with automatic categorization. Categories: staking_reward, lending_deposit/withdrawal/interest, swap_deposit/withdrawal, long_stake/unstake, loan_stake/unstake, dex_deposit/withdrawal, nft_sale/purchase, escrow, transfer. Paginated from Hyperion.',
|
|
883
|
+
description: 'Get on-chain transfer history with automatic categorization. Categories: staking_reward, lending_deposit/withdrawal/interest, swap_deposit/withdrawal, long_stake/unstake, loan_stake/unstake, dex_deposit/withdrawal, nft_sale/purchase, burn, escrow, transfer. Paginated from Hyperion.',
|
|
788
884
|
parameters: {
|
|
789
885
|
type: 'object',
|
|
790
886
|
required: ['account'],
|
|
@@ -887,7 +983,7 @@ function taxSkill(api) {
|
|
|
887
983
|
description: 'Array of token symbols to get rates for, e.g. ["XPR", "XUSDC", "XBTC"]',
|
|
888
984
|
},
|
|
889
985
|
date: { type: 'string', description: 'ISO date for historical rate (default: current). E.g. "2025-03-31"' },
|
|
890
|
-
region: { type: 'string', description: 'Region code
|
|
986
|
+
region: { type: 'string', description: 'Region code: "NZ" (default, NZD) or "US" (USD)' },
|
|
891
987
|
},
|
|
892
988
|
},
|
|
893
989
|
handler: async ({ symbols, date, region }) => {
|
|
@@ -901,27 +997,25 @@ function taxSkill(api) {
|
|
|
901
997
|
// Separate stablecoins from others
|
|
902
998
|
const stableSymbols = symbols.filter(s => STABLECOINS.has(s.toUpperCase()));
|
|
903
999
|
const cryptoSymbols = symbols.filter(s => !STABLECOINS.has(s.toUpperCase()));
|
|
904
|
-
//
|
|
905
|
-
|
|
1000
|
+
// Get USD → local forex rate upfront (needed for stablecoins AND USD fallback)
|
|
1001
|
+
let forexRate = 1;
|
|
1002
|
+
if (currency !== 'usd') {
|
|
906
1003
|
try {
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
forexRate = 1;
|
|
910
|
-
}
|
|
911
|
-
else {
|
|
912
|
-
const forexData = await cgFetch(`/simple/price?ids=usd-coin&vs_currencies=${currency}`);
|
|
913
|
-
forexRate = forexData['usd-coin']?.[currency] || 1;
|
|
914
|
-
}
|
|
915
|
-
const dk = date ? dateKey(date) : 'current';
|
|
916
|
-
for (const sym of stableSymbols) {
|
|
917
|
-
const key = `${sym.toUpperCase()}:${dk}`;
|
|
918
|
-
rates[key] = forexRate;
|
|
919
|
-
}
|
|
1004
|
+
const forexData = await cgFetch(`/simple/price?ids=usd-coin&vs_currencies=${currency}`);
|
|
1005
|
+
forexRate = forexData['usd-coin']?.[currency] || 1;
|
|
920
1006
|
}
|
|
921
1007
|
catch (err) {
|
|
922
1008
|
errors.push(`Forex rate error: ${err.message}`);
|
|
923
1009
|
}
|
|
924
1010
|
}
|
|
1011
|
+
// 1. Handle stablecoins via forex rate (USD → local)
|
|
1012
|
+
if (stableSymbols.length > 0) {
|
|
1013
|
+
const dk = date ? dateKey(date) : 'current';
|
|
1014
|
+
for (const sym of stableSymbols) {
|
|
1015
|
+
const key = `${sym.toUpperCase()}:${dk}`;
|
|
1016
|
+
rates[key] = forexRate;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
925
1019
|
// 2. Handle crypto tokens via CoinGecko
|
|
926
1020
|
if (cryptoSymbols.length > 0) {
|
|
927
1021
|
if (date) {
|
|
@@ -942,10 +1036,14 @@ function taxSkill(api) {
|
|
|
942
1036
|
}
|
|
943
1037
|
try {
|
|
944
1038
|
const histData = await cgFetch(`/coins/${cgId}/history?date=${ddMmYyyy}`);
|
|
945
|
-
|
|
946
|
-
|
|
1039
|
+
// Prefer local currency price; if unavailable, use USD × forex rate
|
|
1040
|
+
let price = histData?.market_data?.current_price?.[currency] || 0;
|
|
1041
|
+
if (!price) {
|
|
1042
|
+
const usdPrice = histData?.market_data?.current_price?.usd || 0;
|
|
1043
|
+
price = usdPrice * forexRate;
|
|
1044
|
+
}
|
|
947
1045
|
rates[cacheKey] = price;
|
|
948
|
-
|
|
1046
|
+
cacheRate(cacheKey, price);
|
|
949
1047
|
}
|
|
950
1048
|
catch (err) {
|
|
951
1049
|
errors.push(`CoinGecko history error for ${upper}: ${err.message}`);
|
|
@@ -966,7 +1064,11 @@ function taxSkill(api) {
|
|
|
966
1064
|
const cgId = TOKEN_TO_COINGECKO[upper];
|
|
967
1065
|
if (!cgId)
|
|
968
1066
|
continue;
|
|
969
|
-
|
|
1067
|
+
// Prefer local currency; if unavailable, use USD × forex rate
|
|
1068
|
+
let price = batchData[cgId]?.[currency] || 0;
|
|
1069
|
+
if (!price) {
|
|
1070
|
+
price = (batchData[cgId]?.usd || 0) * forexRate;
|
|
1071
|
+
}
|
|
970
1072
|
rates[`${upper}:current`] = price;
|
|
971
1073
|
}
|
|
972
1074
|
}
|
|
@@ -983,6 +1085,7 @@ function taxSkill(api) {
|
|
|
983
1085
|
}
|
|
984
1086
|
}
|
|
985
1087
|
}
|
|
1088
|
+
saveRateCache();
|
|
986
1089
|
return {
|
|
987
1090
|
rates,
|
|
988
1091
|
currency: regionConfig.currency,
|
|
@@ -1014,7 +1117,7 @@ function taxSkill(api) {
|
|
|
1014
1117
|
description: 'Rate map from tax_get_rates: {"SYMBOL:YYYY-MM-DD": rate}',
|
|
1015
1118
|
},
|
|
1016
1119
|
method: { type: 'string', description: '"fifo" (default) or "average"' },
|
|
1017
|
-
region: { type: 'string', description: 'Region code (default "
|
|
1120
|
+
region: { type: 'string', description: 'Region code: "NZ" (default) or "US"' },
|
|
1018
1121
|
},
|
|
1019
1122
|
},
|
|
1020
1123
|
handler: async ({ trades, transfers, rates, method, region }) => {
|
|
@@ -1062,9 +1165,9 @@ function taxSkill(api) {
|
|
|
1062
1165
|
required: ['account', 'tax_year'],
|
|
1063
1166
|
properties: {
|
|
1064
1167
|
account: { type: 'string', description: 'XPR Network account name' },
|
|
1065
|
-
tax_year: { type: 'number', description: 'Tax year number
|
|
1168
|
+
tax_year: { type: 'number', description: 'Tax year number. NZ: 2025 = Apr 2024–Mar 2025. US: 2024 = Jan 2024–Dec 2024' },
|
|
1066
1169
|
method: { type: 'string', description: '"fifo" (default) or "average"' },
|
|
1067
|
-
region: { type: 'string', description: 'Region code (default "
|
|
1170
|
+
region: { type: 'string', description: 'Region code: "NZ" (default) or "US"' },
|
|
1068
1171
|
balances_opening: { type: 'object', description: 'Pre-computed opening balances (skip API call)' },
|
|
1069
1172
|
balances_closing: { type: 'object', description: 'Pre-computed closing balances (skip API call)' },
|
|
1070
1173
|
trades: { type: 'array', description: 'Pre-computed trades array (skip API call)' },
|
|
@@ -1217,6 +1320,21 @@ function taxSkill(api) {
|
|
|
1217
1320
|
uniqueDates.add(dateKey(t.timestamp));
|
|
1218
1321
|
}
|
|
1219
1322
|
}
|
|
1323
|
+
// Pre-populate rates from persistent cache (historical prices are immutable)
|
|
1324
|
+
let cacheHits = 0;
|
|
1325
|
+
for (const sym of allSymbols) {
|
|
1326
|
+
const upper = sym.toUpperCase();
|
|
1327
|
+
for (const d of uniqueDates) {
|
|
1328
|
+
const key = `${upper}:${d}`;
|
|
1329
|
+
const cached = rateCache.get(key);
|
|
1330
|
+
if (cached && cached > 0) {
|
|
1331
|
+
rates[key] = cached;
|
|
1332
|
+
cacheHits++;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
if (cacheHits > 0)
|
|
1337
|
+
steps.push(`Loaded ${cacheHits} rates from cache`);
|
|
1220
1338
|
// Get USD→local forex rate (stablecoins and XMD are pegged to USD)
|
|
1221
1339
|
let forexRate = 1;
|
|
1222
1340
|
if (currency !== 'usd') {
|
|
@@ -1261,7 +1379,7 @@ function taxSkill(api) {
|
|
|
1261
1379
|
for (const [key, rate] of Object.entries(dexRatesByDate)) {
|
|
1262
1380
|
if (!rates[key]) {
|
|
1263
1381
|
rates[key] = rate;
|
|
1264
|
-
|
|
1382
|
+
cacheRate(key, rate);
|
|
1265
1383
|
}
|
|
1266
1384
|
}
|
|
1267
1385
|
// Fill gaps: for dates without a DEX trade, use nearest available DEX rate
|
|
@@ -1291,7 +1409,7 @@ function taxSkill(api) {
|
|
|
1291
1409
|
const now = Date.now();
|
|
1292
1410
|
const oneYearMs = 365 * 24 * 60 * 60 * 1000;
|
|
1293
1411
|
let cgFetches = 0;
|
|
1294
|
-
const MAX_CG_FETCHES = cgConfig.hasKey ?
|
|
1412
|
+
const MAX_CG_FETCHES = cgConfig.hasKey ? 2000 : 30;
|
|
1295
1413
|
const CG_DELAY = cgConfig.hasKey ? 100 : 200;
|
|
1296
1414
|
for (const d of sortedDates) {
|
|
1297
1415
|
const dateMs = new Date(d + 'T00:00:00Z').getTime();
|
|
@@ -1314,11 +1432,15 @@ function taxSkill(api) {
|
|
|
1314
1432
|
const ddMmYyyy = `${String(dateObj.getUTCDate()).padStart(2, '0')}-${String(dateObj.getUTCMonth() + 1).padStart(2, '0')}-${dateObj.getUTCFullYear()}`;
|
|
1315
1433
|
try {
|
|
1316
1434
|
const histData = await cgFetch(`/coins/${cgId}/history?date=${ddMmYyyy}`);
|
|
1317
|
-
|
|
1318
|
-
|
|
1435
|
+
// Prefer local currency; if unavailable, use USD × forex rate
|
|
1436
|
+
let price = histData?.market_data?.current_price?.[currency] || 0;
|
|
1437
|
+
if (!price) {
|
|
1438
|
+
const usdPrice = histData?.market_data?.current_price?.usd || 0;
|
|
1439
|
+
price = usdPrice * forexRate;
|
|
1440
|
+
}
|
|
1319
1441
|
if (price > 0) {
|
|
1320
1442
|
rates[key] = price;
|
|
1321
|
-
|
|
1443
|
+
cacheRate(key, price);
|
|
1322
1444
|
}
|
|
1323
1445
|
cgFetches++;
|
|
1324
1446
|
}
|
|
@@ -1326,6 +1448,68 @@ function taxSkill(api) {
|
|
|
1326
1448
|
await sleep(CG_DELAY);
|
|
1327
1449
|
}
|
|
1328
1450
|
}
|
|
1451
|
+
// Forward-fill again after CoinGecko to cover remaining gaps
|
|
1452
|
+
for (const sym of symbolsNeedingRates) {
|
|
1453
|
+
const upper = sym.toUpperCase();
|
|
1454
|
+
let lastKnownRate = 0;
|
|
1455
|
+
for (const d of sortedDates) {
|
|
1456
|
+
const key = `${upper}:${d}`;
|
|
1457
|
+
if (rates[key] && rates[key] > 0) {
|
|
1458
|
+
lastKnownRate = rates[key];
|
|
1459
|
+
}
|
|
1460
|
+
else if (lastKnownRate > 0) {
|
|
1461
|
+
rates[key] = lastKnownRate;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
// Also backward-fill: if the first few dates had no rate but later ones do
|
|
1465
|
+
let firstKnownRate = 0;
|
|
1466
|
+
for (let i = sortedDates.length - 1; i >= 0; i--) {
|
|
1467
|
+
const d = sortedDates[i];
|
|
1468
|
+
const key = `${upper}:${d}`;
|
|
1469
|
+
if (rates[key] && rates[key] > 0) {
|
|
1470
|
+
firstKnownRate = rates[key];
|
|
1471
|
+
}
|
|
1472
|
+
else if (firstKnownRate > 0) {
|
|
1473
|
+
rates[key] = firstKnownRate;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
// Final fallback: fetch current rate for any symbols still missing
|
|
1478
|
+
const missingSymbols = symbolsNeedingRates.filter(sym => {
|
|
1479
|
+
const upper = sym.toUpperCase();
|
|
1480
|
+
return sortedDates.some(d => !rates[`${upper}:${d}`] || rates[`${upper}:${d}`] === 0);
|
|
1481
|
+
});
|
|
1482
|
+
if (missingSymbols.length > 0) {
|
|
1483
|
+
const cgIds = missingSymbols.map(s => TOKEN_TO_COINGECKO[s.toUpperCase()]).filter(Boolean);
|
|
1484
|
+
if (cgIds.length > 0) {
|
|
1485
|
+
try {
|
|
1486
|
+
const currentData = await cgFetch(`/simple/price?ids=${cgIds.join(',')}&vs_currencies=${currency},usd`);
|
|
1487
|
+
for (const sym of missingSymbols) {
|
|
1488
|
+
const upper = sym.toUpperCase();
|
|
1489
|
+
const cgId = TOKEN_TO_COINGECKO[upper];
|
|
1490
|
+
if (!cgId || !currentData[cgId])
|
|
1491
|
+
continue;
|
|
1492
|
+
let price = currentData[cgId][currency] || 0;
|
|
1493
|
+
if (!price) {
|
|
1494
|
+
price = (currentData[cgId].usd || 0) * forexRate;
|
|
1495
|
+
}
|
|
1496
|
+
if (price > 0) {
|
|
1497
|
+
// Apply to all dates that still have no rate
|
|
1498
|
+
for (const d of sortedDates) {
|
|
1499
|
+
const key = `${upper}:${d}`;
|
|
1500
|
+
if (!rates[key] || rates[key] === 0) {
|
|
1501
|
+
rates[key] = price;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
rates[`${upper}:current`] = price;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
catch { /* skip */ }
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
// Persist rate cache to disk
|
|
1512
|
+
saveRateCache();
|
|
1329
1513
|
// Step 6: Calculate gains
|
|
1330
1514
|
steps.push('Calculating gains...');
|
|
1331
1515
|
const tradeEvents = (tradeData || []).map((t) => ({
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tax",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Crypto tax reporting for XPR Network — balances, trades, transfers, gains calculation, and full tax reports with regional support (NZ first)",
|
|
5
|
+
"author": "xpr-agents",
|
|
6
|
+
"category": "tax",
|
|
7
|
+
"tags": ["tax", "crypto", "nz", "reporting", "gains", "transfers"],
|
|
8
|
+
"capabilities": ["crypto-tax-reporting", "balance-snapshots", "trade-history", "transfer-categorization"],
|
|
9
|
+
"tools": [
|
|
10
|
+
"tax_get_balances",
|
|
11
|
+
"tax_get_dex_trades",
|
|
12
|
+
"tax_get_transfers",
|
|
13
|
+
"tax_get_rates",
|
|
14
|
+
"tax_calculate_gains",
|
|
15
|
+
"tax_generate_report"
|
|
16
|
+
],
|
|
17
|
+
"requires": {
|
|
18
|
+
"env": []
|
|
19
|
+
}
|
|
20
|
+
}
|