Add content-level language filter, recheck command, expanded keywords

- isEnglishContent() fetches each job page and drops descriptions with ≥6 German or French indicator words; called for every new job in index.ts before writing to CSV
- New `bun recheck` command (recheck.ts) runs searches in raw mode, lists filtered-out jobs, and prints word-frequency suggestions for SOFTWARE_TITLE_RE
- Expanded KEYWORDS and SOFTWARE_TITLE_RE to cover ML/AI, system/applications engineer, director, informatics, and more title variants
- Added `raw` flag to fetchJobs() to bypass the software-role filter
- Add CLAUDE.md with project workflow docs; add CV files and city temp scripts
This commit is contained in:
2026-05-12 13:42:27 +02:00
parent ffe2b4467a
commit bcb6ae5ea3
19 changed files with 1201 additions and 14 deletions
+92
View File
@@ -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`.
+49 -6
View File
@@ -5,7 +5,8 @@ Scrapes LinkedIn job listings into a local CSV. Run it daily, delete rows you've
## Usage ## Usage
```bash ```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. 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 - **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 - **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 - **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 - **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, intern, trainee, and apprentice titles - **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 delete and re-run - **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`) - **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 ## Configure searches
Edit `config.ts`: Edit `config.ts`:
```ts ```ts
const KEYWORDS = [ export const KEYWORDS = [
"Software Engineer", "Software Engineer",
"Solutions Engineer",
"Full Stack Engineer", "Full Stack Engineer",
"Backend Engineer",
"Data Engineer", "Data Engineer",
"AI Engineer", "AI Engineer",
"Applications Engineer",
"Platform Engineer",
"DevOps Engineer",
"Staff Engineer",
"Principal Engineer",
"Tech Lead",
"Developer",
"Ruby on Rails", "Ruby on Rails",
"Python", "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 "); ].join(" OR ");
export const searches: SearchConfig[] = [ export const searches: SearchConfig[] = [
@@ -59,6 +84,22 @@ export const searches: SearchConfig[] = [
workType: ["onsite", "hybrid"], workType: ["onsite", "hybrid"],
timePosted: "month", 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 - `107117530` — Basel-Country
You can also paste any LinkedIn search URL directly as `url:` instead of building from filters. 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.
+75
View File
@@ -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}/`);
}
+30
View File
@@ -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<string>();
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}`);
}
+12 -1
View File
@@ -16,13 +16,14 @@ export interface SearchConfig {
seniority?: Seniority[]; seniority?: Seniority[];
} }
const KEYWORDS = [ export const KEYWORDS = [
"Software Engineer", "Software Engineer",
"Solutions Engineer", "Solutions Engineer",
"Full Stack Engineer", "Full Stack Engineer",
"Backend Engineer", "Backend Engineer",
"Data Engineer", "Data Engineer",
"AI Engineer", "AI Engineer",
"Applications Engineer",
"Platform Engineer", "Platform Engineer",
"DevOps Engineer", "DevOps Engineer",
"Staff Engineer", "Staff Engineer",
@@ -33,6 +34,16 @@ const KEYWORDS = [
"Python", "Python",
"Node.js", "Node.js",
"TypeScript", "TypeScript",
"Artificial Intelligence",
"Machine Learning",
"Data Science",
"AI Consultant",
"ML Engineer",
"Director of Engineering",
"Engineering Manager",
"Lead Developer",
"System Engineer",
"Solution Engineer",
].join(" OR "); ].join(" OR ");
// --- Define your searches here --- // --- Define your searches here ---
+24 -4
View File
@@ -68,7 +68,7 @@ const NON_ENGLISH_RE = new RegExp(
// /in word-ending (e.g. Mitarbeiter/in) — excludes m/f, n/a, etc. // /in word-ending (e.g. Mitarbeiter/in) — excludes m/f, n/a, etc.
String.raw`[a-zäöü]\/in\b`, String.raw`[a-zäöü]\/in\b`,
// German-only words (case-insensitive handled by flag) // 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 // French gender markers
String.raw`\(H\/F\)|\(F\/H\)|\(h\/f\)|\(f\/h\)`, String.raw`\(H\/F\)|\(F\/H\)|\(h\/f\)|\(f\/h\)`,
// French-only accented characters // 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 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<boolean> {
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 PAGE_SIZE = 25;
const MAX_PAGES = 6; const MAX_PAGES = 6;
@@ -156,7 +176,7 @@ function parseJobs(html: string, label: string): Job[] {
return jobs; return jobs;
} }
export async function fetchJobs(config: SearchConfig): Promise<Job[]> { export async function fetchJobs(config: SearchConfig, { raw = false } = {}): Promise<Job[]> {
const baseUrl = buildUrl(config); const baseUrl = buildUrl(config);
const allJobs: Job[] = []; const allJobs: Job[] = [];
const seen = new Set<string>(); const seen = new Set<string>();
@@ -176,7 +196,7 @@ export async function fetchJobs(config: SearchConfig): Promise<Job[]> {
const jobs = parseJobs(html, config.label) const jobs = parseJobs(html, config.label)
.filter((j) => !NON_ENGLISH_RE.test(j.title)) .filter((j) => !NON_ENGLISH_RE.test(j.title))
.filter((j) => !JUNIOR_TITLE_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; let newOnPage = 0;
for (const job of jobs) { for (const job of jobs) {
+11 -2
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import { searches } from "./config.ts"; import { searches } from "./config.ts";
import { fetchJobs } from "./fetch.ts"; import { fetchJobs, isEnglishContent } from "./fetch.ts";
const CSV = "./jobs.csv"; const CSV = "./jobs.csv";
const HEADER = "date_found,posted_at,search,title,company,link"; const HEADER = "date_found,posted_at,search,title,company,link";
@@ -34,8 +34,17 @@ for (const config of searches) {
} }
let added = 0; let added = 0;
let langDropped = 0;
for (const job of jobs) { for (const job of jobs) {
if (!existingIds.has(job.id)) { 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); existingIds.add(job.id);
newRows.push( newRows.push(
[date, job.postedAt, config.label, job.title, job.company, job.link] [date, job.postedAt, config.label, job.title, job.company, job.link]
@@ -45,7 +54,7 @@ for (const config of searches) {
added++; added++;
} }
} }
console.log(` ${jobs.length} found, ${added} new`); console.log(` ${jobs.length} found, ${added} new, ${langDropped} non-English dropped`);
} }
if (newRows.length === 0) { if (newRows.length === 0) {
+2 -1
View File
@@ -3,6 +3,7 @@
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"fetch": "bun run index.ts" "fetch": "bun run index.ts",
"recheck": "bun run recheck.ts"
} }
} }
+234
View File
@@ -0,0 +1,234 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Rafael Polo - CV</title>
<style>
@page {
size: 8.2681in 11.6929in;
margin: 0.5in;
}
body {
font-family: Calibri, Liberation Sans, sans-serif;
font-size: 11pt;
line-height: 1.2;
margin: 0;
padding: 0;
}
.container {
display: table;
width: 100%;
}
.left-col {
display: table-cell;
width: 100%;
}
.title {
font-size: 24pt;
font-weight: bold;
margin-bottom: 0.05in;
}
.subtitle {
font-size: 11pt;
font-style: italic;
margin-bottom: 0.15in;
color: #444;
}
table.personal {
width: 100%;
border-collapse: collapse;
margin-bottom: 0.2in;
}
table.personal td {
padding: 0.02in 0.05in;
vertical-align: top;
}
table.personal .label {
font-weight: bold;
color: #333;
width: 1in;
}
table.personal .header-row td {
font-weight: bold;
font-size: 11pt;
text-transform: uppercase;
border-bottom: 1px solid #999;
padding-bottom: 0.05in;
}
h3 {
font-size: 11pt;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.05em;
margin: 0.15in 0 0.08in 0;
border-bottom: 1px solid #999;
padding-bottom: 0.03in;
}
ul {
margin: 0.05in 0;
padding-left: 0.15in;
}
li {
margin-bottom: 0.04in;
font-size: 10pt;
}
.job-item {
margin-bottom: 0.08in;
}
.job-title {
font-weight: bold;
font-size: 10pt;
}
.job-meta {
font-style: italic;
font-size: 9pt;
color: #555;
}
.job-desc {
font-style: italic;
font-size: 10pt;
color: #444;
margin-left: 0.1in;
}
.edu-item {
margin-bottom: 0.05in;
}
.edu-degree {
font-weight: bold;
font-size: 10pt;
}
.edu-school {
font-style: italic;
font-size: 10pt;
}
.skill-item {
font-size: 10pt;
margin-bottom: 0.02in;
}
.lang-item {
font-size: 10pt;
margin-bottom: 0.02in;
}
.footer {
font-size: 9pt;
color: #666;
margin-top: 0.2in;
padding-top: 0.1in;
border-top: 1px solid #ccc;
}
</style>
</head>
<body>
<div class="title">Rafael Polo</div>
<div class="subtitle">Senior Software Engineer — Platform, Data & AI</div>
<table class="personal">
<tr>
<td colspan="2" class="header-row">Personal</td>
</tr>
<tr>
<td class="label">First name:</td>
<td>Rafael</td>
</tr>
<tr>
<td class="label">Last name:</td>
<td>Polo</td>
</tr>
<tr>
<td class="label">Citizenship:</td>
<td>Portuguese (EU) · Brazilian — no work permit required</td>
</tr>
<tr>
<td class="label">Address:</td>
<td>Basel, 4055, Switzerland</td>
</tr>
<tr>
<td class="label">Email:</td>
<td>rafael@extrapolo.com</td>
</tr>
<tr>
<td class="label">Web:</td>
<td>linkedin.com/in/rafaelpolo<br>extrapolo.com</td>
</tr>
</table>
<h3>Formal Education</h3>
<ul>
<li class="edu-item">
<span class="edu-degree"><em><strong>Postgraduate in Data Science for Finance</strong></em> – 2021</span><br>
<span class="edu-school">NOVA IMS Information Management School, Lisbon</span>
</li>
<li class="edu-item">
<span class="edu-degree"><em><strong>Master in Information Systems</strong></em> – 2010–2012</span><br>
<span class="edu-school">PPGI/UNIRIO — Knowledge Representation & Reasoning</span>
</li>
<li class="edu-item">
<span class="edu-degree"><em><strong>Bachelor in Computer Science</strong></em> – 2004–2009</span><br>
<span class="edu-school">Metodista Bennett, Rio de Janeiro</span>
</li>
</ul>
<h3>Relevant Professional Experience</h3>
<ul>
<li class="job-item">
<span class="job-title">Swisscom</span> – <span class="job-meta">2024–2025 – Zürich, Switzerland</span><br>
<span class="job-desc"><strong>DevOps Engineer</strong> developing platforms for Telecommunication Operations.</span>
</li>
<li class="job-item">
<span class="job-title">EDGE Empower®</span> – <span class="job-meta">2024–2025 – Baar, Switzerland</span><br>
<span class="job-desc"><strong>Senior Technical Lead</strong> managing projects and external UX & Dev Teams for a SaaS Product.</span>
</li>
<li class="job-item">
<span class="job-title">ExtraPolo — Independent Consulting</span> – <span class="job-meta">2010–2022 – Rio de Janeiro · Berlin · Athens · Lisbon</span><br>
<span class="job-desc">40+ projects across civic tech, data journalism, open-source platforms and interactive installations for public and private clients.</span>
</li>
<li class="job-item">
<span class="job-title">Moedax Digital Services</span> – <span class="job-meta">2018–2021 – Lisbon · Rio de Janeiro</span><br>
<span class="job-desc"><strong>Founder & CTO</strong> – Fintech startup connecting CBDC digital currency with KYC compliance automation.</span>
</li>
<li class="job-item">
<span class="job-title">GFOSS.eu / Code + Create</span> – <span class="job-meta">2017–2018 – Athens, Greece</span><br>
<span class="job-desc"><strong>Web Programming Teacher</strong> – taught HTML, CSS, jQuery to refugee and local youth.</span>
</li>
<li class="job-item">
<span class="job-title">Tactical Tech Collective</span> – <span class="job-meta">2014–2016 – Berlin, Germany</span><br>
<span class="job-desc"><strong>Full-Stack Web Architect</strong> – investigative data tools under Exposing the Invisible team.</span>
</li>
<li class="job-item">
<span class="job-title">Trommsdorf + Drüner</span> – <span class="job-meta">2013–2014 – Berlin, Germany</span><br>
<span class="job-desc"><strong>Full-Stack Engineer</strong> – Innovation Team developing Business Intelligence dashboards.</span>
</li>
<li class="job-item">
<span class="job-title">Cortex Intelligence</span> – <span class="job-meta">2007–2008 – Rio de Janeiro, Brazil</span><br>
<span class="job-desc"><strong>Developer</strong> – Competitive Intelligence and Data Mining R&D team.</span>
</li>
</ul>
<h3>Certifications</h3>
<ul>
<li class="skill-item">Swiss Federal Act on Data Protection (nFADP)</li>
<li class="skill-item">Gen AI Foundational Models — NLP & LLMs</li>
<li class="skill-item">AI Agents using RAG and LangChain</li>
</ul>
<h3>Relevant Tech Skills</h3>
<ul>
<li class="skill-item"><strong>Languages & Frameworks:</strong> Python · Ruby on Rails · TypeScript · React · Java</li>
<li class="skill-item"><strong>DevOps & Infra:</strong> Docker · Kubernetes · CI/CD · AWS · Linux · Git</li>
<li class="skill-item"><strong>Data & AI:</strong> SQL · ETL · Data Engineering · RAG · LangChain · LLM integrations</li>
<li class="skill-item"><strong>Architecture:</strong> REST APIs · MVC · OOP · UML · System Design · Web Crawling</li>
</ul>
<h3>Natural Languages</h3>
<ul>
<li class="lang-item">Native Brazilian Portuguese</li>
<li class="lang-item">Full professional English</li>
<li class="lang-item">Full professional Spanish</li>
<li class="lang-item">German B1 – TELC certified · 3 yrs Berlin + 3 yrs Basel</li>
</ul>
<div class="footer">
References available upon request · CV updated May 2026
</div>
</body>
</html>
+196
View File
@@ -0,0 +1,196 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Rafael Polo - CV</title>
<style>
@page {
size: 8.2681in 11.6929in;
margin: 0.5in;
}
body {
font-family: Liberation Sans, Carlito, Georgia, serif;
font-size: 11pt;
line-height: 1.4;
margin: 0;
padding: 0;
}
.title {
font-size: 36pt;
font-weight: bold;
margin-bottom: 0.1in;
}
.subtitle {
font-size: 14pt;
margin-bottom: 0.3in;
color: #333;
}
h1 {
font-size: 24pt;
font-weight: bold;
margin: 0.2in 0 0.1in 0;
border-bottom: 1px solid #333;
padding-bottom: 0.05in;
}
h2 {
font-size: 18pt;
font-weight: bold;
margin: 0.15in 0 0.08in 0;
}
h3 {
font-size: 14pt;
font-weight: bold;
margin: 0.1in 0 0.05in 0;
}
.section {
margin-bottom: 0.15in;
}
.personalinline {
font-size: 10pt;
margin-bottom: 0.2in;
}
.personalinline span {
margin-right: 0.3in;
}
.key-value {
display: flex;
margin: 0.03in 0;
}
.key {
font-weight: bold;
width: 1.5in;
}
.value {
flex: 1;
}
ul {
margin: 0.05in 0;
padding-left: 0.2in;
}
li {
margin-bottom: 0.03in;
}
.small {
font-size: 10pt;
}
.experience-item {
margin-bottom: 0.1in;
}
.exp-header {
font-weight: bold;
}
.exp-meta {
font-style: italic;
font-size: 10pt;
}
.exp-desc {
margin-left: 0.2in;
}
</style>
</head>
<body>
<div class="title">Rafael Polo</div>
<div class="subtitle">Senior Software Engineer — Platform, Data & AI</div>
<div class="personalinline">
<span>Basel, 4055, Switzerland</span>
<span>rafael@extrapolo.com</span>
<span>linkedin.com/in/rafaelpolo</span>
<span>extrapolo.com</span>
</div>
<div class="personalinline">
<span>Portuguese (EU) · Brazilian — no work permit required</span>
<span>20+ years building production systems across Switzerland, Germany, Greece and Brazil</span>
<span>Fluent English · German B1 TELC (3 yrs Berlin + 3 yrs Basel) · EU citizen, immediately available</span>
</div>
<div class="section">
<h1>P R O F E S S I O N A L E X P E R I E N C E</h1>
<div class="experience-item">
<div class="exp-header">Swisscom</div>
<div class="exp-meta">DevOps Engineer · Zürich, Switzerland</div>
<div class="exp-desc">Modernization of mission-critical internal platforms & CI/CD pipelines for high-availability Telecom infrastructure.</div>
</div>
<div class="experience-item">
<div class="exp-header">EDGE Empower®</div>
<div class="exp-meta">Senior Technical Lead · Baar, Switzerland · 2024 – 2025</div>
<div class="exp-desc">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.</div>
</div>
<div class="experience-item">
<div class="exp-header">ExtraPolo — Independent Consulting</div>
<div class="exp-meta">Software Engineer & Consultant · Rio de Janeiro · Berlin · Athens · Lisbon · 2010 – 2022</div>
<div class="exp-desc">40+ projects across civic tech, data journalism, open-source platforms and interactive installations for public and private clients.</div>
</div>
<div class="experience-item">
<div class="exp-header">Moedax Digital Services</div>
<div class="exp-meta">Founder & CTO · Lisbon · Rio de Janeiro · 2018 – 2021</div>
<div class="exp-desc">Fintech startup connecting CBDC digital currency with KYC compliance automation; built data pipelines and banking integrations in Python.</div>
</div>
<div class="experience-item">
<div class="exp-header">GFOSS.eu / Code + Create</div>
<div class="exp-meta">Web Programming Teacher · Athens, Greece · 2017 – 2018</div>
<div class="exp-desc">Taught web programming (HTML, CSS, jQuery) to refugee and local youth through the EU-funded Code + Create initiative.</div>
</div>
<div class="experience-item">
<div class="exp-header">Tactical Tech Collective</div>
<div class="exp-meta">Full-Stack Web Architect · Berlin, Germany · Dec 2014 – Jun 2016</div>
<div class="exp-desc">Built investigative data tools and platforms under the Exposing the Invisible team (Ruby on Rails, data visualisation).</div>
</div>
<div class="experience-item">
<div class="exp-header">Trommsdorff + Drüner</div>
<div class="exp-meta">Full-Stack Engineer · Berlin, Germany</div>
<div class="exp-desc">Innovation team — Business Intelligence dashboards for marketing consultancy clients.</div>
</div>
<div class="experience-item">
<div class="exp-header">Cortex Intelligence</div>
<div class="exp-meta">Developer · Rio de Janeiro, Brazil</div>
<div class="exp-desc">Competitive Intelligence and Data Mining R&D team.</div>
</div>
</div>
<div class="section">
<h1>E D U C A T I O N</h1>
<div class="key-value"><span class="key">Postgraduate in Data Science for Finance</span><span class="value">NOVA IMS Information Management School, Lisbon — 2021</span></div>
<div class="key-value"><span class="key">Master in Information Systems</span><span class="value">PPGI/UNIRIO — Knowledge Representation & Reasoning — 2010–2012</span></div>
<div class="key-value"><span class="key">Bachelor in Computer Science</span><span class="value">Metodista Bennett, Rio de Janeiro — 2004–2009</span></div>
</div>
<div class="section">
<h1>L A N G U A G E S</h1>
<div class="key-value"><span class="key">Portuguese</span><span class="value">native</span></div>
<div class="key-value"><span class="key">English</span><span class="value">full professional</span></div>
<div class="key-value"><span class="key">Spanish</span><span class="value">full professional</span></div>
<div class="key-value"><span class="key">German</span><span class="value">B1 — TELC certified · 3 yrs Berlin + 3 yrs Basel</span></div>
</div>
<div class="section">
<h1>C E R T I F I C A T I O N S</h1>
<ul>
<li>Swiss Federal Act on Data Protection (nFADP)</li>
<li>Gen AI Foundational Models — NLP & LLMs</li>
<li>AI Agents using RAG and LangChain</li>
</ul>
</div>
<div class="section">
<h1>T E C H S K I L L S</h1>
<div class="key-value"><span class="key">Languages & Frameworks</span><span class="value">Python · Ruby on Rails · TypeScript · React · Java</span></div>
<div class="key-value"><span class="key">DevOps & Infra</span><span class="value">Docker · Kubernetes · CI/CD · AWS · Linux · Git</span></div>
<div class="key-value"><span class="key">Data & AI</span><span class="value">SQL · ETL · Data Engineering · RAG · LangChain · LLM integrations · Data Science</span></div>
<div class="key-value"><span class="key">Architecture</span><span class="value">REST APIs · MVC · OOP · UML · System Design · Web Crawling & Scraping</span></div>
</div>
<div class="small">
References available upon request · CV updated May 2026
</div>
</body>
</html>
+243
View File
@@ -0,0 +1,243 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Rafael Polo - CV</title>
<style>
@page {
size: 8.2681in 11.6929in;
margin: 0.5in;
}
body {
font-family: Liberation Sans, Carlito, Calibri, Georgia, sans-serif;
font-size: 11pt;
line-height: 1.3;
margin: 0;
padding: 0;
}
.container {
display: table;
width: 100%;
}
.left-col {
display: table-cell;
width: 65%;
padding-right: 0.2in;
vertical-align: top;
}
.right-col {
display: table-cell;
width: 35%;
vertical-align: top;
text-align: right;
}
.photo {
width: 2in;
height: auto;
border: 1px solid #ccc;
}
.title {
font-size: 24pt;
font-weight: bold;
margin-bottom: 0.05in;
}
.subtitle {
font-size: 12pt;
font-style: italic;
margin-bottom: 0.2in;
color: #444;
}
h3 {
font-size: 12pt;
font-weight: bold;
margin: 0.15in 0 0.08in 0;
text-transform: uppercase;
letter-spacing: 0.05in;
border-bottom: 1px solid #999;
padding-bottom: 0.03in;
}
.personal-table {
width: 100%;
border-collapse: collapse;
}
.personal-table td {
padding: 0.02in 0;
vertical-align: top;
}
.personal-table .label {
font-weight: bold;
width: 1.2in;
color: #333;
}
.personal-table .header {
font-weight: bold;
font-size: 12pt;
text-transform: uppercase;
letter-spacing: 0.05in;
padding-bottom: 0.05in;
}
.section-header {
font-weight: bold;
font-size: 11pt;
text-transform: uppercase;
letter-spacing: 0.03in;
margin-top: 0.15in;
color: #333;
}
ul {
margin: 0.05in 0;
padding-left: 0.15in;
}
li {
margin-bottom: 0.04in;
font-size: 10pt;
}
.job-title {
font-weight: bold;
}
.job-meta {
font-style: italic;
font-size: 10pt;
}
.job-company {
font-weight: bold;
}
.job-desc {
font-style: italic;
font-size: 10pt;
margin-left: 0.1in;
}
.edu-item {
font-size: 10pt;
margin-bottom: 0.03in;
}
.edu-school {
font-style: italic;
}
.skill-category {
font-weight: bold;
font-size: 10pt;
margin-top: 0.05in;
}
.skill-list {
font-size: 10pt;
margin-left: 0.1in;
}
.footer {
font-size: 9pt;
color: #666;
margin-top: 0.3in;
border-top: 1px solid #ccc;
padding-top: 0.1in;
}
</style>
</head>
<body>
<div class="container">
<div class="left-col">
<p class="title">Rafael Polo</p>
<p class="subtitle">Senior Software Engineer — Platform, Data & AI</p>
<table class="personal-table">
<tr>
<td class="header" colspan="2">Personal</td>
</tr>
<tr>
<td class="label">Address:</td>
<td>Basel, 4055, Switzerland</td>
</tr>
<tr>
<td class="label">Email:</td>
<td>rafael@extrapolo.com</td>
</tr>
<tr>
<td class="label">LinkedIn:</td>
<td>linkedin.com/in/rafaelpolo</td>
</tr>
<tr>
<td class="label">Web:</td>
<td>extrapolo.com</td>
</tr>
<tr>
<td class="label">Citizenship:</td>
<td>Portuguese (EU) · Brazilian — no work permit required</td>
</tr>
<tr>
<td class="label">Languages:</td>
<td>Fluent English · German B1 TELC (3 yrs Berlin + 3 yrs Basel) · EU citizen, immediately available</td>
</tr>
</table>
<h3>Formal Education</h3>
<ul>
<li><em><strong>Postgraduate in Data Science for Finance</strong></em> – 2021<br>
<span class="edu-school">NOVA IMS Information Management School, Lisbon</span></li>
<li><em><strong>Master in Information Systems</strong></em> – 2010–2012<br>
<span class="edu-school">Research Area: Knowledge Representation & Reasoning, PPGI/UNIRIO</span></li>
<li><em><strong>Bachelor in Computer Science</strong></em> – 2004–2009<br>
<span class="edu-school">Metodista Bennett, Rio de Janeiro</span></li>
<li>Thesis: Agile Systems Development for Web 2.0 with Ruby on Rails</li>
</ul>
<h3>Relevant Professional Experience</h3>
<ul>
<li><strong>Swisscom</strong> – <em>2024–2025</em> – <em>Zürich, Switzerland</em>
<p class="job-desc"><strong>DevOps Engineer</strong> developing platforms for Telecommunication Operations.</p></li>
<li><strong>EDGE Empower®</strong> – <em>2024–2025</em> – <em>Baar, Switzerland</em>
<p class="job-desc"><strong>Senior Technical Lead</strong> managing projects and external UX & Dev Teams for a SaaS Company.</p></li>
<li><strong>ExtraPolo — Independent Consulting</strong> – <em>2010–2022</em> – <em>Rio de Janeiro · Berlin · Athens · Lisbon</em>
<p class="job-desc">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.</p></li>
<li><strong>Moedax Digital Services</strong> – <em>2018–2021</em> – <em>Lisbon and Rio de Janeiro</em>
<p class="job-desc"><strong>Founder & CTO</strong> – connected Brazilian CBDC with KYC processes and automation</p></li>
<li><strong>GFOSS.eu / Code + Create</strong> – <em>2017–2018</em> – <em>Athens, Greece</em>
<p class="job-desc"><strong>Web Programming Teacher</strong> – taught web programming to refugee and local youth</p></li>
<li><strong>Tactical Tech Collective</strong> – <em>2014–2016</em> – <em>Berlin, Germany</em>
<p class="job-desc"><strong>Full-Stack Web Architect</strong> on an NGO under the Exposing the Invisible Team.</p></li>
<li><strong>Trommsdorf + Drüner</strong> – <em>2013–2014</em> – <em>Berlin, Germany</em>
<p class="job-desc"><strong>Full-stack Engineer</strong> – Innovation Team developing Business Intelligence dashboards.</p></li>
<li><strong>Cortex Intelligence</strong> – <em>2007–2008</em> – <em>Rio de Janeiro, Brazil</em>
<p class="job-desc"><strong>Developer</strong> in the Competitive Intelligence and Data Mining Team.</p></li>
</ul>
<h3>Certifications</h3>
<ul>
<li>Swiss Federal Act on Data Protection (nFADP)</li>
<li>Gen AI Foundational Models — NLP & LLMs</li>
<li>AI Agents using RAG and LangChain</li>
</ul>
<h3>Relevant Tech Skills</h3>
<ul>
<li>System Analysis & Modeling with UML</li>
<li>Quick Proof-of-Concepts development, deploy and management.</li>
<li>Large Language Models, Gen AI, LangChain integration and Processes Automation</li>
<li>MVC System Architecture, Design Patterns & Object-Oriented Programming</li>
<li>Data Engineering, Data Science, Relational Databases & SQL</li>
<li>Experience with Blockchain, Smart Contracts and P2P systems</li>
<li>Distributing and Scaling Processes with Message Queues</li>
<li>Developing Web Crawlers & Bots for Data Mining & ETL</li>
<li>Linux Systems, VPS, DevOps, SSH & Deploy with Continuous Integration processes</li>
<li>Projects mostly developed in Python, Ruby on Rails, Django, Java</li>
<li>Full Stack Developer & Front-end Web Design with HTML, CSS and JavaScript frameworks</li>
<li>CI/CD, Docker, Kubernetes and GitHub workflows</li>
</ul>
<h3>Natural Languages</h3>
<ul>
<li>Native Brazilian Portuguese</li>
<li>Advanced Spoken Spanish</li>
<li>A2 level Greek – <em>3 years in Athens</em></li>
<li>B1 level German / TELC proof – <em>3 years in Berlin</em> and 3 in Switzerland</li>
<li>Fluent English – <em>13 years in between international teams</em></li>
</ul>
<div class="footer">
References available upon request · CV updated May 2026
</div>
</div>
<div class="right-col">
<!-- Photo placeholder - remove if not needed -->
</div>
</div>
</body>
</html>
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+130
View File
@@ -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
Binary file not shown.
Binary file not shown.
Binary file not shown.
+65
View File
@@ -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<string>();
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<string>();
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<string, number>();
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" : ""}`);
}
+38
View File
@@ -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<string>();
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}/`);
}