Trading from your terminal: setting up Hermes Agent with Montrose

How I connected my Hermes Agent to Montrose for terminal-based portfolio tracking, trade tickets, and automated daily summaries.

Trading from your terminal: setting up Hermes Agent with Montrose

I connected Hermes Agent directly to Montrose’s MCP server. It can check holdings, monitor watchlists, and pre-fill trade tickets from the same terminal where I run everything else.

What you’ll end up with

  • Hermes can query your accounts, holdings, and watchlists via natural language
  • Daily BankID re-authentication handled by a refresh script (long-lived tokens pending a Montrose server-side fix)
  • A daily financial morning summary delivered to Discord at 08:00
  • Stock monitoring that tracks fundamentals alongside price

Prerequisites

  • Hermes Agent installed (docs)
  • A Montrose trading account
  • MCP enabled on your Montrose account
  • A messaging platform connected to Hermes (Discord, Telegram, etc.) for cron deliveries

Step 1: Register the MCP server

Hermes supports MCP natively via the mcp_servers config block. Add Montrose to your config:

hermes config edit
mcp_servers:
  montrose:
    url: https://mcp.montrose.io
    headers:
      Authorization: Bearer ***
    timeout: 180
    connect_timeout: 30

The hermes mcp add command exists, but Montrose uses a custom OAuth2 flow, so direct YAML editing gives you more control over headers and timeouts. The token refresh script (below) updates this same config field automatically.

Step 2: Get your tokens

Montrose uses OAuth2 with the authorization code grant and PKCE. I set up a temporary Express server on localhost to act as the redirect URI during the initial handshake. Here’s the flow:

  1. Register a redirect URI with Montrose pointing to http://localhost:3000/callback
  2. Start a local server that listens for the authorization code
  3. Open the authorization URL in your browser, approve, and the code lands on localhost
  4. Exchange the code + PKCE verifier for access and refresh tokens

The exchange step looks like this. Note the iss parameter pulled from the callback URL. Without it the server returns invalid_grant:

curl -s -X POST https://mcp.montrose.io/token \
  -d "grant_type=authorization_code" \
  -d "code=AUTH_CODE_FROM_CALLBACK" \
  -d "redirect_uri=http://localhost:3000/callback" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "code_verifier=YOUR_PKCE_VERIFIER" \
  -d "iss=https://identity-internal.carnegie.se/oauth/v2/oauth-anonymous"

Save the response. The tokens are opaque strings starting with _0XBPW..., not JWTs:

echo "_0XBPW..." > /tmp/montrose_access_token.txt
echo "_1XBPW..." > /tmp/montrose_refresh_token.txt

Plug the access token into your config.yaml’s Authorization header and test it:

hermes mcp test montrose

You should see the available tools listed. Or just ask Hermes “show me my accounts” and it will call mcp_montrose_get_user_accounts() and return a readable list.

Step 3: Token auto-refresh with cron

The access token expires every 24 hours. Rather than refreshing it by hand, I wrote a bash script that automates it.

Note on token longevity: The refresh endpoint returns new tokens, but currently the MCP server rejects them after the initial 24-hour window. This is a known server-side issue that Montrose has acknowledged and are working on. In the meantime, I re-authenticate with BankID once a day (about 30 seconds). Once the fix is deployed, the refresh flow should keep the token alive automatically without further action.

#!/bin/bash
# ~/.hermes/scripts/refresh-montrose-token.sh
# Runs from cron every 12h

CONFIG_FILE="$HOME/.hermes/config.yaml"
CLIENT_ID="YOUR_CLIENT_ID"
CLIENT_SECRET="YOUR_CLIENT_SECRET"
REFRESH_FILE="/tmp/montrose_refresh_token.txt"

REFRESH_TOKEN=$(cat "$REFRESH_FILE")

RESPONSE=$(curl -s -X POST https://mcp.montrose.io/token \
  -d "grant_type=refresh_token" \
  -d "refresh_token=$REFRESH_TOKEN" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET")

ACCESS_TOKEN=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('access_token',''))")
NEW_REFRESH=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('refresh_token',''))")

echo "$ACCESS_TOKEN" > /tmp/montrose_access_token.txt
echo "$NEW_REFRESH" > "$REFRESH_FILE"

sed -i '/^  montrose:/,/^  [a-z]/{
    s|      Authorization: "Bearer .*"|      Authorization: "Bearer '\"$ACCESS_TOKEN\"'|
}' "$CONFIG_FILE"

if systemctl --user is-active hermes-gateway >/dev/null 2>&1; then
    systemctl --user reload hermes-gateway
fi

Schedule it:

hermes cron create "every 12h" \
  --script ~/.hermes/scripts/refresh-montrose-token.sh \
  --no-agent

Why every 12h instead of 24h? The daily summary runs at 08:00. If you only refresh at the 24h mark, clock drift, DST shifts, or gateway restarts can eat into your window. 12h gives a comfortable margin and splits the day into morning and evening coverage.

The --no-agent flag tells the scheduler to run the script directly without invoking an LLM. Zero tokens consumed, just pure shell execution. The script reloads the Hermes gateway via USR1 signal (systemctl --user reload) so the new token takes effect with zero downtime, no interruption to Discord or Telegram.

Step 4: Daily financial morning summary

The script at ~/.hermes/scripts/financial-morning-summary.sh runs every weekday at 08:00 and posts a formatted summary. It does four things in sequence:

  1. Pulls portfolio snapshot from Montrose via the direct SSE call
  2. Saves yesterday’s total to a JSON file and computes the day-over-day change
  3. Fetches the Nasdaq earnings calendar for today’s reports
  4. Pulls OMX stock prices via yfinance

The output lands in Discord looking like this:

📊 Financial Morning Summary
📅 Monday, June 08, 2026

🏦 Portfolio
  • ISK #345678: 850,450 SEK
  • Savings #789012: 600,000 SEK
  • Depot #123456: 320,200 SEK

💰 Total: 1,770,650 SEK   📈 +12,300 SEK (+0.70%)

📋 Earnings Today
  Pre-market: ORCL (Oracle) | EPS est: $1.48
  After close: CRM (Salesforce) | EPS est: $2.58

🇸🇪 Swedish Market
  📈 Volvo B @ 287.5 SEK (+1.2%)
  📉 Ericsson B @ 82.3 SEK (-0.4%)

Schedule it:

hermes cron create "0 8 * * 1-5" \
  --script ~/.hermes/scripts/financial-morning-summary.sh \
  --no-agent \
  --deliver "discord:#financial"

This is a no_agent job. The script produces already-formatted Markdown, so there’s no reason to run an LLM on top of it every morning. The scheduler just executes the script and delivers its stdout verbatim. If your output format is deterministic, skip the LLM and save the tokens.

Step 5: Stock monitoring

Beyond the daily summary, you can track specific tickers with fundamentals:

hermes cron create "every 2h" \
  --script ~/.hermes/scripts/stock-monitor.py \
  --no-agent \
  --deliver "discord:#financial"

The script uses yfinance to pull price, market cap, trailing and forward P/E, EBITDA, free cash flow, net cash/debt, ROE, dividend yield, 52-week range, and analyst target. Emojis keep it skimmable in a chat window.

The whole system

All three cron jobs use no_agent mode. The gateway runs as a systemd user service. The total configuration is about 30 lines of YAML and 250 lines of shell and Python, 90% of which is formatting emoji output so the Discord messages look good.

A few things I learned along the way

no_agent cron is undersold. Most people default to LLM-driven cron jobs, but for deterministic data pipelines, pure scripts are faster, cheaper, and more reliable. The agent should sit behind the pipeline, reasoning about the output, not rendering it.

Start simple, then add. The first version was just hermes mcp test montrose and a manual get_holdings check. The daily summary grew organically from “I want to know my balance in the morning” to a full portfolio plus earnings plus market snapshot. Let real needs drive the feature list, not what you think you should build.

More Articles

Generate Commit Messages with Ollama in Neovim

Generate Commit Messages with Ollama in Neovim

Generate conventional commit messages from staged diffs using Ollama. Run it locally for privacy and offline, or use cloud models for speed.

Adding Umami analytics to the OpenClaw morning brief

Adding Umami analytics to the OpenClaw morning brief

An agent skill that fetches traffic data from Umami, and how it fits into a daily automated briefing.

Setting up Google Calendar sync for OpenClaw

Setting up Google Calendar sync for OpenClaw

How I set up read-only Google Calendar sync for my personal AI assistant running on a home server VM.

Better Clipboard Handling in Claude Code

Better Clipboard Handling in Claude Code

A plugin that makes clipboard operations in Claude Code more reliable and natural to use.

Get notified when Claude Code needs your input

Get notified when Claude Code needs your input

Stop constantly checking your terminal. Set up notifications that alert you when Claude Code is ready for your input.

Auto-format generated Code with Claude Code Hooks

Auto-format generated Code with Claude Code Hooks

How to set up a PostToolUse hook in Claude Code to automatically run prettier after every file edit or write operation.

PWA Web Share Target on Android: The Absolute URL Fix

PWA Web Share Target on Android: The Absolute URL Fix

Getting the Web Share Target API to work on Android PWAs can be frustrating. Your manifest looks correct, but the app never appears in the share sheet. Here is what finally worked.

Standard function keys on external keyboards in MacOS

Standard function keys on external keyboards in MacOS

Configure Karabiner Elements to use standard function keys on external keyboards while keeping media keys on your MacBook.

How to Clear Cloudflare Cache using a webhook

How to Clear Cloudflare Cache using a webhook

Automatically purge Cloudflare cache using webhooks and API tokens.