Skill: xasus1/scout-commerce (version 1.1.0)

--- name: scout-commerce version: 1.1.0 description: Search for products on Amazon/shopify and buy with USDC on Solana. Swap tokens using Jupiter. homepage: https://scout.trustra.xyz metadata: {"emoji":"🛒","category":"shopping","api_base":"https://scout-api.trustra.xyz/api/v2"} --- # Scout 🛒 Buy from Amazon & Shopify with USDC. Swap tokens via Jupiter. US shipping only. ## Presenting Products (Important!) **Search results:** For each recommended product, send the image as actual media (not markdown links) with the product details as the caption. One product per message so images render properly. **Product details:** When showing details, send images as media attachments alongside the text. The API returns images in the same response — use them immediately, don't make the user ask. **Why:** Markdown image links (`![](url)`) don't render on Telegram/messaging platforms. Always send images as actual media using the message tool with `media` parameter, or via inline if supported. ## Quick Reference **Setup (once)** → `python get_api_key.py --email ... --address "..."` **Find products** → `python search.py "gaming mouse under $50"` **Get Product details** → `python product.py amazon:B07GBZ4Q68` **Check Wallet balance** → `python balance.py` (shows all tokens) **Buy product** → `python buy.py amazon:B07GBZ4Q68` **Check order** → `python order_status.py ord_abc123` **List orders** → `python order_status.py --list` **Swap tokens** → `python swap.py SOL USDC 5` (min $5) **Get swap quote** → `python swap.py --quote SOL USDC 5` **List wallet tokens** → `python swap.py --list` All commands run from `scripts/` folder. API key loads automatically from `credentials.json`. ## Setup (one-time) ```bash python get_api_key.py --email <EMAIL> --address "<NAME>,<STREET>,<CITY>,<STATE>,<ZIP>,<COUNTRY>" ``` Creates a **Crossmint wallet** + **API key** and stores them in `credentials.json`. Fund the wallet with USDC to buy. **Keep API key secure** - it authorizes transactions from your wallet. ## Commands | Command | Usage | |---------|-------| | Search | `python search.py "query"` | | Details | `python product.py amazon:B07GBZ4Q68` | | Balance | `python balance.py` (all tokens) or `balance.py --usdc` | | Buy | `python buy.py amazon:B07GBZ4Q68` | | Orders | `python order_status.py --list` or `order_status.py <orderId>` | | Swap | `python swap.py SOL USDC 5` (min $5 for gasless) | | Quote | `python swap.py --quote SOL USDC 5` | | Tokens | `python swap.py --list` | **Supported tokens:** SOL, USDC, USDT, BONK, TRUST — or use any mint address directly. ## Workflow 1. **No credentials?** → `get_api_key.py` (creates wallet + API key) 2. **No balance?** → Fund wallet address shown by `balance.py` 3. **Ready to buy** → `buy.py <locator>` ## Errors | Error | Fix | |-------|-----| | `INSUFFICIENT_BALANCE` | Fund wallet (`balance.py` shows address) | | `No API key found` | Run `get_api_key.py` | | `OUT_OF_STOCK` | Search for alternatives | | `OVER_LIMIT` | Max $1,500 per order | ## Credentials (`credentials.json`) ```json { "api_key": "scout_sk_...", "wallet_address": "FtbC9x5...", "shipping_profile": { "email": "...", "address": "..." } } ``` Never share the API key.

path: .gitignore
content: # Credentials - DO NOT COMMIT
credentials.json

# Python
__pycache__/
*.pyc
*.pyo
.venv/
venv/


path: scripts/balance.py
content: #!/usr/bin/env python3
"""
Check your Scout wallet balance.

Usage:
    python balance.py          # Show all token balances
    python balance.py --usdc   # Show only USDC balance

Shows all tokens in your wallet with their balances and mint addresses.
"""

import argparse
import json
import sys

import requests

from config import BASE_URL, HEADERS, RPC_URL, get_api_key, get_wallet_address

# USDC mint for reference
USDC_MINT = [REDACTED_SECRET]


def check_usdc_balance(api_key: str) -> dict:
    """Check USDC balance via Scout API."""
    try:
        headers = HEADERS.copy()
        headers["Authorization"] = f"Bearer {api_key}"
        
        response = requests.get(
            f"{BASE_URL}/auth/balance",
            headers=headers,
            timeout=30,
        )
        
        return response.json()
    except requests.exceptions.RequestException as e:
        return {"success": False, "error": {"code": "REQUEST_ERROR", "message": str(e)}}
    except Exception as e:
        return {"success": False, "error": {"code": "ERROR", "message": str(e)}}


def get_token_metadata(mints: list) -> dict:
    """
    Fetch token metadata from Jupiter API.
    Returns dict mapping mint -> {symbol, name}
    """
    metadata = {}
    if not mints:
        return metadata
    
    try:
        # Jupiter tokens API - get metadata for specific mints
        response = requests.get(
            "https://tokens.jup.ag/tokens",
            params={"tags": "verified,community"},
            timeout=10,
        )
        if response.status_code == 200:
            tokens = response.json()
            for token in tokens:
                if token.get("address") in mints:
                    metadata[token["address"]] = {
                        "symbol": token.get("symbol", "???"),
                        "name": token.get("name", "Unknown"),
                    }
    except Exception as e:
        print(f"Warning: Could not fetch token metadata: {e}", file=sys.stderr)
    
    return metadata


def get_all_balances(wallet_address: str) -> dict:
    """Get all token balances in wallet via Solana RPC."""
    try:
        tokens = []
        mints_found = []
        
        # Get SPL token accounts
        payload = {
            "jsonrpc": "2.0",
            "id": 1,
            "method": "getTokenAccountsByOwner",
            "params": [
                wallet_address,
                {"programId": [REDACTED_SECRET]},
                {"encoding": "jsonParsed"}
            ]
        }
        
        response = requests.post(RPC_URL, json=payload, timeout=30)
        data = response.json()
        
        usdc_balance = "0.00"
        
        # Collect all token accounts with balance > 0
        token_accounts = []
        for account in data.get("result", {}).get("value", []):
            info = account.get("account", {}).get("data", {}).get("parsed", {}).get("info", {})
            mint = info.get("mint")
            amount = info.get("tokenAmount", {})
            
            ui_amount = float(amount.get("uiAmount", 0))
            if ui_amount > 0 and mint:
                mints_found.append(mint)
                token_accounts.append({
                    "mint": mint,
                    "balance": amount.get("uiAmountString", "0"),
                    "decimals": amount.get("decimals", 6),
                })
                
                # Track USDC specifically
                if mint == USDC_MINT:
                    usdc_balance = amount.get("uiAmountString", "0")
        
        # Try to get metadata for the tokens we found
        metadata = get_token_metadata(mints_found)
        
        # Build token list with metadata
        for t in token_accounts:
            mint = t["mint"]
            meta = metadata.get(mint, {})
            tokens.append({
                "symbol": meta.get("symbol", "???"),
                "name": meta.get("name", "Unknown Token"),
                "mint": mint,
                "balance": t["balance"],
                "decimals": t["decimals"],
            })
        
        # Get native SOL balance
        sol_payload = {
            "jsonrpc": "2.0",
            "id": 1,
            "method": "getBalance",
            "params": [wallet_address]
        }
        sol_response = requests.post(RPC_URL, json=sol_payload, timeout=30)
        sol_data = sol_response.json()
        sol_lamports = sol_data.get("result", {}).get("value", 0)
        sol_balance = sol_lamports / 1e9
        
        if sol_balance > 0:
            tokens.insert(0, {
                "symbol": "SOL",
                "name": "Solana",
                "mint": [REDACTED_SECRET],
                "balance": f"{sol_balance:.9f}".rstrip('0').rstrip('.'),
                "decimals": 9,
            })
        
        return {
            "success": True,
            "tokens": tokens,
            "usdc_balance": usdc_balance,
        }
    except Exception as e:
        return {"success": False, "error": str(e)}


def main():
    parser = argparse.ArgumentParser(description="Check wallet balance")
    parser.add_argument("--usdc", action="store_true", help="Show only USDC balance")
    args = parser.parse_args()
    
    # Auto-load credentials
    [REDACTED_SECRET])
    wallet_address = get_wallet_address(required=True)
    
    if args.usdc:
        # USDC only mode (legacy behavior)
        result = check_usdc_balance(api_key)
        
        if result.get("success"):
            print(json.dumps({
                "success": True,
                "balance": result.get("balance", "0.00"),
                "currency": "USDC",
                "walletAddress": result.get("walletAddress") or wallet_address,
                "message": "Fund this address with USDC to buy products.",
            }, indent=2))
        else:
            print(json.dumps(result, indent=2))
            sys.exit(1)
    else:
        # All tokens mode (default)
        result = get_all_balances(wallet_address)
        
        if result.get("success"):
            tokens = result.get("tokens", [])
            usdc_balance = result.get("usdc_balance", "0.00")
            
            # Build helpful message
            if not tokens:
                message = "Wallet is empty. Fund with USDC or SOL to get started."
            elif float(usdc_balance) > 0:
                message = f"Ready to buy with {usdc_balance} USDC."
            else:
                # Has tokens but no USDC - suggest swap
                non_usdc = [t for t in tokens if t["mint"] != USDC_MINT]
                if non_usdc:
                    first = non_usdc[0]
                    symbol = first["symbol"] if first["symbol"] != "???" else first["mint"][:8]
                    message = f"No USDC. Swap to USDC first: python swap.py {symbol} USDC <amount>"
                else:
                    message = "Fund wallet with USDC to buy products."
            
            print(json.dumps({
                "success": True,
                "walletAddress": wallet_address,
                "tokens": tokens,
                "usdc_balance": usdc_balance,
                "message": message,
            }, indent=2))
        else:
            print(json.dumps({
                "success": False,
                "error": result.get("error", "Failed to fetch balances"),
            }, indent=2))
            sys.exit(1)


if __name__ == "__main__":
    main()


path: scripts/buy.py
content: #!/usr/bin/env python3
"""
Buy a product on Scout.

Usage:
    python buy.py amazon:B07GBZ4Q68
    python buy.py amazon:B07GBZ4Q68 --quantity 2
    python buy.py amazon:B07GBZ4Q68 --email [REDACTED_SECRET]

Uses saved shipping profile from credentials.json.
Run get_api_key.py first to set up your account.

No private key needed - payment comes from your Crossmint wallet.
Fund your wallet with USDC before buying.
"""

import argparse
import json
import sys

import requests

from config import BASE_URL, HEADERS, get_api_key, get_shipping_profile, load_credentials


def parse_address(address_str: str) -> dict:
    """Parse address string into dict."""
    parts = [p.strip() for p in address_str.split(",")]
    if len(parts) < 5:
        raise ValueError("Address must have: Name,Address,City,State,Zip (and optionally Phone)")
    
    return {
        "recipientName": parts[0],
        "address": parts[1],
        "city": parts[2],
        "state": parts[3],
        "zip": parts[4],
        **({"phone": parts[5]} if len(parts) > 5 else {}),
    }


def check_balance(api_key: str) -> dict:
    """Check wallet balance."""
    try:
        headers = HEADERS.copy()
        headers["Authorization"] = f"Bearer {api_key}"
        
        response = requests.get(
            f"{BASE_URL}/auth/balance",
            headers=headers,
            timeout=30,
        )
        
        return response.json()
    except Exception as e:
        return {"success": False, "error": str(e)}


def buy_product(
    locator: str,
    api_key: str,
    email: str = None,
    address: dict = None,
    variant_id: str = None,
    quantity: int = 1,
) -> dict:
    """Buy a product. Payment comes from your Crossmint wallet."""
    try:
        # Build order payload
        payload = {
            "productLocator": locator,
            "quantity": quantity,
        }
        
        # Only include optional fields if provided
        if email:
            payload["email"] = email
        if address:
            payload.update(address)
        if variant_id:
            payload["variantId"] = variant_id
        
        # Make request
        headers = HEADERS.copy()
        headers["x-api-key"] = api_key
        
        response = requests.post(
            f"{BASE_URL}/order",
            headers=headers,
            json=payload,
            timeout=60,
        )
        
        result = response.json()
        
        if response.status_code == 200 and result.get("success"):
            return result
        elif response.status_code == 402:
            # Insufficient balance
            return {
                "success": False,
                "error": result.get("error", {"code": "INSUFFICIENT_BALANCE", "message": "Insufficient balance"}),
            }
        else:
            return {
                "success": False,
                "error": result.get("error", {"code": "ORDER_FAILED", "message": f"HTTP {response.status_code}"}),
                "details": result,
            }
            
    except requests.exceptions.RequestException as e:
        return {"success": False, "error": {"code": "REQUEST_ERROR", "message": f"Request failed: {e}"}}
    except Exception as e:
        return {"success": False, "error": {"code": "ERROR", "message": str(e)}}


def main():
    parser = argparse.ArgumentParser(description="Buy a product on Scout")
    parser.add_argument("locator", help="Product locator (e.g., amazon:B07GBZ4Q68)")
    parser.add_argument("--email", help="Email for order updates (uses saved profile if omitted)")
    parser.add_argument("--address", help="Shipping: Name,Street,City,State,Zip (uses saved profile if omitted)")
    parser.add_argument("--variant", help="Variant ID (optional)")
    parser.add_argument("--quantity", type=int, default=1, help="Quantity (default: 1)")
    parser.add_argument("--check-balance", action="store_true", help="Check balance before buying")
    
    args = parser.parse_args()
    
    # Auto-load API key from credentials.json
    [REDACTED_SECRET])
    
    # Check balance if requested
    if args.check_balance:
        balance_result = check_balance(api_key)
        if balance_result.get("success"):
            print(f"Balance: {balance_result.get('balance', '0.00')} USDC", file=sys.stderr)
        else:
            print(f"Could not check balance", file=sys.stderr)
    
    # Load saved shipping profile as fallback
    profile = get_shipping_profile()
    
    # Determine email (CLI overrides saved profile)
    email = args.email
    if not email and profile:
        email = profile.get("email")
    
    # Determine address (CLI overrides saved profile)
    address = None
    if args.address:
        try:
            address = parse_address(args.address)
        except ValueError as e:
            print(json.dumps({
                "success": False,
                "error": {"code": "INVALID_ADDRESS", "message": str(e)}
            }, indent=2))
            sys.exit(1)
    # If no address provided, backend will use saved profile
    
    result = buy_product(
        locator=args.locator,
        [REDACTED_SECRET],
        email=email,
        address=address,
        variant_id=args.variant,
        quantity=args.quantity,
    )
    
    # Always output JSON
    print(json.dumps(result, indent=2))
    
    # Exit with error code if failed
    if not result.get("success"):
        sys.exit(1)


if __name__ == "__main__":
    main()


path: scripts/config.py
content: """
Scout API Configuration & Credentials
"""

import sys
import json
import os
import shutil

# Prevent future __pycache__ and clean up existing
sys.dont_write_bytecode = True
_pycache = os.path.join(os.path.dirname(__file__), "__pycache__")
if os.path.exists(_pycache):
    shutil.rmtree(_pycache, ignore_errors=True)

# Paths
SKILL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CREDS_FILE = os.path.join(SKILL_DIR, "credentials.json")

# API URL
BASE_URL = "https://scout-api.trustra.xyz/api/v2"  # Production
# BASE_URL = "http://localhost:8000/api/v2"  # Local development

# Solana RPC
RPC_URL = "https://api.mainnet-beta.solana.com"  # Production
# RPC_URL = "https://api.devnet.solana.com"  # Testing (devnet)

# x402 Network (CAIP-2 format)
NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"  # Production (mainnet)
# NETWORK = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"  # Testing (devnet)

# Request headers
HEADERS = {
    "Content-Type": "application/json",
}


def load_credentials() -> dict:
    """Load credentials from credentials.json file."""
    if not os.path.exists(CREDS_FILE):
        return {}
    
    try:
        with open(CREDS_FILE, "r") as f:
            return json.load(f)
    except (json.JSONDecodeError, IOError):
        return {}


def save_credentials(creds: dict) -> bool:
    """Save credentials to credentials.json file."""
    try:
        with open(CREDS_FILE, "w") as f:
            json.dump(creds, f, indent=2)
        return True
    except IOError as e:
        print(f"❌ Failed to save credentials: {e}", file=sys.stderr)
        return False


def get_api_key(required: bool = True) -> str | None:
    """
    Get API key from credentials.json or environment.
    
    Priority:
    1. SCOUT_API_KEY environment variable
    2. credentials.json file
    
    If required=True and no key found, prints error and exits.
    """
    # Check environment first
    [REDACTED_SECRET]"SCOUT_API_KEY")
    if [REDACTED_SECRET] api_key
    
    # Check credentials file
    creds = load_credentials()
    [REDACTED_SECRET]"api_key")
    
    if [REDACTED_SECRET] api_key
    
    if required:
        print("❌ No API key found.", file=sys.stderr)
        print("", file=sys.stderr)
        print("Run this to register:", file=sys.stderr)
        print("  python get_api_key.py --email [REDACTED_SECRET] --address \"Name,Street,City,State,Zip,Country\"", file=sys.stderr)
        sys.exit(1)
    
    return None


def get_headers(api_key: str | None = None) -> dict:
    """Get request headers with optional API key."""
    headers = HEADERS.copy()
    if [REDACTED_SECRET]"x-api-key"] = api_key
    return headers


def get_shipping_profile() -> dict | None:
    """
    Get saved shipping profile from credentials.json.
    
    Returns dict with keys: email, address, phone (optional)
    Or None if no profile saved.
    """
    creds = load_credentials()
    profile = creds.get("shipping_profile")
    
    if profile and profile.get("email") and profile.get("address"):
        return profile
    
    return None


def save_shipping_profile(email: str, address: str, phone: str | None = None) -> bool:
    """
    Save shipping profile to credentials.json.
    
    Args:
        email: Email for order updates
        address: Full address string (Name,Street,City,State,Zip)
        phone: Optional phone number
    """
    creds = load_credentials()
    
    profile = {
        "email": email,
        "address": address,
    }
    if phone:
        profile["phone"] = phone
    
    creds["shipping_profile"] = profile
    return save_credentials(creds)


def get_wallet_address(required: bool = True) -> str | None:
    """
    Get wallet address from credentials.json.
    
    This is your Crossmint smart wallet address for funding.
    """
    creds = load_credentials()
    wallet = creds.get("wallet_address")
    
    if wallet:
        return wallet
    
    if required:
        print("❌ No wallet address found.", file=sys.stderr)
        print("Run get_api_key.py first to create your wallet.", file=sys.stderr)
        sys.exit(1)
    
    return None


path: scripts/get_api_key.py
content: #!/usr/bin/env python3
"""
Get a Scout API key with Crossmint smart wallet.

Usage:
    python get_api_key.py --email [REDACTED_SECRET] --address "Name,Street,City,State,Zip"

This creates:
- A Crossmint smart wallet for your agent
- An API key for Scout API access
- Saves everything to credentials.json

After registration, fund your wallet with USDC to start buying.
No private key management needed - Crossmint handles wallet security.
"""

import argparse
import json
import sys

import requests

from config import BASE_URL, HEADERS, save_credentials, CREDS_FILE


def get_api_key(email: str, address: str, phone: str = None) -> dict:
    """Register for Scout API and get a Crossmint smart wallet."""
    try:
        # Build request payload
        payload = {
            "email": email,
            "shippingAddress": address,
        }
        
        # Make registration request
        response = requests.post(
            f"{BASE_URL}/auth",
            headers=HEADERS,
            json=payload,
            timeout=30,
        )
        
        result = response.json()
        
        if response.status_code == 200 and result.get("success"):
            return {
                "success": True,
                "apiKey": result.get("apiKey"),
                "walletAddress": result.get("walletAddress"),
                "message": result.get("message"),
                "funding_instructions": result.get("funding_instructions"),
            }
        else:
            return {
                "success": False,
                "error": result.get("error", {"code": "AUTH_FAILED", "message": f"HTTP {response.status_code}"}),
                "details": result,
            }
            
    except requests.exceptions.RequestException as e:
        return {"success": False, "error": {"code": "REQUEST_ERROR", "message": f"Request failed: {e}"}}
    except Exception as e:
        return {"success": False, "error": {"code": "ERROR", "message": str(e)}}


def validate_address(address_str: str) -> bool:
    """Validate address has required parts: Name,Street,City,State,Zip"""
    parts = [p.strip() for p in address_str.split(",")]
    return len(parts) >= 5


def main():
    parser = argparse.ArgumentParser(description="Register for Scout and get a Crossmint smart wallet")
    parser.add_argument("--email", required=True, help="Email for order tracking")
    parser.add_argument("--address", required=True, help="Shipping address: Name,Street,City,State,Zip,Country")
    parser.add_argument("--phone", help="Phone number (optional)")
    
    args = parser.parse_args()
    
    # Validate address format
    if not validate_address(args.address):
        print(json.dumps({
            "success": False,
            "error": {
                "code": "INVALID_ADDRESS",
                "message": "Invalid address format. Required: Name,Street,City,State,Zip"
            }
        }, indent=2))
        sys.exit(1)
    
    result = get_api_key(args.email, args.address, args.phone)
    
    if result.get("success"):
        # Build credentials with API key + shipping profile
        creds = {
            "api_key": result["apiKey"],
            "wallet_address": result["walletAddress"],
            "shipping_profile": {
                "email": args.email,
                "address": args.address,
                **({"phone": args.phone} if args.phone else {}),
            },
        }
        
        save_credentials(creds)
        
        # Add profile to result
        result["shipping_profile"] = creds["shipping_profile"]
        result["credentials_file"] = CREDS_FILE
    
    # Always output JSON
    print(json.dumps(result, indent=2))
    
    # Exit with error code if failed
    if not result.get("success"):
        sys.exit(1)


if __name__ == "__main__":
    main()


path: scripts/order_status.py
content: #!/usr/bin/env python3
"""
Check order status or list order history on Scout.

Usage:
    # Check specific order
    python order_status.py ord_abc123

    # List all orders
    python order_status.py --list

API key is loaded automatically from credentials.json.
Run get_api_key.py first if you don't have one.
"""

import argparse
import json
import sys
import urllib.request
import urllib.error

from config import BASE_URL, get_headers, get_api_key


def get_order_status(order_id: str, api_key: str) -> dict:
    """Get status of a specific order."""
    url = f"{BASE_URL}/orders/{order_id}"
    
    try:
        req = urllib.request.Request(url, headers=get_headers(api_key), method="GET")
        with urllib.request.urlopen(req, timeout=30) as response:
            return json.loads(response.read().decode())
    except urllib.error.HTTPError as e:
        error_body = e.read().decode() if e.fp else ""
        try:
            return json.loads(error_body)
        except:
            return {"success": False, "error": {"code": "HTTP_ERROR", "message": f"HTTP {e.code}: {error_body}"}}
    except Exception as e:
        return {"success": False, "error": {"code": "REQUEST_ERROR", "message": str(e)}}


def list_orders(api_key: str, limit: int = 10) -> dict:
    """List order history."""
    url = f"{BASE_URL}/orders?limit={limit}"
    
    try:
        req = urllib.request.Request(url, headers=get_headers(api_key), method="GET")
        with urllib.request.urlopen(req, timeout=30) as response:
            return json.loads(response.read().decode())
    except urllib.error.HTTPError as e:
        error_body = e.read().decode() if e.fp else ""
        try:
            return json.loads(error_body)
        except:
            return {"success": False, "error": {"code": "HTTP_ERROR", "message": f"HTTP {e.code}: {error_body}"}}
    except Exception as e:
        return {"success": False, "error": {"code": "REQUEST_ERROR", "message": str(e)}}


def main():
    parser = argparse.ArgumentParser(description="Check order status on Scout")
    parser.add_argument("order_id", nargs="?", help="Order ID to check")
    parser.add_argument("--list", action="store_true", help="List all orders")
    
    args = parser.parse_args()
    
    if not args.order_id and not args.list:
        print(json.dumps({
            "success": False,
            "error": {"code": "MISSING_ARG", "message": "Provide an order_id or use --list"}
        }, indent=2))
        sys.exit(1)
    
    # Auto-load API key from credentials.json
    [REDACTED_SECRET])
    
    if args.list:
        result = list_orders(api_key)
    else:
        result = get_order_status(args.order_id, api_key)
    
    # Always output JSON
    print(json.dumps(result, indent=2))
    
    # Exit with error code if failed
    if not result.get("success"):
        sys.exit(1)


if __name__ == "__main__":
    main()


path: scripts/requirements.txt
content: # Scout CLI Dependencies
# Install: pip install -r requirements.txt

# x402 Protocol - handles payment signing/submission
x402[httpx,svm]>=2.0.0

# Solana - for wallet operations
solana>=0.34.0
solders>=0.21.0

# Utilities
base58>=2.1.0


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector:  ()