1
0
Эх сурвалжийг харах

feat: implement byreal sniper bot with onchain monitoring

- Add onchain transaction monitoring for real-time LP detection
- Implement Jupiter Metis Swap API integration with ExactOut mode
- Add Discord webhook notifications for copy/close operations
- Support both API polling and onchain monitoring modes
- Add retry logic for API calls and swaps
- Fix token decimals handling for accurate swap amounts
- Use USDC as base currency for all swaps
- Calculate copy amounts based on target position value, not parent
- Add test scripts for debugging and validation

Breaking changes:
- Requires JUPITER_API_KEY from https://portal.jup.ag
- Requires DISCORD_WEBHOOK_URL for notifications
lushdog@outlook.com 1 сар өмнө
parent
commit
ded880da9b

+ 22 - 6
.env.example

@@ -7,26 +7,42 @@ RPC_URL=https://mainnet.helius-rpc.com/?api-key=20f2bda7-11af-4e71-a3c3-a8fd6567
 PRIVATE_KEY=your_private_key_here
 
 # Your wallet address (optional, will be derived from private key)
-MY_WALLET=
+MY_WALLET=5SCP4PVyqoefiNy2SwDySdcM2KBDncwSkPWYNypSojBk
+
+# Target wallet to snipe
+TARGET_WALLET=dryuRNL9YcdLnhKFgLfdoj1g2suWcZp97G8XiH8U49e
 
 # Data directory for cache files
 DATA_DIR=./data
 
 # Copy multiplier (1.5 = copy with 1.5x the target's position size)
-COPY_MULTIPLIER=1.5
+COPY_MULTIPLIER=0.2
 
 # Position value limits (USD)
 MAX_USD_VALUE=10
-MIN_USD_VALUE=0.1
+MIN_USD_VALUE=1
 
 # Slippage for Jupiter swaps (in basis points, 100 = 1%)
 SLIPPAGE_BPS=100
 
 # Polling interval in milliseconds (10000 = 10 seconds)
-POLL_INTERVAL_MS=10000
+POLL_INTERVAL_MS=60000
+
+# Monitoring mode
+# 'api' - Use API polling (slower, more reliable)
+# 'onchain' - Use onchain transaction monitoring (faster, real-time)
+MONITOR_MODE=onchain
+
+# Jupiter API Key (required for swaps)
+# Get your API key from: https://portal.jup.ag
+JUPITER_API_KEY=da9d807b-6d15-4b7d-b11c-ef96430ee920
 
 # Authentication header for Byreal API
-AUTH_HEADER=Basic YWRtaW46YzU4ODk5Njc=
+AUTH_HEADER=Basic YWRtaW46YzU4ODk5Njch
 
 # Log level (debug, info, warn, error)
-LOG_LEVEL=info
+LOG_LEVEL=info
+
+# Discord Webhook URL (optional)
+# Sends notifications for copy and close operations
+DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/1471905016188112960/xOJXKl5oEvoizTFcqHM5BN2O_IR8K_pd8ZtGZBKQpyAf8OTs_Un2mBVkHLqrUykYMY8T

+ 5 - 0
.gitignore

@@ -5,11 +5,16 @@ yarn.lock
 
 # Environment variables
 .env
+.env.local
 
 # Data and cache files
 data/
 *.json
 
+# Keep important JSON files
+!package.json
+!package-lock.json
+
 # Logs
 logs/
 *.log

+ 34 - 0
package.json

@@ -0,0 +1,34 @@
+{
+  "name": "byreal-sniper",
+  "version": "1.0.0",
+  "description": "Byreal LP Position Sniper Bot - Automated trading bot for copying LP positions",
+  "main": "index.js",
+  "type": "module",
+  "scripts": {
+    "start": "node index.js",
+    "sniper": "node index.js start",
+    "status": "node index.js status",
+    "clear": "node index.js clear"
+  },
+  "keywords": [
+    "solana",
+    "defi",
+    "trading",
+    "sniper",
+    "lp",
+    "byreal",
+    "jupiter"
+  ],
+  "author": "",
+  "license": "MIT",
+  "dependencies": {
+    "@solana/spl-token": "^0.4.0",
+    "@solana/web3.js": "^1.90.0",
+    "axios": "^1.6.0",
+    "bs58": "^5.0.0",
+    "dotenv": "^16.4.0"
+  },
+  "engines": {
+    "node": ">=18.0.0"
+  }
+}

+ 872 - 0
pnpm-lock.yaml

@@ -0,0 +1,872 @@
+lockfileVersion: '9.0'
+
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
+
+importers:
+
+  .:
+    dependencies:
+      '@solana/spl-token':
+        specifier: ^0.4.0
+        version: 0.4.14(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)
+      '@solana/web3.js':
+        specifier: ^1.90.0
+        version: 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)
+      axios:
+        specifier: ^1.6.0
+        version: 1.13.5
+      bs58:
+        specifier: ^5.0.0
+        version: 5.0.0
+      dotenv:
+        specifier: ^16.4.0
+        version: 16.6.1
+
+packages:
+
+  '@babel/runtime@7.28.6':
+    resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
+    engines: {node: '>=6.9.0'}
+
+  '@noble/curves@1.9.7':
+    resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==}
+    engines: {node: ^14.21.3 || >=16}
+
+  '@noble/hashes@1.8.0':
+    resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
+    engines: {node: ^14.21.3 || >=16}
+
+  '@solana/buffer-layout-utils@0.2.0':
+    resolution: {integrity: sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==}
+    engines: {node: '>= 10'}
+
+  '@solana/buffer-layout@4.0.1':
+    resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==}
+    engines: {node: '>=5.10'}
+
+  '@solana/codecs-core@2.0.0-rc.1':
+    resolution: {integrity: sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==}
+    peerDependencies:
+      typescript: '>=5'
+
+  '@solana/codecs-core@2.3.0':
+    resolution: {integrity: sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==}
+    engines: {node: '>=20.18.0'}
+    peerDependencies:
+      typescript: '>=5.3.3'
+
+  '@solana/codecs-data-structures@2.0.0-rc.1':
+    resolution: {integrity: sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==}
+    peerDependencies:
+      typescript: '>=5'
+
+  '@solana/codecs-numbers@2.0.0-rc.1':
+    resolution: {integrity: sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==}
+    peerDependencies:
+      typescript: '>=5'
+
+  '@solana/codecs-numbers@2.3.0':
+    resolution: {integrity: sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==}
+    engines: {node: '>=20.18.0'}
+    peerDependencies:
+      typescript: '>=5.3.3'
+
+  '@solana/codecs-strings@2.0.0-rc.1':
+    resolution: {integrity: sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==}
+    peerDependencies:
+      fastestsmallesttextencoderdecoder: ^1.0.22
+      typescript: '>=5'
+
+  '@solana/codecs@2.0.0-rc.1':
+    resolution: {integrity: sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==}
+    peerDependencies:
+      typescript: '>=5'
+
+  '@solana/errors@2.0.0-rc.1':
+    resolution: {integrity: sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==}
+    hasBin: true
+    peerDependencies:
+      typescript: '>=5'
+
+  '@solana/errors@2.3.0':
+    resolution: {integrity: sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==}
+    engines: {node: '>=20.18.0'}
+    hasBin: true
+    peerDependencies:
+      typescript: '>=5.3.3'
+
+  '@solana/options@2.0.0-rc.1':
+    resolution: {integrity: sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==}
+    peerDependencies:
+      typescript: '>=5'
+
+  '@solana/spl-token-group@0.0.7':
+    resolution: {integrity: sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug==}
+    engines: {node: '>=16'}
+    peerDependencies:
+      '@solana/web3.js': ^1.95.3
+
+  '@solana/spl-token-metadata@0.1.6':
+    resolution: {integrity: sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA==}
+    engines: {node: '>=16'}
+    peerDependencies:
+      '@solana/web3.js': ^1.95.3
+
+  '@solana/spl-token@0.4.14':
+    resolution: {integrity: sha512-u09zr96UBpX4U685MnvQsNzlvw9TiY005hk1vJmJr7gMJldoPG1eYU5/wNEyOA5lkMLiR/gOi9SFD4MefOYEsA==}
+    engines: {node: '>=16'}
+    peerDependencies:
+      '@solana/web3.js': ^1.95.5
+
+  '@solana/web3.js@1.98.4':
+    resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==}
+
+  '@swc/helpers@0.5.18':
+    resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==}
+
+  '@types/connect@3.4.38':
+    resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
+
+  '@types/node@12.20.55':
+    resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
+
+  '@types/node@25.2.3':
+    resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==}
+
+  '@types/uuid@8.3.4':
+    resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
+
+  '@types/ws@7.4.7':
+    resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==}
+
+  '@types/ws@8.18.1':
+    resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
+
+  agentkeepalive@4.6.0:
+    resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
+    engines: {node: '>= 8.0.0'}
+
+  asynckit@0.4.0:
+    resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
+  axios@1.13.5:
+    resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==}
+
+  base-x@3.0.11:
+    resolution: {integrity: sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==}
+
+  base-x@4.0.1:
+    resolution: {integrity: sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==}
+
+  base64-js@1.5.1:
+    resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+
+  bigint-buffer@1.1.5:
+    resolution: {integrity: sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==}
+    engines: {node: '>= 10.0.0'}
+
+  bignumber.js@9.3.1:
+    resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
+
+  bindings@1.5.0:
+    resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
+
+  bn.js@5.2.2:
+    resolution: {integrity: sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==}
+
+  borsh@0.7.0:
+    resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==}
+
+  bs58@4.0.1:
+    resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==}
+
+  bs58@5.0.0:
+    resolution: {integrity: sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==}
+
+  buffer@6.0.3:
+    resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
+
+  bufferutil@4.1.0:
+    resolution: {integrity: sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==}
+    engines: {node: '>=6.14.2'}
+
+  call-bind-apply-helpers@1.0.2:
+    resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+    engines: {node: '>= 0.4'}
+
+  chalk@5.6.2:
+    resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
+    engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+
+  combined-stream@1.0.8:
+    resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+    engines: {node: '>= 0.8'}
+
+  commander@12.1.0:
+    resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
+    engines: {node: '>=18'}
+
+  commander@14.0.3:
+    resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
+    engines: {node: '>=20'}
+
+  commander@2.20.3:
+    resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
+
+  delay@5.0.0:
+    resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==}
+    engines: {node: '>=10'}
+
+  delayed-stream@1.0.0:
+    resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+    engines: {node: '>=0.4.0'}
+
+  dotenv@16.6.1:
+    resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
+    engines: {node: '>=12'}
+
+  dunder-proto@1.0.1:
+    resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+    engines: {node: '>= 0.4'}
+
+  es-define-property@1.0.1:
+    resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+    engines: {node: '>= 0.4'}
+
+  es-errors@1.3.0:
+    resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+    engines: {node: '>= 0.4'}
+
+  es-object-atoms@1.1.1:
+    resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
+    engines: {node: '>= 0.4'}
+
+  es-set-tostringtag@2.1.0:
+    resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+    engines: {node: '>= 0.4'}
+
+  es6-promise@4.2.8:
+    resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==}
+
+  es6-promisify@5.0.0:
+    resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==}
+
+  eventemitter3@5.0.4:
+    resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
+
+  eyes@0.1.8:
+    resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==}
+    engines: {node: '> 0.1.90'}
+
+  fast-stable-stringify@1.0.0:
+    resolution: {integrity: sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==}
+
+  fastestsmallesttextencoderdecoder@1.0.22:
+    resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==}
+
+  file-uri-to-path@1.0.0:
+    resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
+
+  follow-redirects@1.15.11:
+    resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
+    engines: {node: '>=4.0'}
+    peerDependencies:
+      debug: '*'
+    peerDependenciesMeta:
+      debug:
+        optional: true
+
+  form-data@4.0.5:
+    resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
+    engines: {node: '>= 6'}
+
+  function-bind@1.1.2:
+    resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+  get-intrinsic@1.3.0:
+    resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+    engines: {node: '>= 0.4'}
+
+  get-proto@1.0.1:
+    resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+    engines: {node: '>= 0.4'}
+
+  gopd@1.2.0:
+    resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+    engines: {node: '>= 0.4'}
+
+  has-symbols@1.1.0:
+    resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+    engines: {node: '>= 0.4'}
+
+  has-tostringtag@1.0.2:
+    resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+    engines: {node: '>= 0.4'}
+
+  hasown@2.0.2:
+    resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+    engines: {node: '>= 0.4'}
+
+  humanize-ms@1.2.1:
+    resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
+
+  ieee754@1.2.1:
+    resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+
+  isomorphic-ws@4.0.1:
+    resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==}
+    peerDependencies:
+      ws: '*'
+
+  jayson@4.3.0:
+    resolution: {integrity: sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==}
+    engines: {node: '>=8'}
+    hasBin: true
+
+  json-stringify-safe@5.0.1:
+    resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
+
+  math-intrinsics@1.1.0:
+    resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+    engines: {node: '>= 0.4'}
+
+  mime-db@1.52.0:
+    resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+    engines: {node: '>= 0.6'}
+
+  mime-types@2.1.35:
+    resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+    engines: {node: '>= 0.6'}
+
+  ms@2.1.3:
+    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+  node-fetch@2.7.0:
+    resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
+    engines: {node: 4.x || >=6.0.0}
+    peerDependencies:
+      encoding: ^0.1.0
+    peerDependenciesMeta:
+      encoding:
+        optional: true
+
+  node-gyp-build@4.8.4:
+    resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
+    hasBin: true
+
+  proxy-from-env@1.1.0:
+    resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+
+  rpc-websockets@9.3.3:
+    resolution: {integrity: sha512-OkCsBBzrwxX4DoSv4Zlf9DgXKRB0MzVfCFg5MC+fNnf9ktr4SMWjsri0VNZQlDbCnGcImT6KNEv4ZoxktQhdpA==}
+
+  safe-buffer@5.2.1:
+    resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+
+  stream-chain@2.2.5:
+    resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==}
+
+  stream-json@1.9.1:
+    resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==}
+
+  superstruct@2.0.2:
+    resolution: {integrity: sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==}
+    engines: {node: '>=14.0.0'}
+
+  text-encoding-utf-8@1.0.2:
+    resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==}
+
+  tr46@0.0.3:
+    resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+
+  tslib@2.8.1:
+    resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+  typescript@5.9.3:
+    resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+    engines: {node: '>=14.17'}
+    hasBin: true
+
+  undici-types@7.16.0:
+    resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
+
+  utf-8-validate@5.0.10:
+    resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==}
+    engines: {node: '>=6.14.2'}
+
+  uuid@8.3.2:
+    resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
+    hasBin: true
+
+  webidl-conversions@3.0.1:
+    resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+
+  whatwg-url@5.0.0:
+    resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+
+  ws@7.5.10:
+    resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==}
+    engines: {node: '>=8.3.0'}
+    peerDependencies:
+      bufferutil: ^4.0.1
+      utf-8-validate: ^5.0.2
+    peerDependenciesMeta:
+      bufferutil:
+        optional: true
+      utf-8-validate:
+        optional: true
+
+  ws@8.19.0:
+    resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
+    engines: {node: '>=10.0.0'}
+    peerDependencies:
+      bufferutil: ^4.0.1
+      utf-8-validate: '>=5.0.2'
+    peerDependenciesMeta:
+      bufferutil:
+        optional: true
+      utf-8-validate:
+        optional: true
+
+snapshots:
+
+  '@babel/runtime@7.28.6': {}
+
+  '@noble/curves@1.9.7':
+    dependencies:
+      '@noble/hashes': 1.8.0
+
+  '@noble/hashes@1.8.0': {}
+
+  '@solana/buffer-layout-utils@0.2.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)':
+    dependencies:
+      '@solana/buffer-layout': 4.0.1
+      '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)
+      bigint-buffer: 1.1.5
+      bignumber.js: 9.3.1
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - typescript
+      - utf-8-validate
+
+  '@solana/buffer-layout@4.0.1':
+    dependencies:
+      buffer: 6.0.3
+
+  '@solana/codecs-core@2.0.0-rc.1(typescript@5.9.3)':
+    dependencies:
+      '@solana/errors': 2.0.0-rc.1(typescript@5.9.3)
+      typescript: 5.9.3
+
+  '@solana/codecs-core@2.3.0(typescript@5.9.3)':
+    dependencies:
+      '@solana/errors': 2.3.0(typescript@5.9.3)
+      typescript: 5.9.3
+
+  '@solana/codecs-data-structures@2.0.0-rc.1(typescript@5.9.3)':
+    dependencies:
+      '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3)
+      '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.3)
+      '@solana/errors': 2.0.0-rc.1(typescript@5.9.3)
+      typescript: 5.9.3
+
+  '@solana/codecs-numbers@2.0.0-rc.1(typescript@5.9.3)':
+    dependencies:
+      '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3)
+      '@solana/errors': 2.0.0-rc.1(typescript@5.9.3)
+      typescript: 5.9.3
+
+  '@solana/codecs-numbers@2.3.0(typescript@5.9.3)':
+    dependencies:
+      '@solana/codecs-core': 2.3.0(typescript@5.9.3)
+      '@solana/errors': 2.3.0(typescript@5.9.3)
+      typescript: 5.9.3
+
+  '@solana/codecs-strings@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)':
+    dependencies:
+      '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3)
+      '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.3)
+      '@solana/errors': 2.0.0-rc.1(typescript@5.9.3)
+      fastestsmallesttextencoderdecoder: 1.0.22
+      typescript: 5.9.3
+
+  '@solana/codecs@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)':
+    dependencies:
+      '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3)
+      '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.9.3)
+      '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.3)
+      '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
+      '@solana/options': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - fastestsmallesttextencoderdecoder
+
+  '@solana/errors@2.0.0-rc.1(typescript@5.9.3)':
+    dependencies:
+      chalk: 5.6.2
+      commander: 12.1.0
+      typescript: 5.9.3
+
+  '@solana/errors@2.3.0(typescript@5.9.3)':
+    dependencies:
+      chalk: 5.6.2
+      commander: 14.0.3
+      typescript: 5.9.3
+
+  '@solana/options@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)':
+    dependencies:
+      '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3)
+      '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.9.3)
+      '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.3)
+      '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
+      '@solana/errors': 2.0.0-rc.1(typescript@5.9.3)
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - fastestsmallesttextencoderdecoder
+
+  '@solana/spl-token-group@0.0.7(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)':
+    dependencies:
+      '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
+      '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)
+    transitivePeerDependencies:
+      - fastestsmallesttextencoderdecoder
+      - typescript
+
+  '@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)':
+    dependencies:
+      '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
+      '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)
+    transitivePeerDependencies:
+      - fastestsmallesttextencoderdecoder
+      - typescript
+
+  '@solana/spl-token@0.4.14(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)':
+    dependencies:
+      '@solana/buffer-layout': 4.0.1
+      '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)
+      '@solana/spl-token-group': 0.0.7(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
+      '@solana/spl-token-metadata': 0.1.6(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
+      '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)
+      buffer: 6.0.3
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - fastestsmallesttextencoderdecoder
+      - typescript
+      - utf-8-validate
+
+  '@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)':
+    dependencies:
+      '@babel/runtime': 7.28.6
+      '@noble/curves': 1.9.7
+      '@noble/hashes': 1.8.0
+      '@solana/buffer-layout': 4.0.1
+      '@solana/codecs-numbers': 2.3.0(typescript@5.9.3)
+      agentkeepalive: 4.6.0
+      bn.js: 5.2.2
+      borsh: 0.7.0
+      bs58: 4.0.1
+      buffer: 6.0.3
+      fast-stable-stringify: 1.0.0
+      jayson: 4.3.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)
+      node-fetch: 2.7.0
+      rpc-websockets: 9.3.3
+      superstruct: 2.0.2
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - typescript
+      - utf-8-validate
+
+  '@swc/helpers@0.5.18':
+    dependencies:
+      tslib: 2.8.1
+
+  '@types/connect@3.4.38':
+    dependencies:
+      '@types/node': 12.20.55
+
+  '@types/node@12.20.55': {}
+
+  '@types/node@25.2.3':
+    dependencies:
+      undici-types: 7.16.0
+
+  '@types/uuid@8.3.4': {}
+
+  '@types/ws@7.4.7':
+    dependencies:
+      '@types/node': 12.20.55
+
+  '@types/ws@8.18.1':
+    dependencies:
+      '@types/node': 25.2.3
+
+  agentkeepalive@4.6.0:
+    dependencies:
+      humanize-ms: 1.2.1
+
+  asynckit@0.4.0: {}
+
+  axios@1.13.5:
+    dependencies:
+      follow-redirects: 1.15.11
+      form-data: 4.0.5
+      proxy-from-env: 1.1.0
+    transitivePeerDependencies:
+      - debug
+
+  base-x@3.0.11:
+    dependencies:
+      safe-buffer: 5.2.1
+
+  base-x@4.0.1: {}
+
+  base64-js@1.5.1: {}
+
+  bigint-buffer@1.1.5:
+    dependencies:
+      bindings: 1.5.0
+
+  bignumber.js@9.3.1: {}
+
+  bindings@1.5.0:
+    dependencies:
+      file-uri-to-path: 1.0.0
+
+  bn.js@5.2.2: {}
+
+  borsh@0.7.0:
+    dependencies:
+      bn.js: 5.2.2
+      bs58: 4.0.1
+      text-encoding-utf-8: 1.0.2
+
+  bs58@4.0.1:
+    dependencies:
+      base-x: 3.0.11
+
+  bs58@5.0.0:
+    dependencies:
+      base-x: 4.0.1
+
+  buffer@6.0.3:
+    dependencies:
+      base64-js: 1.5.1
+      ieee754: 1.2.1
+
+  bufferutil@4.1.0:
+    dependencies:
+      node-gyp-build: 4.8.4
+    optional: true
+
+  call-bind-apply-helpers@1.0.2:
+    dependencies:
+      es-errors: 1.3.0
+      function-bind: 1.1.2
+
+  chalk@5.6.2: {}
+
+  combined-stream@1.0.8:
+    dependencies:
+      delayed-stream: 1.0.0
+
+  commander@12.1.0: {}
+
+  commander@14.0.3: {}
+
+  commander@2.20.3: {}
+
+  delay@5.0.0: {}
+
+  delayed-stream@1.0.0: {}
+
+  dotenv@16.6.1: {}
+
+  dunder-proto@1.0.1:
+    dependencies:
+      call-bind-apply-helpers: 1.0.2
+      es-errors: 1.3.0
+      gopd: 1.2.0
+
+  es-define-property@1.0.1: {}
+
+  es-errors@1.3.0: {}
+
+  es-object-atoms@1.1.1:
+    dependencies:
+      es-errors: 1.3.0
+
+  es-set-tostringtag@2.1.0:
+    dependencies:
+      es-errors: 1.3.0
+      get-intrinsic: 1.3.0
+      has-tostringtag: 1.0.2
+      hasown: 2.0.2
+
+  es6-promise@4.2.8: {}
+
+  es6-promisify@5.0.0:
+    dependencies:
+      es6-promise: 4.2.8
+
+  eventemitter3@5.0.4: {}
+
+  eyes@0.1.8: {}
+
+  fast-stable-stringify@1.0.0: {}
+
+  fastestsmallesttextencoderdecoder@1.0.22: {}
+
+  file-uri-to-path@1.0.0: {}
+
+  follow-redirects@1.15.11: {}
+
+  form-data@4.0.5:
+    dependencies:
+      asynckit: 0.4.0
+      combined-stream: 1.0.8
+      es-set-tostringtag: 2.1.0
+      hasown: 2.0.2
+      mime-types: 2.1.35
+
+  function-bind@1.1.2: {}
+
+  get-intrinsic@1.3.0:
+    dependencies:
+      call-bind-apply-helpers: 1.0.2
+      es-define-property: 1.0.1
+      es-errors: 1.3.0
+      es-object-atoms: 1.1.1
+      function-bind: 1.1.2
+      get-proto: 1.0.1
+      gopd: 1.2.0
+      has-symbols: 1.1.0
+      hasown: 2.0.2
+      math-intrinsics: 1.1.0
+
+  get-proto@1.0.1:
+    dependencies:
+      dunder-proto: 1.0.1
+      es-object-atoms: 1.1.1
+
+  gopd@1.2.0: {}
+
+  has-symbols@1.1.0: {}
+
+  has-tostringtag@1.0.2:
+    dependencies:
+      has-symbols: 1.1.0
+
+  hasown@2.0.2:
+    dependencies:
+      function-bind: 1.1.2
+
+  humanize-ms@1.2.1:
+    dependencies:
+      ms: 2.1.3
+
+  ieee754@1.2.1: {}
+
+  isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.1.0)(utf-8-validate@5.0.10)):
+    dependencies:
+      ws: 7.5.10(bufferutil@4.1.0)(utf-8-validate@5.0.10)
+
+  jayson@4.3.0(bufferutil@4.1.0)(utf-8-validate@5.0.10):
+    dependencies:
+      '@types/connect': 3.4.38
+      '@types/node': 12.20.55
+      '@types/ws': 7.4.7
+      commander: 2.20.3
+      delay: 5.0.0
+      es6-promisify: 5.0.0
+      eyes: 0.1.8
+      isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.1.0)(utf-8-validate@5.0.10))
+      json-stringify-safe: 5.0.1
+      stream-json: 1.9.1
+      uuid: 8.3.2
+      ws: 7.5.10(bufferutil@4.1.0)(utf-8-validate@5.0.10)
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+
+  json-stringify-safe@5.0.1: {}
+
+  math-intrinsics@1.1.0: {}
+
+  mime-db@1.52.0: {}
+
+  mime-types@2.1.35:
+    dependencies:
+      mime-db: 1.52.0
+
+  ms@2.1.3: {}
+
+  node-fetch@2.7.0:
+    dependencies:
+      whatwg-url: 5.0.0
+
+  node-gyp-build@4.8.4:
+    optional: true
+
+  proxy-from-env@1.1.0: {}
+
+  rpc-websockets@9.3.3:
+    dependencies:
+      '@swc/helpers': 0.5.18
+      '@types/uuid': 8.3.4
+      '@types/ws': 8.18.1
+      buffer: 6.0.3
+      eventemitter3: 5.0.4
+      uuid: 8.3.2
+      ws: 8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)
+    optionalDependencies:
+      bufferutil: 4.1.0
+      utf-8-validate: 5.0.10
+
+  safe-buffer@5.2.1: {}
+
+  stream-chain@2.2.5: {}
+
+  stream-json@1.9.1:
+    dependencies:
+      stream-chain: 2.2.5
+
+  superstruct@2.0.2: {}
+
+  text-encoding-utf-8@1.0.2: {}
+
+  tr46@0.0.3: {}
+
+  tslib@2.8.1: {}
+
+  typescript@5.9.3: {}
+
+  undici-types@7.16.0: {}
+
+  utf-8-validate@5.0.10:
+    dependencies:
+      node-gyp-build: 4.8.4
+    optional: true
+
+  uuid@8.3.2: {}
+
+  webidl-conversions@3.0.1: {}
+
+  whatwg-url@5.0.0:
+    dependencies:
+      tr46: 0.0.3
+      webidl-conversions: 3.0.1
+
+  ws@7.5.10(bufferutil@4.1.0)(utf-8-validate@5.0.10):
+    optionalDependencies:
+      bufferutil: 4.1.0
+      utf-8-validate: 5.0.10
+
+  ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10):
+    optionalDependencies:
+      bufferutil: 4.1.0
+      utf-8-validate: 5.0.10

+ 18 - 5
src/config/index.js

@@ -17,7 +17,7 @@ export const CONFIG = {
   PRIVATE_KEY: process.env.PRIVATE_KEY,
   
   // Target wallet to snipe
-  TARGET_WALLET: 'dryuRNL9YcdLnhKFgLfdoj1g2suWcZp97G8XiH8U49e',
+  TARGET_WALLET: process.env.TARGET_WALLET || 'dryuRNL9YcdLnhKFgLfdoj1g2suWcZp97G8XiH8U49e',
   
   // My wallet (will be derived from private key if not set)
   MY_WALLET: process.env.MY_WALLET || '',
@@ -30,10 +30,15 @@ export const CONFIG = {
   // API Endpoints
   BYREAL_API_BASE: 'https://api2.byreal.io/byreal/api/dex/v2',
   TICK_API: 'https://love.hdlife.me/api/tick-to-price',
-  COPY_ACTION_URL: 'https://love.hdlife.me/api/lp-copy',
-  COPY_CALCULATE_URL: 'https://love.hdlife.me/api/lp-copy/calculate',
-  CLOSE_ACTION_URL: 'https://love.hdlife.me/api/lp-close',
-  JUPITER_API: 'https://quote-api.jup.ag/v6',
+  COPY_ACTION_URL: 'https://byreal.cb.hdlife.me/api/lp-copy',
+  COPY_CALCULATE_URL: 'http://localhost:3000/api/lp-copy/calculate',
+  CLOSE_ACTION_URL: 'https://byreal.cb.hdlife.me/api/lp-close',
+  CHECK_COPY_URL: 'https://api2.byreal.io/byreal/api/dex/v2/copyfarmer/top-positions',
+  
+  // Jupiter API - New Metis Swap API (v1)
+  // Get API key from: https://portal.jup.ag
+  JUPITER_API: 'https://api.jup.ag/swap/v1',
+  JUPITER_API_KEY: process.env.JUPITER_API_KEY || '',
   
   // Jupiter settings
   SLIPPAGE_BPS: parseInt(process.env.SLIPPAGE_BPS) || 100,
@@ -44,8 +49,16 @@ export const CONFIG = {
   // Polling settings
   POLL_INTERVAL_MS: parseInt(process.env.POLL_INTERVAL_MS) || 10000,
   
+  // Monitoring mode
+  // 'api' - Use API polling (original method)
+  // 'onchain' - Use onchain transaction monitoring (faster)
+  MONITOR_MODE: process.env.MONITOR_MODE || 'onchain',
+  
   // Logging
   LOG_LEVEL: process.env.LOG_LEVEL || 'info',
+  
+  // Discord Notifications
+  DISCORD_WEBHOOK_URL: process.env.DISCORD_WEBHOOK_URL || '',
 };
 
 // Token whitelist for common tokens

+ 235 - 36
src/core/sniper.js

@@ -1,6 +1,7 @@
 import { CONFIG } from '../config/index.js';
 import { PositionCache, logger, sleep, formatUsd } from '../utils/index.js';
-import { ByrealAPI, JupiterSwapper } from '../services/index.js';
+import { ByrealAPI, JupiterSwapper, OnchainMonitor } from '../services/index.js';
+import { DiscordWebhook } from '../utils/discord.js';
 
 export class SniperEngine {
   constructor() {
@@ -12,6 +13,9 @@ export class SniperEngine {
     this.swapper = new JupiterSwapper();
     this.isRunning = false;
     this.myWallet = CONFIG.MY_WALLET || this.swapper.getWalletAddress();
+    this.onchainMonitor = null;
+    // Initialize Discord webhook
+    this.discord = new DiscordWebhook(CONFIG.DISCORD_WEBHOOK_URL);
   }
 
   async analyzePosition(positionDetail, poolInfo) {
@@ -39,16 +43,26 @@ export class SniperEngine {
     }
   }
 
-  async executeCopy(parentDetail, poolInfo, parentPositionAddress, targetPositionAddress) {
-    const targetUsdValue = parseFloat(parentDetail.totalUsdValue || parentDetail.liquidityUsd || 0);
+  async executeCopy(parentDetail, poolInfo, parentPositionAddress, targetPositionAddress, targetUsdValue = 0) {
+    // Use provided target value (from onchain transaction), fallback to parent position value
+    const copyBaseValue = targetUsdValue > 0 
+      ? targetUsdValue 
+      : parseFloat(parentDetail.totalDeposit || parentDetail.totalUsdValue || parentDetail.liquidityUsd || 0);
     
-    if (targetUsdValue <= 0) {
-      logger.warn('Parent position has no USD value, skipping');
+    if (copyBaseValue <= 0) {
+      logger.warn('Position has no USD value, skipping');
       return false;
     }
+    
+    // Log which value we're using
+    if (targetUsdValue > 0) {
+      logger.info(`Using TARGET position value: ${formatUsd(targetUsdValue)}`);
+    } else {
+      logger.info(`Using PARENT position value: ${formatUsd(copyBaseValue)}`);
+    }
 
     const copyUsdValue = Math.min(
-      targetUsdValue * CONFIG.COPY_MULTIPLIER,
+      copyBaseValue * CONFIG.COPY_MULTIPLIER,
       CONFIG.MAX_USD_VALUE
     );
 
@@ -57,15 +71,19 @@ export class SniperEngine {
       return false;
     }
 
-    logger.info(`Copying PARENT position with ${CONFIG.COPY_MULTIPLIER}x multiplier`);
-    logger.info(`Parent Target: ${formatUsd(targetUsdValue)} → Copy: ${formatUsd(copyUsdValue)}`);
+    logger.info(`Copying with ${CONFIG.COPY_MULTIPLIER}x multiplier`);
+    logger.info(`Base Value: ${formatUsd(copyBaseValue)} → Copy: ${formatUsd(copyUsdValue)}`);
+
+    // Get correct field names from API response
+    const nftMint = parentDetail.nftMintAddress || parentDetail.nftMint;
+    const poolAddress = parentDetail.pool?.poolAddress || parentDetail.poolAddress;
 
     // Step 1: Calculate exact token amounts needed using the calculate API
     logger.info('Calculating exact token amounts from parent position...');
     const calculation = await ByrealAPI.calculatePosition(
       parentPositionAddress,
       copyUsdValue,
-      parentDetail.nftMint
+      nftMint
     );
 
     if (!calculation) {
@@ -89,12 +107,12 @@ export class SniperEngine {
     if (tokenABalance < requiredAmountA * 0.95) {
       logger.info(`Token A balance insufficient: ${tokenABalance.toFixed(4)} < ${requiredAmountA.toFixed(4)}`);
       logger.info(`Swapping with 10% buffer: ${swapAmountA.toFixed(4)} (required: ${requiredAmountA.toFixed(4)})`);
-      const success = await this.swapper.swapIfNeeded(
-        'So11111111111111111111111111111111111111112', // Use SOL as source
-        tokenA.address,
-        swapAmountA,
-        tokenA.decimals
-      );
+        const success = await this.swapper.swapIfNeeded(
+          'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // Use USDC as source
+          tokenA.address,
+          swapAmountA,
+          tokenA.decimals
+        );
       if (!success) {
         logger.error('Failed to acquire sufficient Token A');
         return false;
@@ -127,21 +145,40 @@ export class SniperEngine {
 
     // Step 4: Execute copy of PARENT position
     const success = await ByrealAPI.copyPosition(
-      parentDetail.nftMint,
+      nftMint,
       parentPositionAddress,
       copyUsdValue
     );
     
     if (success) {
       this.copiedCache.add(parentPositionAddress, {
-        poolAddress: parentDetail.poolAddress,
-        targetUsdValue,
+        poolAddress,
+        targetUsdValue: copyBaseValue,
         copiedAt: new Date().toISOString(),
         targetPositionAddress: targetPositionAddress,
         calculation: calculation, // Store calculation details
       });
       // Map target's position to the parent position we copied
       this.targetToParentMap.set(targetPositionAddress, parentPositionAddress);
+
+      // Send Discord notification
+      this.discord.notifyCopySuccess({
+        parentPosition: parentPositionAddress,
+        poolAddress: poolAddress,
+        poolName: `${poolInfo.mintA?.symbol || '?'}/${poolInfo.mintB?.symbol || '?'}`,
+        tokenA: {
+          amount: tokenA.amount,
+          symbol: tokenA.symbol || '',
+          valueUsd: tokenA.valueUsd || 0,
+        },
+        tokenB: {
+          amount: tokenB.amount,
+          symbol: tokenB.symbol || '',
+          valueUsd: tokenB.valueUsd || 0,
+        },
+        copyUsdValue: copyUsdValue,
+        txSignature: null, // Could capture from API response if available
+      }).catch(err => logger.error('Discord notification failed:', err));
     }
 
     return success;
@@ -174,6 +211,88 @@ export class SniperEngine {
     logger.success(`Initialization complete - monitoring for ${this.initialPositions.size} existing positions and new additions only\n`);
   }
 
+  /**
+   * Handle new position detected from onchain monitor
+   * Onchain mode: Detect via chain, wait 60s, then query parent detail via API
+   * Copy amount is based on TARGET position value, not parent position value
+   */
+  async handleOnchainPosition(positionInfo) {
+    const { parentPositionAddress, targetPositionAddress, transactionSignature, positionDetail } = positionInfo;
+
+    logger.info(`\n🔗 Onchain position detected!`);
+    logger.info(`  Parent Position: ${parentPositionAddress}`);
+    logger.info(`  Transaction: ${transactionSignature.slice(0, 16)}...`);
+
+    // Check 1: Skip if in closed cache
+    if (this.closedCache.has(parentPositionAddress)) {
+      logger.info(`Parent position ${parentPositionAddress} was closed, skipping...`);
+      return;
+    }
+
+    // Check 2: Local cache check (Priority: cache first)
+    if (this.copiedCache.has(parentPositionAddress)) {
+      logger.info(`Parent position ${parentPositionAddress} already in cache, skipping...`);
+      return;
+    }
+
+    // Position detail already fetched from Byreal API (passed from onchain monitor)
+    if (!positionDetail) {
+      logger.error(`Position detail not available from Byreal API: ${parentPositionAddress}`);
+      return;
+    }
+
+    const poolInfo = positionDetail.pool;
+    if (!poolInfo) {
+      logger.warn(`Pool info not found for parent position ${parentPositionAddress}`);
+      return;
+    }
+
+    logger.info(`Position details from Byreal API:`);
+    logger.info(`  Pool: ${poolInfo.mintA?.symbol || '?'}/${poolInfo.mintB?.symbol || '?'}`);
+    logger.info(`  NFT Mint: ${positionDetail.nftMintAddress || positionDetail.nftMint}`);
+    logger.info(`  Total Deposit: $${positionDetail.totalDeposit || 0}`);
+    logger.info(`  Liquidity: ${positionDetail.liquidity}`);
+    logger.info(`  Price Range: ${positionDetail.priceLower} - ${positionDetail.priceUpper}`);
+    logger.info(`  Current Price: ${positionDetail.currentPrice}`);
+
+    // Check 3: API check via CHECK_COPY_URL (as fallback)
+    logger.info(`Checking copy status via API...`);
+    const poolAddress = positionDetail.pool?.poolAddress || positionDetail.poolAddress;
+    const isCopiedViaApi = await ByrealAPI.checkIfCopied(
+      poolAddress,
+      parentPositionAddress,
+      this.myWallet
+    );
+
+    if (isCopiedViaApi) {
+      logger.info('Parent position already copied (confirmed by API)');
+      // Sync cache with API result
+      this.copiedCache.add(parentPositionAddress, { poolAddress });
+      this.targetToParentMap.set(targetPositionAddress, parentPositionAddress);
+      return;
+    }
+
+    logger.success('Parent position ready to copy!');
+
+    // Execute copy using position detail from Byreal API
+    const success = await this.executeCopy(
+      positionDetail,
+      poolInfo,
+      parentPositionAddress,
+      targetPositionAddress,
+      positionDetail.totalDeposit || 0 // Use totalDeposit from Byreal API
+    );
+
+    if (success) {
+      logger.success(`Successfully copied position via onchain detection!`);
+    }
+
+    // Mark as processed
+    if (targetPositionAddress) {
+      this.initialPositions.add(targetPositionAddress);
+    }
+  }
+
   async scanForNewPositions() {
     logger.info(`Scanning for target wallet positions: ${CONFIG.TARGET_WALLET}`);
     
@@ -209,26 +328,43 @@ export class SniperEngine {
 
       logger.info(`\n🆕 New position detected in target wallet: ${positionAddress}`);
       
-      // Fetch position detail to check if it's a copied position
-      logger.info('Fetching position detail to check parent...');
-      const positionDetail = await ByrealAPI.getPositionDetail(positionAddress);
+      // Fetch position detail with retry logic (byreal interface has delay)
+      let positionDetail = null;
+      let parentPositionAddress = null;
+      const maxRetries = 10;
+      const retryIntervalMs = 60000; // 30 seconds
       
-      if (!positionDetail) {
-        logger.error(`Failed to fetch detail for position ${positionAddress}`);
-        continue;
+      for (let attempt = 1; attempt <= maxRetries; attempt++) {
+        logger.info(`Fetching position detail (attempt ${attempt}/${maxRetries})...`);
+        positionDetail = await ByrealAPI.getPositionDetail(positionAddress);
+        
+        if (!positionDetail) {
+          logger.error(`Failed to fetch detail for position ${positionAddress}`);
+          if (attempt < maxRetries) {
+            logger.info(`Waiting ${retryIntervalMs / 1000}s before retry...`);
+            await sleep(retryIntervalMs);
+            continue;
+          }
+        } else {
+          // Check if this is a copied position (has parentPositionAddress)
+          parentPositionAddress = positionDetail.parentPositionAddress;
+          
+          if (parentPositionAddress) {
+            logger.success(`Found copied position! Parent: ${parentPositionAddress}`);
+            break;
+          } else if (attempt < maxRetries) {
+            logger.info(`Position is not a copy yet (no parent), retrying in ${retryIntervalMs / 1000}s...`);
+            await sleep(retryIntervalMs);
+          }
+        }
       }
-
-      // Check if this is a copied position (has parentPositionAddress)
-      const parentPositionAddress = positionDetail.parentPositionAddress;
       
+      // After 10 attempts, if still no parent, mark as processed and skip
       if (!parentPositionAddress) {
-        logger.info('Position is not a copy (no parent), skipping...');
-        // Mark as processed so we don't check again
+        logger.info(`Position ${positionAddress} has no parent after ${maxRetries} attempts, marking as processed and skipping...`);
         this.initialPositions.add(positionAddress);
         continue;
       }
-
-      logger.success(`Found copied position! Parent: ${parentPositionAddress}`);
       
       // Now we copy the PARENT position, not the target's position
       const parentDetail = await ByrealAPI.getPositionDetail(parentPositionAddress);
@@ -245,22 +381,26 @@ export class SniperEngine {
       }
 
       logger.info(`Parent pool: ${poolInfo.mintA?.symbol || '?'}/${poolInfo.mintB?.symbol || '?'}`);
-      logger.info(`Parent position NFT: ${parentDetail.nftMint}`);
+      // Use nftMintAddress instead of nftMint
+      const nftMint = parentDetail.nftMintAddress || parentDetail.nftMint;
+      logger.info(`Parent position NFT: ${nftMint}`);
 
       const analysis = await this.analyzePosition(parentDetail, poolInfo);
 
       if (analysis.inRange) {
         logger.success('Parent position is IN RANGE - ready to copy!');
 
+        // Use pool.poolAddress instead of poolAddress
+        const poolAddress = parentDetail.pool?.poolAddress || parentDetail.poolAddress;
         const isCopied = await ByrealAPI.checkIfCopied(
-          parentDetail.poolAddress,
+          poolAddress,
           parentPositionAddress,
           this.myWallet
         );
         
         if (isCopied) {
           logger.info('Parent position already copied (confirmed via API)');
-          this.copiedCache.add(parentPositionAddress, { poolAddress: parentDetail.poolAddress });
+          this.copiedCache.add(parentPositionAddress, { poolAddress });
           // Still map target's position to parent even if already copied
           this.targetToParentMap.set(positionAddress, parentPositionAddress);
         } else {
@@ -296,12 +436,24 @@ export class SniperEngine {
         const success = await ByrealAPI.closePosition(parentPositionAddress);
         
         if (success) {
+          const copiedData = this.copiedCache.get(parentPositionAddress);
           this.closedCache.add(parentPositionAddress, {
             closedAt: new Date().toISOString(),
             reason: `Target closed their position ${targetPositionAddress}`,
           });
           this.copiedCache.remove(parentPositionAddress);
           this.targetToParentMap.delete(targetPositionAddress);
+
+          // Send Discord notification
+          this.discord.notifyCloseSuccess({
+            positionAddress: parentPositionAddress,
+            poolName: copiedData?.calculation?.tokenA?.symbol && copiedData?.calculation?.tokenB?.symbol 
+              ? `${copiedData.calculation.tokenA.symbol}/${copiedData.calculation.tokenB.symbol}`
+              : 'Unknown Pool',
+            closedAt: new Date().toISOString(),
+            reason: `Target closed their position`,
+            txSignature: null,
+          }).catch(err => logger.error('Discord notification failed:', err));
         }
       }
     }
@@ -321,12 +473,53 @@ export class SniperEngine {
     logger.info(`💼 My Wallet: ${this.myWallet}`);
     logger.info(`📈 Copy Multiplier: ${CONFIG.COPY_MULTIPLIER}x`);
     logger.info(`💰 Max Position: ${formatUsd(CONFIG.MAX_USD_VALUE)}`);
-    logger.info(`⏱️ Poll Interval: ${CONFIG.POLL_INTERVAL_MS / 1000}s`);
+    logger.info(`📋 Monitor Mode: ${CONFIG.MONITOR_MODE}`);
     logger.info('═══════════════════════════════════════════\n');
 
     // Initialize: record existing positions but don't copy them
     await this.initialize();
 
+    // Choose monitoring mode based on configuration
+    if (CONFIG.MONITOR_MODE === 'onchain') {
+      logger.info('Using onchain transaction monitoring mode...\n');
+      await this.startOnchainMonitoring();
+    } else {
+      logger.info('Using API polling mode...\n');
+      await this.startApiPolling();
+    }
+  }
+
+  /**
+   * Start onchain transaction monitoring
+   */
+  async startOnchainMonitoring() {
+    // Create onchain monitor with callback
+    this.onchainMonitor = new OnchainMonitor(
+      this.handleOnchainPosition.bind(this)
+    );
+
+    // Start monitoring in parallel with closed position checking
+    const monitorPromise = this.onchainMonitor.start();
+    
+    // Continue checking for closed positions in background
+    while (this.isRunning) {
+      try {
+        await this.scanForClosedPositions();
+        await sleep(CONFIG.POLL_INTERVAL_MS);
+      } catch (error) {
+        logger.error('Closed position check error:', error.message);
+        await sleep(CONFIG.POLL_INTERVAL_MS);
+      }
+    }
+
+    // Wait for monitor to finish (it won't unless stopped)
+    await monitorPromise;
+  }
+
+  /**
+   * Start API polling mode (original method)
+   */
+  async startApiPolling() {
     while (this.isRunning) {
       try {
         await this.scanForNewPositions();
@@ -344,11 +537,17 @@ export class SniperEngine {
   stop() {
     logger.info('Stopping sniper bot...');
     this.isRunning = false;
+    
+    // Stop onchain monitor if running
+    if (this.onchainMonitor) {
+      this.onchainMonitor.stop();
+    }
   }
 
   getStatus() {
     return {
       isRunning: this.isRunning,
+      monitorMode: CONFIG.MONITOR_MODE,
       copiedPositions: this.copiedCache.getAll(),
       closedPositions: this.closedCache.getAll(),
       walletAddress: this.myWallet,
@@ -356,4 +555,4 @@ export class SniperEngine {
   }
 }
 
-export default SniperEngine;
+export default SniperEngine;

+ 2 - 1
src/services/index.js

@@ -1,2 +1,3 @@
 export { JupiterSwapper } from './jupiter.js';
-export { ByrealAPI } from './byreal.js';
+export { ByrealAPI } from './byreal.js';
+export { OnchainMonitor } from './onchain-monitor.js';

+ 157 - 34
src/services/jupiter.js

@@ -3,19 +3,39 @@ import { Connection, PublicKey, Keypair, VersionedTransaction, Transaction } fro
 import { getAssociatedTokenAddress, createAssociatedTokenAccountInstruction } from '@solana/spl-token';
 import bs58 from 'bs58';
 import { CONFIG } from '../config/index.js';
-import { logger } from '../utils/index.js';
+import { logger, sleep } from '../utils/index.js';
 
 export class JupiterSwapper {
   constructor() {
     this.connection = new Connection(CONFIG.RPC_URL, 'confirmed');
     this.keypair = Keypair.fromSecretKey(bs58.decode(CONFIG.PRIVATE_KEY));
     this.walletAddress = this.keypair.publicKey.toString();
+    
+    // New Jupiter Metis Swap API v1
+    this.jupiterBaseUrl = CONFIG.JUPITER_API || 'https://api.jup.ag/swap/v1';
+    this.apiKey = CONFIG.JUPITER_API_KEY;
   }
 
   getWalletAddress() {
     return this.walletAddress;
   }
 
+  /**
+   * Get headers for Jupiter API requests
+   */
+  getHeaders() {
+    const headers = {
+      'Accept': 'application/json',
+      'Content-Type': 'application/json',
+    };
+    
+    if (this.apiKey) {
+      headers['x-api-key'] = this.apiKey;
+    }
+    
+    return headers;
+  }
+
   async getTokenBalance(mintAddress) {
     try {
       if (mintAddress === 'So11111111111111111111111111111111111111112') {
@@ -43,7 +63,9 @@ export class JupiterSwapper {
   async getTokenPrices(tokenAddresses) {
     try {
       const mints = tokenAddresses.join(',');
-      const response = await axios.get(`https://price.jup.ag/v4/price?ids=${mints}`);
+      const response = await axios.get(`https://price.jup.ag/v4/price?ids=${mints}`, {
+        timeout: 10000,
+      });
       return response.data.data || {};
     } catch (error) {
       logger.error('Error fetching token prices:', error.message);
@@ -51,6 +73,106 @@ export class JupiterSwapper {
     }
   }
 
+  /**
+   * Fetch quote from Jupiter Metis API with retry logic
+   */
+  async fetchQuote(inputMint, outputMint, amount, retries = 3, swapMode = 'ExactIn') {
+    for (let attempt = 1; attempt <= retries; attempt++) {
+      try {
+        logger.info(`Fetching quote from Jupiter Metis API (attempt ${attempt}/${retries})...`);
+        logger.info(`  Mode: ${swapMode}, Amount: ${amount}`);
+        
+        const response = await axios.get(
+          `${this.jupiterBaseUrl}/quote`,
+          {
+            params: {
+              inputMint: inputMint,
+              outputMint: outputMint,
+              amount: amount,
+              swapMode: swapMode,
+              slippageBps: CONFIG.SLIPPAGE_BPS,
+              onlyDirectRoutes: false,
+              restrictIntermediateTokens: true,
+            },
+            timeout: 30000,
+            headers: this.getHeaders(),
+          }
+        );
+
+        if (response.data) {
+          logger.success('Quote received from Jupiter Metis API');
+          return response.data;
+        }
+      } catch (error) {
+        const status = error.response?.status;
+        const message = error.response?.data?.message || error.message;
+        
+        if (status === 401) {
+          logger.error('Jupiter API authentication failed. Please check your API key at https://portal.jup.ag');
+          throw new Error('Jupiter API Key invalid or missing');
+        }
+        
+        logger.warn(`Quote attempt ${attempt} failed: ${message}`);
+        
+        if (attempt < retries) {
+          await sleep(2000 * attempt);
+        } else {
+          throw new Error(`Failed to fetch quote: ${message}`);
+        }
+      }
+    }
+    
+    throw new Error('All quote attempts failed');
+  }
+
+  /**
+   * Execute swap transaction with retry
+   */
+  async executeSwap(quoteData, retries = 3) {
+    for (let attempt = 1; attempt <= retries; attempt++) {
+      try {
+        logger.info(`Executing swap (attempt ${attempt}/${retries})...`);
+        
+        const swapResponse = await axios.post(
+          `${this.jupiterBaseUrl}/swap`,
+          {
+            quoteResponse: quoteData,
+            userPublicKey: this.walletAddress,
+            wrapAndUnwrapSol: true,
+            prioritizationFeeLamports: 10000,
+            dynamicSlippage: false,
+          },
+          {
+            timeout: 30000,
+            headers: this.getHeaders(),
+          }
+        );
+
+        if (!swapResponse.data || !swapResponse.data.swapTransaction) {
+          throw new Error('Failed to get swap transaction');
+        }
+
+        return swapResponse.data;
+      } catch (error) {
+        const status = error.response?.status;
+        const message = error.response?.data?.message || error.message;
+        
+        if (status === 401) {
+          logger.error('Jupiter API authentication failed. Please check your API key at https://portal.jup.ag');
+          throw new Error('Jupiter API Key invalid or missing');
+        }
+        
+        logger.warn(`Swap execution attempt ${attempt} failed: ${message}`);
+        
+        if (attempt < retries) {
+          await sleep(2000 * attempt);
+        } else {
+          throw new Error(`Failed to execute swap: ${message}`);
+        }
+      }
+    }
+  }
+
   async swapIfNeeded(inputMint, outputMint, requiredAmount, decimals = 6) {
     const currentBalance = await this.getTokenBalance(outputMint);
     
@@ -63,44 +185,45 @@ export class JupiterSwapper {
     logger.warn(`Insufficient balance. Need ${neededAmount.toFixed(4)} more.`);
     logger.info(`Initiating swap from ${inputMint} to ${outputMint}...`);
 
+    // Check if API key is configured
+    if (!this.apiKey) {
+      logger.error('═══════════════════════════════════════════════════');
+      logger.error('Jupiter API Key is required!');
+      logger.error('Please apply for an API key at: https://portal.jup.ag');
+      logger.error('Then add it to your .env file: JUPITER_API_KEY=your_key_here');
+      logger.error('═══════════════════════════════════════════════════');
+      return false;
+    }
+
     try {
-      const inputAmount = Math.ceil(neededAmount * Math.pow(10, decimals));
+      // Use ExactOut mode: we need exactly `neededAmount` of the output token
+      // Convert to raw amount with correct decimals (from API)
+      const outputAmount = Math.ceil(neededAmount * Math.pow(10, decimals));
       
-      const quoteResponse = await axios.get(
-        `${CONFIG.JUPITER_API}/quote`,
-        {
-          params: {
-            inputMint: inputMint,
-            outputMint: outputMint,
-            amount: inputAmount,
-            slippageBps: CONFIG.SLIPPAGE_BPS,
-            onlyDirectRoutes: false,
-            asLegacyTransaction: false,
-          },
-        }
+      logger.info(`Swap: USDC -> ${outputMint.slice(0, 8)}...`);
+      logger.info(`Required output: ${neededAmount} (decimals: ${decimals}, raw: ${outputAmount})`);
+      
+      // Fetch quote with ExactOut swap mode
+      const quoteData = await this.fetchQuote(
+        inputMint,
+        outputMint,
+        outputAmount,
+        3,
+        'ExactOut'
       );
 
-      if (!quoteResponse.data) {
-        throw new Error('No swap route found');
+      // Log route info
+      if (quoteData.routePlan && quoteData.routePlan.length > 0) {
+        const routeLabels = quoteData.routePlan.map(r => r.swapInfo?.label || 'Unknown').join(' -> ');
+        logger.info(`Route: ${routeLabels}`);
+        logger.info(`Expected output: ${quoteData.outAmount} (${quoteData.swapMode})`);
+        logger.info(`Price impact: ${quoteData.priceImpactPct}%`);
       }
 
-      logger.success('Quote received from Jupiter');
-
-      const swapResponse = await axios.post(
-        `${CONFIG.JUPITER_API}/swap`,
-        {
-          quoteResponse: quoteResponse.data,
-          userPublicKey: this.walletAddress,
-          wrapAndUnwrapSol: true,
-          prioritizationFeeLamports: 10000,
-        }
-      );
-
-      if (!swapResponse.data || !swapResponse.data.swapTransaction) {
-        throw new Error('Failed to get swap transaction');
-      }
+      // Execute swap with retry
+      const swapData = await this.executeSwap(quoteData);
 
-      const swapTransactionBuf = Buffer.from(swapResponse.data.swapTransaction, 'base64');
+      const swapTransactionBuf = Buffer.from(swapData.swapTransaction, 'base64');
       const transaction = VersionedTransaction.deserialize(swapTransactionBuf);
       
       transaction.sign([this.keypair]);
@@ -163,4 +286,4 @@ export class JupiterSwapper {
   }
 }
 
-export default JupiterSwapper;
+export default JupiterSwapper;

+ 264 - 0
src/services/onchain-monitor.js

@@ -0,0 +1,264 @@
+import { Connection, PublicKey } from '@solana/web3.js';
+import { CONFIG } from '../config/index.js';
+import { logger, sleep } from '../utils/index.js';
+import { ByrealAPI } from './byreal.js';
+
+/**
+ * Onchain Monitor - Monitor target wallet for Byreal LP opening transactions
+ * Detects transactions, waits 60s for Byreal API to sync, then fetches position details
+ */
+export class OnchainMonitor {
+  constructor(callback) {
+    this.connection = new Connection(CONFIG.RPC_URL, 'confirmed');
+    this.targetWallet = new PublicKey(CONFIG.TARGET_WALLET);
+    this.byrealProgramId = new PublicKey('REALQqNEomY6cQGZJUGwywTBD2UmDT32rZcNnfxQ5N2');
+    this.memoProgramId = new PublicKey('MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr');
+    this.callback = callback; // Callback when new position detected
+    this.isRunning = false;
+    this.processedSignatures = new Set(); // Track processed transactions
+    this.lastSignature = null;
+  }
+
+  /**
+   * Start monitoring for new transactions
+   */
+  async start() {
+    if (this.isRunning) {
+      logger.warn('Onchain monitor already running');
+      return;
+    }
+
+    this.isRunning = true;
+    logger.info('═══════════════════════════════════════════');
+    logger.info('🔗 Onchain Monitor Started');
+    logger.info(`🎯 Target: ${CONFIG.TARGET_WALLET}`);
+    logger.info(`📋 Mode: Detect transaction -> Wait 60s -> Fetch from Byreal API`);
+    logger.info('═══════════════════════════════════════════\n');
+
+    // Get initial last signature to avoid processing old transactions
+    await this.getLatestSignature();
+
+    // Start monitoring loop
+    while (this.isRunning) {
+      try {
+        await this.checkNewTransactions();
+        await sleep(CONFIG.POLL_INTERVAL_MS || 10000);
+      } catch (error) {
+        logger.error('Onchain monitor error:', error.message);
+        await sleep(CONFIG.POLL_INTERVAL_MS || 10000);
+      }
+    }
+  }
+
+  /**
+   * Get the latest signature to establish baseline
+   */
+  async getLatestSignature() {
+    try {
+      const signatures = await this.connection.getSignaturesForAddress(
+        this.targetWallet,
+        { limit: 1 }
+      );
+      
+      if (signatures.length > 0) {
+        this.lastSignature = signatures[0].signature;
+        logger.info(`Initial last signature: ${this.lastSignature.slice(0, 16)}...`);
+      }
+    } catch (error) {
+      logger.error('Error getting latest signature:', error.message);
+    }
+  }
+
+  /**
+   * Check for new transactions from target wallet
+   */
+  async checkNewTransactions() {
+    try {
+      const signatures = await this.connection.getSignaturesForAddress(
+        this.targetWallet,
+        { 
+          limit: 10,
+          until: this.lastSignature || undefined
+        }
+      );
+
+      if (signatures.length === 0) {
+        return;
+      }
+
+      // Update last signature for next iteration
+      this.lastSignature = signatures[0].signature;
+
+      // Process signatures in reverse order (oldest first)
+      for (const sigInfo of signatures.reverse()) {
+        const signature = sigInfo.signature;
+        
+        // Skip already processed
+        if (this.processedSignatures.has(signature)) {
+          continue;
+        }
+
+        this.processedSignatures.add(signature);
+        
+        // Limit set size to prevent memory issues
+        if (this.processedSignatures.size > 1000) {
+          const iterator = this.processedSignatures.values();
+          this.processedSignatures.delete(iterator.next().value);
+        }
+
+        // Process the transaction
+        await this.handleNewTransaction(signature);
+      }
+    } catch (error) {
+      logger.error('Error checking new transactions:', error.message);
+    }
+  }
+
+  /**
+   * Handle new transaction - extract parent position, wait 60s, fetch from Byreal API
+   */
+  async handleNewTransaction(signature) {
+    try {
+      logger.info(`Processing transaction: ${signature.slice(0, 16)}...`);
+
+      const tx = await this.connection.getTransaction(signature, {
+        commitment: 'confirmed',
+        maxSupportedTransactionVersion: 0
+      });
+
+      if (!tx || !tx.transaction || !tx.meta) {
+        logger.debug(`Transaction not found or invalid: ${signature.slice(0, 16)}`);
+        return;
+      }
+
+      // Check if transaction involves Byreal program
+      const message = tx.transaction.message;
+      const accountKeys = message.staticAccountKeys || message.accountKeys || [];
+      
+      const hasByrealProgram = accountKeys.some(
+        key => key.toString() === this.byrealProgramId.toString()
+      );
+
+      if (!hasByrealProgram) {
+        logger.debug(`No Byreal program involvement: ${signature.slice(0, 16)}`);
+        return;
+      }
+
+      logger.info(`Found Byreal transaction: ${signature.slice(0, 16)}...`);
+
+      // Extract parent position from memo
+      const parentPosition = this.extractParentPosition(tx);
+      
+      if (!parentPosition) {
+        logger.info(`No parent position found in memo, skipping...`);
+        return;
+      }
+
+      logger.success(`Found parent position from memo: ${parentPosition}`);
+
+      // Wait 60 seconds for Byreal API to sync
+      logger.info(`Waiting 60 seconds for Byreal API to sync...`);
+      await sleep(60000);
+
+      // Fetch position details from Byreal API
+      logger.info(`Fetching position details from Byreal API...`);
+      const positionDetail = await ByrealAPI.getPositionDetail(parentPosition);
+
+      if (!positionDetail) {
+        logger.error(`Failed to fetch position detail from Byreal API: ${parentPosition}`);
+        return;
+      }
+
+      logger.success(`Position details fetched successfully!`);
+      logger.info(`  Pool: ${positionDetail.pool?.mintA?.symbol}/${positionDetail.pool?.mintB?.symbol}`);
+      logger.info(`  NFT Mint: ${positionDetail.nftMintAddress || positionDetail.nftMint}`);
+      logger.info(`  USD Value: $${positionDetail.totalDeposit || positionDetail.totalUsdValue || positionDetail.liquidityUsd || 0}`);
+
+      // Call the callback with position info from Byreal API
+      if (this.callback) {
+        await this.callback({
+          parentPositionAddress: parentPosition,
+          targetPositionAddress: signature,
+          transactionSignature: signature,
+          positionDetail: positionDetail // Full position detail from Byreal API
+        });
+      }
+
+    } catch (error) {
+      logger.error(`Error handling transaction ${signature.slice(0, 16)}:`, error.message);
+    }
+  }
+
+  /**
+   * Extract parent position address from Memo instruction
+   */
+  extractParentPosition(tx) {
+    try {
+      const message = tx.transaction.message;
+      const instructions = message.compiledInstructions || message.instructions || [];
+      
+      for (const instruction of instructions) {
+        // Check if it's a memo instruction
+        const programId = message.staticAccountKeys 
+          ? message.staticAccountKeys[instruction.programIdIndex].toString()
+          : instruction.programId?.toString();
+        
+        if (programId === this.memoProgramId.toString()) {
+          // Parse memo data
+          let memoData;
+          
+          if (instruction.data) {
+            // Try to decode base64 data
+            try {
+              memoData = Buffer.from(instruction.data, 'base64').toString('utf8');
+            } catch {
+              // Try as string directly
+              memoData = instruction.data;
+            }
+          }
+
+          // Check if it's already parsed
+          if (instruction.parsed) {
+            memoData = instruction.parsed;
+          }
+
+          if (memoData) {
+            logger.debug(`Memo data: ${memoData}`);
+            
+            // Extract referer_position from memo
+            // Format: referer_position=GdMQaDtzcaAh4XVcHsUJV8yQShkVXDp9sJLNwvd23TxQ
+            const match = memoData.match(/referer_position=([A-Za-z0-9]+)/);
+            if (match) {
+              return match[1];
+            }
+          }
+        }
+      }
+
+      // Also check log messages as fallback
+      if (tx.meta.logMessages) {
+        for (const log of tx.meta.logMessages) {
+          const match = log.match(/referer_position=([A-Za-z0-9]+)/);
+          if (match) {
+            return match[1];
+          }
+        }
+      }
+
+      return null;
+    } catch (error) {
+      logger.error('Error extracting parent position:', error.message);
+      return null;
+    }
+  }
+
+  /**
+   * Stop monitoring
+   */
+  stop() {
+    logger.info('Stopping onchain monitor...');
+    this.isRunning = false;
+  }
+}
+
+export default OnchainMonitor;

+ 193 - 0
src/utils/discord.js

@@ -0,0 +1,193 @@
+import axios from 'axios';
+import { logger } from './index.js';
+
+/**
+ * Discord Webhook Service
+ * Sends notifications for copy and close operations
+ */
+export class DiscordWebhook {
+  constructor(webhookUrl) {
+    this.webhookUrl = webhookUrl;
+  }
+
+  /**
+   * Send notification to Discord
+   */
+  async sendNotification(embed) {
+    if (!this.webhookUrl) {
+      logger.debug('Discord webhook URL not configured, skipping notification');
+      return;
+    }
+
+    try {
+      const response = await axios.post(
+        this.webhookUrl,
+        {
+          embeds: [embed],
+          username: 'Byreal Sniper Bot',
+          avatar_url: 'https://i.imgur.com/4M34hi2.png', // Optional bot avatar
+        },
+        {
+          timeout: 10000,
+          headers: {
+            'Content-Type': 'application/json',
+          },
+        }
+      );
+
+      if (response.status === 204) {
+        logger.success('Discord notification sent successfully');
+      }
+    } catch (error) {
+      logger.error('Failed to send Discord notification:', error.message);
+      if (error.response) {
+        logger.error('Discord API response:', error.response.data);
+      }
+    }
+  }
+
+  /**
+   * Send copy success notification
+   */
+  async notifyCopySuccess({
+    parentPosition,
+    poolAddress,
+    poolName,
+    tokenA,
+    tokenB,
+    copyUsdValue,
+    txSignature,
+  }) {
+    const embed = {
+      title: '✅ Position Copied Successfully',
+      color: 0x00ff00, // Green
+      timestamp: new Date().toISOString(),
+      fields: [
+        {
+          name: '📋 Parent Position',
+          value: `[${parentPosition.slice(0, 8)}...${parentPosition.slice(-8)}](https://solscan.io/account/${parentPosition})`,
+          inline: true,
+        },
+        {
+          name: '🏊 Pool',
+          value: `${poolName}\n[${poolAddress.slice(0, 8)}...](https://solscan.io/account/${poolAddress})`,
+          inline: true,
+        },
+        {
+          name: '💰 Copy Amount',
+          value: `$${copyUsdValue.toFixed(2)}`,
+          inline: true,
+        },
+        {
+          name: '💱 Token A',
+          value: `${tokenA.amount} ${tokenA.symbol || ''}\n($${tokenA.valueUsd || 0})`,
+          inline: true,
+        },
+        {
+          name: '💱 Token B',
+          value: `${tokenB.amount} ${tokenB.symbol || ''}\n($${tokenB.valueUsd || 0})`,
+          inline: true,
+        },
+      ],
+      footer: {
+        text: 'Byreal Sniper Bot',
+      },
+    };
+
+    if (txSignature) {
+      embed.fields.push({
+        name: '🔗 Transaction',
+        value: `[View on Solscan](https://solscan.io/tx/${txSignature})`,
+        inline: false,
+      });
+    }
+
+    await this.sendNotification(embed);
+  }
+
+  /**
+   * Send close success notification
+   */
+  async notifyCloseSuccess({
+    positionAddress,
+    poolName,
+    closedAt,
+    reason,
+    txSignature,
+  }) {
+    const embed = {
+      title: '🔴 Position Closed',
+      color: 0xff0000, // Red
+      timestamp: new Date().toISOString(),
+      fields: [
+        {
+          name: '📋 Position',
+          value: `[${positionAddress.slice(0, 8)}...${positionAddress.slice(-8)}](https://solscan.io/account/${positionAddress})`,
+          inline: true,
+        },
+        {
+          name: '🏊 Pool',
+          value: poolName || 'Unknown',
+          inline: true,
+        },
+        {
+          name: '📝 Reason',
+          value: reason || 'Manual close',
+          inline: false,
+        },
+      ],
+      footer: {
+        text: 'Byreal Sniper Bot',
+      },
+    };
+
+    if (txSignature) {
+      embed.fields.push({
+        name: '🔗 Transaction',
+        value: `[View on Solscan](https://solscan.io/tx/${txSignature})`,
+        inline: false,
+      });
+    }
+
+    await this.sendNotification(embed);
+  }
+
+  /**
+   * Send error notification
+   */
+  async notifyError({
+    operation,
+    positionAddress,
+    error,
+  }) {
+    const embed = {
+      title: '⚠️ Operation Failed',
+      color: 0xffa500, // Orange
+      timestamp: new Date().toISOString(),
+      fields: [
+        {
+          name: '❌ Operation',
+          value: operation,
+          inline: true,
+        },
+        {
+          name: '📋 Position',
+          value: `[${positionAddress.slice(0, 8)}...${positionAddress.slice(-8)}](https://solscan.io/account/${positionAddress})`,
+          inline: true,
+        },
+        {
+          name: '🔴 Error',
+          value: error.slice(0, 1000), // Discord field limit is 1024
+          inline: false,
+        },
+      ],
+      footer: {
+        text: 'Byreal Sniper Bot',
+      },
+    };
+
+    await this.sendNotification(embed);
+  }
+}
+
+export default DiscordWebhook;

+ 2 - 1
src/utils/index.js

@@ -1,3 +1,4 @@
 export { PositionCache } from './cache.js';
 export { formatUsd, formatTokenAmount, sleep, formatDate, parseTokenAmount } from './helpers.js';
-export { log, logger, setLogLevel } from './logger.js';
+export { log, logger, setLogLevel } from './logger.js';
+export { DiscordWebhook } from './discord.js';

+ 222 - 0
test-copy.js

@@ -0,0 +1,222 @@
+#!/usr/bin/env node
+
+/**
+ * Test Copy Script
+ * Directly test the copy functionality for a specific parent position
+ * 
+ * Usage: 
+ *   node test-copy.js <parent_position_address> [target_usd_value]
+ * 
+ * Examples:
+ *   # Use parent position value (default)
+ *   node test-copy.js Cbgjt5zfXd9QTpneDdADc9cKYwXxXRfe57cyfmBbQM41
+ *   
+ *   # Use specific target value (e.g., from transaction analysis)
+ *   node test-copy.js Cbgjt5zfXd9QTpneDdADc9cKYwXxXRfe57cyfmBbQM41 13.55
+ */
+
+import { ByrealAPI, JupiterSwapper } from './src/services/index.js';
+import { CONFIG } from './src/config/index.js';
+import { logger, setLogLevel, formatUsd } from './src/utils/index.js';
+
+// Set log level to see detailed info
+setLogLevel('info');
+
+const PARENT_POSITION = process.argv[2] || '7rxWLdhk6Hk914QSBBztXJ35DUs4exr7mFJJdYWR2YNw';
+const MANUAL_TARGET_VALUE = parseFloat(process.argv[3]) || 0; // Optional: manual target value
+
+async function testCopy() {
+  console.log('═══════════════════════════════════════════');
+  console.log('🧪 Test Copy Script');
+  console.log(`📍 Parent Position: ${PARENT_POSITION}`);
+  if (MANUAL_TARGET_VALUE > 0) {
+    console.log(`💰 Using Manual Target Value: $${MANUAL_TARGET_VALUE}`);
+  }
+  console.log('═══════════════════════════════════════════\n');
+
+  // Step 1: Fetch parent position details
+  console.log('Step 1: Fetching parent position details...');
+  const parentDetail = await ByrealAPI.getPositionDetail(PARENT_POSITION);
+  
+  if (!parentDetail) {
+    console.error('❌ Failed to fetch position detail');
+    process.exit(1);
+  }
+
+  const poolInfo = parentDetail.pool;
+  if (!poolInfo) {
+    console.error('❌ Pool info not found');
+    process.exit(1);
+  }
+
+  console.log('✅ Position details:');
+  console.log(`  Pool: ${poolInfo.mintA?.symbol}/${poolInfo.mintB?.symbol}`);
+  console.log(`  Token A: ${poolInfo.mintA?.address}`);
+  console.log(`  Token B: ${poolInfo.mintB?.address}`);
+  
+  // Get correct field names
+  const nftMint = parentDetail.nftMintAddress || parentDetail.nftMint;
+  const poolAddress = parentDetail.pool?.poolAddress || parentDetail.poolAddress;
+  
+  // Get base value for calculation
+  const parentUsdValue = parseFloat(parentDetail.totalDeposit || parentDetail.totalUsdValue || parentDetail.liquidityUsd || 0);
+  
+  // Use manual target value if provided, otherwise use parent value
+  const targetUsdValue = MANUAL_TARGET_VALUE > 0 ? MANUAL_TARGET_VALUE : parentUsdValue;
+  
+  console.log(`  NFT Mint: ${nftMint}`);
+  console.log(`  Pool Address: ${poolAddress}`);
+  console.log(`  Parent Position Value: $${parentUsdValue}`);
+  if (MANUAL_TARGET_VALUE > 0) {
+    console.log(`  ⚠️  Using Target Value: $${targetUsdValue} (not parent value!)`);
+  } else {
+    console.log(`  Using Value: $${targetUsdValue}`);
+  }
+
+  // Step 2: Calculate copy amount
+  console.log('\nStep 2: Calculating copy amount...');
+  const copyUsdValue = Math.min(
+    targetUsdValue * CONFIG.COPY_MULTIPLIER,
+    CONFIG.MAX_USD_VALUE
+  );
+  
+  console.log(`  Base Value: $${targetUsdValue}`);
+  console.log(`  Multiplier: ${CONFIG.COPY_MULTIPLIER}x`);
+  console.log(`  Calculated: $${(targetUsdValue * CONFIG.COPY_MULTIPLIER).toFixed(2)}`);
+  console.log(`  Max Allowed: $${CONFIG.MAX_USD_VALUE}`);
+  console.log(`  ✅ Final Copy Amount: $${copyUsdValue}`);
+  
+  if (copyUsdValue < CONFIG.MIN_USD_VALUE) {
+    console.error(`❌ Copy value $${copyUsdValue} below minimum $${CONFIG.MIN_USD_VALUE}`);
+    process.exit(1);
+  }
+
+  // Step 3: Calculate token amounts
+  console.log('\nStep 3: Calculating token amounts...');
+  console.log(`  Calling Byreal API with maxUsdValue: $${copyUsdValue}`);
+  
+  const calculation = await ByrealAPI.calculatePosition(
+    PARENT_POSITION,
+    copyUsdValue,
+    nftMint
+  );
+
+  if (!calculation) {
+    console.error('❌ Calculation failed');
+    process.exit(1);
+  }
+
+  console.log('✅ Calculation result from Byreal API:');
+  console.log(`  Token A: ${calculation.tokenA?.amount} ${calculation.tokenA?.symbol} ($${calculation.tokenA?.valueUsd})`);
+  console.log(`  Token B: ${calculation.tokenB?.amount} ${calculation.tokenB?.symbol} ($${calculation.tokenB?.valueUsd})`);
+  console.log(`  Estimated Total: $${calculation.estimatedValue}`);
+  console.log(`\n  ⚠️  Note: These amounts are based on copy value $${copyUsdValue},`);
+  console.log(`      not the full parent position value ($${parentUsdValue})!`);
+  
+  // Debug: Check if amount is token quantity or USD value
+  console.log('\n  🔍 Checking token amounts:');
+  console.log(`    Token A amount field: ${calculation.tokenA?.amount} (type: ${typeof calculation.tokenA?.amount})`);
+  console.log(`    Token A valueUsd field: ${calculation.tokenA?.valueUsd}`);
+  console.log(`    Token B amount field: ${calculation.tokenB?.amount} (type: ${typeof calculation.tokenB?.amount})`);
+  console.log(`    Token B valueUsd field: ${calculation.tokenB?.valueUsd}`);
+  console.log(`    If amount ≈ valueUsd, then amount is USD value, not token quantity!`);
+
+  // Step 4: Check balances and swap
+  console.log('\nStep 4: Checking balances and swapping...');
+  const swapper = new JupiterSwapper();
+  
+  const tokenA = calculation.tokenA;
+  const tokenB = calculation.tokenB;
+  
+  // Token A
+  console.log(`\n  Token A (${tokenA.symbol}):`);
+  const tokenABalance = await swapper.getTokenBalance(tokenA.address);
+  const requiredAmountA = parseFloat(tokenA.amount);
+  const swapAmountA = requiredAmountA * 1.1;
+  
+  console.log(`    Balance: ${tokenABalance.toFixed(6)}`);
+  console.log(`    Required: ${requiredAmountA.toFixed(6)}`);
+  console.log(`    With 10% buffer: ${swapAmountA.toFixed(6)}`);
+  
+  if (tokenABalance < requiredAmountA * 0.95) {
+    console.log(`    ⚠️  Insufficient balance, need to swap from USDC`);
+
+    // Ask for confirmation
+    console.log('\n    Ready to swap. Continue? (Ctrl+C to cancel)');
+    await new Promise(resolve => setTimeout(resolve, 3000));
+
+    const success = await swapper.swapIfNeeded(
+      'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
+      tokenA.address,
+      swapAmountA,
+      tokenA.decimals
+    );
+    
+    if (!success) {
+      console.error('❌ Failed to acquire Token A');
+      process.exit(1);
+    }
+  } else {
+    console.log(`    ✅ Balance sufficient`);
+  }
+
+  // Token B
+  console.log(`\n  Token B (${tokenB.symbol}):`);
+  const tokenBBalance = await swapper.getTokenBalance(tokenB.address);
+  const requiredAmountB = parseFloat(tokenB.amount);
+  const swapAmountB = requiredAmountB * 1.1;
+  
+  console.log(`    Balance: ${tokenBBalance.toFixed(6)}`);
+  console.log(`    Required: ${requiredAmountB.toFixed(6)}`);
+  console.log(`    With 10% buffer: ${swapAmountB.toFixed(6)}`);
+  
+  if (tokenBBalance < requiredAmountB * 0.95) {
+    console.log(`    ⚠️  Insufficient balance, need to swap from USDC`);
+    
+    const success = await swapper.swapIfNeeded(
+      'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
+      tokenB.address,
+      swapAmountB,
+      tokenB.decimals
+    );
+    
+    if (!success) {
+      console.error('❌ Failed to acquire Token B');
+      process.exit(1);
+    }
+  } else {
+    console.log(`    ✅ Balance sufficient`);
+  }
+
+  // Step 5: Execute copy
+  console.log('\n═══════════════════════════════════════════');
+  console.log('Step 5: Ready to execute copy!');
+  console.log('═══════════════════════════════════════════');
+  console.log(`Parent Position: ${PARENT_POSITION}`);
+  console.log(`NFT Mint: ${nftMint}`);
+  console.log(`Copy Amount: $${copyUsdValue}`);
+  console.log('\n⚠️  This will execute the actual copy transaction!');
+
+  console.log('Executing copy...');
+  const success = await ByrealAPI.copyPosition(
+    nftMint,
+    PARENT_POSITION,
+    copyUsdValue
+  );
+
+  if (success) {
+    console.log('\n✅ Copy executed successfully!');
+  } else {
+    console.log('\n❌ Copy failed!');
+    process.exit(1);
+  }
+
+  console.log('\n═══════════════════════════════════════════');
+  console.log('Test completed');
+  console.log('═══════════════════════════════════════════');
+}
+
+testCopy().catch(error => {
+  console.error('Fatal error:', error);
+  process.exit(1);
+});

+ 169 - 0
test-target-value.js

@@ -0,0 +1,169 @@
+#!/usr/bin/env node
+
+/**
+ * Test: Copy with target position value (not parent value)
+ * This test verifies that copy amount is calculated based on what the target wallet deposited,
+ * not the parent position's total value.
+ */
+
+import { Connection, PublicKey } from '@solana/web3.js';
+import { CONFIG } from './src/config/index.js';
+import { logger, setLogLevel } from './src/utils/index.js';
+
+setLogLevel('info');
+
+const TEST_TX = '5SnXmX8T4sytTFu8xeG7RXmui1eK65no8B3eJ3knTtdXqr3PqmDma5CaY9C7NG2oycvNs7KKZ75pPNDXB2T5XYQE';
+
+// Expected values from manual analysis
+const EXPECTED = {
+  parentPosition: 'Cbgjt5zfXd9QTpneDdADc9cKYwXxXRfe57cyfmBbQM41',
+  targetTokenA: 0.001521, // XAUt0
+  targetTokenB: 6.00,     // USDT
+  targetTotalUsd: 0.001521 * 4965 + 6.00 * 1, // ~13.55
+  positionNft: 'GUwGkK3PHnGVrLd3fPTphAMxuUFqkad7HKYyyeHzEW7p'
+};
+
+async function testTargetValueExtraction() {
+  console.log('═══════════════════════════════════════════════════════');
+  console.log('🧪 Test: Target Position Value Extraction');
+  console.log(`📍 TX: ${TEST_TX.slice(0, 25)}...`);
+  console.log('═══════════════════════════════════════════════════════\n');
+
+  const connection = new Connection(CONFIG.RPC_URL, 'confirmed');
+
+  try {
+    // Fetch transaction
+    console.log('1. Fetching transaction...');
+    const tx = await connection.getTransaction(TEST_TX, {
+      commitment: 'confirmed',
+      maxSupportedTransactionVersion: 0
+    });
+
+    if (!tx) {
+      console.error('❌ Transaction not found');
+      return false;
+    }
+    console.log('✅ Transaction found\n');
+
+    // Extract parent position from memo
+    console.log('2. Extracting parent position...');
+    let parentPosition = null;
+    const message = tx.transaction.message;
+    const accountKeys = message.staticAccountKeys || message.accountKeys || [];
+    const instructions = message.compiledInstructions || message.instructions || [];
+    const MEMO_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr';
+
+    for (const instruction of instructions) {
+      const programId = accountKeys[instruction.programIdIndex]?.toString();
+      if (programId === MEMO_PROGRAM_ID) {
+        let memoData = instruction.parsed || '';
+        if (instruction.data) {
+          try {
+            memoData = Buffer.from(instruction.data, 'base64').toString('utf8');
+          } catch {}
+        }
+        const match = memoData.match(/referer_position=([A-Za-z0-9]+)/);
+        if (match) parentPosition = match[1];
+      }
+    }
+
+    console.log(`   Expected: ${EXPECTED.parentPosition}`);
+    console.log(`   Got:      ${parentPosition}`);
+    console.log(`   ${parentPosition === EXPECTED.parentPosition ? '✅ PASS' : '❌ FAIL'}\n`);
+
+    // Extract position NFT
+    console.log('3. Extracting position NFT...');
+    let positionNft = null;
+    if (tx.meta?.postTokenBalances) {
+      for (const balance of tx.meta.postTokenBalances) {
+        if (balance.uiTokenAmount?.uiAmount === 1 && balance.uiTokenAmount?.decimals === 0) {
+          positionNft = balance.mint;
+          break;
+        }
+      }
+    }
+
+    console.log(`   Expected: ${EXPECTED.positionNft}`);
+    console.log(`   Got:      ${positionNft}`);
+    console.log(`   ${positionNft === EXPECTED.positionNft ? '✅ PASS' : '❌ FAIL'}\n`);
+
+    // Extract target position value
+    console.log('4. Extracting target position value...');
+    const transfers = [];
+    
+    if (tx.meta?.preTokenBalances && tx.meta?.postTokenBalances) {
+      const preBalances = {};
+      tx.meta.preTokenBalances.forEach(balance => {
+        const key = `${balance.accountIndex}-${balance.mint}`;
+        preBalances[key] = parseFloat(balance.uiTokenAmount?.uiAmountString || 0);
+      });
+
+      tx.meta.postTokenBalances.forEach(balance => {
+        const key = `${balance.accountIndex}-${balance.mint}`;
+        const pre = preBalances[key] || 0;
+        const post = parseFloat(balance.uiTokenAmount?.uiAmountString || 0);
+        const diff = pre - post;
+        
+        if (diff > 0.0001) {
+          transfers.push({
+            mint: balance.mint,
+            uiAmount: diff,
+            decimals: balance.decimals
+          });
+        }
+      });
+    }
+
+    transfers.sort((a, b) => b.uiAmount - a.uiAmount);
+    
+    const tokenA = transfers[0] || {};
+    const tokenB = transfers[1] || {};
+
+    // Known prices
+    const TOKEN_PRICES = {
+      'AymATz4TCL9sWNEEV9Kvyz45CHVhDZ6kUgjTJPzLpU9P': 4965,
+      'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB': 1
+    };
+
+    const tokenAPrice = TOKEN_PRICES[tokenA.mint] || 0;
+    const tokenBPrice = TOKEN_PRICES[tokenB.mint] || 0;
+    const totalUsd = (tokenA.uiAmount || 0) * tokenAPrice + (tokenB.uiAmount || 0) * tokenBPrice;
+
+    console.log(`   Token A: ${tokenA.uiAmount?.toFixed(6)} (expected: ${EXPECTED.targetTokenA})`);
+    console.log(`   Token B: ${tokenB.uiAmount?.toFixed(6)} (expected: ${EXPECTED.targetTokenB})`);
+    console.log(`   Total USD: $${totalUsd.toFixed(2)} (expected: $${EXPECTED.targetTotalUsd.toFixed(2)})`);
+    
+    const tolerance = 0.001;
+    const tokenAOk = Math.abs((tokenA.uiAmount || 0) - EXPECTED.targetTokenA) < tolerance;
+    const tokenBOk = Math.abs((tokenB.uiAmount || 0) - EXPECTED.targetTokenB) < tolerance;
+    const totalOk = Math.abs(totalUsd - EXPECTED.targetTotalUsd) < 1;
+    
+    console.log(`   ${tokenAOk && tokenBOk && totalOk ? '✅ PASS' : '❌ FAIL'}\n`);
+
+    // Calculate copy amount
+    console.log('5. Calculating copy amount...');
+    const copyMultiplier = CONFIG.COPY_MULTIPLIER;
+    const maxUsd = CONFIG.MAX_USD_VALUE;
+    const copyUsd = Math.min(totalUsd * copyMultiplier, maxUsd);
+
+    console.log(`   Target Value: $${totalUsd.toFixed(2)}`);
+    console.log(`   Multiplier: ${copyMultiplier}x`);
+    console.log(`   Calculated: $${(totalUsd * copyMultiplier).toFixed(2)}`);
+    console.log(`   Max Limit: $${maxUsd}`);
+    console.log(`   ✅ Final Copy Amount: $${copyUsd.toFixed(2)}`);
+    console.log(`\n   IMPORTANT: This is based on TARGET position ($${totalUsd.toFixed(2)}),`);
+    console.log(`   NOT parent position (which might be much larger)!\n`);
+
+    console.log('═══════════════════════════════════════════════════════');
+    console.log('✅ Test completed successfully!');
+    console.log('═══════════════════════════════════════════════════════');
+    
+    return true;
+
+  } catch (error) {
+    console.error('❌ Test failed:', error);
+    return false;
+  }
+}
+
+testTargetValueExtraction();

+ 248 - 0
test-tx-analysis.js

@@ -0,0 +1,248 @@
+#!/usr/bin/env node
+
+/**
+ * Test: Calculate copy amount from target position (not parent)
+ * Analyzes a specific transaction to get the target's position value
+ * Transaction: 5SnXmX8T4sytTFu8xeG7RXmui1eK65no8B3eJ3knTtdXqr3PqmDma5CaY9C7NG2oycvNs7KKZ75pPNDXB2T5XYQE
+ */
+
+import { Connection, PublicKey } from '@solana/web3.js';
+import { CONFIG } from './src/config/index.js';
+import { logger, setLogLevel } from './src/utils/index.js';
+
+setLogLevel('debug');
+
+const TX_SIGNATURE = '5SnXmX8T4sytTFu8xeG7RXmui1eK65no8B3eJ3knTtdXqr3PqmDma5CaY9C7NG2oycvNs7KKZ75pPNDXB2T5XYQE';
+
+async function analyzeTransaction() {
+  console.log('═══════════════════════════════════════════');
+  console.log('🧪 Analyzing Target Position Transaction');
+  console.log(`📍 TX: ${TX_SIGNATURE.slice(0, 20)}...`);
+  console.log('═══════════════════════════════════════════\n');
+
+  const connection = new Connection(CONFIG.RPC_URL, 'confirmed');
+
+  try {
+    // Fetch transaction details
+    console.log('Fetching transaction from Solana...');
+    const tx = await connection.getTransaction(TX_SIGNATURE, {
+      commitment: 'confirmed',
+      maxSupportedTransactionVersion: 0
+    });
+
+    if (!tx) {
+      console.error('❌ Transaction not found');
+      return;
+    }
+
+    console.log('✅ Transaction found!\n');
+
+    // Extract token transfers from pre/post token balances
+    console.log('Step 1: Extracting token transfers from balance changes...');
+    const transfers = [];
+    
+    // Compare pre and post token balances to find what was transferred
+    if (tx.meta && tx.meta.preTokenBalances && tx.meta.postTokenBalances) {
+      // Create a map of pre balances
+      const preBalances = {};
+      tx.meta.preTokenBalances.forEach(balance => {
+        const key = `${balance.accountIndex}-${balance.mint}`;
+        preBalances[key] = parseFloat(balance.uiTokenAmount?.uiAmountString || 0);
+      });
+      
+      // Compare with post balances
+      tx.meta.postTokenBalances.forEach(balance => {
+        const key = `${balance.accountIndex}-${balance.mint}`;
+        const preAmount = preBalances[key] || 0;
+        const postAmount = parseFloat(balance.uiTokenAmount?.uiAmountString || 0);
+        const diff = postAmount - preAmount;
+        
+        // If balance decreased significantly (transferred out), record it
+        if (diff < -0.0001) {
+          transfers.push({
+            mint: balance.mint,
+            uiAmount: Math.abs(diff),
+            decimals: balance.decimals,
+            accountIndex: balance.accountIndex,
+            owner: balance.owner
+          });
+        }
+      });
+    }
+    
+    // Also try inner instructions as fallback
+    if (transfers.length === 0 && tx.meta && tx.meta.innerInstructions) {
+      for (const innerInst of tx.meta.innerInstructions) {
+        for (const inst of innerInst.instructions) {
+          if (inst.parsed?.type === 'transferChecked') {
+            const info = inst.parsed.info;
+            transfers.push({
+              mint: info.mint,
+              amount: info.tokenAmount?.amount,
+              uiAmount: parseFloat(info.tokenAmount?.uiAmount || 0),
+              decimals: info.tokenAmount?.decimals,
+              destination: info.destination
+            });
+          }
+        }
+      }
+    }
+
+    console.log(`Found ${transfers.length} token transfers:\n`);
+    transfers.forEach((t, i) => {
+      console.log(`  Transfer ${i + 1}:`);
+      console.log(`    Mint: ${t.mint}`);
+      console.log(`    Amount: ${t.uiAmount} (decimals: ${t.decimals})`);
+      console.log(`    Raw: ${t.amount}`);
+      console.log(`    Destination: ${t.destination?.slice(0, 16)}...`);
+    });
+
+    // Get token prices to calculate USD value
+    console.log('\nStep 2: Fetching token prices...');
+    const mints = [...new Set(transfers.map(t => t.mint))];
+    console.log(`Token mints: ${mints.join(', ')}`);
+
+    // Try to get prices from Jupiter price API
+    let totalUsdValue = 0;
+    try {
+      const axios = (await import('axios')).default;
+      const response = await axios.get(
+        `https://price.jup.ag/v4/price?ids=${mints.join(',')}`,
+        { timeout: 10000 }
+      );
+      
+      const prices = response.data.data || {};
+      
+      console.log('\nToken prices:');
+      transfers.forEach(t => {
+        const price = prices[t.mint]?.price || 0;
+        const value = t.uiAmount * price;
+        totalUsdValue += value;
+        
+        console.log(`  ${t.mint.slice(0, 8)}...: $${price} × ${t.uiAmount} = $${value.toFixed(2)}`);
+      });
+      
+      console.log(`\n💰 Total Position Value: $${totalUsdValue.toFixed(2)}`);
+    } catch (error) {
+      console.log('⚠️  Could not fetch prices, showing raw amounts only');
+    }
+
+    // Calculate what we should copy
+    console.log('\n═══════════════════════════════════════════');
+    console.log('Step 3: Calculating copy amount');
+    console.log('═══════════════════════════════════════════');
+    
+    const copyMultiplier = CONFIG.COPY_MULTIPLIER;
+    const maxUsdValue = CONFIG.MAX_USD_VALUE;
+    
+    if (totalUsdValue > 0) {
+      const copyUsdValue = Math.min(totalUsdValue * copyMultiplier, maxUsdValue);
+      
+      console.log(`Target Position Value: $${totalUsdValue.toFixed(2)}`);
+      console.log(`Copy Multiplier: ${copyMultiplier}x`);
+      console.log(`Calculated Copy: $${(totalUsdValue * copyMultiplier).toFixed(2)}`);
+      console.log(`Max Allowed: $${maxUsdValue}`);
+      console.log(`\n✅ Final Copy Amount: $${copyUsdValue.toFixed(2)}`);
+      
+      // Calculate token amounts proportionally
+      console.log('\nToken amounts to acquire:');
+      transfers.forEach(t => {
+        const ratio = (t.uiAmount * copyMultiplier) / totalUsdValue * copyUsdValue;
+        const withBuffer = ratio * 1.1; // 10% buffer
+        console.log(`  ${t.mint.slice(0, 8)}...: ${ratio.toFixed(6)} (with 10% buffer: ${withBuffer.toFixed(6)})`);
+      });
+    }
+
+    // Extract position NFT and parent info from memo
+    console.log('\n═══════════════════════════════════════════');
+    console.log('Step 4: Extracting position info from memo');
+    console.log('═══════════════════════════════════════════');
+    
+    const message = tx.transaction.message;
+    const accountKeys = message.staticAccountKeys || message.accountKeys || [];
+    const instructions = message.compiledInstructions || message.instructions || [];
+    
+    let parentPosition = null;
+    let positionNftMint = null;
+    
+    // Look for memo instruction
+    const MEMO_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr';
+    
+    for (const instruction of instructions) {
+      const programId = accountKeys[instruction.programIdIndex]?.toString();
+      
+      if (programId === MEMO_PROGRAM_ID) {
+        let memoData;
+        
+        if (instruction.data) {
+          try {
+            memoData = Buffer.from(instruction.data, 'base64').toString('utf8');
+          } catch {
+            memoData = instruction.data;
+          }
+        }
+        
+        if (instruction.parsed) {
+          memoData = instruction.parsed;
+        }
+        
+        if (memoData) {
+          console.log(`Memo found: ${memoData}`);
+          
+          // Extract parent position
+          const match = memoData.match(/referer_position=([A-Za-z0-9]+)/);
+          if (match) {
+            parentPosition = match[1];
+            console.log(`✅ Parent Position: ${parentPosition}`);
+          }
+        }
+      }
+      
+      // Look for Byreal program to extract position NFT
+      const BYREAL_PROGRAM_ID = 'REALQqNEomY6cQGZJUGwywTBD2UmDT32rZcNnfxQ5N2';
+      if (programId === BYREAL_PROGRAM_ID) {
+        console.log('Found Byreal program instruction');
+        
+        // Position NFT is usually minted in this transaction
+        // Look for it in postTokenBalances
+        if (tx.meta && tx.meta.postTokenBalances) {
+          for (const balance of tx.meta.postTokenBalances) {
+            if (balance.uiTokenAmount?.uiAmount === 1 && balance.uiTokenAmount?.decimals === 0) {
+              positionNftMint = balance.mint;
+              console.log(`✅ Position NFT Mint: ${positionNftMint}`);
+              break;
+            }
+          }
+        }
+      }
+    }
+
+    // Also check log messages
+    if (tx.meta && tx.meta.logMessages) {
+      for (const log of tx.meta.logMessages) {
+        const match = log.match(/referer_position=([A-Za-z0-9]+)/);
+        if (match && !parentPosition) {
+          parentPosition = match[1];
+          console.log(`✅ Parent Position (from logs): ${parentPosition}`);
+        }
+      }
+    }
+
+    console.log('\n═══════════════════════════════════════════');
+    console.log('Summary');
+    console.log('═══════════════════════════════════════════');
+    console.log(`Transaction: ${TX_SIGNATURE}`);
+    console.log(`Parent Position: ${parentPosition || 'Not found'}`);
+    console.log(`Position NFT: ${positionNftMint || 'Not found'}`);
+    console.log(`Target Position Value: $${totalUsdValue.toFixed(2)}`);
+    console.log(`Recommended Copy: $${Math.min(totalUsdValue * copyMultiplier, maxUsdValue).toFixed(2)}`);
+    console.log('\nNote: Copy amount is based on TARGET position value, not parent!');
+    console.log('═══════════════════════════════════════════');
+
+  } catch (error) {
+    console.error('Error analyzing transaction:', error);
+    process.exit(1);
+  }
+}
+
+analyzeTransaction();