Skill: shallify/clawcost (version 1.0.2)

--- name: clawcost description: Track OpenClaw agent costs. Check daily/weekly spending and model breakdown. metadata: openclaw: emoji: "💰" requires: bins: - python3 --- # ClawCost Run this command: ```bash python3 {baseDir}/scripts/clawcost.py --budget 10 ``` ## Output JSON with: - `balance`: {initial, spent, remaining} or null - `today`: cost, budget, pct - `week`: total week cost - `total`: all-time cost, tokens - `models`: breakdown all-time - `models_today`: breakdown for today only - `daily`: cost per day (last 7 days) ## Set Balance User sets INITIAL balance (when they top up): ```bash python3 {baseDir}/scripts/clawcost.py --set-balance 50.00 ``` Remaining auto-calculates: initial - total_spent ## How to Present **Tone:** Friendly, like a helpful assistant checking expenses. Use emojis sparingly. **Format:** Use tree-style (├ └) for clean output: ``` 💰 clawleaks ├ Balance $42.98 / $50 remaining ├ Today $1.36 / $10 (14%) ✅ ├ Week $7.02 └ Total $7.02 (15.5M tok) 📈 Sonnet $3.99 (57%) • Haiku $2.06 (29%) • Opus $0.97 (14%) ``` **Rules:** - Skip $0 models - Add brief insight ("Opus only 14%, nice savings 👍") **Alerts (IMPORTANT):** - If `today.pct` > 80%: Start with ⚠️ **"Warning: Daily budget {pct}% used!"** - If `today.pct` > 100%: Start with 🚨 **"OVER BUDGET! ${cost} spent"** - If `balance.remaining` < 5: Warn "💸 Low balance: ${remaining} left" - If `balance` is null: Suggest "Set initial balance with --set-balance" - If budget is fine: End with ✅ **Contextual:** - Quick question → short answer - Wants detail → full breakdown + daily - Over budget → always show warning first, suggest switching to Haiku

path: README.md
content: # ClawCost

Track your OpenClaw agent costs with budget alerts and balance tracking.

## Features

- **Daily/Weekly/Total Cost** - Track spending over time
- **Model Breakdown** - See costs per model (Opus, Sonnet, Haiku)
- **Budget Alerts** - Warnings when approaching limits
- **Balance Tracking** - Auto-calculate remaining balance
- **Daily History** - Last 7 days breakdown

## Installation

```bash
clawhub install clawcost
```

Or manually copy to your OpenClaw skills directory.

## Usage

The skill responds to natural language:
- "check cost"
- "how much spent today?"
- "cost breakdown"

### Set Balance

Set your initial balance (when you top up):
```bash
python3 scripts/clawcost.py --set-balance 50.00
```

Remaining auto-calculates: `initial - total_spent`

### Set Daily Budget

```bash
python3 scripts/clawcost.py --budget 10
```

## Output Example

```
💰 username
├ Balance $42.98 / $50 remaining
├ Today   $1.36 / $10 (14%) ✅
├ Week    $7.02
└ Total   $7.02 (15.5M tok)

📈 Sonnet $3.99 (57%) • Haiku $2.06 (29%) • Opus $0.97 (14%)
```

## Alerts

- ⚠️ Warning when >80% of daily budget used
- 🚨 Alert when over budget
- 💸 Low balance warning when <$5 remaining

## How It Works

ClawCost reads your OpenClaw session logs to calculate costs. It only accesses:
- `~/.openclaw/agents/main/sessions/*.jsonl` (read-only)
- `~/.clawcost/config.json` (read/write for settings)

No data is transmitted externally. All processing is local.

## Requirements

- Python 3.x
- OpenClaw installed with session logs at `~/.openclaw/agents/main/sessions/`

## Config

Config stored at `~/.clawcost/config.json`:
```json
{
  "initial_balance": 50.0,
  "budget_daily": 10.0
}
```

## Security

- Reads only current user's session logs (uses `$HOME`)
- No network calls or external data transmission
- Config is stored locally in user's home directory

## License

MIT


path: references/pricing.md
content: # Model Pricing Reference

Updated: February 2026

## Anthropic Claude Models

All prices in USD per 1 million tokens.

### Claude Opus 4.6 / 4.5
| Token Type | Price |
|------------|-------|
| Input | $15.00 |
| Output | $75.00 |
| Cache Read | $1.50 (90% discount) |
| Cache Write | $18.75 (25% premium) |

### Claude Sonnet 4.5
| Token Type | Price |
|------------|-------|
| Input | $3.00 |
| Output | $15.00 |
| Cache Read | $0.30 (90% discount) |
| Cache Write | $3.75 (25% premium) |

### Claude Haiku 4.5
| Token Type | Price |
|------------|-------|
| Input | $0.80 |
| Output | $4.00 |
| Cache Read | $0.08 (90% discount) |
| Cache Write | $1.00 (25% premium) |

## Cost Comparison

For a typical task with 10K input + 2K output tokens:

| Model | Estimated Cost |
|-------|----------------|
| Opus 4.6 | $0.30 |
| Sonnet 4.5 | $0.06 |
| Haiku 4.5 | $0.016 |

**Savings:**
- Sonnet vs Opus: ~80% cheaper
- Haiku vs Opus: ~95% cheaper
- Haiku vs Sonnet: ~73% cheaper

## Tips

1. Use **Haiku** for simple tasks (summarization, formatting)
2. Use **Sonnet** for coding and complex reasoning
3. Reserve **Opus** for tasks requiring maximum capability
4. Leverage **prompt caching** for repeated contexts (90% savings on cached reads)


path: scripts/clawcost.py
content: #!/usr/bin/env python3
"""
ClawCost - OpenClaw Agent Cost Monitor
Track spending across models with budget alerts.
"""

import json
import glob
import os
import sys
import argparse
from datetime import datetime, timedelta
from collections import defaultdict
from pathlib import Path


def get_config_path() -> Path:
    """Get config file path for current user."""
    return Path.home() / ".clawcost" / "config.json"


def load_config() -> dict:
    """Load config from file."""
    config_path = get_config_path()
    if config_path.exists():
        try:
            with open(config_path, 'r') as f:
                return json.load(f)
        except (json.JSONDecodeError, IOError):
            pass
    return {"initial_balance": None, "budget_daily": 10.0}


def save_config(config: dict):
    """Save config to file."""
    config_path = get_config_path()
    config_path.parent.mkdir(parents=True, exist_ok=True)
    with open(config_path, 'w') as f:
        json.dump(config, f, indent=2)


def get_openclaw_dir() -> Path:
    """Get OpenClaw sessions directory for current user."""
    return Path.home() / ".openclaw" / "agents" / "main" / "sessions"


def parse_sessions(sessions_dir: Path) -> list:
    """Parse all JSONL session files and extract usage data."""
    usage_data = []

    for filepath in glob.glob(str(sessions_dir / "*.jsonl")):
        try:
            with open(filepath, 'r') as f:
                for line in f:
                    try:
                        data = json.loads(line)
                        if data.get("type") == "message" and "message" in data:
                            msg = data["message"]
                            if "usage" in msg and msg.get("role") == "assistant":
                                usage = msg["usage"]
                                cost_data = usage.get("cost", {})

                                entry = {
                                    "timestamp": data.get("timestamp", ""),
                                    "model": msg.get("model", "unknown"),
                                    "input_tokens": usage.get("input", 0),
                                    "output_tokens": usage.get("output", 0),
                                    "cache_read": usage.get("cacheRead", 0),
                                    "cache_write": usage.get("cacheWrite", 0),
                                    "cost": cost_data.get("total", 0),
                                }
                                usage_data.append(entry)
                    except json.JSONDecodeError:
                        continue
        except (IOError, PermissionError):
            continue

    return usage_data


def aggregate_data(usage_data: list) -> dict:
    """Aggregate usage data by model and date."""
    today = datetime.now().strftime("%Y-%m-%d")
    week_ago = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")

    result = {
        "total": {"cost": 0, "tokens": 0},
        "today": {"cost": 0, "tokens": 0},
        "week": {"cost": 0, "tokens": 0},
        "by_model": defaultdict(lambda: {"cost": 0, "tokens": 0, "calls": 0}),
        "by_model_today": defaultdict(lambda: {"cost": 0, "tokens": 0, "calls": 0}),
        "by_date": defaultdict(float),
    }

    for entry in usage_data:
        date = entry["timestamp"][:10] if entry["timestamp"] else ""
        cost = entry["cost"]
        tokens = entry["input_tokens"] + entry["output_tokens"] + entry["cache_read"] + entry["cache_write"]
        model = entry["model"]

        result["total"]["cost"] += cost
        result["total"]["tokens"] += tokens

        if date == today:
            result["today"]["cost"] += cost
            result["today"]["tokens"] += tokens
            # Track models for today
            result["by_model_today"][model]["cost"] += cost
            result["by_model_today"][model]["tokens"] += tokens
            result["by_model_today"][model]["calls"] += 1

        if date >= week_ago:
            result["week"]["cost"] += cost
            result["week"]["tokens"] += tokens

        result["by_model"][model]["cost"] += cost
        result["by_model"][model]["tokens"] += tokens
        result["by_model"][model]["calls"] += 1

        if date:
            result["by_date"][date] += cost

    return result


def format_tokens(n):
    """Format token count to human readable."""
    if n >= 1_000_000:
        return f"{n/1_000_000:.1f}M"
    elif n >= 1_000:
        return f"{n/1_000:.1f}K"
    return str(n)


def main():
    parser = argparse.ArgumentParser(description="ClawCost - OpenClaw Agent Cost Monitor")
    parser.add_argument("--budget", "-b", type=float, default=None, help="Daily budget in USD")
    parser.add_argument("--set-balance", type=float, default=None, help="Set initial balance (USD)")
    parser.add_argument("--format", "-f", choices=["text", "json"], default="text", help="Output format")

    args = parser.parse_args()

    # Get current user
    user = os.environ.get("USER", "unknown")

    # Load config
    config = load_config()

    # Handle --set-balance (sets initial balance for auto-calculation)
    if args.set_balance is not None:
        config["initial_balance"] = args.set_balance
        save_config(config)
        print(f"Initial balance set to ${args.set_balance:.2f}")
        print("Remaining will auto-calculate: initial - total_spent")
        return

    # Use budget from args or config
    budget = args.budget if args.budget is not None else config.get("budget_daily", 10.0)

    sessions_dir = get_openclaw_dir()

    if not sessions_dir.exists():
        print(f"Error: No sessions found at {sessions_dir}", file=sys.stderr)
        sys.exit(1)

    usage_data = parse_sessions(sessions_dir)
    data = aggregate_data(usage_data)

    # Get initial balance from config
    initial_balance = config.get("initial_balance")

    if args.format == "json":
        output = {
            "user": args.user,
            "today": round(data["today"]["cost"], 2),
            "week": round(data["week"]["cost"], 2),
            "total": round(data["total"]["cost"], 2),
            "tokens": data["total"]["tokens"],
            "models": {k: {"cost": round(v["cost"], 2), "calls": v["calls"]}
                      for k, v in data["by_model"].items() if v["cost"] > 0}
        }
        print(json.dumps(output, indent=2))
        return

    # Output JSON for AI to format naturally
    pct = (data["today"]["cost"] / budget * 100) if budget > 0 else 0

    # Clean model names helper
    def clean_name(model):
        return model.replace("claude-", "").replace("-20250929", "").replace("-20251001", "")

    # Models aggregate (all time)
    models = {}
    for model, stats in sorted(data["by_model"].items(), key=lambda x: x[1]["cost"], reverse=True):
        if stats["cost"] > 0:
            pct_model = (stats["cost"] / data["total"]["cost"] * 100) if data["total"]["cost"] > 0 else 0
            models[clean_name(model)] = {
                "cost": round(stats["cost"], 2),
                "calls": stats["calls"],
                "pct": round(pct_model, 1)
            }

    # Models today only
    models_today = {}
    for model, stats in sorted(data["by_model_today"].items(), key=lambda x: x[1]["cost"], reverse=True):
        if stats["cost"] > 0:
            pct_model = (stats["cost"] / data["today"]["cost"] * 100) if data["today"]["cost"] > 0 else 0
            models_today[clean_name(model)] = {
                "cost": round(stats["cost"], 2),
                "calls": stats["calls"],
                "pct": round(pct_model, 1)
            }

    # Daily costs
    daily = {}
    for date in sorted(data["by_date"].keys(), reverse=True)[:7]:
        daily[date] = round(data["by_date"][date], 2)

    # Calculate remaining balance
    balance_info = None
    if initial_balance is not None:
        remaining = initial_balance - data["total"]["cost"]
        balance_info = {
            "initial": round(initial_balance, 2),
            "spent": round(data["total"]["cost"], 2),
            "remaining": round(remaining, 2)
        }

    output = {
        "user": user,
        "balance": balance_info,
        "today": {"cost": round(data["today"]["cost"], 2), "budget": budget, "pct": round(pct, 1)},
        "week": round(data["week"]["cost"], 2),
        "total": {"cost": round(data["total"]["cost"], 2), "tokens": format_tokens(data["total"]["tokens"])},
        "models": models,
        "models_today": models_today,
        "daily": daily
    }

    print(json.dumps(output, indent=2))


if __name__ == "__main__":
    main()


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