Skill: ivaavimusic/x402-compute (version 1.4.1)

--- name: x402-compute version: 1.4.1 description: | This skill should be used when the user asks to "provision GPU instance", "spin up a cloud server", "list compute plans", "browse GPU pricing", "extend compute instance", "resize compute instance", "destroy server instance", "check instance status", "list my instances", "top up compute credits", "check credit balance", or manage x402 Singularity Compute / x402Compute infrastructure. Handles GPU and VPS provisioning across Vultr and DigitalOcean with USDC payment on Base or Solana, USDm payment on MegaETH via x402, optional MPP/Mppx payment flows, or pre-loaded USD credits for payment-free provisioning and extending. Includes optional OWS-backed auth and management flows. homepage: https://studio.x402layer.cc/docs/agentic-access/x402-compute metadata: clawdbot: emoji: "🖥️" homepage: https://compute.x402layer.cc os: - linux - darwin requires: bins: - python3 env: - name: PRIVATE_KEY description: EVM private key for Base/MegaETH payment signing (use a dedicated low-balance wallet) sensitive: true optional: true - name: WALLET_ADDRESS description: EVM wallet address corresponding to PRIVATE_KEY optional: true - name: SOLANA_SECRET_KEY description: Solana signer key for Solana payment signing (use a dedicated low-balance wallet) sensitive: true optional: true - name: SOLANA_WALLET_ADDRESS description: Solana wallet address optional: true - name: COMPUTE_API_KEY description: Reusable API key for management endpoints (created via POST /compute/api-keys) sensitive: true optional: true - name: COMPUTE_AUTH_CHAIN description: Auth chain override — base, megaeth, or solana optional: true - name: OWS_BIN description: Explicit path to a locally installed OWS binary (avoids runtime npx downloads) optional: true allowed-tools: - Read - Write - Edit - Bash - WebFetch --- # x402 Singularity Compute Provision, manage, resize, and extend GPU/VPS instances on Vultr or DigitalOcean paid with x402, MPP, or pre-loaded credits. **Base URL:** `https://compute.x402layer.cc` **Providers:** Vultr • DigitalOcean **x402 Networks:** Base (EVM) • Solana • MegaETH **x402 Currency:** USDC (Base/Solana) • USDm (MegaETH) **MPP Methods:** Tempo • Stripe/card when enabled by the service **Credits:** Pre-load USD via x402 topup, then provision/extend with `use_credits: true` **Protocol:** HTTP 402 Payment Required (`X-Payment` for x402, `Authorization: Payment` for MPP) **Access Note:** Preferred access is SSH public key. If no SSH key is provided, a one-time password fallback can be fetched once via API. **DigitalOcean Note:** DigitalOcean instances require SSH key access because one-time root passwords are not exposed through the DigitalOcean API. --- ## Quick Start ### 1. Install Dependencies ```bash pip install -r {baseDir}/requirements.txt ``` ### 2. Choose Wallet Mode #### Option A: Direct signing keys (Base, MegaETH, or Solana) > **Use a dedicated low-balance wallet.** Never use your primary custody wallet. ```bash # Base (EVM) — same keys work for MegaETH export PRIVATE_KEY=<your-evm-private-key> export WALLET_ADDRESS=<your-evm-wallet-address> # MegaETH (uses same EVM keys as Base) export PRIVATE_KEY=<your-evm-private-key> export WALLET_ADDRESS=<your-evm-wallet-address> export COMPUTE_AUTH_CHAIN="megaeth" # Solana export SOLANA_SECRET_KEY=<your-solana-secret-key> export SOLANA_WALLET_ADDRESS=<your-solana-wallet-address> export COMPUTE_AUTH_CHAIN="solana" ``` #### Option B: OpenWallet / OWS (optional-first) ```bash npm install -g @open-wallet-standard/core@0.5.0 export OWS_WALLET="compute-wallet" export COMPUTE_AUTH_MODE="ows" ``` Create `COMPUTE_API_KEY` (optional) for management endpoints: ```bash python {baseDir}/scripts/create_api_key.py --label "my-agent" ``` OWS is best for compute auth and routine management flows. Direct x402 provision and extend still use local payment-signing paths. MPP provision/extend should use `mppx` or Tempo Wallet. Resize is a management action, not a second payment flow. The API preserves remaining prepaid dollar credit by recalculating `expires_at` for the target hourly rate after the provider accepts the resize. --- ## ⚠️ Security Notice > **IMPORTANT**: This skill handles private keys for signing blockchain transactions. > > - **Never use your primary custody wallet** - Create a dedicated wallet with limited funds > - **Private keys are used locally only** - They sign transactions locally and are never transmitted > - **For testing**: Use a throwaway wallet with minimal USDC/USDm --- ## Scripts Overview | Script | Purpose | |--------|---------| | `browse_plans.py` | List available GPU/VPS plans with pricing | | `browse_regions.py` | List deployment regions | | `provision.py` | Provision a new instance (x402 payment, `--months` or `--days`) | | `create_api_key.py` | Create an API key for agent access (optional) | | `list_instances.py` | List your active instances | | `instance_details.py` | Get details for a specific instance | | `get_one_time_password.py` | Retrieve one-time root password fallback | | `extend_instance.py` | Extend instance lifetime (x402 payment) | | `resize_instance.py` | Resize an instance in place (compute auth only) | | `destroy_instance.py` | Destroy an instance | | `ows_cli.py` | Run OpenWallet / OWS wallet, sign-message, and key commands | | `solana_signing.py` | Internal helper for Solana x402 payment signing | --- ## Instance Lifecycle ``` Browse Plans → Choose Provider/Plan → Provision (x402/MPP/Credits) → Active → Extend / Destroy → Expired ``` Instances expire after their prepaid duration. Extend before expiry to keep them running. --- ## Workflows ### A. Browse and Provision ```bash # List GPU plans python {baseDir}/scripts/browse_plans.py # Filter by type (gpu/vps/high-performance) python {baseDir}/scripts/browse_plans.py --type vcg # Check available regions python {baseDir}/scripts/browse_regions.py # Generate a dedicated SSH key once (recommended for agents) ssh-keygen -t ed25519 -N "" -f ~/.ssh/x402_compute # Provision an instance for 1 month (triggers x402 payment) python {baseDir}/scripts/provision.py vcg-a100-1c-2g-6gb lax --months 1 --label "my-gpu" --ssh-key-file ~/.ssh/x402_compute.pub # DigitalOcean plans are prefixed with do: # They require SSH key access. python {baseDir}/scripts/provision.py do:s-1vcpu-1gb nyc3 --days 1 --label "do-test" --ssh-key-file ~/.ssh/x402_compute.pub # Provision a daily instance (cheaper, use-and-throw) python {baseDir}/scripts/provision.py vc2-1c-1gb ewr --days 1 --label "test-daily" --ssh-key-file ~/.ssh/x402_compute.pub # Provision for 3 days python {baseDir}/scripts/provision.py vc2-1c-1gb ewr --days 3 --label "short-task" --ssh-key-file ~/.ssh/x402_compute.pub # Provision on Solana python {baseDir}/scripts/provision.py vc2-1c-1gb ewr --months 1 --label "my-sol-vps" --network solana --ssh-key-file ~/.ssh/x402_compute.pub # Provision on MegaETH (pays with USDm) python {baseDir}/scripts/provision.py vc2-1c-1gb ewr --months 1 --label "my-mega-vps" --network megaeth --ssh-key-file ~/.ssh/x402_compute.pub # Provision via MPP / mppx (Tempo by default; Stripe/card if your mppx config supports it) npx mppx https://compute.x402layer.cc/compute/provision \ -X POST \ -J '{"plan":"vc2-1c-1gb","region":"ewr","os_id":2284,"label":"mpp-vps","prepaid_hours":24,"ssh_public_key":"ssh-ed25519 AAAA... agent"}' # If the response includes management_api_key, store it for later instance management: export COMPUTE_API_KEY="x402c_..." # ⚠️ After provisioning, wait 2-3 minutes for Vultr to complete setup # Then fetch your instance details (IP, status): python {baseDir}/scripts/instance_details.py <instance_id> ``` ### B. Manage Instances ```bash # Optional: create a reusable API key (avoids message signing each request) python {baseDir}/scripts/create_api_key.py --label "my-agent" # List all your instances python {baseDir}/scripts/list_instances.py # Get details for one instance python {baseDir}/scripts/instance_details.py <instance_id> # Optional fallback if no SSH key was provided during provisioning python {baseDir}/scripts/get_one_time_password.py <instance_id> # Extend by 1 day python {baseDir}/scripts/extend_instance.py <instance_id> --hours 24 # Extend by 1 month python {baseDir}/scripts/extend_instance.py <instance_id> --hours 720 # Extend on Solana python {baseDir}/scripts/extend_instance.py <instance_id> --hours 720 --network solana # Extend on MegaETH (pays with USDm) python {baseDir}/scripts/extend_instance.py <instance_id> --hours 720 --network megaeth # Extend via MPP. MPP extension requires compute auth; use the management API key # returned from MPP provisioning or normal wallet signature auth. npx mppx https://compute.x402layer.cc/compute/instances/<instance_id>/extend \ -X POST \ -H "X-[REDACTED_SECRET]" \ -J '{"extend_hours":720}' # Resize via bundled helper script python {baseDir}/scripts/resize_instance.py <instance_id> vc2-2c-4gb # Resize in place with management auth only (no x402 or MPP payment) curl -X POST https://compute.x402layer.cc/compute/instances/<instance_id>/resize \ -H "Content-Type: application/json" \ -H "X-[REDACTED_SECRET]" \ -d '{"plan":"vc2-2c-4gb"}' # DigitalOcean disk growth is irreversible and must be confirmed explicitly curl -X POST https://compute.x402layer.cc/compute/instances/<instance_id>/resize \ -H "Content-Type: application/json" \ -H "X-[REDACTED_SECRET]" \ -d '{"plan":"do:s-2vcpu-4gb","confirm_disk_resize":true}' # Destroy python {baseDir}/scripts/destroy_instance.py <instance_id> ``` ### C. OpenWallet / OWS ```bash # List local OWS wallets python {baseDir}/scripts/ows_cli.py wallet-list # Sign a Base-compatible compute auth message python {baseDir}/scripts/ows_cli.py sign-message --chain eip155:8453 --wallet compute-wallet --message "hello" # Sign a MegaETH-compatible compute auth message python {baseDir}/scripts/ows_cli.py sign-message --chain eip155:4326 --wallet compute-wallet --message "hello" # Sign a Solana-compatible compute auth message python {baseDir}/scripts/ows_cli.py sign-message --chain solana --wallet compute-wallet --message "hello" # Create an OWS agent key python {baseDir}/scripts/ows_cli.py key-create --name codex-compute --wallet compute-wallet ``` ### D. Credits (payment-free provisioning) ```bash # Top up credits via x402 payment (one-time) curl -X POST https://compute.x402layer.cc/compute/credits/topup \ -H "Content-Type: application/json" \ -H "X-[REDACTED_SECRET]" \ -d '{"amount": 100, "network": "base"}' # Returns 402 → pay → credits added to wallet balance # Check credit balance curl https://compute.x402layer.cc/compute/credits/balance \ -H "X-[REDACTED_SECRET]" # Provision using credits (no x402/MPP payment needed) curl -X POST https://compute.x402layer.cc/compute/provision \ -H "Content-Type: application/json" \ -H "X-[REDACTED_SECRET]" \ -d '{ "plan": "vc2-1c-1gb", "region": "ewr", "os_id": 2284, "label": "credit-vps", "prepaid_hours": 720, "ssh_public_key": "ssh-ed25519 AAAA... agent", "use_credits": true }' # Extend using credits curl -X POST https://compute.x402layer.cc/compute/instances/<instance_id>/extend \ -H "Content-Type: application/json" \ -H "X-[REDACTED_SECRET]" \ -d '{"extend_hours": 720, "use_credits": true}' ``` Credits are scoped per wallet. If the cloud provider rejects the instance after credits are deducted, the full amount is automatically refunded. --- ## x402 Payment Flow 1. Request provision/extend → server returns `HTTP 402` with payment requirements 2. Script signs payment locally: - Base: USDC `TransferWithAuthorization` (EIP-712) - MegaETH: USDm ERC-2612 `permit` (EIP-712) — gasless for the user, facilitator settles on-chain - Solana: signed SPL transfer transaction payload 3. Script resends request with `X-Payment` header containing signed payload 4. Server verifies payment, settles on-chain, provisions/extends instance MegaETH uses an embedded facilitator (no external CDP dependency). The user signs an off-chain ERC-2612 permit, and the facilitator calls `permit()` + `transferFrom()` on MegaETH (~10ms blocks, near-zero gas). For Solana, transient facilitator failures can happen. Retry once or twice if you get a temporary 5xx verify error. ## MPP Payment Flow MPP is available side-by-side with x402 on the same paid endpoints. 1. Request provision/extend -> server returns `HTTP 402` with `WWW-Authenticate: Payment` 2. `mppx` or Tempo Wallet creates an MPP credential 3. Client retries with `Authorization: Payment ...` 4. Server verifies the MPP payment, provisions/extends the instance, and returns `Payment-Receipt` Notes: - `POST /compute/provision` can be paid via MPP without wallet auth. In that case the response includes `management_api_key`; store it because it is shown once and is required for later management. - `POST /compute/instances/:id/extend` via MPP requires compute auth, usually `X-[REDACTED_SECRET]`. - `POST /compute/instances/:id/resize` uses compute auth only. It preserves remaining prepaid value by changing expiry instead of charging again. - x402 remains fully supported through the Python scripts and `X-Payment` header flow. - MPP methods are service-configured. Tempo is used by default by `mppx`; Stripe/card requires a Stripe-capable MPP client/config. --- ## Plan Types | Type | Plan Prefix | Description | |------|-------------|-------------| | GPU | `vcg-*` | GPU-accelerated (A100, H100, etc.) | | VPS | `vc2-*` | Standard cloud compute | | High-Perf | `vhp-*` | High-performance dedicated | | Dedicated | `vdc-*` | Dedicated bare-metal | | DigitalOcean | `do:*` | DigitalOcean Droplets (provider-prefixed size slugs) | --- ## Environment Reference | Variable | Required For | Description | |----------|--------------|-------------| | `PRIVATE_KEY` | Base/MegaETH payment signing | EVM private key (0x...) | | `WALLET_ADDRESS` | Base/MegaETH direct-signing mode | EVM wallet address (0x...) | | `SOLANA_SECRET_KEY` | Solana direct-signing mode | Solana signer key (base58 or JSON byte array) | | `SOLANA_WALLET_ADDRESS` | Solana direct-signing mode | Solana wallet address (optional if derivable from secret) | | `COMPUTE_AUTH_CHAIN` | Chain auth override | `base`, `megaeth`, or `solana` | | `COMPUTE_API_KEY` | Optional | Reusable API key for compute management endpoints | | `COMPUTE_AUTH_MODE` | Optional | `auto`, `private-key`, or `ows` | | `OWS_WALLET` | OWS auth mode | OWS wallet name or ID | | `OWS_BIN` | OWS auth mode | Optional explicit path to the `ows` executable | | `COMPUTE_API_KEY` | MPP/no-wallet management | API key returned once after an MPP provision without wallet auth | --- ## API Reference For full endpoint details, see: - [references/api-reference.md](references/api-reference.md) - [references/openwallet-ows.md](references/openwallet-ows.md) --- ## Resources - 📖 **Documentation:** [studio.x402layer.cc/docs/agentic-access/x402-compute](https://studio.x402layer.cc/docs/agentic-access/x402-compute) - 🖥️ **Compute Dashboard:** [compute.x402layer.cc](https://compute.x402layer.cc) - 🌐 **x402 Studio:** [studio.x402layer.cc](https://studio.x402layer.cc) --- ## OWS scope note OWS support is optional-first in this release: - use it for compute auth and management/API-key flows - keep direct Base, MegaETH, or Solana signing keys for the paid provision and extend flows - resize, list, details, password fallback, and destroy work with normal compute auth / API keys

path: requirements.txt
content: eth-account>=0.8.0
web3>=6.0.0
requests>=2.28.0
python-dotenv>=1.0.0
solders>=0.20.0


path: scripts/browse_regions.py
content: #!/usr/bin/env python3
"""
x402Compute — Browse available deployment regions.

Usage:
  python browse_regions.py
"""

import json
import sys

import requests

BASE_URL = "https://compute.x402layer.cc"


def browse_regions() -> dict:
    """Fetch available deployment regions."""
    response = requests.get(f"{BASE_URL}/compute/regions", timeout=30)

    if response.status_code != 200:
        return {"error": f"HTTP {response.status_code}", "response": response.text[:500]}

    data = response.json()
    regions = data.get("regions", [])

    print(f"Found {len(regions)} region(s)")
    for r in regions:
        print(f"  {r['id']:6s}  {r['city']}, {r['country']} ({r['continent']})")

    return data


if __name__ == "__main__":
    result = browse_regions()
    if "error" in result:
        print(json.dumps(result, indent=2))
        sys.exit(1)


path: scripts/browse_plans.py
content: #!/usr/bin/env python3
"""
x402Compute — Browse available compute plans.

Lists GPU, VPS, and dedicated plans with pricing.
Usage:
  python browse_plans.py                  # all plans
  python browse_plans.py --type vcg       # GPU plans only
  python browse_plans.py --type vps       # VPS plans only
"""

import argparse
import json
import sys

import requests

BASE_URL = "https://compute.x402layer.cc"


def browse_plans(plan_type: str = None) -> dict:
    """Fetch available compute plans."""
    url = f"{BASE_URL}/compute/plans"
    params = {}
    if plan_type:
        params["type"] = plan_type

    response = requests.get(url, params=params, timeout=30)

    if response.status_code != 200:
        return {"error": f"HTTP {response.status_code}", "response": response.text[:500]}

    data = response.json()
    plans = data.get("plans", [])

    print(f"Found {len(plans)} plan(s)")
    for p in plans:
        monthly = p.get("our_monthly")
        if monthly is None:
            monthly = p.get("monthly_cost", p.get("vultr_monthly", 0))
        gpu_info = f" | GPU: {p.get('gpu_type', 'N/A')} {p.get('gpu_vram_gb', '')}GB" if p.get("gpu_vram_gb") else ""
        print(
            f"  {p['id']:30s}  ${float(monthly):>8.2f}/mo  "
            f"{p['vcpu_count']} vCPU  {p['ram'] // 1024}GB RAM  {p['disk']}GB disk"
            f"{gpu_info}"
        )

    return data


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Browse x402Compute plans")
    parser.add_argument("--type", choices=["vps", "vhp", "vdc", "vcg"], help="Filter by plan type")
    args = parser.parse_args()

    result = browse_plans(plan_type=args.type)
    if "error" in result:
        print(json.dumps(result, indent=2))
        sys.exit(1)


path: scripts/wallet_signing.py
content: #!/usr/bin/env python3
"""
Shared Base payment signing utilities for x402 scripts.
Uses local EVM private key signing for USDC TransferWithAuthorization.
"""

import base64
import hashlib
import json
import os
import re
import secrets
import shutil
import subprocess
import time
import uuid
from typing import Any, Dict, Optional

from eth_account import Account
from eth_account.messages import encode_defunct

USDC_ADDRESS = [REDACTED_SECRET]
USDC_NAME = "USD Coin"
USDC_VERSION = "2"
BASE_CHAIN_ID = 8453
BASE_CAIP2 = "eip155:8453"


def load_dotenv_if_available() -> None:
    try:
        from dotenv import load_dotenv  # type: ignore

        load_dotenv()
    except Exception:
        pass


def _load_solders_keypair() -> Any:
    try:
        from solders.keypair import Keypair  # type: ignore
    except ImportError as exc:
        raise ValueError("Solana signing requires solders. Install solders>=0.20.0") from exc

    load_dotenv_if_available()
    secret_key = (os.getenv("SOLANA_SECRET_KEY") or "").strip()
    if not secret_key:
        raise ValueError("Set SOLANA_SECRET_KEY for Solana signing")

    # Support either JSON array format or base58 keypair string.
    if secret_key.startswith("["):
        try:
            secret_bytes = bytes(json.loads(secret_key))
            return Keypair.from_bytes(secret_bytes)
        except Exception as exc:
            raise ValueError("Invalid SOLANA_SECRET_KEY JSON array format") from exc

    try:
        return Keypair.from_base58_string(secret_key)
    except Exception as exc:
        raise ValueError("Invalid SOLANA_SECRET_KEY base58 format") from exc


def load_solana_wallet_address(required: bool = True) -> Optional[str]:
    load_dotenv_if_available()
    explicit = (os.getenv("SOLANA_WALLET_ADDRESS") or os.getenv("WALLET_ADDRESS_SECONDARY") or "").strip()
    if explicit:
        return explicit

    try:
        keypair = _load_solders_keypair()
        return str(keypair.pubkey())
    except Exception:
        if required:
            raise ValueError("Set SOLANA_WALLET_ADDRESS or SOLANA_SECRET_KEY")
        return None


def load_wallet_address(required: bool = True) -> Optional[str]:
    load_dotenv_if_available()
    wallet = os.getenv("WALLET_ADDRESS")

    if required and not wallet:
        raise ValueError("Set WALLET_ADDRESS environment variable")
    return wallet


def load_compute_chain() -> str:
    """
    Resolve compute auth chain from env/wallet context.
    Priority:
    1) COMPUTE_AUTH_CHAIN (base|solana)
    2) WALLET_ADDRESS shape (0x... => base)
    3) SOLANA credentials present => solana
    4) default base
    """
    load_dotenv_if_available()
    explicit = (os.getenv("COMPUTE_AUTH_CHAIN") or "").strip().lower()
    if explicit in ("base", "solana"):
        return explicit

    wallet = (os.getenv("WALLET_ADDRESS") or "").strip()
    if wallet:
        return "base" if wallet.lower().startswith("0x") else "solana"

    if os.getenv("SOLANA_SECRET_KEY") or os.getenv("SOLANA_WALLET_ADDRESS"):
        return "solana"
    return "base"


def load_compute_auth_mode() -> str:
    """
    Supported values:
    - auto (default): OWS if OWS_WALLET is set, otherwise local key mode
    - private-key: force local signing
    - ows: force Open Wallet Standard message signing
    """
    load_dotenv_if_available()
    explicit = (os.getenv("COMPUTE_AUTH_MODE") or "auto").strip().lower()
    if explicit in {"private-key", "ows"}:
        return explicit
    if os.getenv("OWS_WALLET"):
        return "ows"
    return "private-key"


def is_ows_mode() -> bool:
    return load_compute_auth_mode() == "ows"


def _build_ows_command(args: list[str]) -> list[str]:
    explicit_bin = (os.getenv("OWS_BIN") or "").strip()
    if explicit_bin:
        return [explicit_bin, *args]

    local_ows = shutil.which("ows")
    if local_ows:
        return [local_ows, *args]

    npx_bin = shutil.which("npx")
    if npx_bin:
        return [npx_bin, "-y", "@open-wallet-standard/core", *args]

    raise ValueError(
        "OWS binary not found. Install it with `npm install -g @open-wallet-standard/core`, "
        "ensure `npx` is available, or set OWS_BIN to the full executable path."
    )


def _run_ows(args: list[str], timeout: int = 180) -> str:
    proc = subprocess.run(
        _build_ows_command(args),
        text=True,
        capture_output=True,
        timeout=timeout,
    )
    if proc.returncode != 0:
        raise ValueError((proc.stderr or proc.stdout or "OWS command failed").strip())
    return (proc.stdout or "").strip()


def _load_ows_wallet_name(cli_wallet: Optional[str] = None) -> str:
    load_dotenv_if_available()
    wallet = (cli_wallet or os.getenv("OWS_WALLET") or "").strip()
    if not wallet:
        raise ValueError("Set OWS_WALLET or pass an explicit OWS wallet name/ID")
    return wallet


def _lookup_ows_wallet_address(chain: str, wallet_name: Optional[str] = None) -> str:
    wallet = _load_ows_wallet_name(wallet_name)
    output = _run_ows(["wallet", "list"])

    blocks = [block.strip() for block in output.split("\n\n") if block.strip()]
    for block in blocks:
        if f"Name:    {wallet}" not in block and f"ID:      {wallet}" not in block:
            continue

        if chain == "solana":
            match = re.search(r"solana:[^\n]*→\s*([1-9A-HJ-NP-Za-km-z]{32,64})", block)
            if match:
                return match.group(1).strip()
        else:
            match = re.search(r"eip155:[^\n]*→\s*(0x[a-fA-F0-9]{40})", block)
            if match:
                return match.group(1).strip().lower()

    raise ValueError(f"Could not resolve an OWS {chain} address for wallet '{wallet}'")


def _sign_message_with_ows(chain: str, message: str, wallet_name: Optional[str] = None) -> str:
    wallet = _load_ows_wallet_name(wallet_name)
    ows_chain = "solana" if chain == "solana" else BASE_CAIP2
    output = _run_ows(
        [
            "sign",
            "message",
            "--chain",
            ows_chain,
            "--wallet",
            wallet,
            "--message",
            message,
            "--json",
        ]
    )

    try:
        payload = json.loads(output)
    except Exception as exc:
        raise ValueError("Could not parse OWS sign-message output") from exc

    signature = str(payload.get("signature") or "").strip()
    if not signature:
        raise ValueError("OWS sign-message did not return a signature")
    return signature


class PaymentSigner:
    def __init__(self, wallet: str, private_key: str) -> None:
        self.wallet = wallet
        self.private_key = private_key

    def sign_typed_data(self, typed_data: Dict[str, Any]) -> str:
        signed = Account.from_key(self.private_key).sign_typed_data(
            typed_data["domain"],
            {"TransferWithAuthorization": typed_data["types"]["TransferWithAuthorization"]},
            typed_data["message"],
        )
        sig = signed.signature.hex()
        # Ensure 0x prefix for EVM compatibility
        if not sig.startswith("0x"):
            sig = "0x" + sig
        return sig

    def create_x402_payment_payload(
        self,
        pay_to: str,
        amount: int,
        valid_after: int = 0,
        valid_before: Optional[int] = None,
        nonce: Optional[str] = None,
    ) -> Dict[str, Any]:
        if valid_before is None:
            valid_before = int(time.time()) + 3600
        if nonce is None:
            nonce = "0x" + secrets.token_hex(32)

        typed_data = {
            "types": {
                "EIP712Domain": [
                    {"name": "name", "type": "string"},
                    {"name": "version", "type": "string"},
                    {"name": "chainId", "type": "uint256"},
                    {"name": "verifyingContract", "type": "address"},
                ],
                "TransferWithAuthorization": [
                    {"name": "from", "type": "address"},
                    {"name": "to", "type": "address"},
                    {"name": "value", "type": "uint256"},
                    {"name": "validAfter", "type": "uint256"},
                    {"name": "validBefore", "type": "uint256"},
                    {"name": "nonce", "type": "bytes32"},
                ],
            },
            "primaryType": "TransferWithAuthorization",
            "domain": {
                "name": USDC_NAME,
                "version": USDC_VERSION,
                "chainId": BASE_CHAIN_ID,
                "verifyingContract": USDC_ADDRESS,
            },
            "message": {
                "from": self.wallet,
                "to": pay_to,
                "value": amount,
                "validAfter": valid_after,
                "validBefore": valid_before,
                "nonce": nonce,
            },
        }

        signature = self.sign_typed_data(typed_data)

        return {
            "x402Version": 1,
            "scheme": "exact",
            "network": "base",
            "payload": {
                "signature": signature,
                "authorization": {
                    "from": self.wallet,
                    "to": pay_to,
                    "value": str(amount),
                    "validAfter": str(valid_after),
                    "validBefore": str(valid_before),
                    "nonce": nonce,
                },
            },
        }

    def create_x402_payment_header(self, pay_to: str, amount: int) -> str:
        payload = self.create_x402_payment_payload(pay_to=pay_to, amount=amount)
        return base64.b64encode(json.dumps(payload).encode()).decode()


def load_payment_signer() -> PaymentSigner:
    load_dotenv_if_available()

    if is_ows_mode():
        raise ValueError(
            "OWS mode currently supports compute auth and API-key creation flows. "
            "Provision and extend still require direct Base or Solana signing keys."
        )

    wallet = load_wallet_address(required=True)
    private_key = os.getenv("PRIVATE_KEY")

    if not private_key or not wallet:
        raise ValueError("Set PRIVATE_KEY and WALLET_ADDRESS for Base payments")

    return PaymentSigner(wallet=wallet, private_key=private_key)


def _compute_auth_message(
    chain: str,
    address: str,
    method: str,
    path: str,
    body_hash: str,
    timestamp_ms: int,
    nonce: str,
) -> str:
    return "\n".join(
        [
            "X402-COMPUTE-AUTH",
            "v1",
            chain,
            address,
            method.upper(),
            path,
            body_hash,
            str(timestamp_ms),
            nonce,
        ]
    )


def create_compute_auth_headers(
    method: str,
    path: str,
    body_str: str = "",
    chain: Optional[str] = None,
    [REDACTED_SECRET] = None,
) -> Dict[str, str]:
    """
    Create auth headers for x402 Compute management endpoints.
    Supports:
    - API key auth via COMPUTE_API_KEY
    - Base wallet message signature (EIP-191)
    - Solana wallet message signature
    """
    [REDACTED_SECRET] or os.getenv("COMPUTE_API_KEY")
    if [REDACTED_SECRET] {"X-API-Key": api_key}

    resolved_chain = (chain or load_compute_chain()).strip().lower()
    if resolved_chain not in ("base", "solana"):
        raise ValueError("Compute auth chain must be 'base' or 'solana'")

    body_hash = hashlib.sha256((body_str or "").encode()).hexdigest()
    timestamp_ms = int(time.time() * 1000)
    nonce = uuid.uuid4().hex

    if is_ows_mode():
        address = _lookup_ows_wallet_address(resolved_chain)
        message = _compute_auth_message(
            chain=resolved_chain,
            address=address,
            method=method,
            path=path,
            body_hash=body_hash,
            timestamp_ms=timestamp_ms,
            nonce=nonce,
        )
        signature_hex = _sign_message_with_ows(resolved_chain, message)

        if resolved_chain == "base":
            if not signature_hex.startswith("0x"):
                signature_hex = f"0x{signature_hex}"
            return {
                "X-Auth-Address": address,
                "X-Auth-Chain": "base",
                "X-Auth-Signature": signature_hex,
                "X-Auth-Timestamp": str(timestamp_ms),
                "X-Auth-Nonce": nonce,
                "X-Auth-Sig-Encoding": "hex",
            }

        try:
            signature_base64 = base64.b64encode(bytes.fromhex(signature_hex)).decode()
        except Exception as exc:
            raise ValueError("OWS Solana signature was not valid hex") from exc

        return {
            "X-Auth-Address": address,
            "X-Auth-Chain": "solana",
            "X-Auth-Signature": signature_base64,
            "X-Auth-Timestamp": str(timestamp_ms),
            "X-Auth-Nonce": nonce,
            "X-Auth-Sig-Encoding": "base64",
        }

    if resolved_chain == "base":
        wallet = load_wallet_address(required=True)
        private_key = os.getenv("PRIVATE_KEY")
        if not private_key:
            raise ValueError("Set PRIVATE_KEY for Base auth signing")

        address = wallet.lower()
        message = _compute_auth_message(
            chain="base",
            address=address,
            method=method,
            path=path,
            body_hash=body_hash,
            timestamp_ms=timestamp_ms,
            nonce=nonce,
        )
        signed = Account.from_key(private_key).sign_message(encode_defunct(text=message))
        signature = signed.signature.hex()
        if not signature.startswith("0x"):
            signature = "0x" + signature

        return {
            "X-Auth-Address": address,
            "X-Auth-Chain": "base",
            "X-Auth-Signature": signature,
            "X-Auth-Timestamp": str(timestamp_ms),
            "X-Auth-Nonce": nonce,
            "X-Auth-Sig-Encoding": "hex",
        }

    # Solana auth signing
    address = load_solana_wallet_address(required=True)
    keypair = _load_solders_keypair()
    message = _compute_auth_message(
        chain="solana",
        address=address,
        method=method,
        path=path,
        body_hash=body_hash,
        timestamp_ms=timestamp_ms,
        nonce=nonce,
    )
    sig = keypair.sign_message(message.encode())
    signature = base64.b64encode(bytes(sig)).decode()

    return {
        "X-Auth-Address": address,
        "X-Auth-Chain": "solana",
        "X-Auth-Signature": signature,
        "X-Auth-Timestamp": str(timestamp_ms),
        "X-Auth-Nonce": nonce,
        "X-Auth-Sig-Encoding": "base64",
    }


path: scripts/ows_cli.py
content: #!/usr/bin/env python3
"""
Thin OpenWallet / OWS CLI wrapper for x402-compute agents.

This keeps x402-compute Python-first while allowing agents to use
Open Wallet Standard commands without exposing raw keys.
"""

import argparse
import os
import shutil
import subprocess
import sys
from typing import List


OWS_PINNED_PACKAGE = "@open-wallet-standard/core@0.5.0"


def build_ows_command(args: List[str]) -> List[str]:
    explicit_bin = os.getenv("OWS_BIN", "").strip()
    if explicit_bin:
        return [explicit_bin, *args]

    local_ows = shutil.which("ows")
    if local_ows:
        return [local_ows, *args]

    raise ValueError(
        "OWS binary not found. Install it with:\n"
        f"  npm install -g {OWS_PINNED_PACKAGE}\n"
        "Then ensure `ows` is in your PATH, or set OWS_BIN to the full executable path.\n"
        "Avoid runtime npx downloads for wallet operations."
    )


def run_ows(args: List[str], timeout: int = 180) -> int:
    proc = subprocess.run(build_ows_command(args), text=True, capture_output=True, timeout=timeout)
    if proc.stdout:
        print(proc.stdout, end="")
    if proc.stderr:
        print(proc.stderr, end="", file=sys.stderr)
    return int(proc.returncode)


def require_ows_wallet(cli_wallet: str | None = None) -> str:
    wallet = (cli_wallet or os.getenv("OWS_WALLET") or "").strip()
    if not wallet:
        raise ValueError("Set OWS_WALLET or pass --wallet <wallet-name-or-id>")
    return wallet


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Run OpenWallet / OWS commands from x402-compute skill",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python ows_cli.py run wallet list
  python ows_cli.py sign-message --chain eip155:8453 --wallet compute-wallet --message "hello"
  python ows_cli.py sign-message --chain solana --wallet compute-wallet --message "hello"
  python ows_cli.py key-create --name codex-compute --wallet compute-wallet

Notes:
  - Wrapper uses OWS_BIN first, then local `ows` in PATH.
  - Install OWS with a pinned version:
      npm install -g @open-wallet-standard/core@0.5.0
  - Do NOT rely on runtime npx downloads for wallet operations.
  - OWS currently fits compute auth and management/API-key workflows best.
  - Provision and extend still use direct payment-signing keys today.
""",
    )

    sub = parser.add_subparsers(dest="command", required=True)

    run_parser = sub.add_parser("run", help="Pass OWS args directly")
    run_parser.add_argument("ows_args", nargs=argparse.REMAINDER, help="Raw OWS arguments")

    sign_parser = sub.add_parser("sign-message", help="Sign a compute auth or test message through OWS")
    sign_parser.add_argument("--chain", required=True, help="Chain (use eip155:8453 for Base, or solana)")
    sign_parser.add_argument("--wallet", help="OWS wallet name or ID (falls back to OWS_WALLET)")
    sign_parser.add_argument("--message", required=True, help="Message to sign")
    sign_parser.add_argument("--json", action="store_true", help="Emit structured JSON from OWS")

    wallet_list_parser = sub.add_parser("wallet-list", help="List saved OWS wallets")

    key_create_parser = sub.add_parser("key-create", help="Create an OWS API key for agent access")
    key_create_parser.add_argument("--name", required=True, help="Key name")
    key_create_parser.add_argument("--wallet", action="append", default=[], help="Repeatable wallet name or ID")
    key_create_parser.add_argument("--policy", action="append", default=[], help="Repeatable policy ID")
    key_create_parser.add_argument("--expires-at", help="Optional ISO-8601 expiry")

    args = parser.parse_args()

    if args.command == "run":
        raw = list(args.ows_args)
        if raw and raw[0] == "--":
            raw = raw[1:]
        if not raw:
            parser.error("run requires at least one OWS argument, e.g.: run wallet list")
        return run_ows(raw)

    if args.command == "sign-message":
        wallet = require_ows_wallet(args.wallet)
        cmd = [
            "sign",
            "message",
            "--chain",
            args.chain,
            "--wallet",
            wallet,
            "--message",
            args.message,
        ]
        if args.json:
            cmd.append("--json")
        return run_ows(cmd)

    if args.command == "wallet-list":
        return run_ows(["wallet", "list"])

    if args.command == "key-create":
        cmd = ["key", "create", "--name", args.name]
        for wallet in args.wallet:
            cmd += ["--wallet", wallet]
        for policy in args.policy:
            cmd += ["--policy", policy]
        if args.expires_at:
            cmd += ["--expires-at", args.expires_at]
        return run_ows(cmd)

    parser.print_help()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/list_instances.py
content: #!/usr/bin/env python3
"""
x402Compute — List your compute instances.

Usage:
  python list_instances.py
"""

import json
import sys

import requests

from wallet_signing import create_compute_auth_headers

BASE_URL = "https://compute.x402layer.cc"


def list_instances() -> dict:
    """List all compute instances for the current wallet."""
    path = "/compute/instances"
    auth_headers = create_compute_auth_headers("GET", path)
    response = requests.get(
        f"{BASE_URL}/compute/instances",
        headers=auth_headers,
        timeout=30,
    )

    if response.status_code != 200:
        return {"error": f"HTTP {response.status_code}", "response": response.text[:500]}

    data = response.json()
    instances = data.get("instances", data.get("orders", []))

    print(f"Found {len(instances)} instance(s)")
    for inst in instances:
        status = inst.get("status", "unknown")
        label = inst.get("label", inst.get("id", "N/A"))
        ip = inst.get("ip_address", "pending")
        plan = inst.get("plan") or inst.get("vultr_plan") or "N/A"
        region = inst.get("region") or inst.get("vultr_region") or "N/A"
        expires = inst.get("expires_at", "N/A")
        print(f"  [{status:11s}] {label:20s}  {plan:16s}  {region:6s}  IP: {ip:16s}  Expires: {expires}")

    return data


if __name__ == "__main__":
    result = list_instances()
    if "error" in result:
        print(json.dumps(result, indent=2))
        sys.exit(1)


path: scripts/resize_instance.py
content: #!/usr/bin/env python3
"""
x402Compute — Resize an active compute instance in place.

Usage:
  python resize_instance.py <instance_id> <plan> [--confirm-disk-resize]

Examples:
  python resize_instance.py abc-123 vc2-2c-4gb
  python resize_instance.py abc-123 do:s-2vcpu-4gb --confirm-disk-resize
"""

import argparse
import json
import sys

import requests

from wallet_signing import create_compute_auth_headers

BASE_URL = "https://compute.x402layer.cc"


def resize_instance(instance_id: str, plan: str, confirm_disk_resize: bool = False) -> dict:
    """Resize a compute instance using compute auth or API key auth."""
    body = {
        "plan": plan,
        "confirm_disk_resize": confirm_disk_resize,
    }
    body_json = json.dumps(body, separators=(",", ":"))
    path = f"/compute/instances/{instance_id}/resize"
    auth_headers = create_compute_auth_headers("POST", path, body_json)

    response = requests.post(
        f"{BASE_URL}/compute/instances/{instance_id}/resize",
        data=body_json,
        headers={
            "Content-Type": "application/json",
            **auth_headers,
        },
        timeout=30,
    )

    if response.status_code != 200:
        return {"error": f"HTTP {response.status_code}", "response": response.text[:500]}

    return response.json()


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Resize a compute instance")
    parser.add_argument("instance_id", help="Instance ID to resize")
    parser.add_argument("plan", help="Target plan ID from /compute/plans")
    parser.add_argument(
        "--confirm-disk-resize",
        action="store_true",
        help="Required when the target resize increases disk size",
    )
    args = parser.parse_args()

    result = resize_instance(
        instance_id=args.instance_id,
        plan=args.plan,
        confirm_disk_resize=args.confirm_disk_resize,
    )
    if "error" in result:
        print(json.dumps(result, indent=2))
        sys.exit(1)

    print("Resize started:")
    print(f"  Provider:        {result.get('provider')}")
    print(f"  From Plan:       {result.get('from_plan')}")
    print(f"  To Plan:         {result.get('to_plan')}")
    print(f"  Old Expiry:      {result.get('old_expires_at')}")
    print(f"  New Expiry:      {result.get('new_expires_at')}")
    print(f"  Credit Preserved: ${result.get('remaining_credit', 0):.2f}")
    print(f"  Hours Before:    {result.get('remaining_hours_before')}")
    print(f"  Hours After:     {result.get('remaining_hours_after')}")
    if result.get("provider_action_id"):
        print(f"  Provider Action: {result.get('provider_action_id')}")


path: scripts/destroy_instance.py
content: #!/usr/bin/env python3
"""
x402Compute — Destroy a compute instance.

Usage:
  python destroy_instance.py <instance_id> [--yes]
"""

import argparse
import json
import sys

import requests

from wallet_signing import create_compute_auth_headers

BASE_URL = "https://compute.x402layer.cc"


def destroy_instance(instance_id: str, skip_confirm: bool = False) -> dict:
    """Destroy a compute instance after confirmation."""
    path = f"/compute/instances/{instance_id}"
    auth_headers = create_compute_auth_headers("GET", path)

    details_resp = requests.get(
        f"{BASE_URL}/compute/instances/{instance_id}",
        headers=auth_headers,
        timeout=15,
    )

    if details_resp.status_code == 200:
        info = details_resp.json().get("instance", details_resp.json())
        print(f"\n--- Instance Details ---")
        print(f"  ID:       {info.get('id', instance_id)}")
        print(f"  Label:    {info.get('label', 'N/A')}")
        print(f"  Plan:     {info.get('plan', 'N/A')}")
        print(f"  IP:       {info.get('ip_address', 'N/A')}")
        print(f"  Status:   {info.get('status', 'N/A')}")
        print(f"  Expires:  {info.get('expires_at', 'N/A')}")
    else:
        print(f"  Instance: {instance_id}")
        print(f"  (Could not fetch details: HTTP {details_resp.status_code})")

    if not skip_confirm:
        confirm = input("\n⚠️  This is irreversible. Destroy this instance? [y/N] ").strip().lower()
        if confirm not in ("y", "yes"):
            return {"error": "Destroy cancelled by user"}

    print(f"Destroying instance {instance_id}...")

    auth_headers = create_compute_auth_headers("DELETE", path)
    response = requests.delete(
        f"{BASE_URL}/compute/instances/{instance_id}",
        headers=auth_headers,
        timeout=30,
    )

    if response.status_code == 200:
        data = response.json()
        print("Instance destroyed successfully")
        return data

    return {"error": f"HTTP {response.status_code}", "response": response.text[:500]}


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Destroy a compute instance")
    parser.add_argument("instance_id", help="Instance ID to destroy")
    parser.add_argument("--yes", "-y", action="store_true", help="Skip confirmation prompt")
    args = parser.parse_args()

    result = destroy_instance(args.instance_id, skip_confirm=args.yes)
    if "error" in result:
        print(json.dumps(result, indent=2))
        sys.exit(1)


path: scripts/create_api_key.py
content: #!/usr/bin/env python3
"""
x402Compute — Create an API key for agent access.

Usage:
  python create_api_key.py [--label LABEL]
"""

import argparse
import json
import sys

import requests

from wallet_signing import create_compute_auth_headers

BASE_URL = "https://compute.x402layer.cc"


def create_api_key(label: str | None = None) -> dict:
    body = {"label": label} if label else {}
    body_json = json.dumps(body, separators=(",", ":")) if body else ""
    path = "/compute/api-keys"
    auth_headers = create_compute_auth_headers("POST", path, body_json)

    response = requests.post(
        f"{BASE_URL}/compute/api-keys",
        data=body_json if body_json else None,
        headers={
            "Content-Type": "application/json",
            **auth_headers,
        },
        timeout=30,
    )

    if response.status_code != 201:
        return {"error": f"HTTP {response.status_code}", "response": response.text[:500]}

    return response.json()


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Create a compute API key")
    parser.add_argument("--label", default=None, help="Optional key label")
    args = parser.parse_args()

    result = create_api_key(label=args.label)
    if "error" in result:
        print(json.dumps(result, indent=2))
        sys.exit(1)

    print("API Key created:")
    print(f"  ID:      {result.get('id')}")
    print(f"  Label:   {result.get('label') or 'N/A'}")
    print(f"  Key:     {result.get('api_key')}")
    print(f"  Created: {result.get('created_at')}")
    print("Store this key securely; it will not be shown again.")


path: scripts/extend_instance.py
content: #!/usr/bin/env python3
"""
x402Compute — Extend an instance's lifetime.

Handles the full x402 payment flow:
1. POST /compute/instances/:id/extend → get 402 challenge
2. Sign USDC TransferWithAuthorization locally
3. Resend with X-Payment header → instance extended

Usage:
  python extend_instance.py <instance_id> [--hours N] [--network base|solana]

Example:
  python extend_instance.py abc-123 --hours 720  # extend by 1 month
"""

import argparse
import json
import os
import sys
from typing import Dict

import requests

from solana_signing import create_solana_xpayment_from_accept, ensure_solana_destination_ready
from wallet_signing import create_compute_auth_headers, load_compute_chain, load_payment_signer

BASE_URL = "https://compute.x402layer.cc"


def _find_accept_option(challenge: dict, requested_network: str) -> dict:
    for option in challenge.get("accepts", []):
        network = str(option.get("network", "")).lower()
        if requested_network == "base" and (network == "base" or "8453" in network):
            return option
        if requested_network == "solana" and (network == "solana" or network.startswith("solana:")):
            return option
    raise ValueError(f"No {requested_network} payment option found in 402 challenge")


MAX_SPEND_USD = float(os.getenv("COMPUTE_MAX_SPEND_USD", "500"))


def extend_instance(instance_id: str, hours: int = 720, network: str = "base", skip_confirm: bool = False) -> dict:
    """Extend a compute instance with x402 payment."""
    body = {
        "extend_hours": hours,
        "network": network,
    }
    body_json = json.dumps(body, separators=(",", ":"))
    auth_chain = load_compute_chain()

    print(f"Extending instance {instance_id} by {hours} hours...")

    # Step 1: Get 402 challenge
    path = f"/compute/instances/{instance_id}/extend"
    auth_headers: Dict[str, str] = {}
    try:
        auth_headers = create_compute_auth_headers("POST", path, body_json, chain=auth_chain)
    except Exception as exc:
        return {"error": f"Failed to build auth headers: {exc}"}
    response = requests.post(
        f"{BASE_URL}/compute/instances/{instance_id}/extend",
        data=body_json,
        headers={
            "Content-Type": "application/json",
            **auth_headers,
        },
        timeout=30,
    )

    if response.status_code == 200:
        print("Instance extended (no payment required)")
        return response.json()

    if response.status_code != 402:
        return {"error": f"Unexpected status {response.status_code}", "response": response.text[:500]}

    challenge = response.json()

    option = _find_accept_option(challenge, network)

    pay_to = option["payTo"]
    amount = int(option["maxAmountRequired"])
    cost_usd = amount / 1_000_000

    if cost_usd > MAX_SPEND_USD:
        return {"error": f"Cost ${cost_usd:.2f} exceeds max spend limit ${MAX_SPEND_USD:.2f}. "
                f"Set COMPUTE_MAX_SPEND_USD to override."}

    print(f"\n--- Payment Confirmation ---")
    print(f"  Instance:    {instance_id}")
    print(f"  Extend by:   {hours} hours")
    print(f"  Network:     {network}")
    print(f"  Cost:        ${cost_usd:.2f} USDC")
    print(f"  Pay to:      {pay_to}")

    if not skip_confirm:
        confirm = input("\nProceed with payment? [y/N] ").strip().lower()
        if confirm not in ("y", "yes"):
            return {"error": "Payment cancelled by user"}

    if network == "base":
        try:
            signer = load_payment_signer()
        except Exception as exc:
            return {"error": str(exc)}
        x_payment = signer.create_x402_payment_header(pay_to=pay_to, amount=amount)
    else:
        ensure_solana_destination_ready(option)
        x_payment = create_solana_xpayment_from_accept(option)

    # Step 2: Pay and extend
    # NOTE: Do NOT send compute auth headers here.
    # The x402 X-Payment header authenticates the payer via on-chain payment.
    # Sending auth headers causes 401 (nonce already consumed from step 1).
    response = requests.post(
        f"{BASE_URL}/compute/instances/{instance_id}/extend",
        data=body_json,
        headers={
            "Content-Type": "application/json",
            "X-Payment": x_payment,
        },
        timeout=60,
    )

    print(f"Response: {response.status_code}")

    if response.status_code in (200, 201):
        data = response.json()
        order = data.get("order", {})
        print(f"✅ Instance extended!")
        print(f"   New Expiry: {order.get('expires_at', 'N/A')}")
        if data.get("tx_hash"):
            print(f"   TX: {data['tx_hash']}")
        return data

    return {"error": f"Extension failed: {response.status_code}", "response": response.text[:500]}


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Extend a compute instance")
    parser.add_argument("instance_id", help="Instance ID to extend")
    parser.add_argument("--hours", type=int, default=720, help="Hours to extend (default: 720 = ~1 month)")
    parser.add_argument("--network", default="base", choices=["base", "solana"], help="Payment network")
    parser.add_argument("--yes", "-y", action="store_true", help="Skip payment confirmation prompt")
    parser.add_argument("--max-spend", type=float, help="Max USD spend limit (default: $500 or COMPUTE_MAX_SPEND_USD)")
    args = parser.parse_args()
    if args.max_spend is not None:
        MAX_SPEND_USD = args.max_spend

    result = extend_instance(
        instance_id=args.instance_id,
        hours=args.hours,
        network=args.network,
        skip_confirm=args.yes,
    )
    if "error" in result:
        print(json.dumps(result, indent=2))
        sys.exit(1)


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