diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3701267 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,92 @@ +# linkme — Claude skill + +This project scrapes LinkedIn jobs and filters/ranks them for the user's job search. +The user's CV is at `polo.md`. Always use it when ranking opportunities. + +--- + +## Commands + +| command | what it does | +|---|---| +| `bun fetch` | scrape Basel area (3 searches), append new jobs to `jobs.csv` | +| `bun recheck` | fetch in raw mode, list filtered-out jobs + word-frequency keyword suggestions | + +--- + +## Searching any city temporarily + +Write a throwaway script (e.g. `bern-temp.ts`) — never touch `config.ts` unless the user explicitly asks to save the change. + +```ts +import { KEYWORDS } from "./config.ts"; +import { fetchJobs } from "./fetch.ts"; +import type { SearchConfig } from "./config.ts"; + +const searches: SearchConfig[] = [ + { label: "City Name", keywords: KEYWORDS, location: "City, Switzerland", workType: ["onsite", "hybrid"], timePosted: "month" }, +]; +// collect, deduplicate, print +``` + +Use `location: "City, Switzerland"` (no geoId) when the geoId is unknown — LinkedIn resolves by name. + +Known Swiss geoIds: +- Basel city: `103383283` +- Basel Metropolitan Area: `90009885` +- Basel-Country: `107117530` + +--- + +## Filtering job descriptions by language + +Use the `bern-langcheck.ts` pattern: fetch each job page, extract the description, count German/French indicator words, drop anything with ≥ 6 matches. + +```ts +const DE_CONTENT_RE = /\b(Sie|Ihre?|Ihnen|Wir|wir|und\s|für|mit\s|von\s|bei\s|das\s|ist\s|sind\s|haben\s|werden\s|können|suchen|bieten|Kenntnisse|Erfahrung|Anforderungen|Aufgaben|Stellenbeschreibung|Stellenangebot|Bewerbung|Ausbildung|Berufserfahrung|Teamarbeit|Quereinsteiger|Vollzeit|Teilzeit|unbefristet|Abschluss|gesucht|Deutsch|Englisch|Schweizer)\b/g; +const FR_CONTENT_RE = /\b(vous|nous|votre|notre|pour\s|avec\s|profil|poste|emploi|entreprise|compétences|expérience|formation|équipe|rejoindre|recherchons|offrons|missions|candidature|CDI|CDD|temps\s+plein|temps\s+partiel)\b/g; +``` + +Extract description from `class="show-more-less-html__markup"`, fall back to stripping all HTML. Add 350 ms delay between requests. + +Title-only filtering runs during scraping via `NON_ENGLISH_RE` in `fetch.ts`. Content-level filtering via `isEnglishContent()` is now always applied in `index.ts` before writing to CSV — every new job has its description page fetched and checked. + +--- + +## Ranking jobs against the CV + +Read `polo.md` for the full profile. Key signals to match against: + +- **Seniority**: Senior / Staff / Principal / Lead / Architect — weight titles accordingly +- **Core stack**: Python · TypeScript · Ruby on Rails · React · Node.js +- **Domain strengths**: platform engineering · data pipelines · AI/LLM integrations · DevOps (Docker, Kubernetes, CI/CD, AWS) +- **Background edges**: fintech/CBDC (Moedax), civic tech (Tactical Tech / GFOSS), independent consulting (ExtraPolo, 40+ projects) +- **Languages**: English (full) · German B1 TELC certified · Spanish (full) — flag roles that require C1+ German +- **Location**: based in Basel, open to Basel / Bern / Zürich; EU citizen, no permit needed + +--- + +## Output format + +Always present jobs as a markdown table with columns: `#`, `Title`, `Company`, `Why it fits`, `Link`. + +Links use just the job ID as the anchor text: +``` +[4392954199](https://www.linkedin.com/jobs/view/4392954199/) +``` + +--- + +## Workflow for "find jobs in X city" + +1. Write a temp script for that city, run it, collect all passing jobs. +2. If the user says "English only" or "filter language": run the content-language check script against the results. +3. Rank survivors against `polo.md`, present top N as a table. +4. Delete the temp scripts after the session unless the user asks to keep them. + +## Workflow for "check if new keywords should be added" + +1. Run `bun recheck`. +2. Review the filtered-out list manually — identify false negatives (real software roles that got dropped). +3. Check the word-frequency suggestions but ignore generic terms (`engineer`, `development`, `senior`). +4. Propose specific additions to `SOFTWARE_TITLE_RE` in `fetch.ts` or words to `NON_ENGLISH_RE`. diff --git a/README.md b/README.md index 5c4d01f..47bf1e6 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ Scrapes LinkedIn job listings into a local CSV. Run it daily, delete rows you've ## Usage ```bash -bun fetch +bun fetch # scrape and append new jobs to jobs.csv +bun recheck # show jobs currently filtered out + keyword suggestions ``` No setup needed. Results are appended to `jobs.csv` — reruns are safe, already-seen jobs are skipped by ID. @@ -30,24 +31,48 @@ Open in any spreadsheet app. Delete rows as you apply or dismiss. - **Paginated scraping** — fetches up to 6 pages per search (150 jobs) with a 400 ms delay between pages to avoid rate limiting - **Card-boundary-safe parsing** — each job card is isolated before extracting fields, preventing content from adjacent cards bleeding into company or date values - **Language filter** — drops German (`m/w/d`, umlauts, gender-marked vocabulary) and French (`H/F`, accented characters) job titles automatically -- **Software-role filter** — requires the title to contain a relevant term (software, developer, data, AI, backend, devops, platform, architect, technical, solutions, consultant, product/IT/engineering manager, etc.) — eliminates aviation, mechanical, and pharma-process roles that match "Engineer" broadly -- **Seniority filter** — drops junior, intern, trainee, and apprentice titles -- **Global deduplication** — job IDs are indexed from the existing CSV on startup; jobs you've already seen are never re-added, even if you delete and re-run +- **Software-role filter** — requires the title to contain a relevant term (software, developer, data, AI, backend, frontend, devops, platform, architect, tech lead, typescript, python, rails, node.js, ML, cloud, security, SRE, solutions, consultant, applications/system engineer, director, product/IT/engineering manager, staff/principal engineer, CTO, VP engineering, etc.) — eliminates aviation, mechanical, and pharma-process roles that match "Engineer" broadly +- **Seniority filter** — drops junior, jr., intern, trainee, apprentice, werkstudent, and entry-level titles +- **Global deduplication** — job IDs are indexed from the existing CSV on startup; jobs you've already seen are never re-added, even if you re-run - **30-day recency** — only fetches jobs posted in the last month (`f_TPR=r2592000`) +## recheck + +`bun recheck` fetches all three search areas in raw mode (bypassing the software-role filter) and lists every job that would be filtered out — useful for spotting false negatives. It also prints a word-frequency table of the most common terms in filtered titles so you can decide what to add to `SOFTWARE_TITLE_RE`. + ## Configure searches Edit `config.ts`: ```ts -const KEYWORDS = [ +export const KEYWORDS = [ "Software Engineer", + "Solutions Engineer", "Full Stack Engineer", + "Backend Engineer", "Data Engineer", "AI Engineer", + "Applications Engineer", + "Platform Engineer", + "DevOps Engineer", + "Staff Engineer", + "Principal Engineer", + "Tech Lead", + "Developer", "Ruby on Rails", "Python", - // add or remove freely + "Node.js", + "TypeScript", + "Artificial Intelligence", + "Machine Learning", + "Data Science", + "AI Consultant", + "ML Engineer", + "Director of Engineering", + "Engineering Manager", + "Lead Developer", + "System Engineer", + "Solution Engineer", ].join(" OR "); export const searches: SearchConfig[] = [ @@ -59,6 +84,22 @@ export const searches: SearchConfig[] = [ workType: ["onsite", "hybrid"], timePosted: "month", }, + { + label: "Basel Metropolitan Area", + keywords: KEYWORDS, + geoId: "90009885", + location: "Basel Metropolitan Area", + workType: ["onsite", "hybrid"], + timePosted: "month", + }, + { + label: "Basel-Country", + keywords: KEYWORDS, + geoId: "107117530", + location: "Basel-Country", + workType: ["onsite", "hybrid"], + timePosted: "month", + }, ]; ``` @@ -68,3 +109,5 @@ export const searches: SearchConfig[] = [ - `107117530` — Basel-Country You can also paste any LinkedIn search URL directly as `url:` instead of building from filters. + +`SearchConfig` also supports `jobType` (full-time, part-time, contract, temporary, internship) and `seniority` (internship, entry, associate, mid-senior, director, executive) to pass LinkedIn's native API filters. diff --git a/bern-langcheck.ts b/bern-langcheck.ts new file mode 100644 index 0000000..f4b1f36 --- /dev/null +++ b/bern-langcheck.ts @@ -0,0 +1,75 @@ +#!/usr/bin/env bun +const DE_CONTENT_RE = /\b(Sie|Ihre?|Ihnen|Wir|wir|und\s|für|mit\s|von\s|bei\s|das\s|ist\s|sind\s|haben\s|werden\s|können|suchen|bieten|Kenntnisse|Erfahrung|Anforderungen|Aufgaben|Stellenbeschreibung|Stellenangebot|Bewerbung|Ausbildung|Berufserfahrung|Teamarbeit|Quereinsteiger|Vollzeit|Teilzeit|unbefristet|Abschluss|gesucht|Deutsch|Englisch|Schweizer)\b/g; +const FR_CONTENT_RE = /\b(vous|nous|votre|notre|pour\s|avec\s|profil|poste|emploi|entreprise|compétences|expérience|formation|équipe|rejoindre|recherchons|offrons|missions|candidature|CDI|CDD|temps\s+plein|temps\s+partiel)\b/g; + +const HEADERS = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", + "Accept-Language": "en-US,en;q=0.9", + Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", +}; + +const jobs = [ + { id: "4412445330", title: "Senior Software and System Architect", company: "NVIDIA AI" }, + { id: "4406964785", title: "Digital Platform Engineer – EasyGov.swiss", company: "Swiss Federal Administration" }, + { id: "4405193749", title: "Technical Lead Web Portals", company: "Smart Energy Link AG" }, + { id: "4411445039", title: "Senior System Engineer (Cloud Native, Remote-First)", company: "Adfinis" }, + { id: "4411290320", title: "Automation Data Engineer", company: "Takeda Pharmaceuticals" }, + { id: "4411348405", title: "Solutions Architect", company: "RJC Group" }, + { id: "4406980033", title: "Enterprise Architect", company: "Swiss Federal Administration" }, + { id: "4367874399", title: "Software Engineer", company: "Cartier" }, + { id: "4399418573", title: "Data Science Specialist", company: "Nestlé" }, + { id: "4411369976", title: "Data Scientist 80–100%", company: "Fincons Group" }, + { id: "4412464434", title: "Solution Engineer Management Systems", company: "Siemens" }, + { id: "4401638413", title: "Fullstack Developer (Java/React)", company: "soobr" }, + { id: "4399356316", title: "Senior Java Software Engineer", company: "Smart Energy Link AG" }, + { id: "4407778229", title: "Software Engineer", company: "Softcom" }, + { id: "4402476156", title: "Software Developer", company: "SwissPeak Partners" }, + // remaining from the 20 + { id: "4404649130", title: "Senior System Engineer (100%)", company: "nanoTRONIC" }, + { id: "4408770074", title: "System Engineer", company: "Infopro AG" }, + { id: "4408864023", title: "System Engineer – Workplace Engineering", company: "Bedag Informatique SA" }, + { id: "4405214433", title: "Cloud Platform Engineer (Kubernetes/Azure/DevOps)", company: "Gramian Consulting" }, +]; + +function detectLanguage(text: string): "de" | "fr" | "en" { + const deMatches = (text.match(DE_CONTENT_RE) ?? []).length; + const frMatches = (text.match(FR_CONTENT_RE) ?? []).length; + if (deMatches >= 6) return "de"; + if (frMatches >= 6) return "fr"; + return "en"; +} + +function extractDescription(html: string): string { + const m = html.match(/class="show-more-less-html__markup[^"]*">([\s\S]{0,8000}?)<\/div>/); + if (m) return m[1].replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim(); + // fallback: grab large text blocks + return html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").slice(0, 5000); +} + +const passed: typeof jobs = []; +const dropped: { id: string; title: string; company: string; lang: string }[] = []; + +for (const job of jobs) { + await Bun.sleep(350); + process.stdout.write(` checking ${job.company} — ${job.title} … `); + try { + const res = await fetch(`https://www.linkedin.com/jobs/view/${job.id}/`, { headers: HEADERS }); + const html = await res.text(); + const desc = extractDescription(html); + const lang = detectLanguage(desc); + console.log(lang); + if (lang === "en") passed.push(job); + else dropped.push({ ...job, lang }); + } catch (e) { + console.log("fetch error, keeping"); + passed.push(job); + } +} + +console.log(`\n── ${dropped.length} dropped (non-English) ──`); +for (const j of dropped) console.log(` [${j.lang}] ${j.title} @ ${j.company}`); + +console.log(`\n── ${passed.length} English jobs ──`); +for (const [i, j] of passed.entries()) { + console.log(` ${i + 1}. ${j.title} — ${j.company} https://www.linkedin.com/jobs/view/${j.id}/`); +} diff --git a/bern-temp.ts b/bern-temp.ts new file mode 100644 index 0000000..2857686 --- /dev/null +++ b/bern-temp.ts @@ -0,0 +1,30 @@ +#!/usr/bin/env bun +import { KEYWORDS } from "./config.ts"; +import { fetchJobs, SOFTWARE_TITLE_RE } from "./fetch.ts"; +import type { SearchConfig } from "./config.ts"; + +const JUNIOR_RE = /\b(junior|jr\.?|entry.?level|graduate|intern(?:ship)?|trainee|apprentice|werkstudent)\b/i; + +const searches: SearchConfig[] = [ + { label: "Bern (city)", keywords: KEYWORDS, location: "Bern, Switzerland", workType: ["onsite", "hybrid"], timePosted: "month" }, +]; + +const seen = new Set(); +const results: { title: string; company: string; link: string; search: string }[] = []; + +for (const config of searches) { + console.log(`\n[${config.label}]`); + const jobs = await fetchJobs(config, { raw: false }); + for (const job of jobs) { + if (!seen.has(job.id)) { + seen.add(job.id); + results.push({ title: job.title, company: job.company, link: job.link, search: job.search }); + } + } +} + +console.log(`\n── ${results.length} jobs found in Bern ──\n`); +for (const j of results) { + console.log(` [${j.search}] ${j.title}`); + console.log(` ${j.company} ${j.link}`); +} diff --git a/config.ts b/config.ts index 0bdb99a..0b4dabb 100644 --- a/config.ts +++ b/config.ts @@ -16,13 +16,14 @@ export interface SearchConfig { seniority?: Seniority[]; } -const KEYWORDS = [ +export const KEYWORDS = [ "Software Engineer", "Solutions Engineer", "Full Stack Engineer", "Backend Engineer", "Data Engineer", "AI Engineer", + "Applications Engineer", "Platform Engineer", "DevOps Engineer", "Staff Engineer", @@ -33,6 +34,16 @@ const KEYWORDS = [ "Python", "Node.js", "TypeScript", + "Artificial Intelligence", + "Machine Learning", + "Data Science", + "AI Consultant", + "ML Engineer", + "Director of Engineering", + "Engineering Manager", + "Lead Developer", + "System Engineer", + "Solution Engineer", ].join(" OR "); // --- Define your searches here --- diff --git a/fetch.ts b/fetch.ts index 6962c61..06a8ec4 100644 --- a/fetch.ts +++ b/fetch.ts @@ -68,7 +68,7 @@ const NON_ENGLISH_RE = new RegExp( // /in word-ending (e.g. Mitarbeiter/in) — excludes m/f, n/a, etc. String.raw`[a-zäöü]\/in\b`, // German-only words (case-insensitive handled by flag) - String.raw`\b(Mitarbeiter|Pflegehelfer|Köchin|Koch EFZ|Kellner|Sozialpädagog|Lehrperson|Produktionsmitarbeiter|Produktionsplaner|Geschäftsführer|Spontanbewerbung|Arztsekretär|Physiotherapeut|Empfangsmitarbeiter|Automobilmechatroniker|Verkäufer|Projektleiter|Crewmitarbeiter|Crewmember|Systemgastronomie|Studienteilnehmende|Teilnehmende|Bauingenieur|Tiefbau|Hochbau|Elektroingenieur|Maschinenbau|gesucht|EFZ|Schweiz|Filiale)\b`, + String.raw`\b(Deine|Mitarbeiter|Pflegehelfer|Köchin|Koch EFZ|Kellner|Sozialpädagog|Lehrperson|Produktionsmitarbeiter|Produktionsplaner|Geschäftsführer|Spontanbewerbung|Arztsekretär|Physiotherapeut|Empfangsmitarbeiter|Automobilmechatroniker|Verkäufer|Projektleiter|Crewmitarbeiter|Crewmember|Systemgastronomie|Studienteilnehmende|Teilnehmende|Bauingenieur|Tiefbau|Hochbau|Elektroingenieur|Maschinenbau|gesucht|EFZ|Schweiz|Filiale)\b`, // French gender markers String.raw`\(H\/F\)|\(F\/H\)|\(h\/f\)|\(f\/h\)`, // French-only accented characters @@ -89,7 +89,27 @@ const HEADERS = { const JUNIOR_TITLE_RE = /\b(junior|jr\.?|entry.?level|graduate|intern(?:ship)?|trainee|apprentice|werkstudent)\b/i; -const SOFTWARE_TITLE_RE = /\b(software|developer|architect|devops|platform|data|backend|frontend|fullstack|full.stack|tech\s*lead|technical|typescript|python|rails|node\.?js|ai\b|machine\s*learning|cloud|security|analytics|sre|reliability|solutions|consultant|product\s*manager|engineering\s*manager|it\s*manager|it\s*project|staff\s*engineer|principal\s*engineer|cto|vp\s*eng)\b/i; +export const SOFTWARE_TITLE_RE = /\b(software|developer|architect|devops|platform|data|backend|frontend|fullstack|full.stack|tech\s*lead|technical|typescript|python|rails|node\.?js|ai\b|machine\s*learning|cloud|security|analytics|sre|reliability|solutions?|consultant|applications?\s*engineer|system[\s-]*engineer|informatics|director|product\s*manager|engineering\s*manager|it\s*manager|it\s*project|staff\s*engineer|principal\s*engineer|cto|vp\s*eng)\b/i; + +const DE_CONTENT_RE = /\b(Sie|Ihre?|Ihnen|Wir|wir|und\s|für|mit\s|von\s|bei\s|das\s|ist\s|sind\s|haben\s|werden\s|können|suchen|bieten|Kenntnisse|Erfahrung|Anforderungen|Aufgaben|Stellenbeschreibung|Stellenangebot|Bewerbung|Ausbildung|Berufserfahrung|Teamarbeit|Vollzeit|Teilzeit|unbefristet|Abschluss|gesucht|Deutsch|Englisch|Schweizer)\b/g; +const FR_CONTENT_RE = /\b(vous|nous|votre|notre|pour\s|avec\s|profil|poste|emploi|entreprise|compétences|expérience|formation|équipe|rejoindre|recherchons|offrons|missions|candidature|CDI|CDD|temps\s+plein|temps\s+partiel)\b/g; + +export async function isEnglishContent(id: string): Promise { + try { + await Bun.sleep(350); + const res = await fetch(`https://www.linkedin.com/jobs/view/${id}/`, { headers: HEADERS }); + const html = await res.text(); + const m = html.match(/class="show-more-less-html__markup[^"]*">([\s\S]{0,8000}?)<\/div>/); + const text = m + ? m[1].replace(/<[^>]+>/g, " ") + : html.replace(/<[^>]+>/g, " ").slice(0, 5000); + const de = (text.match(DE_CONTENT_RE) ?? []).length; + const fr = (text.match(FR_CONTENT_RE) ?? []).length; + return de < 6 && fr < 6; + } catch { + return true; // keep on fetch error + } +} const PAGE_SIZE = 25; const MAX_PAGES = 6; @@ -156,7 +176,7 @@ function parseJobs(html: string, label: string): Job[] { return jobs; } -export async function fetchJobs(config: SearchConfig): Promise { +export async function fetchJobs(config: SearchConfig, { raw = false } = {}): Promise { const baseUrl = buildUrl(config); const allJobs: Job[] = []; const seen = new Set(); @@ -176,7 +196,7 @@ export async function fetchJobs(config: SearchConfig): Promise { const jobs = parseJobs(html, config.label) .filter((j) => !NON_ENGLISH_RE.test(j.title)) .filter((j) => !JUNIOR_TITLE_RE.test(j.title)) - .filter((j) => SOFTWARE_TITLE_RE.test(j.title)); + .filter((j) => raw || SOFTWARE_TITLE_RE.test(j.title)); let newOnPage = 0; for (const job of jobs) { diff --git a/index.ts b/index.ts index 7d24926..f0e412d 100644 --- a/index.ts +++ b/index.ts @@ -1,6 +1,6 @@ #!/usr/bin/env bun import { searches } from "./config.ts"; -import { fetchJobs } from "./fetch.ts"; +import { fetchJobs, isEnglishContent } from "./fetch.ts"; const CSV = "./jobs.csv"; const HEADER = "date_found,posted_at,search,title,company,link"; @@ -34,8 +34,17 @@ for (const config of searches) { } let added = 0; + let langDropped = 0; for (const job of jobs) { if (!existingIds.has(job.id)) { + process.stdout.write(` checking content: ${job.title} … `); + const english = await isEnglishContent(job.id); + if (!english) { + console.log("dropped (non-English)"); + langDropped++; + continue; + } + console.log("ok"); existingIds.add(job.id); newRows.push( [date, job.postedAt, config.label, job.title, job.company, job.link] @@ -45,7 +54,7 @@ for (const config of searches) { added++; } } - console.log(` ${jobs.length} found, ${added} new`); + console.log(` ${jobs.length} found, ${added} new, ${langDropped} non-English dropped`); } if (newRows.length === 0) { diff --git a/package.json b/package.json index 42b9f0e..7a5c0fa 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "type": "module", "scripts": { - "fetch": "bun run index.ts" + "fetch": "bun run index.ts", + "recheck": "bun run recheck.ts" } } diff --git a/polo-cv-accurate.html b/polo-cv-accurate.html new file mode 100644 index 0000000..af1b388 --- /dev/null +++ b/polo-cv-accurate.html @@ -0,0 +1,234 @@ + + + + + Rafael Polo - CV + + + +
Rafael Polo
+
Senior Software Engineer — Platform, Data & AI
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Personal
First name:Rafael
Last name:Polo
Citizenship:Portuguese (EU) · Brazilian — no work permit required
Address:Basel, 4055, Switzerland
Email:rafael@extrapolo.com
Web:linkedin.com/in/rafaelpolo
extrapolo.com
+ +

Formal Education

+
    +
  • + Postgraduate in Data Science for Finance – 2021
    + NOVA IMS Information Management School, Lisbon +
  • +
  • + Master in Information Systems – 2010–2012
    + PPGI/UNIRIO — Knowledge Representation & Reasoning +
  • +
  • + Bachelor in Computer Science – 2004–2009
    + Metodista Bennett, Rio de Janeiro +
  • +
+ +

Relevant Professional Experience

+
    +
  • + Swisscom – 2024–2025 – Zürich, Switzerland
    + DevOps Engineer developing platforms for Telecommunication Operations. +
  • +
  • + EDGE Empower® – 2024–2025 – Baar, Switzerland
    + Senior Technical Lead managing projects and external UX & Dev Teams for a SaaS Product. +
  • +
  • + ExtraPolo — Independent Consulting – 2010–2022 – Rio de Janeiro · Berlin · Athens · Lisbon
    + 40+ projects across civic tech, data journalism, open-source platforms and interactive installations for public and private clients. +
  • +
  • + Moedax Digital Services – 2018–2021 – Lisbon · Rio de Janeiro
    + Founder & CTO – Fintech startup connecting CBDC digital currency with KYC compliance automation. +
  • +
  • + GFOSS.eu / Code + Create – 2017–2018 – Athens, Greece
    + Web Programming Teacher – taught HTML, CSS, jQuery to refugee and local youth. +
  • +
  • + Tactical Tech Collective – 2014–2016 – Berlin, Germany
    + Full-Stack Web Architect – investigative data tools under Exposing the Invisible team. +
  • +
  • + Trommsdorf + Drüner – 2013–2014 – Berlin, Germany
    + Full-Stack Engineer – Innovation Team developing Business Intelligence dashboards. +
  • +
  • + Cortex Intelligence – 2007–2008 – Rio de Janeiro, Brazil
    + Developer – Competitive Intelligence and Data Mining R&D team. +
  • +
+ +

Certifications

+
    +
  • Swiss Federal Act on Data Protection (nFADP)
  • +
  • Gen AI Foundational Models — NLP & LLMs
  • +
  • AI Agents using RAG and LangChain
  • +
+ +

Relevant Tech Skills

+
    +
  • Languages & Frameworks: Python · Ruby on Rails · TypeScript · React · Java
  • +
  • DevOps & Infra: Docker · Kubernetes · CI/CD · AWS · Linux · Git
  • +
  • Data & AI: SQL · ETL · Data Engineering · RAG · LangChain · LLM integrations
  • +
  • Architecture: REST APIs · MVC · OOP · UML · System Design · Web Crawling
  • +
+ +

Natural Languages

+
    +
  • Native Brazilian Portuguese
  • +
  • Full professional English
  • +
  • Full professional Spanish
  • +
  • German B1 – TELC certified · 3 yrs Berlin + 3 yrs Basel
  • +
+ + + + \ No newline at end of file diff --git a/polo-cv.html b/polo-cv.html new file mode 100644 index 0000000..ec614a8 --- /dev/null +++ b/polo-cv.html @@ -0,0 +1,196 @@ + + + + + Rafael Polo - CV + + + +
Rafael Polo
+
Senior Software Engineer — Platform, Data & AI
+ +
+ Basel, 4055, Switzerland + rafael@extrapolo.com + linkedin.com/in/rafaelpolo + extrapolo.com +
+ +
+ Portuguese (EU) · Brazilian — no work permit required + 20+ years building production systems across Switzerland, Germany, Greece and Brazil + Fluent English · German B1 TELC (3 yrs Berlin + 3 yrs Basel) · EU citizen, immediately available +
+ +
+

P R O F E S S I O N A L E X P E R I E N C E

+ +
+
Swisscom
+
DevOps Engineer · Zürich, Switzerland
+
Modernization of mission-critical internal platforms & CI/CD pipelines for high-availability Telecom infrastructure.
+
+ +
+
EDGE Empower®
+
Senior Technical Lead · Baar, Switzerland · 2024 – 2025
+
Built a PayTool cutting processing time by 98%; reduced CI/CD pipeline from 14 → 4 minutes; managed 3 developers and a UX designer for a SaaS product.
+
+ +
+
ExtraPolo — Independent Consulting
+
Software Engineer & Consultant · Rio de Janeiro · Berlin · Athens · Lisbon · 2010 – 2022
+
40+ projects across civic tech, data journalism, open-source platforms and interactive installations for public and private clients.
+
+ +
+
Moedax Digital Services
+
Founder & CTO · Lisbon · Rio de Janeiro · 2018 – 2021
+
Fintech startup connecting CBDC digital currency with KYC compliance automation; built data pipelines and banking integrations in Python.
+
+ +
+
GFOSS.eu / Code + Create
+
Web Programming Teacher · Athens, Greece · 2017 – 2018
+
Taught web programming (HTML, CSS, jQuery) to refugee and local youth through the EU-funded Code + Create initiative.
+
+ +
+
Tactical Tech Collective
+
Full-Stack Web Architect · Berlin, Germany · Dec 2014 – Jun 2016
+
Built investigative data tools and platforms under the Exposing the Invisible team (Ruby on Rails, data visualisation).
+
+ +
+
Trommsdorff + Drüner
+
Full-Stack Engineer · Berlin, Germany
+
Innovation team — Business Intelligence dashboards for marketing consultancy clients.
+
+ +
+
Cortex Intelligence
+
Developer · Rio de Janeiro, Brazil
+
Competitive Intelligence and Data Mining R&D team.
+
+
+ +
+

E D U C A T I O N

+
Postgraduate in Data Science for FinanceNOVA IMS Information Management School, Lisbon — 2021
+
Master in Information SystemsPPGI/UNIRIO — Knowledge Representation & Reasoning — 2010–2012
+
Bachelor in Computer ScienceMetodista Bennett, Rio de Janeiro — 2004–2009
+
+ +
+

L A N G U A G E S

+
Portuguesenative
+
Englishfull professional
+
Spanishfull professional
+
GermanB1 — TELC certified · 3 yrs Berlin + 3 yrs Basel
+
+ +
+

C E R T I F I C A T I O N S

+
    +
  • Swiss Federal Act on Data Protection (nFADP)
  • +
  • Gen AI Foundational Models — NLP & LLMs
  • +
  • AI Agents using RAG and LangChain
  • +
+
+ +
+

T E C H S K I L L S

+
Languages & FrameworksPython · Ruby on Rails · TypeScript · React · Java
+
DevOps & InfraDocker · Kubernetes · CI/CD · AWS · Linux · Git
+
Data & AISQL · ETL · Data Engineering · RAG · LangChain · LLM integrations · Data Science
+
ArchitectureREST APIs · MVC · OOP · UML · System Design · Web Crawling & Scraping
+
+ +
+ References available upon request · CV updated May 2026 +
+ + \ No newline at end of file diff --git a/polo-new-cv.html b/polo-new-cv.html new file mode 100644 index 0000000..c1f6460 --- /dev/null +++ b/polo-new-cv.html @@ -0,0 +1,243 @@ + + + + + Rafael Polo - CV + + + +
+
+

Rafael Polo

+

Senior Software Engineer — Platform, Data & AI

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Personal
Address:Basel, 4055, Switzerland
Email:rafael@extrapolo.com
LinkedIn:linkedin.com/in/rafaelpolo
Web:extrapolo.com
Citizenship:Portuguese (EU) · Brazilian — no work permit required
Languages:Fluent English · German B1 TELC (3 yrs Berlin + 3 yrs Basel) · EU citizen, immediately available
+ +

Formal Education

+
    +
  • Postgraduate in Data Science for Finance – 2021
    + NOVA IMS Information Management School, Lisbon
  • +
  • Master in Information Systems – 2010–2012
    + Research Area: Knowledge Representation & Reasoning, PPGI/UNIRIO
  • +
  • Bachelor in Computer Science – 2004–2009
    + Metodista Bennett, Rio de Janeiro
  • +
  • Thesis: Agile Systems Development for Web 2.0 with Ruby on Rails
  • +
+ +

Relevant Professional Experience

+
    +
  • Swisscom – 2024–2025 – Zürich, Switzerland +

    DevOps Engineer developing platforms for Telecommunication Operations.

  • +
  • EDGE Empower® – 2024–2025 – Baar, Switzerland +

    Senior Technical Lead managing projects and external UX & Dev Teams for a SaaS Company.

  • +
  • ExtraPolo — Independent Consulting – 2010–2022 – Rio de Janeiro · Berlin · Athens · Lisbon +

    For more than 10 years, worked as an independent Software Developer and IT Consultant, developing dozens of Projects for public and private contractors in Brazil, Germany, Greece and Switzerland.

  • +
  • Moedax Digital Services – 2018–2021 – Lisbon and Rio de Janeiro +

    Founder & CTO – connected Brazilian CBDC with KYC processes and automation

  • +
  • GFOSS.eu / Code + Create – 2017–2018 – Athens, Greece +

    Web Programming Teacher – taught web programming to refugee and local youth

  • +
  • Tactical Tech Collective – 2014–2016 – Berlin, Germany +

    Full-Stack Web Architect on an NGO under the Exposing the Invisible Team.

  • +
  • Trommsdorf + Drüner – 2013–2014 – Berlin, Germany +

    Full-stack Engineer – Innovation Team developing Business Intelligence dashboards.

  • +
  • Cortex Intelligence – 2007–2008 – Rio de Janeiro, Brazil +

    Developer in the Competitive Intelligence and Data Mining Team.

  • +
+ +

Certifications

+
    +
  • Swiss Federal Act on Data Protection (nFADP)
  • +
  • Gen AI Foundational Models — NLP & LLMs
  • +
  • AI Agents using RAG and LangChain
  • +
+ +

Relevant Tech Skills

+
    +
  • System Analysis & Modeling with UML
  • +
  • Quick Proof-of-Concepts development, deploy and management.
  • +
  • Large Language Models, Gen AI, LangChain integration and Processes Automation
  • +
  • MVC System Architecture, Design Patterns & Object-Oriented Programming
  • +
  • Data Engineering, Data Science, Relational Databases & SQL
  • +
  • Experience with Blockchain, Smart Contracts and P2P systems
  • +
  • Distributing and Scaling Processes with Message Queues
  • +
  • Developing Web Crawlers & Bots for Data Mining & ETL
  • +
  • Linux Systems, VPS, DevOps, SSH & Deploy with Continuous Integration processes
  • +
  • Projects mostly developed in Python, Ruby on Rails, Django, Java
  • +
  • Full Stack Developer & Front-end Web Design with HTML, CSS and JavaScript frameworks
  • +
  • CI/CD, Docker, Kubernetes and GitHub workflows
  • +
+ +

Natural Languages

+
    +
  • Native Brazilian Portuguese
  • +
  • Advanced Spoken Spanish
  • +
  • A2 level Greek – 3 years in Athens
  • +
  • B1 level German / TELC proof – 3 years in Berlin and 3 in Switzerland
  • +
  • Fluent English – 13 years in between international teams
  • +
+ + +
+
+ +
+
+ + \ No newline at end of file diff --git a/polo-new-cv.odt b/polo-new-cv.odt new file mode 100644 index 0000000..e25f172 Binary files /dev/null and b/polo-new-cv.odt differ diff --git a/polo-new-cv.pdf b/polo-new-cv.pdf new file mode 100644 index 0000000..4c6bdb2 Binary files /dev/null and b/polo-new-cv.pdf differ diff --git a/polo.md b/polo.md new file mode 100644 index 0000000..7406c5e --- /dev/null +++ b/polo.md @@ -0,0 +1,130 @@ +Rafael Polo + +Basel, 4055, Switzerland + +rafael@extrapolo.com + +linkedin.com/in/rafaelpolo + +extrapolo.com + +Senior Software Engineer — Platform, Data & AI + +Portuguese (EU) · Brazilian — no work permit required + +20+ years building production systems across Switzerland, Germany, Greece and Brazil. Specialised in platform + +engineering, data pipelines and AI/LLM integrations. Fluent English · German B1 TELC (3 yrs Berlin + 3 yrs Basel) · EU + +citizen, immediately available. + +P R O F E S S I O N A L E X P E R I E N C E + +Swisscom +DevOps Engineer · Zürich, Switzerland + +Modernization of mission-critical internal platforms & CI/CD pipelines for high-availability Telecom infrastructure. + +EDGE Empower® + +Senior Technical Lead · Baar, Switzerland + +2024 – 2025 + +Mar 2023 – Mar 2024 + +Built a PayTool cutting processing time by 98%; reduced CI/CD pipeline from 14 → 4 minutes; managed 3 developers and a UX + +designer for a SaaS product. + +ExtraPolo — Independent Consulting +Software Engineer & Consultant · Rio de Janeiro · Berlin · Athens · Lisbon + +2010 – 2022 + +40+ projects across civic tech, data journalism, open-source platforms and interactive installations for public and private clients. + +Moedax Digital Services + +Founder & CTO · Lisbon · Rio de Janeiro + +2018 – 2021 + +Fintech startup connecting CBDC digital currency with KYC compliance automation; built data pipelines and banking integrations in +Python. + +GFOSS.eu / Code + Create +Web Programming Teacher · Athens, Greece + +2017 – 2018 + +Taught web programming (HTML, CSS, jQuery) to refugee and local youth through the EU-funded Code + Create initiative. + +Tactical Tech Collective +Full-Stack Web Architect · Berlin, Germany + +Dec 2014 – Jun 2016 + +Built investigative data tools and platforms under the Exposing the Invisible team (Ruby on Rails, data visualisation). + +Trommsdorff + Drüner + +Full-Stack Engineer · Berlin, Germany + +Innovation team — Business Intelligence dashboards for marketing consultancy clients. + +Cortex Intelligence +Developer · Rio de Janeiro, Brazil + +Competitive Intelligence and Data Mining R&D team. + +E D U C A T I O N + +L A N G U A G E S + +Dec 2013 – Jan 2014 + +2008 – 2010 + +Postgraduate in Data Science for Finance +NOVA IMS Information Management School, Lisbon — 2021 + +Master in Information Systems +PPGI/UNIRIO — Knowledge Representation & Reasoning — +2010–2012 + +Bachelor in Computer Science +Metodista Bennett, Rio de Janeiro — 2004–2009 + +Portuguese — native + +English — full professional + +Spanish — full professional + +German B1 — TELC certified · 3 yrs Berlin + 3 yrs Basel + +C E R T I F I C A T I O N S + +Swiss Federal Act on Data Protection (nFADP) + +Gen AI Foundational Models — NLP & LLMs + +AI Agents using RAG and LangChain + +T E C H S K I L L S + + Languages & Frameworks · Python · Ruby on Rails · +TypeScript · React · Java + +DevOps & Infra · Docker · Kubernetes · CI/CD · AWS · Linux · +Git + +Data & AI · SQL · ETL · Data Engineering · RAG · LangChain · +LLM integrations · Data Science + +Architecture · REST APIs · MVC · OOP · UML · System Design · +Web Crawling & Scraping + +References available upon request  ·  CV updated May 2026 + diff --git a/rafael-polo-cv-2026-senior.odt b/rafael-polo-cv-2026-senior.odt new file mode 100644 index 0000000..02656e4 Binary files /dev/null and b/rafael-polo-cv-2026-senior.odt differ diff --git a/rafael-polo-cv-2026-senior.pdf b/rafael-polo-cv-2026-senior.pdf new file mode 100644 index 0000000..fac8f41 Binary files /dev/null and b/rafael-polo-cv-2026-senior.pdf differ diff --git a/rafael-polo-cv.pdf b/rafael-polo-cv.pdf new file mode 100644 index 0000000..35c207c Binary files /dev/null and b/rafael-polo-cv.pdf differ diff --git a/recheck.ts b/recheck.ts new file mode 100644 index 0000000..564b7f6 --- /dev/null +++ b/recheck.ts @@ -0,0 +1,65 @@ +#!/usr/bin/env bun +import { searches } from "./config.ts"; +import { fetchJobs, SOFTWARE_TITLE_RE } from "./fetch.ts"; + +const CSV = "./jobs.csv"; + +const existingIds = new Set(); +try { + const text = await Bun.file(CSV).text(); + for (const line of text.split("\n").slice(1)) { + const id = line.match(/\/jobs\/view\/(\d+)\//)?.[1]; + if (id) existingIds.add(id); + } +} catch { /* first run */ } + +const missed: { title: string; company: string; link: string }[] = []; +const seenIds = new Set(); + +for (const config of searches) { + console.log(`\n[${config.label}]`); + const all = await fetchJobs(config, { raw: true }); + for (const job of all) { + if (!seenIds.has(job.id) && !existingIds.has(job.id) && !SOFTWARE_TITLE_RE.test(job.title)) { + seenIds.add(job.id); + missed.push({ title: job.title, company: job.company, link: job.link }); + } + } +} + +if (missed.length === 0) { + console.log("\nNo missed jobs — all new titles pass the filter."); + process.exit(0); +} + +console.log(`\n── ${missed.length} new jobs currently filtered out ──\n`); +for (const j of missed) { + console.log(` ${j.title}`); + console.log(` ${j.company} ${j.link}`); +} + +// Word frequency analysis to suggest keywords +const STOP = new Set([ + "and", "or", "of", "the", "a", "an", "in", "for", "to", "at", "by", + "with", "on", "as", "is", "its", "be", "are", "was", "were", "our", + "your", "this", "that", "we", "you", "all", "new", "use", +]); + +const wordCounts = new Map(); +for (const { title } of missed) { + const words = title.match(/\b[a-zA-Z]{3,}\b/g) ?? []; + for (const w of words) { + const key = w.toLowerCase(); + if (!STOP.has(key)) wordCounts.set(key, (wordCounts.get(key) ?? 0) + 1); + } +} + +const suggestions = [...wordCounts.entries()] + .sort((a, b) => b[1] - a[1]) + .filter(([, n]) => n >= 1) + .slice(0, 20); + +console.log("\n── Suggested keywords to consider ──\n"); +for (const [word, count] of suggestions) { + console.log(` "${word}" — ${count} title${count > 1 ? "s" : ""}`); +} diff --git a/zurich-temp.ts b/zurich-temp.ts new file mode 100644 index 0000000..42c960a --- /dev/null +++ b/zurich-temp.ts @@ -0,0 +1,38 @@ +#!/usr/bin/env bun +import { KEYWORDS } from "./config.ts"; +import { fetchJobs, isEnglishContent } from "./fetch.ts"; +import type { SearchConfig } from "./config.ts"; + +const searches: SearchConfig[] = [ + { label: "Zürich", keywords: KEYWORDS, location: "Zurich, Switzerland", workType: ["onsite", "hybrid"], timePosted: "month" }, +]; + +const seen = new Set(); +const results: { id: string; title: string; company: string; search: string }[] = []; + +for (const config of searches) { + console.log(`\n[${config.label}]`); + const jobs = await fetchJobs(config); + for (const job of jobs) { + if (!seen.has(job.id)) { + seen.add(job.id); + results.push({ id: job.id, title: job.title, company: job.company, search: job.search }); + } + } +} + +console.log(`\n── ${results.length} jobs after title filter — checking content language ──\n`); + +const english: typeof results = []; +for (const job of results) { + process.stdout.write(` ${job.company} — ${job.title} … `); + const ok = await isEnglishContent(job.id); + console.log(ok ? "en" : "dropped"); + if (ok) english.push(job); +} + +console.log(`\n── ${english.length} English jobs in Zürich ──\n`); +for (const [i, j] of english.entries()) { + console.log(` ${i + 1}. [${j.search}] ${j.title} — ${j.company}`); + console.log(` https://www.linkedin.com/jobs/view/${j.id}/`); +}