Build a Second Brain Savings Tracker
Track how your Obsidian notes affect Gemini AI token usage and cost.
Introduction
30 Second Summary
Building a second brain sounds great in theory. But when you paste your notes into an AI prompt, are you actually getting better answers for fewer tokens, or just making your input longer?
In this project, you will build a command-line Python tool that answers that question with real data. It queries Gemini with and without your Obsidian notes, logging every token detail to a local database.
What You'll Build
A command-line tool that compares AI token usage when answering questions with and without your Obsidian notes as context.
By the end of this project, you'll have:
- A working Python tool that queries Gemini with and without your Obsidian notes, tracking every token detail in a local SQLite database.
- Auto-generated weekly trend charts built with matplotlib showing your average token usage and estimated cost savings.
- A complete A/B comparison system that gives you hard data on whether your second brain saves tokens and money.
- Secret Mission: A context efficiency score that ranks your most valuable Obsidian notes by how much they reduce token usage.
Are there any prerequisites?
This project assumes you're comfortable writing and running Python scripts. You'll also need an existing Obsidian vault with some notes in it.
Set Up Your Environment
Before you can measure how your Obsidian notes affect AI token usage, you need a few tools in place. This step gets your environment ready with the right Python packages, a free API key, and a verified connection to Google's Gemini model.
You will install the Google GenAI SDK for sending prompts and reading token data. You will also set up matplotlib for generating charts later, then grab a free Gemini API key and confirm everything works.
In this step, get ready to:
- Install the Python packages for AI queries and chart generation.
- Get a free Gemini API key from Google AI Studio.
- Verify the connection by sending a test prompt and reading token metadata.
Install Python dependencies
The Google GenAI SDK lets you send prompts to Gemini and read token usage data from every response. Matplotlib handles the chart generation you will build later in the project.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir brain-tracker
cd brain-tracker
- Install the two Python packages by running:
pip install google-genai==1.74.0 matplotlib==3.10.9
- You should see Successfully installed messages for both packages at the end of the output.
Seeing 'pip' is not recognized?
Python may not be added to your system PATH. Try running python -m pip install instead of pip install.
If you have multiple Python versions installed, try pip3 install instead.
Help me fix my pip installation issue.
Get your free Gemini API key
An API key is like a password that lets your Python script talk to Gemini's servers. Google offers a free tier for Gemini that requires no credit card and has no expiration date.
- Open your browser and go to aistudio.google.com.
- Sign in with your Google account.
- Click Get API Key in the left sidebar.
- Click Create API Key.
- Copy the key that appears. You will need it in a moment.
Free tier data privacy
On Gemini's free tier, your prompts and responses may be used to improve Google products. This means your Obsidian vault content could be reviewed by Google when sent through the API.
Avoid sending highly sensitive notes through the tracker. On the paid tier, your content is not used for product improvement.
The Google GenAI SDK automatically reads your API key from an environment variable called GEMINI_API_KEY. You need to set this in the same terminal session where you will run your scripts.
- Back in your terminal, set the environment variable by running this command (replace your-api-key-here with the key you just copied):
macOS
export GEMINI_API_KEY=your-api-key-here
Windows
If you are using PowerShell:
$env:GEMINI_API_KEY="your-api-key-here"
If you are using Command Prompt instead:
set GEMINI_API_KEY=your-api-key-here
Why an environment variable?
Storing the key in an environment variable keeps it out of your code. The SDK reads it automatically, so you never have to paste your key into a Python file.
This variable only lasts for your current terminal session. If you close the terminal, you will need to set it again.
Not sure which terminal you have?
If your terminal prompt starts with PS C:\, you are in PowerShell. If it shows C:\>, you are in Command Prompt.
Help me figure out which terminal I'm using on Windows.
Verify the connection
Now let's confirm everything works by sending a test prompt to Gemini and reading the token usage data that comes back. Every Gemini response includes metadata showing exactly how many tokens went in and came out.
- In your text editor, create a new file called test_gemini.py inside your brain-tracker folder.
- Copy the following code into the file:
# Import the Google GenAI SDK
from google import genai
import os
# Create a client (reads GEMINI_API_KEY from your environment)
client = genai.Client()
# Send a test prompt to Gemini 2.5 Flash
response = client.models.generate_content(
model='gemini-2.5-flash',
contents='Say hello in one sentence.',
)
# Print the AI response and the token usage metadata
print(response.text)
print(response.usage_metadata)
What does this code do?
- genai.Client() creates a client that connects to Google's API using the GEMINI_API_KEY you just set.
- client.models.generate_content() sends your prompt to the gemini-2.5-flash model and returns a response object.
- response.text contains the AI's answer.
- response.usage_metadata contains the token counts you will track throughout this project.
- Save test_gemini.py.
- Run the test script by running this command in your terminal:
python test_gemini.py
You should see two things printed. First, a short greeting sentence from Gemini. Second, a block of usage metadata showing fields like prompt_token_count, candidates_token_count, and total_token_count.
What are these token fields?
- prompt_token_count is the number of input tokens Gemini processed from your prompt.
- candidates_token_count is the number of output tokens Gemini generated in its response.
- total_token_count is the sum of input and output tokens.
These three numbers are the foundation of everything you will build in this project. Every query you track will log these values so you can compare costs over time.
Seeing an API key error?
If you see a message about a missing or invalid API key, your environment variable may not be set in the current terminal session. Run the export (macOS) or $env: / set (Windows) command from the previous substep again in the same terminal window.
If you opened a new terminal since setting the variable, you need to set it again. The variable only persists for the session where you created it.
Help me debug my Gemini API key connection issue.
✔️ Awesome, I've got everything!
Great. Double check you have saved test_gemini.py and that your terminal printed both the response text and the usage metadata.
ⓧ I'd like to double check the full code
Here is the complete test_gemini.py file. Make sure yours matches exactly.
from google import genai
import os
client = genai.Client()
response = client.models.generate_content(
model='gemini-2.5-flash',
contents='Say hello in one sentence.',
)
print(response.text)
print(response.usage_metadata)
Your environment is ready and talking to Gemini. Next up, you will build the vault search module that finds relevant notes inside your Obsidian vault.
Build the Obsidian Vault Search
Your environment is set up and Gemini is confirmed working with token metadata. Now it's time to build the first module of your tracker: a search tool that finds relevant notes in your Obsidian vault.
Obsidian vaults are just folders of markdown files sitting on your computer. Your search module will scan those files, score them by keyword relevance, and return the best matches as context for your Gemini prompts.
In this step, get ready to:
- Create vault_search.py with a keyword-matching search function.
- Build a context formatter that prepares matched notes for AI prompts.
- Test the search against your actual Obsidian vault.
Create the vault search function
The search function needs to walk through every folder in your vault, open each .md file, and score it by how many of your query words appear inside. Only files with at least one match make the cut.
- Create a new file called vault_search.py in your project folder (the same folder where test_gemini.py lives).
- Add the imports and the main search function by copying this code:
import os
import re
def search_vault(vault_path, query, max_results=3):
"""Search Obsidian vault for notes relevant to the query.
Scores each markdown file by keyword overlap with the query.
Returns a list of dicts with 'path', 'filename', 'content', and 'score'.
"""
query_words = set(re.findall(r'\w+', query.lower()))
results = []
for root, dirs, files in os.walk(vault_path):
for filename in files:
if not filename.endswith('.md'):
continue
filepath = os.path.join(root, filename)
try:
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
except (UnicodeDecodeError, PermissionError):
continue
content_lower = content.lower()
score = sum(1 for word in query_words if word in content_lower)
if score > 0:
results.append({
'path': filepath,
'filename': filename,
'content': content,
'score': score,
})
results.sort(key=lambda x: x['score'], reverse=True)
return results[:max_results]
What does this code do?
- re.findall(r'\w+', query.lower()) splits your search query into individual lowercase words. These become the keywords to match against.
- os.walk(vault_path) recursively walks through every subfolder in your Obsidian vault, so nested notes are included too.
- Each .md file gets a score counting how many query words appear in its content. Only files with a score above zero make the results list.
- The try/except block gracefully skips files that can't be read due to encoding issues or permission restrictions.
- Results are sorted highest-score-first and capped at max_results (default 3).
You won't see any output yet. The next function and test block will let you run everything.
The search function returns raw note data, but before you can feed it to Gemini you need to format those notes into a single string. That's what the context formatter handles.
- Below the search_vault function in vault_search.py, add the context formatter by copying this code:
def format_context(notes):
"""Format matched notes into a context string for the AI prompt."""
if not notes:
return ""
parts = []
for note in notes:
parts.append(f"--- {note['filename']} ---\n{note['content']}\n")
return "\n".join(parts)
What does format_context do?
This function takes the list of matched notes and combines them into a single string. Each note is wrapped with a --- filename --- header so the AI model can tell where one note ends and another begins.
You won't see a change yet. The next section adds a test block to run everything.
Test it against your vault
Time to see if the search actually finds your notes. You'll add a test block that prompts you for a vault path and a search query, then prints the results.
- At the very bottom of vault_search.py, add the test block by copying this code:
if __name__ == '__main__':
import sys
vault = input("Enter your Obsidian vault path: ").strip()
query = input("Enter a search query: ").strip()
matches = search_vault(vault, query)
if not matches:
print("No matching notes found.")
else:
for note in matches:
print(f"\n[{note['filename']}] (score: {note['score']})")
print(note['content'][:200] + "...")
What does this test block do?
- The if __name__ == '__main__': guard means this code only runs when you execute the file directly. It won't run when other modules import vault_search later.
- It prompts you for your vault path and a search query, then prints each matching note's filename, score, and a 200-character content preview.
- Save vault_search.py.
- Run the test by executing this command:
python vault_search.py
- When prompted, enter the full path to your Obsidian vault folder (for example, C:\Users\YourName\Documents\MyVault).
- Enter a search query for a topic you know you have notes on.
You should see matching filenames with scores and content previews printed in your terminal.
No matching notes found?
- Double check that the vault path points to the actual folder containing your .md files. Some Obsidian vaults have the notes inside a subfolder.
- Try a simpler, single-word query that you know appears in at least one note.
- Make sure your Obsidian vault contains .md files (not just folders or image attachments).
- Why isn't vault_search.py finding any notes in my Obsidian vault?
✔️ Awesome, I've got everything!
Double check you've saved vault_search.py and you're ready to move on.
ⓧ I'd like to double check the full code
Here is the complete vault_search.py file. Make sure yours matches exactly.
import os
import re
def search_vault(vault_path, query, max_results=3):
"""Search Obsidian vault for notes relevant to the query.
Scores each markdown file by keyword overlap with the query.
Returns a list of dicts with 'path', 'filename', 'content', and 'score'.
"""
query_words = set(re.findall(r'\w+', query.lower()))
results = []
for root, dirs, files in os.walk(vault_path):
for filename in files:
if not filename.endswith('.md'):
continue
filepath = os.path.join(root, filename)
try:
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
except (UnicodeDecodeError, PermissionError):
continue
content_lower = content.lower()
score = sum(1 for word in query_words if word in content_lower)
if score > 0:
results.append({
'path': filepath,
'filename': filename,
'content': content,
'score': score,
})
results.sort(key=lambda x: x['score'], reverse=True)
return results[:max_results]
def format_context(notes):
"""Format matched notes into a context string for the AI prompt."""
if not notes:
return ""
parts = []
for note in notes:
parts.append(f"--- {note['filename']} ---\n{note['content']}\n")
return "\n".join(parts)
if __name__ == '__main__':
import sys
vault = input("Enter your Obsidian vault path: ").strip()
query = input("Enter a search query: ").strip()
matches = search_vault(vault, query)
if not matches:
print("No matching notes found.")
else:
for note in matches:
print(f"\n[{note['filename']}] (score: {note['score']})")
print(note['content'][:200] + "...")
Your vault search is working and finding relevant notes. Next up, you'll build the database that stores every token detail from your Gemini queries.
Build the Token Usage Database
Your vault search can find relevant Obsidian notes for any question. The next challenge is keeping track of what happens when you send those notes to Gemini.
Every time you query Gemini, the API response includes token counts for how many tokens went in and how many came back. You need a place to store those numbers over time so you can spot trends and calculate savings. In this step, you will create a db.py module that logs token usage into a local SQLite database.
In this step, get ready to:
- Create the database connection and table schema for tracking queries.
- Add helper functions for logging token data and retrieving weekly summaries.
- Test the database with dummy data to confirm everything works.
Set up the database connection and schema
SQLite is a lightweight database engine that comes built into Python. There is no server to install or configure.
Why SQLite instead of a CSV or JSON file?
A flat file works for simple logging, but you will need to group data by week, calculate averages, and filter by mode. SQLite handles all of that with built-in SQL queries instead of you writing custom parsing code.
It also handles concurrent writes safely if you ever run multiple trackers at once.
- Create a new file called db.py in your project folder (the same folder where vault_search.py lives).
- Add the following code to set up the imports, database path, and connection function:
import sqlite3
import os
from datetime import datetime
# Store the database file next to this script
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'usage.db')
def get_connection():
"""Get a connection to the SQLite database, creating tables if needed."""
conn = sqlite3.connect(DB_PATH)
# Row factory lets us access columns by name instead of index
conn.row_factory = sqlite3.Row
conn.execute('''
CREATE TABLE IF NOT EXISTS queries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
question TEXT NOT NULL,
mode TEXT NOT NULL,
prompt_token_count INTEGER NOT NULL,
candidates_token_count INTEGER NOT NULL,
total_token_count INTEGER NOT NULL,
context_files TEXT,
vault_path TEXT
)
''')
conn.commit()
return conn
What does this code do?
- DB_PATH builds the path to usage.db so the database always lives next to your script, no matter where you run it from.
- get_connection() opens the database and creates the queries table if it does not exist yet. The CREATE TABLE IF NOT EXISTS clause makes this safe to call every time.
- The mode column stores either with_context or without_context so you can compare the two approaches later.
- Save db.py.
You will not see any output yet. The next chunk adds the functions that write data to this database.
Add the helper functions
The database schema is ready. Now you need functions to insert rows when a query runs and to pull aggregated summaries when you generate reports.
- In db.py, add the log_query() and get_weekly_summary() functions below get_connection() by copying the following code:
def log_query(question, mode, prompt_tokens, candidates_tokens, total_tokens,
context_files="", vault_path=""):
"""Log a single query's token usage to the database."""
conn = get_connection()
conn.execute(
'''INSERT INTO queries
(timestamp, question, mode, prompt_token_count,
candidates_token_count, total_token_count, context_files, vault_path)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)''',
(
datetime.now().isoformat(),
question,
mode,
prompt_tokens,
candidates_tokens,
total_tokens,
context_files,
vault_path,
)
)
conn.commit()
conn.close()
def get_weekly_summary():
"""Return weekly averages grouped by mode.
Returns a list of dicts with keys: week, mode, avg_prompt, avg_candidates,
avg_total, query_count.
"""
conn = get_connection()
rows = conn.execute('''
SELECT
strftime('%Y-W%W', timestamp) AS week,
mode,
AVG(prompt_token_count) AS avg_prompt,
AVG(candidates_token_count) AS avg_candidates,
AVG(total_token_count) AS avg_total,
COUNT(*) AS query_count
FROM queries
GROUP BY week, mode
ORDER BY week
''').fetchall()
conn.close()
return [dict(row) for row in rows]
What does this code do?
- log_query() inserts a single row into the queries table. The ? placeholders prevent SQL injection by letting SQLite handle the value escaping.
- datetime.now().isoformat() stamps each row with the current date and time. This is what the weekly grouping relies on.
- get_weekly_summary() uses strftime('%Y-W%W', timestamp) to group rows by year and week number. It then averages the token counts for each mode so you can compare "with context" vs "without context" over time.
- Save db.py.
You will not see a change yet. The next chunk adds one more helper and a test block to verify everything works.
- Below get_weekly_summary(), add the get_all_queries() function and the test block at the bottom of db.py:
def get_all_queries():
"""Return all logged queries ordered by timestamp."""
conn = get_connection()
rows = conn.execute(
'SELECT * FROM queries ORDER BY timestamp'
).fetchall()
conn.close()
return [dict(row) for row in rows]
if __name__ == '__main__':
# Insert two dummy rows to test the database
log_query("test question", "with_context", 150, 80, 230, "note1.md", "/vault")
log_query("test question", "without_context", 50, 120, 170, "", "/vault")
# Retrieve and print weekly averages
summary = get_weekly_summary()
for row in summary:
print(row)
print("Database test passed.")
What does this code do?
- get_all_queries() is a utility that returns every row in the database. The report script will use this later.
- The if __name__ == '__main__' block only runs when you execute db.py directly. It inserts two dummy rows (one "with context" and one "without") then prints the weekly summary to confirm everything works end to end.
- Save db.py.
- Run the test by running this command in your terminal:
python db.py
You should see two dictionaries printed, one for with_context and one for without_context, followed by Database test passed.
What should I see?
Each dictionary contains a week key (like 2026-W17), a mode, average token counts, and a query_count of 1. Your week number will differ depending on when you run it.
Seeing an error?
- If you see NameError: name 'datetime' is not defined, check that from datetime import datetime is at the top of your file.
- If you see sqlite3.OperationalError, make sure the SQL column names in your INSERT statement match the column names in your CREATE TABLE statement exactly.
- If you already ran the test and want to run it again, delete the usage.db file first to start fresh. Otherwise you will add duplicate test rows.
Still stuck? Help me debug my db.py database test.
✔️ Awesome, I've got everything!
Double check you have saved db.py and you can see the usage.db file in your project folder.
ⓧ I'd like to double check the full code
Here is the complete db.py file. Compare it against yours and fix any differences.
import sqlite3
import os
from datetime import datetime
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'usage.db')
def get_connection():
"""Get a connection to the SQLite database, creating tables if needed."""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
conn.execute('''
CREATE TABLE IF NOT EXISTS queries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
question TEXT NOT NULL,
mode TEXT NOT NULL,
prompt_token_count INTEGER NOT NULL,
candidates_token_count INTEGER NOT NULL,
total_token_count INTEGER NOT NULL,
context_files TEXT,
vault_path TEXT
)
''')
conn.commit()
return conn
def log_query(question, mode, prompt_tokens, candidates_tokens, total_tokens,
context_files="", vault_path=""):
"""Log a single query's token usage to the database."""
conn = get_connection()
conn.execute(
'''INSERT INTO queries
(timestamp, question, mode, prompt_token_count,
candidates_token_count, total_token_count, context_files, vault_path)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)''',
(
datetime.now().isoformat(),
question,
mode,
prompt_tokens,
candidates_tokens,
total_tokens,
context_files,
vault_path,
)
)
conn.commit()
conn.close()
def get_weekly_summary():
"""Return weekly averages grouped by mode.
Returns a list of dicts with keys: week, mode, avg_prompt, avg_candidates,
avg_total, query_count.
"""
conn = get_connection()
rows = conn.execute('''
SELECT
strftime('%Y-W%W', timestamp) AS week,
mode,
AVG(prompt_token_count) AS avg_prompt,
AVG(candidates_token_count) AS avg_candidates,
AVG(total_token_count) AS avg_total,
COUNT(*) AS query_count
FROM queries
GROUP BY week, mode
ORDER BY week
''').fetchall()
conn.close()
return [dict(row) for row in rows]
def get_all_queries():
"""Return all logged queries ordered by timestamp."""
conn = get_connection()
rows = conn.execute(
'SELECT * FROM queries ORDER BY timestamp'
).fetchall()
conn.close()
return [dict(row) for row in rows]
if __name__ == '__main__':
log_query("test question", "with_context", 150, 80, 230, "note1.md", "/vault")
log_query("test question", "without_context", 50, 120, 170, "", "/vault")
summary = get_weekly_summary()
for row in summary:
print(row)
print("Database test passed.")
Your database is ready to store every token detail from your Gemini queries. Next up, you will build the main tracker script that ties your vault search and database together into a single command.
Build the Main Tracker
You now have a vault search module that finds relevant Obsidian notes and a database that logs every token detail. The two halves of your analytics engine are ready.
In this step, you'll build the main script that ties everything together. It sends the same question to Gemini twice: once with your Obsidian notes injected as context, once without. Then it compares the token usage side by side so you can see exactly what your second brain costs or saves.
In this step, get ready to:
- Create the main brain_tracker.py script with a Gemini helper function.
- Build an A/B comparison that queries Gemini with and without vault context and displays token differences.
- Add a command-line interface supporting ask and report modes.
Create brain_tracker.py and the Gemini helper
The main script needs to import from report.py, which you'll build in the next step. To prevent an import error right now, you'll create a quick placeholder first.
- Create a file called report.py in your project folder with this placeholder code:
def generate_charts(output_dir=None):
"""Placeholder - full implementation coming in the next step."""
print("Report generation not yet implemented.")
return []
Why create a placeholder?
Python resolves all top-level imports when a script starts. If report.py doesn't exist, the script crashes before any of your code runs. This stub gives the import something to find. You'll replace it with the real implementation in Step 5.
- Save report.py.
Now for the real work. The ask_gemini() function is your core helper. When Obsidian context is provided, it prepends your notes to the question so Gemini can draw on them. When there's no context, it sends the raw question alone.
- Create a new file called brain_tracker.py in your project folder and add the following imports, constants, and helper function:
import sys
import os
from google import genai
from vault_search import search_vault, format_context
from db import log_query
from report import generate_charts
# Read vault path from environment, or prompt the user later
VAULT_PATH = os.environ.get('OBSIDIAN_VAULT_PATH', '')
MODEL = 'gemini-2.5-flash'
def ask_gemini(client, question, context=""):
"""Send a question to Gemini, optionally with Obsidian context."""
if context:
# Prepend the Obsidian notes so Gemini can reference them
contents = f"I have the following notes from my knowledge base:\n\n{context}\n\nBased on these notes (and your own knowledge), please answer this question:\n{question}"
else:
contents = question
response = client.models.generate_content(
model=MODEL,
contents=contents,
)
return response
What does this code do?
- The imports pull in your vault_search and db modules from previous steps, plus the Gemini SDK and the placeholder report module.
- VAULT_PATH reads from an environment variable so you can set your vault path once instead of typing it every time.
- ask_gemini() builds a prompt with or without your Obsidian notes. When notes are provided, they get prepended so Gemini sees them as reference material before the question.
Free tier data privacy
On Gemini's free tier, your prompts and responses may be used to improve Google products. This means your Obsidian note contents could be seen by Google when sent through the API.
If you have sensitive notes, consider testing with a non-sensitive subset of your vault. On the paid tier, your content is not used for training.
- Save brain_tracker.py.
You won't see a change yet. The next chunk builds the comparison function that actually calls this helper.
Build the A/B comparison
The heart of the tracker is its A/B comparison. For every question, it sends two requests: one with your Obsidian notes as context, one without. Then it extracts the token metadata from both and shows a side-by-side breakdown.
Why will input tokens increase with context?
The "with context" request will always have more input tokens because your Obsidian notes are prepended to the prompt. The potential savings come from the output side. With relevant context, Gemini can give shorter, more precise answers instead of verbose explanations.
- In brain_tracker.py, add the run_comparison() function below the ask_gemini() function:
def run_comparison(question, vault_path):
"""Run A/B comparison: with vs without Obsidian context."""
client = genai.Client()
# Search vault for relevant notes
notes = search_vault(vault_path, question)
context = format_context(notes)
context_files = ", ".join(n['filename'] for n in notes)
if not notes:
print("No matching notes found in your vault for this question.")
print("Running without context only.\n")
# Query WITH context (if notes found)
if notes:
print("Querying Gemini WITH Obsidian context...")
response_with = ask_gemini(client, question, context)
meta_with = response_with.usage_metadata
log_query(
question, "with_context",
meta_with.prompt_token_count,
meta_with.candidates_token_count,
meta_with.total_token_count,
context_files, vault_path,
)
# Query WITHOUT context
print("Querying Gemini WITHOUT context...")
response_without = ask_gemini(client, question)
meta_without = response_without.usage_metadata
log_query(
question, "without_context",
meta_without.prompt_token_count,
meta_without.candidates_token_count,
meta_without.total_token_count,
"", vault_path,
)
# Display results
print("\n" + "=" * 60)
print("TOKEN USAGE COMPARISON")
print("=" * 60)
if notes:
print(f"\n{'Metric':<25} {'With Context':>15} {'Without':>15} {'Difference':>15}")
print("-" * 70)
print(f"{'Input tokens':<25} {meta_with.prompt_token_count:>15,} {meta_without.prompt_token_count:>15,} {meta_with.prompt_token_count - meta_without.prompt_token_count:>+15,}")
print(f"{'Output tokens':<25} {meta_with.candidates_token_count:>15,} {meta_without.candidates_token_count:>15,} {meta_with.candidates_token_count - meta_without.candidates_token_count:>+15,}")
print(f"{'Total tokens':<25} {meta_with.total_token_count:>15,} {meta_without.total_token_count:>15,} {meta_with.total_token_count - meta_without.total_token_count:>+15,}")
# Calculate estimated cost using Gemini 2.5 Flash pricing
input_price = 0.30 / 1_000_000
output_price = 2.50 / 1_000_000
cost_with = (meta_with.prompt_token_count * input_price +
meta_with.candidates_token_count * output_price)
cost_without = (meta_without.prompt_token_count * input_price +
meta_without.candidates_token_count * output_price)
print(f"\n{'Est. cost (Gemini Flash)':<25} ${cost_with:>14.6f} ${cost_without:>14.6f} ${cost_with - cost_without:>+14.6f}")
context_notes_str = ", ".join(n['filename'] for n in notes)
print(f"\nObsidian notes used: {context_notes_str}")
print(f"\nAnswer (with context):\n{response_with.text}")
else:
print(f"\nInput tokens: {meta_without.prompt_token_count:,}")
print(f"Output tokens: {meta_without.candidates_token_count:,}")
print(f"Total tokens: {meta_without.total_token_count:,}")
print(f"\nAnswer:\n{response_without.text}")
What does this function do?
- It creates a genai.Client() and searches your vault for notes matching the question. If no notes match, it skips the "with context" request entirely.
- Both responses have a usage_metadata attribute containing prompt_token_count (input), candidates_token_count (output), and total_token_count. Each response gets logged to your SQLite database via log_query().
- The cost estimate uses Gemini 2.5 Flash pricing: $0.30 per 1M input tokens and $2.50 per 1M output tokens. These are the paid-tier rates used to show what you'd save at scale.
- The function also prints the AI's actual response from the "with context" version, so the tracker doubles as a genuinely useful Q&A tool.
- Save brain_tracker.py.
You won't see a change yet. The next chunk adds the CLI so you can actually run the script.
Wire up the command-line interface
The CLI gives your tracker two modes. The ask mode runs the A/B comparison for a question. The report mode generates charts from your accumulated data (coming in the next step).
- In brain_tracker.py, add the main() function and entry point below run_comparison():
def main():
if len(sys.argv) < 2:
print("Usage:")
print(" python brain_tracker.py ask \"your question here\"")
print(" python brain_tracker.py report")
print("\nSet OBSIDIAN_VAULT_PATH environment variable to your vault folder.")
sys.exit(1)
command = sys.argv[1]
if command == 'ask':
if len(sys.argv) < 3:
print("Please provide a question: python brain_tracker.py ask \"your question\"")
sys.exit(1)
# Use env var or prompt the user for their vault path
vault_path = VAULT_PATH
if not vault_path:
vault_path = input("Enter your Obsidian vault path: ").strip()
if not os.path.isdir(vault_path):
print(f"Vault path not found: {vault_path}")
sys.exit(1)
question = sys.argv[2]
run_comparison(question, vault_path)
elif command == 'report':
print("Generating weekly trend charts...")
files = generate_charts()
if files:
for f in files:
print(f"Saved: {f}")
print("\nOpen the PNG files to view your charts.")
else:
print("No data yet. Run some queries with 'ask' first.")
else:
print(f"Unknown command: {command}")
print("Use 'ask' or 'report'.")
sys.exit(1)
if __name__ == '__main__':
main()
What does this code do?
- main() reads the first argument from sys.argv to decide which mode to run. If no arguments are provided, it prints usage instructions and exits.
- The ask command checks for the OBSIDIAN_VAULT_PATH environment variable first. If it's not set, the script prompts you to type the path interactively.
- The report command calls generate_charts() from the report module. Right now it uses the placeholder, but you'll wire up the real charts in the next step.
- Save brain_tracker.py.
✔️ Awesome, I've got everything!
Double check you've saved brain_tracker.py before moving on.
ⓧ I'd like to double check the full code
import sys
import os
from google import genai
from vault_search import search_vault, format_context
from db import log_query
from report import generate_charts
VAULT_PATH = os.environ.get('OBSIDIAN_VAULT_PATH', '')
MODEL = 'gemini-2.5-flash'
def ask_gemini(client, question, context=""):
"""Send a question to Gemini, optionally with Obsidian context."""
if context:
contents = f"I have the following notes from my knowledge base:\n\n{context}\n\nBased on these notes (and your own knowledge), please answer this question:\n{question}"
else:
contents = question
response = client.models.generate_content(
model=MODEL,
contents=contents,
)
return response
def run_comparison(question, vault_path):
"""Run A/B comparison: with vs without Obsidian context."""
client = genai.Client()
# Search vault for relevant notes
notes = search_vault(vault_path, question)
context = format_context(notes)
context_files = ", ".join(n['filename'] for n in notes)
if not notes:
print("No matching notes found in your vault for this question.")
print("Running without context only.\n")
# Query WITH context (if notes found)
if notes:
print("Querying Gemini WITH Obsidian context...")
response_with = ask_gemini(client, question, context)
meta_with = response_with.usage_metadata
log_query(
question, "with_context",
meta_with.prompt_token_count,
meta_with.candidates_token_count,
meta_with.total_token_count,
context_files, vault_path,
)
# Query WITHOUT context
print("Querying Gemini WITHOUT context...")
response_without = ask_gemini(client, question)
meta_without = response_without.usage_metadata
log_query(
question, "without_context",
meta_without.prompt_token_count,
meta_without.candidates_token_count,
meta_without.total_token_count,
"", vault_path,
)
# Display results
print("\n" + "=" * 60)
print("TOKEN USAGE COMPARISON")
print("=" * 60)
if notes:
print(f"\n{'Metric':<25} {'With Context':>15} {'Without':>15} {'Difference':>15}")
print("-" * 70)
print(f"{'Input tokens':<25} {meta_with.prompt_token_count:>15,} {meta_without.prompt_token_count:>15,} {meta_with.prompt_token_count - meta_without.prompt_token_count:>+15,}")
print(f"{'Output tokens':<25} {meta_with.candidates_token_count:>15,} {meta_without.candidates_token_count:>15,} {meta_with.candidates_token_count - meta_without.candidates_token_count:>+15,}")
print(f"{'Total tokens':<25} {meta_with.total_token_count:>15,} {meta_without.total_token_count:>15,} {meta_with.total_token_count - meta_without.total_token_count:>+15,}")
# Calculate estimated cost using Gemini 2.5 Flash pricing
input_price = 0.30 / 1_000_000
output_price = 2.50 / 1_000_000
cost_with = (meta_with.prompt_token_count * input_price +
meta_with.candidates_token_count * output_price)
cost_without = (meta_without.prompt_token_count * input_price +
meta_without.candidates_token_count * output_price)
print(f"\n{'Est. cost (Gemini Flash)':<25} ${cost_with:>14.6f} ${cost_without:>14.6f} ${cost_with - cost_without:>+14.6f}")
context_notes_str = ", ".join(n['filename'] for n in notes)
print(f"\nObsidian notes used: {context_notes_str}")
print(f"\nAnswer (with context):\n{response_with.text}")
else:
print(f"\nInput tokens: {meta_without.prompt_token_count:,}")
print(f"Output tokens: {meta_without.candidates_token_count:,}")
print(f"Total tokens: {meta_without.total_token_count:,}")
print(f"\nAnswer:\n{response_without.text}")
def main():
if len(sys.argv) < 2:
print("Usage:")
print(" python brain_tracker.py ask \"your question here\"")
print(" python brain_tracker.py report")
print("\nSet OBSIDIAN_VAULT_PATH environment variable to your vault folder.")
sys.exit(1)
command = sys.argv[1]
if command == 'ask':
if len(sys.argv) < 3:
print("Please provide a question: python brain_tracker.py ask \"your question\"")
sys.exit(1)
vault_path = VAULT_PATH
if not vault_path:
vault_path = input("Enter your Obsidian vault path: ").strip()
if not os.path.isdir(vault_path):
print(f"Vault path not found: {vault_path}")
sys.exit(1)
question = sys.argv[2]
run_comparison(question, vault_path)
elif command == 'report':
print("Generating weekly trend charts...")
files = generate_charts()
if files:
for f in files:
print(f"Saved: {f}")
print("\nOpen the PNG files to view your charts.")
else:
print("No data yet. Run some queries with 'ask' first.")
else:
print(f"Unknown command: {command}")
print("Use 'ask' or 'report'.")
sys.exit(1)
if __name__ == '__main__':
main()
Time to take it for a spin. You need to set your Obsidian vault path so the tracker knows where to find your notes.
- Set the OBSIDIAN_VAULT_PATH environment variable to your vault's folder path. In Command Prompt, run:
set OBSIDIAN_VAULT_PATH=your-vault-path-here
Using PowerShell instead?
In PowerShell, the syntax is different. Run $env:OBSIDIAN_VAULT_PATH="your-vault-path-here" instead. You can skip the environment variable entirely if you prefer to type the path when prompted each time.
- Run your first tracked query by running this command (pick a topic you know you have Obsidian notes about):
python brain_tracker.py ask "What do I know about productivity?"
What should I see?
The script prints status messages as it queries Gemini twice. After both requests complete, you should see a comparison table showing input tokens, output tokens, and total tokens for both modes. The "Difference" column shows how many more (or fewer) tokens the context version used.
Below the table, you'll see the estimated cost line, the Obsidian filenames that were used as context, and the full AI response.
Not seeing the comparison table?
- If you see Vault path not found, double check that your OBSIDIAN_VAULT_PATH points to an existing folder. Use the full path with no quotes around it in Command Prompt.
- If you see No matching notes found, the keyword search didn't find any notes matching your question. Try a different question using words you know appear in your notes.
- If you see ModuleNotFoundError: No module named 'report', make sure you created the report.py placeholder file earlier in this step and saved it in the same folder as brain_tracker.py.
- If you see an API error, confirm your GEMINI_API_KEY environment variable is still set in this terminal session. Environment variables reset when you close the terminal.
Still stuck? Ask for help.
Your tracker is live and logging real data. Every query you run from here gets stored in usage.db with full token breakdowns. Next up, you'll build the chart generator that turns all this data into visual trend reports.
Generate Weekly Trend Charts
Your tracker is logging every query's token breakdown to a local database. The terminal comparison is useful in the moment, but raw numbers are hard to analyze across multiple sessions.
In this step, you will build a reporting module that reads your SQLite database and generates weekly trend charts as PNG files. These charts will show you how your token usage changes over time and whether your Obsidian notes are actually saving you tokens and money.
In this step, get ready to:
- Create report.py to generate weekly trend charts from your database.
- Verify the CLI wiring that connects the report to your main script.
- Run real queries and generate your first charts to visualize token savings.
Create the report module with the token usage chart
The report module will use matplotlib to turn your database rows into visual charts. It reads the weekly summary data you already built in db.py and renders two PNG images.
- Create a new file called report.py in the same folder as your other scripts.
- Add the imports, pricing constants, and the first half of the generate_charts() function by pasting this code:
import os
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from db import get_weekly_summary
# Gemini 2.5 Flash pricing for estimated cost calculations
FLASH_INPUT_PRICE_PER_TOKEN = 0.30 / 1_000_000
FLASH_OUTPUT_PRICE_PER_TOKEN = 2.50 / 1_000_000
def generate_charts(output_dir=None):
"""Generate weekly trend charts from the usage database.
Returns a list of file paths to the saved PNG files.
"""
if output_dir is None:
output_dir = os.path.dirname(os.path.abspath(__file__))
summary = get_weekly_summary()
if not summary:
print("No data to chart yet. Run some queries first!")
return []
# Split summary rows into with-context and without-context lists
weeks_with = []
avg_total_with = []
weeks_without = []
avg_total_without = []
for row in summary:
if row['mode'] == 'with_context':
weeks_with.append(row['week'])
avg_total_with.append(row['avg_total'])
else:
weeks_without.append(row['week'])
avg_total_without.append(row['avg_total'])
saved_files = []
# Chart 1: Average Tokens Per Query by Week
fig, ax = plt.subplots(figsize=(10, 6))
if weeks_with:
ax.plot(weeks_with, avg_total_with, marker='o', label='With Obsidian Context',
color='#2ecc71', linewidth=2)
if weeks_without:
ax.plot(weeks_without, avg_total_without, marker='s', label='Without Context',
color='#e74c3c', linewidth=2)
ax.set_xlabel('Week')
ax.set_ylabel('Average Total Tokens Per Query')
ax.set_title('Average Token Usage Per Query by Week')
ax.legend()
ax.grid(True, alpha=0.3)
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
path1 = os.path.join(output_dir, 'weekly_tokens.png')
fig.savefig(path1, dpi=150)
plt.close(fig)
saved_files.append(path1)
What does this code do?
matplotlib.use('Agg') sets a non-interactive backend so matplotlib saves charts as files instead of trying to open a display window. This is important for CLI tools that generate images without a GUI.
The two pricing constants match Gemini 2.5 Flash rates ($0.30 per 1M input tokens, $2.50 per 1M output tokens). These are used later to convert token differences into dollar estimates.
The function calls get_weekly_summary() from your database module, splits the results into two separate lists (with context vs without), then plots both as lines on the same chart. Green represents queries made with Obsidian context. Red represents queries without.
You will not see any output yet. The chart will not generate until you finish the file and run it.
- Now add the savings bar chart, the return statement, and the __main__ block directly below the code you just pasted:
# Chart 2: Estimated Weekly Savings
all_weeks = sorted(set(weeks_with) & set(weeks_without))
if all_weeks:
# Build lookup maps for quick access by week
with_map = dict(zip(weeks_with, avg_total_with))
without_map = dict(zip(weeks_without, avg_total_without))
savings_tokens = []
savings_dollars = []
for w in all_weeks:
diff = without_map[w] - with_map[w]
savings_tokens.append(diff)
dollar_saving = diff * FLASH_INPUT_PRICE_PER_TOKEN
savings_dollars.append(dollar_saving * 1000)
fig2, ax2 = plt.subplots(figsize=(10, 6))
# Green bars for positive savings, red for negative
colors = ['#2ecc71' if s >= 0 else '#e74c3c' for s in savings_tokens]
ax2.bar(all_weeks, savings_tokens, color=colors, alpha=0.8)
ax2.set_xlabel('Week')
ax2.set_ylabel('Token Savings (Without - With Context)')
ax2.set_title('Estimated Weekly Token Savings from Obsidian Context')
ax2.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
ax2.grid(True, alpha=0.3, axis='y')
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
path2 = os.path.join(output_dir, 'weekly_savings.png')
fig2.savefig(path2, dpi=150)
plt.close(fig2)
saved_files.append(path2)
else:
print("Need matching weeks of data for both modes to chart savings.")
return saved_files
if __name__ == '__main__':
files = generate_charts()
for f in files:
print(f"Saved: {f}")
What does this code do?
Chart 2 is a bar chart showing the token difference between "without context" and "with context" for each week. A positive bar (green) means your Obsidian notes helped reduce total token usage. A negative bar (red) means the extra context cost more tokens than it saved.
The chart only appears when you have data from both modes in the same week. That is why the code uses a set intersection (set(weeks_with) & set(weeks_without)) to find overlapping weeks.
The __main__ block at the bottom lets you test the report module directly by running python report.py.
- Save report.py.
- Test it by running this command:
python report.py
You should see one or two lines printed that say Saved: followed by the file path to each PNG chart. The test data from db.py is enough to generate at least the first chart.
Seeing a ModuleNotFoundError?
If you see ModuleNotFoundError: No module named 'db', make sure report.py is in the same folder as db.py. Python looks for imports relative to the script's location.
If you see No data to chart yet, run python db.py first to insert the test rows into your database.
Help me debug my report.py script
✔️ Awesome, I've got everything!
Great. Double check you have saved report.py.
ⓧ I'd like to double check the full code
import os
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from db import get_weekly_summary
FLASH_INPUT_PRICE_PER_TOKEN = 0.30 / 1_000_000
FLASH_OUTPUT_PRICE_PER_TOKEN = 2.50 / 1_000_000
def generate_charts(output_dir=None):
"""Generate weekly trend charts from the usage database.
Returns a list of file paths to the saved PNG files.
"""
if output_dir is None:
output_dir = os.path.dirname(os.path.abspath(__file__))
summary = get_weekly_summary()
if not summary:
print("No data to chart yet. Run some queries first!")
return []
weeks_with = []
avg_total_with = []
weeks_without = []
avg_total_without = []
for row in summary:
if row['mode'] == 'with_context':
weeks_with.append(row['week'])
avg_total_with.append(row['avg_total'])
else:
weeks_without.append(row['week'])
avg_total_without.append(row['avg_total'])
saved_files = []
# Chart 1: Average Tokens Per Query by Week
fig, ax = plt.subplots(figsize=(10, 6))
if weeks_with:
ax.plot(weeks_with, avg_total_with, marker='o', label='With Obsidian Context',
color='#2ecc71', linewidth=2)
if weeks_without:
ax.plot(weeks_without, avg_total_without, marker='s', label='Without Context',
color='#e74c3c', linewidth=2)
ax.set_xlabel('Week')
ax.set_ylabel('Average Total Tokens Per Query')
ax.set_title('Average Token Usage Per Query by Week')
ax.legend()
ax.grid(True, alpha=0.3)
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
path1 = os.path.join(output_dir, 'weekly_tokens.png')
fig.savefig(path1, dpi=150)
plt.close(fig)
saved_files.append(path1)
# Chart 2: Estimated Weekly Savings
all_weeks = sorted(set(weeks_with) & set(weeks_without))
if all_weeks:
with_map = dict(zip(weeks_with, avg_total_with))
without_map = dict(zip(weeks_without, avg_total_without))
savings_tokens = []
savings_dollars = []
for w in all_weeks:
diff = without_map[w] - with_map[w]
savings_tokens.append(diff)
dollar_saving = diff * FLASH_INPUT_PRICE_PER_TOKEN
savings_dollars.append(dollar_saving * 1000)
fig2, ax2 = plt.subplots(figsize=(10, 6))
colors = ['#2ecc71' if s >= 0 else '#e74c3c' for s in savings_tokens]
ax2.bar(all_weeks, savings_tokens, color=colors, alpha=0.8)
ax2.set_xlabel('Week')
ax2.set_ylabel('Token Savings (Without - With Context)')
ax2.set_title('Estimated Weekly Token Savings from Obsidian Context')
ax2.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
ax2.grid(True, alpha=0.3, axis='y')
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
path2 = os.path.join(output_dir, 'weekly_savings.png')
fig2.savefig(path2, dpi=150)
plt.close(fig2)
saved_files.append(path2)
else:
print("Need matching weeks of data for both modes to chart savings.")
return saved_files
if __name__ == '__main__':
files = generate_charts()
for f in files:
print(f"Saved: {f}")
Verify the CLI wiring
Your brain_tracker.py script already imports generate_charts from report.py and has a report command built into the CLI. Now that report.py exists, that import resolves and the report command is fully functional.
- Open brain_tracker.py and confirm the import at the top of the file:
from report import generate_charts
- Scroll down to the main() function and confirm the report command handler exists:
elif command == 'report':
print("Generating weekly trend charts...")
files = generate_charts()
if files:
for f in files:
print(f"Saved: {f}")
print("\nOpen the PNG files to view your charts.")
else:
print("No data yet. Run some queries with 'ask' first.")
Why is the wiring already in place?
When you built brain_tracker.py in the previous step, it was designed to support both the ask and report commands from the start. The report branch calls generate_charts(), prints the file paths, and tells you to open the PNGs. No changes needed here.
Run real queries and generate your first charts
Your charts need real data to be meaningful. The test rows from db.py gave you a quick sanity check, but now it is time to run a few actual queries through the tracker.
- Pick a topic you know you have Obsidian notes on. Run your first tracked query by running this command (replace the question with your own):
python brain_tracker.py ask "What are the key principles of spaced repetition?"
You should see the token comparison table you built in the previous step, showing input, output, and total tokens for both modes.
What should I ask?
Pick questions about topics you actually have notes on in your Obsidian vault. The tracker needs to find matching notes for the "with context" comparison to work. If you ask about something you have no notes on, the tracker runs in "without context" mode only.
- Run 2-4 more queries on different topics from your vault. Each query sends two requests to Gemini (one with context, one without) and logs both to your database.
Free tier rate limits
Gemini 2.5 Flash's free tier allows 10 requests per minute and 250 requests per day. Each tracked question uses 2 requests (with and without context). Running 3-5 questions in a row is well within these limits.
- Once you have run at least 3 queries, generate your weekly trend charts by running:
python brain_tracker.py report
You should see output listing the saved PNG file paths, such as Saved: /path/to/weekly_tokens.png.
- Open weekly_tokens.png in your file explorer. You should see a line chart with a green line (with context) and a red line (without context) showing average total tokens per query.
- Open weekly_savings.png if it was generated. You should see a bar chart showing the token difference between the two modes. Green bars mean your Obsidian notes saved tokens. Red bars mean they cost more than they saved.
Why might the savings chart not appear?
The savings chart requires data from both modes (with context and without context) in the same calendar week. If all your queries ran today, they are in the same week. If the tracker could not find matching vault notes for any of your questions, it only logged "without context" rows and the savings chart has nothing to compare.
Charts not generating or looking wrong?
If you see No data yet when running the report, your database might be empty. Run at least one python brain_tracker.py ask query first.
If the chart shows only one line instead of two, try asking a question about a topic you have detailed notes on. The vault search needs keyword overlap to find matches.
Help me debug why my charts are not generating correctly
You now have a complete analytics pipeline: ask questions, track tokens, and visualize trends over time. The more queries you run, the richer your charts become. Next up, take on the Secret Mission to find out which of your Obsidian notes are pulling their weight.
Secret mission
Add a Context Efficiency Score
You've been tracking total token savings, but which individual Obsidian notes are actually pulling their weight? Some notes might save you hundreds of output tokens. Others might just be adding noise. In this secret mission, you'll build a scoring system that measures each note's contribution and ranks your most valuable knowledge.
Before you go
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. The Gemini API free tier does not charge you and does not require a credit card. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Python project files: brain_tracker.py, vault_search.py, db.py, report.py, test_gemini.py.
- SQLite database: usage.db.
- Generated chart images: weekly_tokens.png and weekly_savings.png.
- Python packages: google-genai and matplotlib.
- Gemini API key (stored as an environment variable).
- Google AI Studio account.
Keep everything running
No action needed. Choose this if you are still actively building or want to keep tracking your Obsidian vault usage over time.
- Your project files, database, and charts stay on your Desktop.
- Your GEMINI_API_KEY environment variable stays set for future terminal sessions.
- Keep running queries with brain_tracker.py ask to build up more data for your weekly reports.
Pause - I'll come back to this later
Clear your API key from your current session to keep things tidy. Your project files and database stay on disk for when you return.
- In Command Prompt, unset your API key by running:
set GEMINI_API_KEY=
- If you are using PowerShell instead, run:
$env:GEMINI_API_KEY=""
- Your project folder, usage.db database, and chart PNGs all stay on your Desktop.
- When you come back, set the GEMINI_API_KEY environment variable again and pick up right where you left off.
Delete - I don't want to use this again
Remove all project files, the database, and your API key. This is a clean slate.
- Delete the entire project folder from your Desktop.
- This removes all Python files, the usage.db database, and any generated chart PNGs.
- Unset your API key in Command Prompt by running:
set GEMINI_API_KEY=
- If you added the key as a permanent environment variable through System Properties, remove it there as well.
- Optionally, revoke your Gemini API key in Google AI Studio. Go to aistudio.google.com, click Get API Key, find your key, and delete it.
- Optionally, uninstall the Python packages by running:
pip uninstall google-genai matplotlib -y
Mission accomplished
That's a Wrap!
Nice work! You just built a real analytics tool that puts hard numbers on something most people only guess about.
You've learned how to:
- Connect to the Gemini API free tier and extract detailed token metadata from every response to understand exactly what your AI usage costs.
- Build an A/B comparison system that searches your Obsidian vault for relevant notes, queries Gemini with and without that context, and logs every token detail to a local SQLite database.
- Generate weekly trend charts with matplotlib that visualize your average token usage over time and estimate the dollar savings from your second brain.
- Secret Mission: Build a context efficiency score and leaderboard that ranks your Obsidian notes by how much they save in output tokens.
Ready to quiz yourself?