Setting Up Automatic Position Monitoring in Google and Yandex
Search positions are not static: Google and Yandex algorithm updates, competitor activity, site changes—all affect ranking. Knowing about changes a week or two later means reacting too late. We configure automatic monitoring that sends a signal within 24 hours. Our practice includes automating tracking for 50+ projects, and with over 10 years of experience, we guarantee reliability and accuracy.
Why Manual Monitoring Is Inefficient
Manual position checking is a labor-intensive process that gives only an instant snapshot. For daily control, it takes at least 30 minutes per 100 keywords. Automation works without human intervention and captures even short-term dips caused by test algorithm updates. Compare: manual collection of 500 keywords takes 2.5 hours per day (60+ hours per month), while automatic setup takes 5 minutes and zero time for collection. Manual entry errors occur in 5-10% of records; the system eliminates them completely. One client lost 30% of traffic because they didn't notice a drop in positions for a week—automatic alerts would have prevented the loss.
How We Solve the Data Delay Problem
We use API sources that return current positions with a delay of no more than a day. Combining Google Search Console (free, historical data) and DataForSEO (real-time check) gives the full picture. For Yandex, we use the official XML API, which is also free within its quota.
| Parameter | Manual Check | Google Search Console | DataForSEO API | Yandex XML API |
|---|---|---|---|---|
| Cost | Free (time) | Free | From a few cents per query | Free (quota) |
| Freshness | Instant | 2–3 days | Real-time | Real-time |
| Check Volume | 10–50 queries/hour | Up to 1000 queries/day | Unlimited | Quota 1000–10000 queries/day |
| History | None | 28 days | From connection | Not stored |
DataForSEO beats Google Search Console in speed, and manual checking in scalability. For Yandex, we use the official XML API, which is free within quota.
| Frequency | Manual | Automatic | Recommendation |
|---|---|---|---|
| Daily | 2.5 hours for 500 keywords | 10 minutes setup + 0 collection | For competitive niches |
| Weekly | 30 minutes for 100 keywords | 0 minutes | For stable queries |
| Monthly | 10 minutes for 50 keywords | 0 minutes | For brand queries |
Automatic monitoring saves up to 30 times the time for daily checks.
What's Included in Monitoring Setup
- API integration (DataForSEO, Google Search Console, Yandex XML) — registration, key acquisition, traffic routing.
- Daily collection scripts — in Python with error handling, retries, logging.
- PostgreSQL database — structure for storing position history with indexes for fast queries.
- Alert system — comparing current with previous positions, sending to Telegram when threshold is exceeded.
- Documentation — architecture description, instructions for adding and removing keywords.
- Guarantee — free support for 30 days after launch, bug fixes under warranty.
How It Works: Step by Step
- Analysis — collect all target keywords, define regions and languages.
- API setup — connect data sources, test queries.
-
Database deployment — create tables
tracked_keywordsandposition_history. - Script implementation — daily scheduled run (cron or Airflow).
- Alert integration — set up Telegram bot, define thresholds.
- Launch and monitoring — verify correctness, provide dashboard access.
Request monitoring setup right now to avoid missing position changes.
Example Integration with DataForSEO
import requests, json, base64
class DataForSEOClient:
BASE_URL = 'https://api.dataforseo.com/v3'
def __init__(self, login: str, password: str):
creds = base64.b64encode(f'{login}:{password}'.encode()).decode()
self.headers = {
'Authorization': f'Basic {creds}',
'Content-Type': 'application/json',
}
def check_positions(self, keyword, target_domain, location_code=2840, language_code='en', depth=100):
payload = [{
'keyword': keyword,
'target': target_domain,
'location_code': location_code,
'language_code': language_code,
'depth': depth,
}]
resp = requests.post(f'{self.BASE_URL}/serp/google/organic/live/advanced',
headers=self.headers, data=json.dumps(payload), timeout=60)
resp.raise_for_status()
return resp.json()
def parse_position(self, response, target_domain):
tasks = response.get('tasks', [])
if not tasks:
return None
items = tasks[0].get('result', [{}])[0].get('items', [])
for item in items:
if item.get('type') == 'organic' and target_domain in item.get('domain', ''):
return {'position': item.get('rank_absolute'), 'url': item.get('url'),
'title': item.get('title'), 'featured_snippet': item.get('rank_absolute') == 0}
return None
Database Structure for Position History
CREATE TABLE tracked_keywords (
id SERIAL PRIMARY KEY,
keyword TEXT NOT NULL,
target_domain TEXT NOT NULL,
search_engine VARCHAR(20) DEFAULT 'google',
location_code INTEGER,
language_code VARCHAR(10),
active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE position_history (
id SERIAL PRIMARY KEY,
keyword_id INTEGER REFERENCES tracked_keywords(id),
position INTEGER,
url TEXT,
checked_at DATE NOT NULL,
UNIQUE(keyword_id, checked_at)
);
CREATE INDEX idx_positions_keyword_date ON position_history(keyword_id, checked_at DESC);
Daily Monitoring Run and Alerts
import psycopg2
from datetime import date
def run_daily_check(db_conn, dfs_client, target_domain):
today = date.today().isoformat()
with db_conn.cursor() as cur:
cur.execute('SELECT id, keyword, search_engine, location_code, language_code FROM tracked_keywords WHERE active = true')
keywords = cur.fetchall()
for kw_id, keyword, engine, loc_code, lang_code in keywords:
try:
response = dfs_client.check_positions(keyword=keyword, target_domain=target_domain,
location_code=loc_code or 2840, language_code=lang_code or 'en')
result = dfs_client.parse_position(response, target_domain)
position = result['position'] if result else None
url = result['url'] if result else None
with db_conn.cursor() as cur:
cur.execute('''
INSERT INTO position_history (keyword_id, position, url, checked_at)
VALUES (%s, %s, %s, %s)
ON CONFLICT (keyword_id, checked_at) DO UPDATE
SET position = EXCLUDED.position, url = EXCLUDED.url
''', (kw_id, position, url, today))
db_conn.commit()
except Exception as e:
print(f'Error checking {keyword}: {e}')
def detect_significant_changes(db_conn, threshold=5):
with db_conn.cursor() as cur:
cur.execute('''
WITH ranked AS (
SELECT k.keyword, p.position, p.checked_at,
LAG(p.position) OVER (PARTITION BY p.keyword_id ORDER BY p.checked_at) AS prev_position
FROM position_history p
JOIN tracked_keywords k ON k.id = p.keyword_id
WHERE p.checked_at >= CURRENT_DATE - INTERVAL '2 days'
)
SELECT keyword, prev_position, position,
(COALESCE(prev_position, 101) - COALESCE(position, 101)) AS change
FROM ranked
WHERE prev_position IS NOT NULL
AND ABS(COALESCE(prev_position, 101) - COALESCE(position, 101)) >= %s
ORDER BY ABS(change) DESC
''', (threshold,))
return [{'keyword': row[0], 'prev': row[1], 'current': row[2], 'change': row[3],
'direction': 'up' if row[3] > 0 else 'down'} for row in cur.fetchall()]
Telegram Notifications
import httpx
def send_telegram_alert(bot_token, chat_id, changes):
if not changes:
return
lines = ['*Position changes for the day:*\n']
for ch in changes[:20]:
arrow = '↑' if ch['direction'] == 'up' else '↓'
prev = ch['prev'] or '100+'
curr = ch['current'] or '100+'
lines.append(f"{arrow} `{ch['keyword']}`: {prev} → {curr}")
text = '\n'.join(lines)
httpx.post(f'https://api.telegram.org/bot{bot_token}/sendMessage',
json={'chat_id': chat_id, 'text': text, 'parse_mode': 'Markdown'})
Timelines and Cost
Setting up monitoring for one domain with PostgreSQL storage and Telegram alerts takes 2–3 business days. Adding visualization (Grafana/Metabase), support for multiple sites, and automatic keyword import from Search Console takes 4–6 days. Cost is calculated individually based on keyword volume and required integrations. Get a consultation for an accurate estimate.
DataForSEO SERP API documentation
Additionally: Google Search Algorithms — understanding ranking helps better interpret monitoring data.







