Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
YouTube Security VideosfreeCodeCamp.org: TimescaleDB Course – PostgreSQL for Time-Series Data(23.09.2026 um 12:30 Uhr)
Windows Tipps & SecurityAndroid 17: Rollout auf Samsung-Galaxy-Smartphones verzögert sich(23.09.2026 um 11:42 Uhr)
Unix & Linux ServerUSN-8733-2: Gzip vulnerabilities(22.09.2026 um 18:04 Uhr)
Sichere ProgrammierungHow to Build Custom PowerPoint Add-Ins for Enterprise Teams(23.09.2026 um 11:25 Uhr)
Sichere ProgrammierungSearch Google Jobs in Real-Time with Go and SerpApi 🚀(23.09.2026 um 12:13 Uhr)
Sichere ProgrammierungA Psychological State is a Coefficient Vector(23.09.2026 um 12:16 Uhr)
YouTube Security VideosfreeCodeCamp.org: TimescaleDB Course – PostgreSQL for Time-Series Data(23.09.2026 um 12:30 Uhr)
Windows Tipps & SecurityAndroid 17: Rollout auf Samsung-Galaxy-Smartphones verzögert sich(23.09.2026 um 11:42 Uhr)
Unix & Linux ServerUSN-8733-2: Gzip vulnerabilities(22.09.2026 um 18:04 Uhr)
Sichere ProgrammierungHow to Build Custom PowerPoint Add-Ins for Enterprise Teams(23.09.2026 um 11:25 Uhr)
Sichere ProgrammierungSearch Google Jobs in Real-Time with Go and SerpApi 🚀(23.09.2026 um 12:13 Uhr)
Sichere ProgrammierungA Psychological State is a Coefficient Vector(23.09.2026 um 12:16 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

How to Automate Upwork Proposals with Python (Real Code Inside)

How to Automate Upwork Proposals with Python (Real Code Inside) Last month I sent 47 proposals on Upwork. I personally wrote 3 of them. The other 44 were drafted by Claude AI, filtered through a scoring algorithm I built over two…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!




How to Automate Upwork Proposals with Python (Real Code Inside)



Last month I sent 47 proposals on Upwork. I personally wrote 3 of them.



The other 44 were drafted by Claude AI, filtered through a scoring algorithm I built over two weekends, and delivered to my inbox via Telegram before most freelancers even saw the job posting. My response rate on those AI-assisted proposals? 31%. Higher than my hand-written average from the previous quarter.



This article shows you exactly how I built that system.









The Real Problem With Upwork Proposals



If you've freelanced on Upwork for more than a month, you know the grind. You refresh the job feed. You see something promising. You spend 20 minutes writing a tailored proposal. You hit submit. Nothing. Meanwhile, the client already hired someone who responded 4 minutes after posting.



The platforms reward speed and volume. A thoughtful proposal submitted 6 hours late loses to a mediocre one submitted in 6 minutes. That's not a hot take — it's arithmetic.



The naive solution is to write faster. The engineering solution is to build a system that monitors the feed continuously, filters out garbage jobs automatically, and generates a tailored first draft the moment something good appears.



Here's the architecture:





  1. RSS feed monitor — Upwork exposes RSS feeds for saved searches. We poll these.


  2. Scoring engine — Each job gets a score based on keyword match, budget range, and client history signals.


  3. Claude AI proposal generator — High-scoring jobs get a tailored draft generated via the Anthropic API.


  4. Telegram notifier — The draft and job details land in my Telegram within seconds.



I review, adjust, and submit. The system handles discovery and first drafts. I handle judgment and the final send.



Important note on Upwork TOS: Upwork's Terms of Service prohibit automated bidding — meaning you cannot auto-submit proposals programmatically. This system does not do that. It automates monitoring and drafting, not submission. You review everything before it goes anywhere. Know the rules, stay inside them.









Step 1: Parsing the Upwork RSS Feed



Upwork generates RSS feeds for your saved searches. Log into Upwork, save a search for your niche, and grab the RSS URL from the feed icon. It looks like:




https://www.upwork.com/ab/feed/jobs/rss?q=python+automation&sort=recency&paging=0%3B10&api_params=1&securityToken=YOUR_TOKEN&userUid=YOUR_UID&orgUid=YOUR_ORG






The token is tied to your session, so treat it like a password.



Here's the RSS parser and job monitor:




import feedparser
import hashlib
import json
import time
import logging
from datetime import datetime
from pathlib import Path
from dataclasses import dataclass, field
from typing import Optional

logging.basicConfig(level=logging.INFO, format="%(asctime)s — %(levelname)s — %(message)s")
logger = logging.getLogger(__name__)

SEEN_JOBS_FILE = Path("seen_jobs.json")
POLL_INTERVAL_SECONDS = 300 # 5 minutes — don't hammer the feed


@dataclass
class UpworkJob:
title: str
url: str
description: str
published: str
budget: Optional[str] = None
job_type: Optional[str] = None
skills: list[str] = field(default_factory=list)
job_id: str = ""

def __post_init__(self):
self.job_id = hashlib.md5(self.url.encode()).hexdigest()


def load_seen_jobs() -> set:
if SEEN_JOBS_FILE.exists():
return set(json.loads(SEEN_JOBS_FILE.read_text()))
return set()


def save_seen_jobs(seen: set):
SEEN_JOBS_FILE.write_text(json.dumps(list(seen)))


def parse_budget_from_description(description: str) -> Optional[str]:
"""
Upwork embeds budget info in the description HTML.
Budget: $500.00-$1,000.00 or Hourly Range: $25.00-$50.00/hr
"""
import re
patterns = [
r"Budget:\s*\$?([\d,]+\.?\d*)\s*[-–]\s*\$?([\d,]+\.?\d*)",
r"Hourly Range:\s*\$?([\d,]+\.?\d*)\s*[-–]\s*\$?([\d,]+\.?\d*)",
r"Budget:\s*\$?([\d,]+\.?\d*)",
]
for pattern in patterns:
match = re.search(pattern, description, re.IGNORECASE)
if match:
return match.group(0)
return None


def parse_skills_from_description(description: str) -> list[str]:
import re
match = re.search(r"Skills?:\s*([^\n<]+)", description, re.IGNORECASE)
if match:
skills_raw = match.group(1)
return [s.strip() for s in re.split(r"[,;]", skills_raw) if s.strip()]
return []


def fetch_jobs(feed_url: str) -> list[UpworkJob]:
feed = feedparser.parse(feed_url)

if feed.bozo:
logger.warning(f"Feed parse warning: {feed.bozo_exception}")

jobs = []
for entry in feed.entries:
description = entry.get("summary", "")
job = UpworkJob(
title=entry.get("title", "No title"),
url=entry.get("link", ""),
description=description,
published=entry.get("published", ""),
budget=parse_budget_from_description(description),
skills=parse_skills_from_description(description),
)
jobs.append(job)

logger.info(f"Fetched {len(jobs)} jobs from feed")
return jobs


def monitor_feed(feed_urls: list[str], callback):
"""
Continuously polls feed URLs and calls callback(job) for new jobs.
"""
seen = load_seen_jobs()

while True:
for url in feed_urls:
try:
jobs = fetch_jobs(url)
new_jobs = [j for j in jobs if j.job_id not in seen]

for job in new_jobs:
logger.info(f"New job found: {job.title}")
callback(job)
seen.add(job.job_id)

save_seen_jobs(seen)

except Exception as e:
logger.error(f"Error fetching feed {url}: {e}")

logger.info(f"Sleeping {POLL_INTERVAL_SECONDS}s until next poll...")
time.sleep(POLL_INTERVAL_SECONDS)






A few things worth noting about this implementation:



feedparser handles malformed XML gracefully, which matters because Upwork's RSS occasionally has encoding issues — I've seen bozo_exception: <class 'xml.etree.ElementTree.ParseError'> on feeds that nevertheless parse fine. The hashlib.md5 job ID means you won't process the same listing twice even across restarts. And the 5-minute poll interval is deliberate — aggressive polling will get your IP rate-limited.









Step 2: The Scoring Algorithm



Not every job deserves a proposal. The scoring engine is where you encode your professional judgment into math.



My scoring weights are tuned for Python automation work. You'll adjust these based on your niche, but the structure transfers directly:




import re
from dataclasses import dataclass

@dataclass
class ScoringConfig:
must_have_keywords: list[str]
nice_to_have_keywords: list[str]
dealbreaker_keywords: list[str]
min_budget_fixed: float
min_budget_hourly: float
max_budget_fixed: float # avoid scope monsters
keyword_match_weight: float = 0.5
budget_weight: float = 0.35
recency_weight: float = 0.15


DEFAULT_CONFIG = ScoringConfig(
must_have_keywords=["python", "automation", "api", "scraping", "bot", "pipeline"],
nice_to_have_keywords=["anthropic", "claude", "openai", "fastapi", "postgresql", "aws", "trading"],
dealbreaker_keywords=["wordpress", "shopify", "wix", "php", "react native", "unity", "c#", "java"],
min_budget_fixed=300.0,
min_budget_hourly=25.0,
max_budget_fixed=50000.0,
)


def extract_budget_value(budget_str: str) -> tuple[float, str]:
"""
Returns (mid_point_value, job_type) where job_type is
'fixed' or 'hourly'.
"""
if not budget_str:
return 0.0, "unknown"

is_hourly = "hr" in budget_str.lower() or "hour" in budget_str.lower()
numbers = re.findall(r"[\d,]+\.?\d*", budget_str)
values = [float(n.replace(",", "")) for n in numbers]

if not values:
return 0.0, "hourly" if is_hourly else "fixed"

midpoint = sum(values) / len(values)
return midpoint, "hourly" if is_hourly else "fixed"


def score_job(job, config: ScoringConfig = DEFAULT_CONFIG) -> dict:
text = f"{job.title} {job.description}".lower()
scores = {}

# --- Dealbreaker check ---
for kw in config.dealbreaker_keywords:
if kw in text:
return {
"total": 0.0,
"disqualified": True,
"reason": f"Dealbreaker keyword: '{kw}'",
"breakdown": {}
}

# --- Keyword scoring ---
must_have_hits = [kw for kw in config.must_have_keywords if kw in text]
nice_to_have_hits = [kw for kw in config.nice_to_have_keywords if kw in text]

must_have_ratio = len(must_have_hits) / len(config.must_have_keywords)
nice_ratio = len(nice_to_have_hits) / max(len(config.nice_to_have_keywords), 1)

keyword_score = (must_have_ratio * 0.7) + (nice_ratio * 0.3)
scores["keywords"] = round(keyword_score * 100, 1)

# --- Budget scoring ---
budget_val, job_type = extract_budget_value(job.budget or "")
budget_score = 0.0

if job_type == "fixed":
if budget_val < config.min_budget_fixed:
budget_score = 0.0
elif budget_val > config.max_budget_fixed:
budget_score = 0.2 # red flag: scope too large or unrealistic
else:
# Normalize: sweet spot is $1k-$10k
normalized = min(budget_val / 10000, 1.0)
budget_score = 0.4 + (normalized * 0.6)
elif job_type == "hourly":
if budget_val >= config.min_budget_hourly:
normalized = min((budget_val - config.min_budget_hourly) / 75, 1.0)
budget_score = 0.5 + (normalized * 0.5)

scores["budget"] = round(budget_score * 100, 1)

# --- Composite score ---
total = (
keyword_score * config.keyword_match_weight +
budget_score * config.budget_weight
)
# Recency handled upstream by feed sort=recency; give partial credit
total += 0.1 * config.recency_weight # baseline recency bonus

total_clamped = min(round(total * 100, 1), 100.0)

return {
"total": total_clamped,
"disqualified": False,
"reason": None,
"breakdown": {
"keyword_score": scores["keywords"],
"budget_score": scores["budget"],
"must_have_hits": must_have_hits,
"nice_to_have_hits": nice_to_have_hits,
"budget_value": budget_val,
"job_type": job_type,
}
}






When I run this against a real job feed, output looks like:




2024-01-15 09:23:11 — INFO — Fetched 10 jobs from feed
2024-01-15 09:23:11 — INFO — New job found: Python Developer Needed for Trading Bot Automation
Score result: {'total': 78.2, 'disqualified': False, 'breakdown': {'keyword_score': 83.3, 'budget_score': 71.0, 'must_have_hits': ['python', 'automation', 'bot'], 'nice_to_have_hits': ['trading'], 'budget_value': 2500.0, 'job_type': 'fixed'}}

2024-01-15 09:23:11 — INFO — New job found: Shopify Theme Customization
Score result: {'total': 0.0, 'disqualified': True, 'reason': "Dealbreaker keyword: 'shopify'", 'breakdown': {}}






Jobs scoring above 60 go to the proposal generator. Jobs below that get logged and skipped. You can tune that threshold — I've found 60 catches genuinely relevant work without drowning me in noise.









Step 3: Generating Tailored Proposals with Claude



This is where the time savings stack up. The proposal generator takes the scored job, pulls relevant context from my profile template, and produces a draft that's actually specific to the posting — not a mail-merge.







python
import anthropic
import os

client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])

MY_PROFILE = """
Name: Mike G.
Core skills: Python automation, API integrations, web scraping, data pipelines, trading bots
Years of experience: 8
Notable projects: Built a crypto arbitrage system processing 50k ticks/minute; scraped and structured 2M+ product records for an e-commerce client; built a Telegram trading signal bot with live P&L tracking
Tone: Direct, technical, no fluff. I explain what I'll build and why my approach works.
Availability: 20hrs/week. Based in US Eastern timezone.
"""

PROPOSAL_SYSTEM_PROMPT = """
You are writing a freelance proposal on behalf of a senior Python engineer.

Rules:
- Open with a direct reference to the specific problem described in the job post. Never use generic openers like "I saw your posting" or "I would love to help."
- Demonstrate you understood the technical requirements by briefly describing your approach.
- Reference 1-2 relevant past projects (from the profile provided) that map to this job.
- Keep it under 200 words. Clients skim proposals. Respect their time.
- End with one specific clarifying question that shows you thought about scope.
- Do NOT use bullet points. Flowing paragraphs only.
- Do NOT say "I am a senior Python engineer" or state your title. Show, don't tell.
"""


def generate_proposal(job, score_result: dict) -> str:
job_context = f"""
Job Title: {job.title}
Job Description: {job.description[:2000]}
Budget: {job.budget or 'Not specified'}
Skills mentioned: {', '.join(job.skills) if job.skills else 'Not listed'}
Keyword matches: {', '.join(score_result['breakdown'].get('must_have_hits', []))}
"""

prompt = f"""
My profile:
{MY_PROFILE}

Job details:
{job_context}

Write a proposal for this job following all rules in your instructions.
"""

message = client.messages.create(
model="claude-opus-4-5",
max_tokens=512,
system=PROPOSAL_SYSTEM_PROMPT,


Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten How to Automate Upwork Proposals with Python (Real Code Inside)

Thematisch verwandte Begriffe: Automate, Upwork, Proposals, with · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-19438 | Improper Limitation of a Pathname to a Restricted Directory ('Path Trave…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick