Skip to main content
Real-world automation scripts that run on a schedule and notify you when something matters. Each workflow includes the full script, scheduler setup (cron + macOS launchd), realistic output, and customization tips.
All scripts assume you’ve already authenticated with octav auth set-key YOUR_API_KEY. See Authentication for setup. For basic CLI usage and one-liners, see Examples.

1. Daily Airdrop Scanner

Checks a Solana wallet for unclaimed airdrops every day at 2pm. Sends a macOS desktop notification when new airdrops are found and logs results for history.
#!/bin/bash
# airdrop-scanner.sh — Daily airdrop check with desktop notifications

ADDR="7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
LOG_DIR="$HOME/.octav/logs"
LOG_FILE="$LOG_DIR/airdrops.log"

mkdir -p "$LOG_DIR"

RESULT=$(octav airdrop --address "$ADDR" --raw 2>&1)
if [ $? -ne 0 ]; then
  echo "[$(date)] ERROR: $RESULT" >> "$LOG_FILE"
  exit 1
fi

# Count unclaimed airdrops
UNCLAIMED=$(echo "$RESULT" | jq '[.airdrops[] | select(.eligible == true)] | length')
TOTAL_VALUE=$(echo "$RESULT" | jq '[.airdrops[] | select(.eligible == true) | .value_usd] | add // 0')

# Log the check
echo "[$(date)] Checked $ADDR$UNCLAIMED unclaimed airdrop(s), \$$TOTAL_VALUE total" >> "$LOG_FILE"

# Send macOS notification if airdrops found
if [ "$UNCLAIMED" -gt 0 ]; then
  DETAILS=$(echo "$RESULT" | jq -r '
    [.airdrops[] | select(.eligible == true) | "\(.program): \(.amount) \(.token) (~$\(.value_usd))"]
    | join(", ")
  ')

  osascript -e "display notification \"$DETAILS\" with title \"Octav Airdrop Alert\" subtitle \"$UNCLAIMED unclaimed — \$$TOTAL_VALUE total\""

  echo "[$(date)] NOTIFIED: $DETAILS" >> "$LOG_FILE"
fi
Sample log output:
[Mon Jan 15 14:00:01 2025] Checked 7xKXtg...gAsU — 2 unclaimed airdrop(s), $601 total
[Mon Jan 15 14:00:01 2025] NOTIFIED: Jupiter (JUP): 847 JUP (~$512), Parcl (PRCL): 340 PRCL (~$89)
[Tue Jan 16 14:00:01 2025] Checked 7xKXtg...gAsU — 2 unclaimed airdrop(s), $601 total
[Wed Jan 17 14:00:02 2025] Checked 7xKXtg...gAsU — 1 unclaimed airdrop(s), $89 total
# Run daily at 2pm
0 14 * * * /path/to/airdrop-scanner.sh
Add with crontab -e or pipe it in:
(crontab -l 2>/dev/null; echo "0 14 * * * $HOME/scripts/airdrop-scanner.sh") | crontab -
  • Multiple wallets: Loop over an array of addresses and check each one
  • Slack instead of macOS notifications: Replace the osascript line with a curl to a Slack webhook (see the Transaction Watchdog below for an example)
  • Minimum value filter: Add | select(.value_usd > 50) to the jq filter to ignore dust airdrops

2. Transaction Watchdog

Monitors wallets every 10 minutes for new activity. Tracks state to avoid duplicate alerts and supports both macOS notifications and Slack webhooks.
#!/bin/bash
# tx-watchdog.sh — Detect new transactions and send alerts

ADDR="0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68"
STATE_DIR="$HOME/.octav/state"
STATE_FILE="$STATE_DIR/tx-watchdog-last-tx.txt"
LOG_FILE="$HOME/.octav/logs/tx-watchdog.log"

# Optional: set to your Slack webhook URL, or leave empty for macOS notifications only
SLACK_WEBHOOK=""

mkdir -p "$STATE_DIR" "$(dirname "$LOG_FILE")"

# Get the last known transaction hash
LAST_SEEN=""
[ -f "$STATE_FILE" ] && LAST_SEEN=$(cat "$STATE_FILE")

# Fetch recent transactions
RESULT=$(octav transactions get --addresses "$ADDR" --limit 10 --raw 2>&1)
if [ $? -ne 0 ]; then
  echo "[$(date)] ERROR: $RESULT" >> "$LOG_FILE"
  exit 1
fi

# Get the latest transaction hash
LATEST_TX=$(echo "$RESULT" | jq -r '.transactions[0].hash // empty')

if [ -z "$LATEST_TX" ]; then
  echo "[$(date)] No transactions found" >> "$LOG_FILE"
  exit 0
fi

# Compare with last seen
if [ "$LATEST_TX" = "$LAST_SEEN" ]; then
  echo "[$(date)] No new transactions" >> "$LOG_FILE"
  exit 0
fi

# New transactions found — collect all new ones
NEW_TXS=$(echo "$RESULT" | jq -r --arg last "$LAST_SEEN" '
  .transactions
  | if $last == "" then .[:5] else [.[] | select(.hash != $last)] end
  | .[]
  | "\(.type) \(.amount) \(.token_symbol) ($\(.value_usd)) on \(.chain) — \(.date)"
')

NEW_COUNT=$(echo "$RESULT" | jq --arg last "$LAST_SEEN" '
  .transactions
  | if $last == "" then .[:5] else [.[] | select(.hash != $last)] end
  | length
')

# Update state
echo "$LATEST_TX" > "$STATE_FILE"

# Log
echo "[$(date)] $NEW_COUNT new transaction(s) detected:" >> "$LOG_FILE"
echo "$NEW_TXS" >> "$LOG_FILE"

# Send macOS notification
osascript -e "display notification \"$NEW_COUNT new transaction(s) on ${ADDR:0:6}...${ADDR: -4}\" with title \"Octav Transaction Alert\""

# Send Slack notification if webhook is configured
if [ -n "$SLACK_WEBHOOK" ]; then
  SLACK_MSG=$(echo "$NEW_TXS" | sed 's/"/\\"/g' | paste -sd '\n' -)
  curl -s -X POST "$SLACK_WEBHOOK" \
    -H 'Content-Type: application/json' \
    -d "{\"text\": \"*Transaction Alert* — ${ADDR:0:6}...${ADDR: -4}\n\`\`\`$SLACK_MSG\`\`\`\"}" \
    > /dev/null
fi
Sample alert output:
[2025-01-15 14:30:01] 2 new transaction(s) detected:
swap 2.5 ETH ($5875.00) on ethereum — 2025-01-15
transfer 1000.0 USDC ($1000.00) on ethereum — 2025-01-15
# Run every 10 minutes
*/10 * * * * /path/to/tx-watchdog.sh
  • Multiple wallets: Create separate state files per address — tx-watchdog-${ADDR:0:8}.txt
  • Value filter: Add | select(.value_usd > 1000) to only alert on large transactions
  • Slack setup: Create a webhook at api.slack.com/messaging/webhooks and paste the URL into SLACK_WEBHOOK

3. Nightly Transaction Export

Runs at midnight, exports the day’s transactions to a monthly CSV file. Handles pagination for wallets with high daily activity and appends without duplicates.
#!/bin/bash
# nightly-export.sh — Export today's transactions to a monthly CSV

ADDR="0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68"
EXPORT_DIR="$HOME/.octav/exports"
TODAY=$(date +%Y-%m-%d)
MONTH_FILE="$EXPORT_DIR/transactions-$(date +%Y-%m).csv"

mkdir -p "$EXPORT_DIR"

# Initialize CSV with header if it doesn't exist
if [ ! -f "$MONTH_FILE" ]; then
  echo "date,type,chain,token,amount,value_usd,hash" > "$MONTH_FILE"
fi

# Fetch today's transactions with pagination
OFFSET=0
LIMIT=250
TOTAL_EXPORTED=0

while true; do
  RESULT=$(octav transactions get \
    --addresses "$ADDR" \
    --start-date "$TODAY" \
    --end-date "$TODAY" \
    --limit "$LIMIT" \
    --offset "$OFFSET" \
    --raw 2>&1)

  if [ $? -ne 0 ]; then
    echo "[$(date)] ERROR: $RESULT" >&2
    exit 1
  fi

  # Extract transaction count
  COUNT=$(echo "$RESULT" | jq '.transactions | length')

  if [ "$COUNT" -eq 0 ]; then
    break
  fi

  # Append to CSV (skip duplicates by checking hash)
  EXISTING_HASHES=""
  [ -f "$MONTH_FILE" ] && EXISTING_HASHES=$(cut -d',' -f7 "$MONTH_FILE" | tail -n +2)

  echo "$RESULT" | jq -r --arg existing "$EXISTING_HASHES" '
    .transactions[] |
    select(.hash as $h | ($existing | split("\n") | index($h)) | not) |
    [.date, .type, .chain, .token_symbol, .amount, .value_usd, .hash] | @csv
  ' >> "$MONTH_FILE"

  NEW_ROWS=$(echo "$RESULT" | jq --arg existing "$EXISTING_HASHES" '
    [.transactions[] | select(.hash as $h | ($existing | split("\n") | index($h)) | not)] | length
  ')
  TOTAL_EXPORTED=$((TOTAL_EXPORTED + NEW_ROWS))

  # If we got fewer than the limit, we've reached the end
  if [ "$COUNT" -lt "$LIMIT" ]; then
    break
  fi

  OFFSET=$((OFFSET + LIMIT))
done

echo "[$(date)] Exported $TOTAL_EXPORTED transaction(s) for $TODAY to $MONTH_FILE"
Sample CSV output (transactions-2025-01.csv):
date,type,chain,token,amount,value_usd,hash
"2025-01-01","swap","ethereum","ETH","2.5","5875.00","0x1a2b3c..."
"2025-01-01","swap","ethereum","USDC","-5875.00","-5875.00","0x1a2b3c..."
"2025-01-03","transfer","arbitrum","ARB","500.0","475.00","0x4d5e6f..."
"2025-01-07","transfer","ethereum","ETH","1.0","2340.00","0x7g8h9i..."
File structure:
~/.octav/exports/
  transactions-2025-01.csv
  transactions-2025-02.csv
  transactions-2025-03.csv
# Run at midnight every day
0 0 * * * /path/to/nightly-export.sh
  • Multiple wallets: Loop over addresses and write to separate files or add an address column
  • Weekly instead of monthly files: Change the filename to transactions-$(date +%Y-W%V).csv
  • Compression: Add gzip "$MONTH_FILE.bak" at the end of each month to archive old files

4. Portfolio Tracker with Weekly Report

Two scripts that work together: a daily NAV snapshot recorder and a weekly report generator that runs every Sunday.

Daily snapshot

#!/bin/bash
# portfolio-tracker.sh — Record daily NAV snapshot

ADDR="0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68"
DATA_DIR="$HOME/.octav/portfolio"
NAV_FILE="$DATA_DIR/nav-history.csv"

mkdir -p "$DATA_DIR"

# Initialize CSV if it doesn't exist
[ ! -f "$NAV_FILE" ] && echo "date,nav" > "$NAV_FILE"

# Fetch current NAV
RESULT=$(octav portfolio nav --addresses "$ADDR" --raw 2>&1)
if [ $? -ne 0 ]; then
  echo "[$(date)] ERROR: $RESULT" >&2
  exit 1
fi

NAV=$(echo "$RESULT" | jq -r '.nav')
TODAY=$(date +%Y-%m-%d)

# Skip if already recorded today
if grep -q "^$TODAY," "$NAV_FILE" 2>/dev/null; then
  echo "[$(date)] Already recorded NAV for $TODAY"
  exit 0
fi

echo "$TODAY,$NAV" >> "$NAV_FILE"
echo "[$(date)] Recorded NAV: \$$NAV"

Weekly report (Sundays)

#!/bin/bash
# weekly-report.sh — Generate weekly portfolio summary

DATA_DIR="$HOME/.octav/portfolio"
NAV_FILE="$DATA_DIR/nav-history.csv"
ADDR="0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68"

# Optional: Slack webhook for posting the report
SLACK_WEBHOOK=""

# Get this week's data (last 7 days)
WEEK_START=$(date -v-6d +%Y-%m-%d 2>/dev/null || date -d '6 days ago' +%Y-%m-%d)
WEEK_END=$(date +%Y-%m-%d)

# Extract this week's NAV values
WEEK_DATA=$(awk -F',' -v start="$WEEK_START" -v end="$WEEK_END" '
  NR > 1 && $1 >= start && $1 <= end { print $2 }
' "$NAV_FILE")

if [ -z "$WEEK_DATA" ]; then
  echo "No data for this week"
  exit 0
fi

# Calculate stats
OPEN=$(echo "$WEEK_DATA" | head -1)
CLOSE=$(echo "$WEEK_DATA" | tail -1)
HIGH=$(echo "$WEEK_DATA" | sort -rn | head -1)
LOW=$(echo "$WEEK_DATA" | sort -n | head -1)
CHANGE=$(echo "scale=2; (($CLOSE - $OPEN) / $OPEN) * 100" | bc)
SIGN=$([ "$(echo "$CHANGE >= 0" | bc)" -eq 1 ] && echo "+" || echo "")

# Fetch current top holdings for the report
TOP_HOLDINGS=$(octav portfolio get --addresses "$ADDR" --raw 2>/dev/null | jq -r '
  [.tokens | sort_by(-.value) | .[:3][] | "  \(.symbol): $\(.value | round) (\(.chain))"]
  | join("\n")
')

# Build report
REPORT="Portfolio Weekly Report
$WEEK_START to $WEEK_END
================================
Opening NAV:  \$$OPEN
Closing NAV:  \$$CLOSE
Weekly High:  \$$HIGH
Weekly Low:   \$$LOW
Change:       ${SIGN}${CHANGE}%
================================
Top Holdings:
$TOP_HOLDINGS
================================"

echo "$REPORT"

# Save report
REPORT_FILE="$DATA_DIR/report-$(date +%Y-W%V).txt"
echo "$REPORT" > "$REPORT_FILE"

# Post to Slack if configured
if [ -n "$SLACK_WEBHOOK" ]; then
  SLACK_TEXT=$(echo "$REPORT" | sed 's/"/\\"/g')
  curl -s -X POST "$SLACK_WEBHOOK" \
    -H 'Content-Type: application/json' \
    -d "{\"text\": \"\`\`\`$SLACK_TEXT\`\`\`\"}" \
    > /dev/null
  echo "Posted to Slack."
fi
Sample weekly report:
Portfolio Weekly Report
2025-01-13 to 2025-01-19
================================
Opening NAV:  $181043.22
Closing NAV:  $184230.41
Weekly High:  $186102.88
Weekly Low:   $178934.10
Change:       +1.76%
================================
Top Holdings:
  ETH: $82903 (ethereum)
  USDC: $38088 (ethereum)
  WBTC: $27634 (ethereum)
================================
Scheduler setup:
# Daily NAV snapshot at 9am
0 9 * * * /path/to/portfolio-tracker.sh

# Weekly report every Sunday at 10am
0 10 * * 0 /path/to/weekly-report.sh
  • Multiple wallets: Track each wallet in a separate CSV and combine totals in the weekly report
  • Monthly reports: Add a similar script triggered on the 1st of each month with 0 10 1 * *
  • Historical chart: Feed nav-history.csv into a plotting tool like gnuplot or a Google Sheet for visual trends

5. Credit Usage Guardian

A wrapper script that checks remaining credits before running any CLI command. Tracks daily consumption and alerts when credits drop below a configurable threshold.
#!/bin/bash
# octav-safe — Credit-aware CLI wrapper
# Usage: octav-safe portfolio get --addresses 0x...
# Drop-in replacement for `octav` that prevents accidental credit burn

CREDIT_THRESHOLD=100       # Warn below this many credits
LOG_DIR="$HOME/.octav/logs"
USAGE_LOG="$LOG_DIR/credit-usage.log"

mkdir -p "$LOG_DIR"

# Check current credits
CREDITS_RESULT=$(octav credits --raw 2>&1)
if [ $? -ne 0 ]; then
  echo "ERROR: Could not check credits — $CREDITS_RESULT" >&2
  exit 1
fi

CREDITS=$(echo "$CREDITS_RESULT" | jq -r '.credits')

# Log usage
echo "[$(date)] Credits remaining: $CREDITS | Command: octav $*" >> "$USAGE_LOG"

# Block if below threshold
if [ "$CREDITS" -lt "$CREDIT_THRESHOLD" ]; then
  echo "CREDIT GUARD: Only $CREDITS credits remaining (threshold: $CREDIT_THRESHOLD)"
  echo ""
  echo "  Command blocked: octav $*"
  echo ""
  echo "  To override, run the command directly with \`octav\` instead of \`octav-safe\`."
  echo "  To adjust the threshold, edit CREDIT_THRESHOLD in $(realpath "$0")"
  exit 1
fi

# Show credit count and proceed
echo "[octav-safe] Credits: $CREDITS — running: octav $*"
octav "$@"
Installation:
# Make it executable and add to PATH
chmod +x /path/to/octav-safe
ln -s /path/to/octav-safe /usr/local/bin/octav-safe

# Now use it as a drop-in replacement
octav-safe portfolio nav --addresses 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68
Sample output — normal operation:
[octav-safe] Credits: 4872 — running: octav portfolio nav --addresses 0x742d...
{
  "nav": 184230.41,
  "currency": "USD",
  "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68"
}
Sample output — credit guard triggered:
CREDIT GUARD: Only 47 credits remaining (threshold: 100)

  Command blocked: octav portfolio nav --addresses 0x742d...

  To override, run the command directly with `octav` instead of `octav-safe`.
  To adjust the threshold, edit CREDIT_THRESHOLD in /Users/you/scripts/octav-safe
Sample daily usage log (credit-usage.log):
[Mon Jan 15 09:00:01 2025] Credits remaining: 4872 | Command: octav portfolio nav --addresses 0x742d...
[Mon Jan 15 09:00:05 2025] Credits remaining: 4871 | Command: octav transactions get --addresses 0x742d...
[Mon Jan 15 14:00:01 2025] Credits remaining: 4870 | Command: octav airdrop --address 7xKXtg...
[Mon Jan 15 14:10:01 2025] Credits remaining: 4869 | Command: octav transactions get --addresses 0x742d...
  • Shell alias: Add alias octav="octav-safe" to your ~/.bashrc or ~/.zshrc to protect all commands by default
  • Daily summary: Add a cron job that parses the usage log and reports daily consumption — grep "$(date +%Y-%m-%d)" "$USAGE_LOG" | wc -l gives you the day’s command count
  • Tiered warnings: Add a second threshold (e.g., 500) that prints a warning but still allows the command to run

Setting Up launchd on macOS

macOS uses launchd instead of cron for scheduled tasks. While cron works on macOS, launchd is the native scheduler and handles sleep/wake correctly — your task runs after waking up even if the Mac was asleep at the scheduled time.

Step 1: Create the plist file

Each scheduled task needs a .plist file in ~/Library/LaunchAgents/. Use reverse-DNS naming:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>fi.octav.your-script-name</string>

  <key>ProgramArguments</key>
  <array>
    <string>/bin/bash</string>
    <string>/Users/YOU/scripts/your-script.sh</string>
  </array>

  <!-- Option A: Run at a specific time (daily at 2pm) -->
  <key>StartCalendarInterval</key>
  <dict>
    <key>Hour</key>
    <integer>14</integer>
    <key>Minute</key>
    <integer>0</integer>
  </dict>

  <!-- Option B: Run at a fixed interval (every 600 seconds = 10 min) -->
  <!-- <key>StartInterval</key> -->
  <!-- <integer>600</integer> -->

  <!-- Log output for debugging -->
  <key>StandardOutPath</key>
  <string>/tmp/your-script.stdout</string>
  <key>StandardErrorPath</key>
  <string>/tmp/your-script.stderr</string>
</dict>
</plist>

Step 2: Load and manage

# Load (start scheduling)
launchctl load ~/Library/LaunchAgents/fi.octav.your-script-name.plist

# Unload (stop scheduling)
launchctl unload ~/Library/LaunchAgents/fi.octav.your-script-name.plist

# Check if it's running
launchctl list | grep fi.octav

# Run immediately (for testing)
launchctl start fi.octav.your-script-name

Step 3: Debug

If your script isn’t running, check the logs:
# Check launchd's own logs
log show --predicate 'subsystem == "com.apple.xpc.launchd"' --last 1h | grep fi.octav

# Check your script's output
cat /tmp/your-script.stdout
cat /tmp/your-script.stderr
Common pitfall: launchd doesn’t source your shell profile. If octav isn’t found, use the full path (/usr/local/bin/octav or wherever it’s installed). Find it with which octav.

Next Steps