Skip to content
Pegel Jobs

Changelog

What shipped, in the order it shipped. Short notes only.

  • infra

    AI training crawlers now skip filtered listings

    Two AI training crawlers were most of the traffic reaching Pegel this week. ClaudeBot and GPTBot walked through every combination of the job and company filters, about 1.6 million requests a day, for pages that are already marked not to be indexed. robots.txt now asks training crawlers to leave filtered listings alone. Search engines and the assistants that fetch a page while answering someone's question keep full access, so Pegel stays findable and quotable.

    Job pages also rebuild less often. A role only changes when the poll runs, and the poll already refreshes every page it changed, so the page no longer re-renders every five minutes in between.

    What this will not do: robots.txt is a request, and a crawler that ignores it keeps coming. A rate limit at the network edge is the backstop for that case. And a job page that falls outside the poll's refresh cap, which only happens during a mass change, can now sit up to an hour behind instead of five minutes. The same goes for a German-requirement label that the nightly classifier fills in: the role shows unknown a little longer before the label appears. A correction made by hand still shows at once.

  • data

    Seventy-one more companies now feed the board

    Pegel now polls 541 companies, up from 470. The new names came from other job boards, mostly the company directory on databerlin.net and the listings on Berlin Startup Jobs. Pegel treated each listing as a lead and checked the company's own careers feed before adding it.

    The list reaches past startups this time. JetBrains, Axel Springer, Statista and Pennylane sit next to young teams like Zasta and Nucs AI. Each one was added because its public feed carried at least one Berlin role on the day of the check, and every one of those feeds ran through the same adapter that polls it three times a day.

    YGO Trips moved its openings from JOIN to Ashby, so its roles had stopped appearing. Pegel now reads the new board.

    What this will not do: a company whose feed lists no Berlin role stays off the board, even when it hires in other cities. Pegel also left out a recruiting platform whose feed mixes its own openings with roles it fills for clients, because those jobs would carry the wrong employer's name. Company profiles for the new names are thin for now; where a fact such as the founding year could not be traced to a source, the field stays empty.

    The site now speaks as Pegel throughout. Blog posts, landing pages, the FAQ and older changelog entries used to say "I", which blurred who was speaking; they now name Pegel. The founder's own note on the About page stays in his words. A check in the voice linter keeps first person from creeping back. It will not rewrite a quotation: an interview answer or a quoted line stays in the speaker's own words.

  • infra

    Pegel now runs on Cloudflare

    Pegel moved its hosting from Vercel to Cloudflare Workers. What you see does not change. The board works exactly as it did yesterday, now served from Cloudflare's network instead of Vercel's. What changed is where the code runs and what it costs Pegel to run it, which matters for a free board with no employer fees.

    Two details worth knowing. Page-view counting now runs through Cloudflare Web Analytics, still cookieless, with aggregate counts kept for six months. The privacy page lists Cloudflare as a sub-processor in place of Vercel, with the EU-US Data Privacy Framework as the transfer basis.

  • feature

    Filter jobs by role family

    Pegel added a Role filter to the jobs page and the public jobs API. Saved email alerts and the iOS app use the same matching rules. Pick Finance together with Legal and you get roles tagged as either one. Alerts are saved separately on the website and in the app.

    The filter reads the role a job was already classified as, never the words in its title. A "Finance Software Engineer" lands under Finance when that is how Pegel tagged it, not under Engineering. A job Pegel has not reviewed yet stays out of every role family instead of being guessed into one, so a family's count means roles with real evidence behind them rather than an estimate. About three in four active Berlin roles carry a role tag today. The rest fill in as the weekly classification pass reviews them.

  • fix

    Pegel reads direct "German not required" statements, not just "nice to have" ones

    The language classifier already caught "German is a plus" and "no German required" as signals that a role does not need German. A posting that negates the requirement directly, like "German is not required" or "Keine Deutschkenntnisse erforderlich", used neither phrasing, so it fell through every pattern and abstained. That silently dropped the role from the No-German filter even though the employer had said, in plain words, that German was not needed.

    Pegel added seven more patterns, four in English and three in German, covering standalone direct-negation statements. The recognized German requirement patterns still take precedence.

    "Fluent German is not required, but basic German is mandatory" still means German is required. Pegel now catches explicit basic or spoken German requirements alongside these exemptions. An exemption on its own stays unknown; the posting being written in English does not override that uncertainty.

    Pegel still only matches a fixed set of phrasings. A negation worded some other way, in either language, still abstains to unknown rather than guessing.

  • feature

    One link for Pegel stickers

    Pegel added `/go` as a permanent destination for sticker QR codes. It sends browsers identified as iPhones to Pegel's App Store listing. Other devices open the jobs website. Android stays on the website for now.

    The redirect does not set tracking cookies or cache a device's destination. It opens the App Store listing, not the installed app. The destinations can change later without reprinting the QR code.

  • infra

    Filtered searches now have a daily aggregate

    Pegel added one daily filtered-search count for the private office display. The website records a search only after a valid filter change has settled. The iPhone and Android apps use the same rule, but they will only contribute after those versions are released.

    The count is one combined integer for the Europe/Berlin day. It does not keep the search terms or selected filters. It does not keep visitor or device identifiers either. Recording is best-effort, so this is not a count of unique people and it can undercount.

    Filtered job results also moved onto one verified snapshot. Different filter URLs now share the same cached data response instead of opening new database work for every combination.

  • fix

    RSS items now carry every sector, not just the primary one

    A company can belong to more than one sector on Pegel; the filters on `/companies` and the sector pages already match on any of them. The RSS feed did not. Each item's `<category>` only ever carried the primary sector, so a feed reader filtering on a secondary sector silently missed jobs Pegel itself lists under that sector.

    Every item now emits one `<category>` per sector the hiring company belongs to, primary first. A sector that is listed as both primary and secondary only appears once.

    What this will not do: it does not add any new sector to a company, and it does not change which items appear in the feed. It only makes the categories already implied by a company's listed sectors visible to readers that filter on them.

  • fix

    More job pages keep verified publication dates

    On 4 September Pegel removed every ATS creation timestamp from job markup because Personio's XML date is not a reliable publication date. That rule was too broad. In the provider pages Pegel checked, JOIN and Lever expose creation dates that match their public posting dates.

    Pegel now uses `publishedAt` first for JOIN, with its verified `createdAt` as the fallback. Lever uses its verified `createdAt`. For Personio, Pegel only uses `datePosted` when the public job page includes it in `JobPosting` data.

    If a provider does not supply that evidence, the date stays empty. Pegel's first-seen date is still not used as a substitute. A failed Personio detail request also cannot erase a date that was already observed.

    This gives more eligible pages complete job markup. It does not promise that Google will crawl or index them. Placement in the jobs result remains Google's decision.

  • fix

    Filter protection no longer blocks other visitors

    A burst of automated filter combinations could fill the database connection queue. Pegel's first response was a shared application limit. It protected the database, but one busy client could consume the allowance for everyone. Real visitors then saw a 429 error while applying filters.

    That shared limit is gone. Repeated automated filter traffic is now restricted per IP by Vercel Firewall before it reaches the app. One client can no longer block another visitor's search.

    Pegel still runs on the same small database budget.

  • fix

    The privacy policy now covers Android alerts

    Android alerts already used Firebase Cloud Messaging, but the privacy policy only described the iPhone path. It now names both providers and the small set of installation data each one needs.

    The alert model did not change. There is still no account. Push registration identifiers stay encrypted on the server. Each alert keeps its own update and delete capability, while alert names remain on the device. The Android capability is protected with Android Keystore. The policy now says this directly.

  • fix

    Android alerts now name the right identifier

    Pegel corrected the privacy note shown in Android alerts. It now says that Pegel sends the app installation ID and the chosen filters, which matches the current Firebase registration path.

    The alert model stays the same. There is no account, and the alert name stays on the device.

  • fix

    Job schema stops guessing dates

    Pegel stopped filling job markup with its own observation dates, or with ATS creation and edit dates. An active role now needs an employer publication date before it emits `JobPosting`. When the employer has not supplied a close date, Pegel leaves `validThrough` out.

    This changes the data Pegel sends in markup. It does not promise Google crawl, indexing, placement, or a jobs result. Existing rows also need a successful repoll through the corrected adapters before they can be treated as refreshed source data.

  • feature

    The Claude skill keeps the job decisions you log

    Every new job search could return a role you had already dismissed or applied to. The Pegel API had no reason to know your decision, so the skill forgot it between sessions. That got annoying quickly.

    Pegel added a local Job Log with dated status events. Normal searches now hide every role already logged. Shortlisted roles and full histories remain available when you ask for them.

    The file stays on your machine and nothing is written back to Pegel. A rejection reason exists only when you provide one; silence never becomes a rejection.

    Similar-role matching and catalogue refresh are not part of this release. They need separate contracts before Pegel can make honest promises about them.

  • fix

    A repeated city name no longer splits the hiring chart's city bars

    Greenhouse posts some locations as "Berlin, Berlin, Germany", repeating the city ahead of the country name. The earlier country-suffix strip only removed the ", Germany" part, leaving "Berlin, Berlin" behind as its own bar next to plain "Berlin" on the hiring chart. Pegel now collapses an exact, case-insensitive repeat of the city before it buckets the location, so both forms count as the same office.

    This only fires when the two comma-separated parts match. A genuine two-city location, like "Berlin, Barcelona", still shows as two cities.

  • fix

    Company filters now clear one at a time

    The remove button on a company filter sent the jobs page back to the same filtered URL. The chip looked removable, but only Clear all changed the results.

    The missing company case is fixed. The button now removes that company filter and keeps the other active filters.

  • fix

    Viola now answers the interview in her own voice

    The Berlin job-search interview asked Viola seven questions, then described most of her answers in the third person. It read like a summary when it was meant to be a conversation.

    Each answer is back in first person, lightly edited from what Viola sent. Pegel's own notes and external guidance now sit under separate headings, so it is clear who is speaking.

    Pegel did not add new advice or market claims to Viola's answers.

  • fix

    Dead application links now close the role

    An Ahrefs crawl found active Pegel job pages whose employer application URL already returned 404 or 410. The role page still offered Apply. People reached a dead end, and crawlers recorded a broken outbound link.

    Pegel now requires the same conclusive failure twice before marking a role expired. The first missing response only records evidence. A second consecutive 404 or 410 removes the role from active listings and sends the existing search update.

    Bot blocks, rate limits and network failures do not close a role. If the company feed later supplies a different application URL, Pegel clears the evidence about the old URL and can reopen the role.

  • fix

    Pegel reads salaries quoted number-first, not just symbol-first

    The salary parser already caught "up to €120k" and "from €60k", where the currency sign comes before the number. A posting that quotes the same figure the other way round, like "up to 120k EUR" or a bare "80.000 EUR", fell through every pattern and abstained, so a real disclosed salary went missing from the job page and the salary filter.

    Pegel added the number-first mirror of each pattern: directional ("up to X EUR", "from X EUR", German "ab X EUR") and bare singles ("80.000 EUR"). The same compensation-context check and the €25k-€500k sanity bounds still apply, so this only recovers real disclosures, it does not loosen what counts as one.

    There is also a dry-run-first repair for active roles that were already stored without a salary. It uses this same parser, makes no model calls, and never replaces an existing salary.

    Pegel still only strips a trailing "{number}{symbol}" pair. A currency embedded mid-sentence in some other shape, or a range written as "60 to 90 thousand EUR", is still unread.

  • fix

    RSS items get a stable id, a full title, and a byline

    Every RSS item's guid was the job's permalink URL, and that URL embeds the job title. When a company retitled a posting, the guid changed with it, so every subscriber saw the same role reappear as a brand new item.

    The guid is now the job's own id, which never changes even when the title does. Alongside that fix, each item's title now reads "role at company" instead of the bare role, the category carries the sector's slug instead of its display label, and each item names the hiring company as its creator.

    What this will not do: on the first fetch after this ships, the guid change means every item currently in the feed will look new to your reader one more time. After that one-time reshuffle, retitling a role will not create a duplicate again.

  • feature

    A clearer path from search to alert

    Pegel made it easier to turn a useful search into a daily alert. Filtered listings now point to the existing alert form, while the confirmation screen explains exactly what happens next.

    Visa support on role pages now names the evidence level. “This role” means the posting says it. “Company signal” means the posting is silent and Pegel has a verified company-level signal. A company signal is not a promise for every opening.

    The rendered weight of the jobs and company directories also came down. The August hiring pulse is published with a dated snapshot, and its figures describe listings rather than hires. The native iOS launch presentation has been refreshed too.

  • infra

    The crawler Pegel turned away came back under a new name

    Two days ago Pegel asked Meta's crawler to stop reading the job list. It was making about 11,900 requests an hour, which was most of the traffic arriving at the site. The polite way to ask is a line in robots.txt, and compliant crawlers honour it. Google's research crawler did when Pegel asked it the same thing last week.

    Today's check: the company pages are down 97 percent and the quieter pages are down about the same, so the request did land. The job list is unchanged. What used to call itself meta-externalagent now calls itself meta-webindexer, and the robots.txt line names the old one.

    It is the same fleet. Same network, same machine fingerprint, same four browser signatures in the same proportions, addresses shifted one block over. Only the name is new.

    So Pegel has done two things. The new name goes in robots.txt, which costs nothing and is still the right way to ask. And there is now a rule at the edge that turns away this particular crawler on the job list, matched on how its connection is built rather than on what it calls itself. A name can be changed in a deployment. The rest is harder to move.

    What this will not do. The rule is deliberately narrow: it covers the job list and nothing else, so the separate Meta service that builds the preview card when you share a Pegel link on Facebook or WhatsApp is untouched. It also means the crawler could move to another part of the site, and Pegel will be watching for that.

    Pegel should also correct something implied on Tuesday. That note said letting the edge hold filtered job lists would blunt this crawl. For you it helps, because you come back to the same filters. For a crawler walking every combination once it does almost nothing, since a cache only pays off the second time a page is asked for. About 96 percent of its requests still reach the server. The caching was worth doing. It was not the defence it was presented as.

  • infra

    The job list page got a lot lighter

    The job list was 654 KB of HTML. Not images, not JavaScript, the HTML itself. Roughly half of that was the filter sidebar down the left side.

    The sidebar puts every filter option into the page, including the ones tucked behind "Show 146 more". It has to. That expander is plain HTML with nothing running behind it, so there is no way to go and fetch the rest when you open it. The tech tag group alone carries 154 options, and all 154 ship on every request.

    Each row was carrying about 400 characters of styling instructions, repeated on every single row, then repeated a second time in the data the page sends along so links feel instant. Ninety nine distinct styling strings appeared one thousand nine hundred times between them.

    The repeated ones moved into the stylesheet, which your browser fetches once and then keeps. A filter row went from 580 bytes to 257.

    What this will not do. Nothing looks different, and if anything does look different to you then something broke and Pegel would like to know. This is also not really a saving on the amount you download, because compression was already squashing that 654 KB to about 26 KB on the wire. What it saves is the work your browser does after the download, building and holding all that markup, which is the part that is felt on a mid-range phone rather than on a developer's laptop. The page is still bigger than it should be. The 154 tech tags are the next question, and that one is about whether a sidebar is the right home for a list that long, not about bytes.

  • infra

    Meta's crawler was most of Pegel's traffic

    Meta runs a crawler called meta-externalagent that reads pages to train AI models. Today it was asking for the job list about 11,900 times an hour on its own. That worked out to 86 percent of everything arriving at the site, far more than every real visitor put together.

    The job list is the most expensive page Pegel serves. Any version of it with a filter applied has to be built from scratch on every request, because the filters live in the address bar and the answer changes with them. Meta was walking 142 different filter combinations in a loop. Every one of those already carries a "do not index" instruction, and 295 of the 296 filter links on the page ask crawlers not to follow them. It followed them anyway.

    So robots.txt now asks meta-externalagent not to crawl. The crawler that builds the preview card when someone shares a Pegel link on Facebook or WhatsApp is a different one, and it stays untouched, so share cards still work. The assistants Pegel does want reading the site keep full access, because being quotable inside an answer is worth paying for.

    The second change is the one that matters longer term. Filtered job and company lists can now be held at the edge for five minutes. Before today, every filtered view was rebuilt from nothing on every single request, whether that was a crawler on a loop or you clicking a filter chip. Now the second request for the same filtered view is served from the copy that already exists.

    What this will not do. The five minute window is a ceiling rather than a promise, and it is not where freshness comes from: a new role still shows up the moment the poll finishes, because the poll clears the cache itself. Blocking a crawler in robots.txt only works on a crawler that reads robots.txt, and Meta says this one does. If the traffic does not drop over the next day, the next step is blocking it at the network instead, which Pegel would rather not need. None of this makes the job list a lighter page to load, which is a separate problem still open.

  • fix

    The Jobs tiles sit still now

    The four teal Jobs tiles in the Pegel wordmark used to lean in different directions and carry a faint printed texture. The email version lost those details in transit and looked better for it.

    That cleaner result is now intentional. The tiles are now flat and evenly aligned on the website and in email. The iPhone launch screen keeps them beneath Pegel, where the vertical arrangement fits, but uses the same quiet treatment.

  • feature

    Job rows now show when Pegel last confirmed they're still live

    Every row in the job list already said when a role was posted. It never said whether Pegel had checked lately that the listing still holds up.

    Each row now carries a small "Seen today" chip (or "Seen yesterday", or a date, if the poll hasn't touched it since). It comes from the same poll that keeps the whole list current, so it answers a different question than the posting date: not when the role first appeared, but whether Pegel looked again recently and it was still there.

    What this will not do: it can't promise the role is still accepting applications this exact second, only that it was live as of the last poll. And it says nothing about roles the poll hasn't reached yet. This just puts the freshness signal on the row it actually vouches for, instead of only in the "Last updated" line at the top of the page.

  • infra

    Pegel turned away one of Google's crawlers

    Google runs more than one crawler. Googlebot is the one that reads pages for search results, and it is welcome here, because search is how almost everyone finds Pegel. GoogleOther is a different one, used by teams inside Google for research and product work, and Google says plainly that it plays no part in search.

    A look at where the traffic actually comes from today settled it. GoogleOther was the single largest source hitting the site, ahead of every real visitor put together, and most of what it asked for was filtered views of the job list that carry a "do not index" instruction anyway. It was fetching pages that nothing would ever do anything with.

    So robots.txt now asks GoogleOther not to crawl. Googlebot is untouched, and so are the assistants Pegel does want reading the site: ChatGPT, Claude, Perplexity and Gemini's crawler all keep full access, because being quotable inside an answer is worth paying for.

    Pegel also blocked a scraper running from a cloud provider that was walking through every filter combination on the job and company lists, and a request that had been hammering a page which does not exist.

    What this will not do: none of it changes what you see, and none of it touches search. If anything it means the pages you do load are competing with less machine traffic for the same server.

  • infra

    The pre-launch password gate is gone

    Before the soft launch, Pegel sat behind a password. Visiting any page without the right cookie bounced you to an unlock screen. That gate has been switched off since the soft launch, and the password it checked for was set in no live environment, so for months it had been code that ran on every request and did nothing.

    The gate is gone: the check, the unlock screen, and the endpoint behind it. The `/unlock` URL no longer exists.

    Two things that lived in the same file stay exactly as they were, because deleting the file wholesale would have quietly taken them with it. Addresses that differ only by capitalisation still redirect to one canonical lowercase version, which is what stops search engines seeing two copies of a page. And the 404 logging that shows which missing pages people actually land on still works.

    What this will not do: it does not change who can see anything, because the gate was already open to everyone. Nothing about the site becomes more or less public than it was yesterday.

  • infra

    A lighter company list, and one more crawler turned away

    The company list was asking the database for every single thing it knows about all 413 companies, on every request, and then rendering about nine of those things. It was carrying founder histories, funding round details and postal addresses across the wire so it could show you a name, a sector and a role count.

    It now asks only for what it uses, plus the handful of extra fields the public API needs. Nothing you see changes, and the API returns exactly what it returned before, field for field.

    Pegel also asked Amazonbot to stop crawling. It feeds Alexa and Amazon shopping, neither of which lists Berlin startup jobs, and it was requesting more pages per hour than every real visitor combined. The assistants Pegel does want here, ChatGPT, Claude, Perplexity and Gemini's crawler, all keep full access, because being quotable in an answer is worth the traffic.

    What this will not do: it does not change the company list, the filters, or anything in the API. If you are reading Pegel through the API, nothing about your integration needs to change.

  • infra

    The job and company lists are served from a cache now

    Opening the jobs list or the companies list used to rebuild the whole page on the server, every single time, for every visitor. Two database queries, a fresh render, no reuse. That was fine when a few hundred people a day were reading it. Since the 19th of July an automated client has been requesting those two pages about 140,000 times a day, and every one of those was doing the full work.

    Both lists are now built once and reused. When a poll finds new roles, the cached copy is thrown away immediately, so a new posting still shows up as fast as it did before. The rebuild timer is only a fallback for anything that changes by some other route, and it is set to five minutes.

    Filtered views are untouched. The moment a URL carries anything after the question mark, a search term, a filter, a page number, it goes down the old live path and is built fresh. That is deliberate: serving a cached unfiltered list to someone who asked for a filtered one would be worse than any amount of saved compute.

    What this will not do: it does not make the site faster for the first person to arrive after each update, who still pays for the rebuild. It does not reduce the number of requests arriving, only what each one costs Pegel. And between polls the lists can sit behind reality for as long as that fallback timer, where before they were always current to the second.

  • infra

    The pageview counter now skips the admin area and preview builds

    Pegel counts pageviews through Vercel Web Analytics, which the privacy page describes: aggregate counts and an approximate region, cookieless, no cross-site identifier. That part is unchanged.

    What was wrong is where it ran. It reported every page of every deployment, including the admin screens used to moderate companies and read the inbox. Those URLs carry database row ids, and there was no reason for them to reach a third party. It also ran on preview deployments, which Vercel records as though they were the live site, so half-finished branches were quietly mixed into the real numbers.

    Both are now excluded. Public pages are counted exactly as before.

    What this will not do: it does not change what gets collected on a public page, and it does not remove anything already recorded. It also does not touch the automated traffic that has been hitting the job list since the 19th of July, which is a separate problem Pegel is handling at the network layer rather than in the page.

  • fix

    Searching no longer throws away your filters

    If you set a filter on the jobs page and then typed a word into the search box, six of the fifteen filter dimensions quietly vanished. The sidebar checkbox stayed ticked and the chip stayed on screen, so the page told you a filter was active while the results ignored it. The ones that got dropped were company scope, workplace mode, remote mode, tech tags, the ghost-role toggle, and the disclosed-salary toggle.

    The search form carried a hand-written copy of the filter list, and the copy fell behind as new dimensions arrived. It now derives from the same code that builds the canonical URL, with a test that fails if the two ever drift apart again.

    Two smaller repairs rode along. Job descriptions and the API reference could not be selected or copied, because a rule written for the dashboard's number tiles had been applied to the shared card frame that also wraps written content. You can copy a posting into your notes again. The command palette also stopped reopening with a stale row highlighted, which used to send the Enter key to whatever you had arrowed onto last time.

    What this will not do: none of it changes what the filters mean or adds new ones. It stops the search box from discarding the ones you already chose.

  • fix

    Google indexing runs at full speed again

    Pegel tells Google about every new Berlin job page so the posting has a chance to appear in search. That notifier has been running at half speed, and the submission log is where it showed up: Google received 180 URLs on the 21st of July, nothing on the 22nd, 180 on the 23rd, nothing on the 24th. IndexNow, the other search notifier, ran on every one of those days, so the queue was never empty.

    The cause was two clocks that did not agree. The job runs once a day at a fixed time, but the budget check asked how many URLs had gone out in the previous 24 hours. Each run writes its log entries a few seconds after it reads that budget, so yesterday's batch always landed just inside the window and the budget came out at zero. A day that sends nothing also logs nothing, which left the window clear for the next day, so the job settled into an on-off rhythm and stayed there. The check now counts from the moment Google's daily quota actually resets.

    The same pass also stopped making new roles wait. The job used to run once a day, so a role that appeared at lunchtime sat until the next morning before Google heard about it. It now runs again after each of the three daily company checks, at 08:15, 11:15 and 17:15 UTC. New roles are announced ahead of older queued pages, so in practice they go out within a few hours of appearing on the board.

    What this will not do: it does not raise the ceiling. Pegel still sends at most 180 URLs a day against an allowance of 200, so the extra runs improve how quickly a fresh role is announced, not how many pages move in total. With 2,734 pages queued the backlog still needs roughly two more weeks. Telling Google about a page is also not the same as Google indexing it. It is a request, and the decision is theirs.

  • fix

    Job alerts stop notifying you about roles outside Berlin

    Pegel's iPhone alerts have been pushing roles that are not in Berlin. The delivery log this morning showed it: of the first 786 notifications sent, 623 pointed at jobs outside the city, in places like San Francisco and Dubai, or at postings whose only stated location was "Remote". Tapping one opened the app and produced "Couldn't open that role", because the job page applies a Berlin filter that the notification path skipped.

    The cause was one missing condition. Every other surface on Pegel narrows to roles whose location says Berlin. The query deciding which new jobs deserve a notification did not, so it picked up the global postings from the companies Pegel tracks and sent them to your phone. The alert query now uses that same Berlin filter, and a new test refuses to let a role outside the city reach a device.

    What this will not do: it cannot recall the notifications you have already received, so those still point at roles the app will not open. It also does not widen what counts as Berlin. A role qualifies when its stated location contains the word, which means a genuinely Berlin-based job advertised only as "Remote" still will not trigger an alert.

  • data

    English-only postings now count as not requiring German

    Some roles never say a word about language. If a posting was written entirely in English and never mentioned German, Pegel used to leave the German-requirement label blank rather than guess. That left a few hundred live roles marked "unknown" even though the signal was sitting right there: the company wrote the whole ad in English.

    Pegel now reads that as what it is. A role advertised entirely in English, with no German requirement stated anywhere, is one where English is enough to apply, so it gets the label "German not required." This is the mirror of a rule Pegel already runs in reverse, where a posting written in German is treated as needing German.

    What this will not do: it does not override the posting. If an English-language ad asks for German somewhere in the text, that explicit line still wins and the role is marked as needing German. And when a posting is too short or too mixed to tell which language it is written in, Pegel still leaves the label blank instead of guessing.

  • data

    Jobs now refresh three times a day

    Until today Pegel polled every company's careers feed once a day, before dawn. A role posted at 9am Berlin would not show up here until the following morning. That is a long wait for something often filled within days.

    Pegel now polls three times a day. The first pass still runs before dawn. Two more follow during Berlin's working hours, around midday and again in the early evening. A job posted during the workday surfaces the same day, and if you use the iOS app, its alert reaches you at lunch instead of the next morning.

    Nothing else about a listing changes. The posting text is still copied word for word from the source, and Pegel still shows only the salary an employer chooses to disclose.

    What this will not do: the classifier that decides whether a role needs German still runs once a day, overnight. A job picked up at the midday poll can sit with an unknown German requirement until that nightly pass fills it in. Pegel also will not poll more often than three times a day. If a role is posted at 2pm and pulled at 4pm, that two-hour lag is the tightest Pegel offers.

  • feature

    A real menu on phones

    Until today the phone header held the wordmark and a search icon, nothing else. Every navigation link lived in the desktop row that never renders below 640 pixels. If you landed on a job page from Google, the only way out was the wordmark.

    Phones now get a menu button next to search. It opens a drawer listing the same pages the desktop header carries, read from the same source, so the two can never drift apart. The header also stays pinned while you scroll on small screens; on a list of 2,700 roles that matters.

    The homepage got tightened in the same pass. The headline sat under 96 pixels of empty cream on a phone; it is 48 now. The Claude skill install block no longer shows on phone screens, since you cannot run a terminal command from one. It still lives on desktop and at /tools/claude-skill.

    What this will not do: the drawer has no gestures. There is no swipe to open and no swipe to close. You tap the button, and you tap your destination.

  • fix

    Country names no longer split the hiring chart's city bars

    Several ATS feeds post a location as "Berlin, Germany" instead of plain "Berlin". The hiring chart on company pages read those as two different offices, so one team's roles split across two bars. Pegel now strips a trailing Germany, Austria, or Switzerland (in English or German) before it buckets the location, so both forms count as the same city.

    Pegel only strips the DACH countries it actually covers. A location tagged with any other country, like "Amsterdam, Netherlands", still falls through unrecognized.

  • fix

    Searching a company name now works

    Typing "GetYourGuide" into the search box used to return nothing, even with 45 of its roles live. Search only looked at job titles, and no title contains the company's own name.

    Search now matches the company name too, on the site and in the iOS app. Type a role, type an employer, either way you get results. The placeholder text says so now instead of promising less than the box does.

    What it still will not do: search does not read job descriptions. A term that only appears deep in the posting body will not surface it; titles and company names are the whole index.

  • fix

    The FAQ and terms now describe how reporting actually works

    Both pages claimed every role page carries a "Report this listing" link. No such link exists. What does exist: reporting a listing works by email, and every company page links a removal-request form for taking a whole company off Pegel. The FAQ and terms now say exactly that.

    This entry changes copy only. Role pages themselves are unchanged, and whether they should carry a report link one day is a separate decision.

  • fix

    The privacy policy caught up with what actually runs

    The policy still said the email digest was coming and that only one classifier ran. Both claims aged out: the digest ships with unsubscribe and manage links in every email, and five classifiers run in production. Pegel moved those sections to the present tense and corrected the forms section too. The suggest-a-company form is live and stores no IP address; there is no report-listing form, so flagging a listing works by email.

    One promise gained an implementation instead of an edit. The policy said form submissions are deleted after 90 days, but no cleanup existed. The nightly retention sweep now deletes company requests older than 90 days, so that sentence stays because it is finally true.

    This pass does not touch the FAQ or the terms page. Both still mention a report link that role pages do not have, and that correction is a separate change.

  • fix

    Internal phase numbers no longer appear in public copy

    The privacy policy and the design preview page referred to internal build phases ("Phase 14", "Phase 23") that mean nothing outside the founder's planning notes. Pegel removed the numbers, so the pages now describe features as shipped or planned in plain words. One stale line was corrected too: there is no photo submission flow, and the policy no longer promises one.

    This entry does not rewrite the privacy policy itself. A few sections still describe shipped features in the future tense, and a fuller accuracy pass is pending.

  • data

    The work-mode label now reads the whole posting

    The location chip on every role carries a work mode, and until today Pegel took that mode from a thin signal: a keyword in the location line, or the quick scan the poll runs as each role arrives. That scan gives up on most postings, so a role with no "remote" or "hybrid" in its location line fell back to Onsite even when its description told a different story.

    Pegel now runs a scheduled pass that reads the full description for the roughly seven thousand roles the quick scan could not place. It records Hybrid or Remote only when the text is clear, and leaves the role without a mode when the posting stays silent. Pegel does not guess a mode to fill a gap.

    The pass runs on the Claude Code subscription, not a paid model, and reads each unclear posting once. A role whose description later changes gets read again. Labels sharpen over the next day as the pass works through the backlog.

  • data

    Location now reads as Berlin plus a work mode

    Job locations used to show whatever the company's system sent. "Berlin, BE, Germany" on one role, "Berlin, Berlin (Hybrid)" on another, sometimes a four-city line with Berlin buried at the end. Every role on Pegel is a Berlin role by definition, so most of that just repeats the city and hides the part you weigh. Each listing now reads as Berlin plus one work mode.

    There are three modes. Onsite means the role wants you in the office. Hybrid means it splits the week. Remote means the office is optional. A role that offers a Berlin desk or fully remote counts as Remote here, since the remote option is the part that changes your decision.

    The mode comes from the classifier when it has read the posting. When it has not, Pegel takes the mode from the words in the original location line: "remote" reads as remote and "hybrid" as hybrid. A plain Berlin with no such word is read as onsite. That last rule is a reading of the common case, not a claim the employer made.

    One thing Pegel does not do is drop the other cities from the record. A role open in London and Berlin still lists both for search engines and aggregators. The board just shows you the Berlin side of it.

  • fix

    Job pages stop scrolling sideways on wide screens

    On a wide screen, some job pages scrolled sideways. The description column would not shrink to its share of the width, so the Apply button and the company sidebar beside it slid off the right edge, and long lines of description text were cut off at the border.

    The cause sat in two places. The page splits into a main column and a fixed sidebar, and the main column was told to hold its content's natural width instead of giving way. Separately, a few employers post descriptions where the words are joined by non-breaking spaces, so a whole sentence became one long unbreakable run that refused to wrap. Between them they forced the layout wider than the window.

    The main column now yields to the space available, and the description wraps even across those non-breaking runs. The Apply button and the sidebar stay on screen now, and the text stays inside its card.

    Descriptions that glue a whole line together with non-breaking spaces now wrap at the spaces between words, the same as ordinary text, while a short binding like a quantity and its unit stays on one line. The layout keeps a second guard for anything else that will not fit, such as a single very long link with no break point at all. That still breaks wherever it must, which is rare and never cuts text off.

  • fix

    Fourth-level headings in job descriptions now look like headings

    Some employers structure their postings with fourth-level section titles. Greenhouse and Recruitee feeds use them constantly. The job page styled the first three heading levels and quietly skipped the fourth, and because the stylesheet resets headings to plain text, those section titles rendered as ordinary body copy that ran straight into the paragraph below.

    They now render like their siblings: bold at body size, with space above them. Nothing else about the description changes. The text itself is still shown verbatim, exactly as the employer published it.

  • feature

    The job detail API now returns the formatted description

    The public detail endpoint, /api/v1/jobs/{id}, already returned the plain-text job description. It now also returns the formatted version: the same sanitized HTML the website itself renders. A native or third-party client can keep the employer's headings and lists instead of collapsing everything into one flat block of text.

    The formatting is the employer's own. Pegel does not rewrite or restyle it. The HTML runs through the same allowlist sanitizer the website uses, and it stays on the detail endpoint only. The list endpoint still returns no description at all, so a page of many roles stays small.

  • feature

    The company API now shares the address and stability signals

    The company detail endpoint under /api/v1 now returns the office address and the same stability signals the website shows: the CEO and their tenure, the founders, total raised, recent funding rounds and the verification date. The iPhone app uses this to render the company page with the same cards as the web instead of a thinner summary.

    The gate is unchanged. Signals appear only for companies whose facts were verified by hand, and everything else returns null rather than a guess. An address shows only when one has been curated; nothing is scraped or inferred.

  • infra

    API responses can now cache on the device

    The public API under /api/v1 already told shared caches they could hold a response for five minutes. Devices never got the same offer: the CDN consumes that instruction and strips it on the way through, so a phone re-downloaded the full JSON on every request. Responses now carry a plain max-age as well, which means an iOS client (or any private HTTP cache) can reuse a response for five minutes without asking again.

    Each response also ships an ETag, a fingerprint of the body. Once the five minutes are up, a client can send the fingerprint back and ask whether anything changed. If nothing did, the answer is an empty 304 instead of the full payload, so revalidating a stale cache costs almost nothing on a metered connection. What the API returns did not change, only how often a client has to re-download it.

    The limit: a cached list can lag up to five minutes behind the daily poll, so a role that just expired may linger in a client's cache for that window. And the mobile alert endpoints stay uncached on purpose; those responses are per-client and must never be reused.

  • infra

    The remaining readers stop hauling descriptions

    Yesterday's fix stopped the main jobs listing from fetching the full job description and the raw feed payload on every page load. Today the same projection reaches every other reader that still did it. The full snapshot behind llms-full.txt and the resume matcher now selects only what it prints. So does the per-company role list, which feeds the company page and its RSS feed and the company API endpoint. So does the query that builds the daily email digest. None of these ever showed a description; all of them transferred one per row.

    The public API also caches now. Responses from the jobs and company read endpoints carry a five minute shared cache header, so repeated identical pulls are served by the CDN instead of hitting the database each time. The API used to declare itself uncacheable on every response. That was a launch-era decision from before the database bill made the cost visible, and Pegel is reversing it for the read endpoints only. Errors stay uncached, and so do the mobile alert endpoints, because their responses are private to the requesting device.

    The limit to know about: an API response can now be up to five minutes behind the database, so a freshly expired role can appear active in the API for that window. The data itself changes once a day with the poll, which makes the window invisible in practice, but it is real and Pegel would rather say so than have you discover it.

  • feature

    Posting age caps at thirty days

    Every role shows the age of its posting: today, yesterday, then a plain count of days. That count now stops at thirty. A role older than that reads "30+ days ago" whether it has been open five weeks or five months.

    Past a month, precision is noise. A reader deciding whether to apply weighs "posted this week" against "posted a while ago"; nobody weighs three months against five. The old display also invited a question the number cannot answer, which is whether a months-old role is really still open. The expiry sweep already removes roles that leave their company's feed, so an old age on Pegel means genuinely long-open, and "30+ days" says exactly that without pretending more precision than the signal carries.

    What this will never become: an urgency label. No "be an early applicant", no countdowns, no nudges. The date is the whole signal, and Pegel intends to keep it that way.

  • infra

    The poll clears the cache behind it

    This morning's caching work left one seam. A page could keep serving its cached copy for a few minutes after the poll changed the data behind it, and the sitemap could lag a full hour. Small windows, but they sat exactly where the data moves.

    Now the poll and the expiry sweep purge what they touch. When a role expires, its own page is cleared in the same cron run that recorded the change, and so are the company page and the sitemap. A role whose title changed gets its old address cleared, because that is where the cached page actually lives. A newly ingested role clears the sitemap and the company page so it becomes discoverable at once; it has no cached page of its own yet, so there is nothing else to clear.

    The limits: purges cap at 200 role pages per run, so a mass event such as a company folding falls back to the normal five minute self-heal for the overflow. Landing pages and the sector and tag views are not purged one by one; they refresh on their own short windows, which already outpaces how fast the underlying data changes. And purging happens only when the cron runs, which is fine, because that is the only time the data changes at all.

  • infra

    Pages cache again

    Since launch, every page on Pegel has been rebuilt from the database on every single visit. Two small defects caused it. The 404 page read the request headers to log broken links, and a root 404 page that reads request data switches the entire site from cached pages to live rendering. Next.js does this silently; the cache settings on every other page kept looking correct while doing nothing. Separately, the job, company, sector, tag and landing pages declared a cache window without the companion export that tells Next.js the route may be cached at all, so those settings were dead code twice over.

    Both are fixed. Job pages now serve from cache and refresh every 5 minutes, company pages the same, sector pages every minute, tag and landing pages every 10. The 404 logging that started all this still works; it moved into the browser, which as a side effect stops vulnerability scanners (which never run JavaScript) from writing to the log at all. A test now pins the 404 page so nobody can reintroduce the header read without a loud failure.

    What this still will not do: the main /jobs listing is deliberately not cached. Its filters live in the URL and a cached page there would show the wrong roles, so it renders fresh on every request. And a role that expires can keep its cached page up for a few minutes until the window rolls over; wiring the poll to purge pages the moment it notices a change is the next thing to ship.

  • infra

    The jobs listing stops fetching what it never shows

    Pegel went looking for why its database traffic had climbed so far past what the traffic justified, and the answer was sitting in the listing query. Every row Pegel stores carries the full job description twice, once as HTML and once as plain text, plus the entire raw response the applicant tracking system sent back. The listing query asked for every column, so all three came across the wire on every page load. The listing renders none of them. It shows a title, a company, a location, and a few labels.

    Measured against the live database, one page of twenty five roles was pulling about 535 KB of description bodies and raw payloads to render 4 KB of visible fields. The listing now asks for the columns it renders and nothing else. Job pages and the API are unaffected: the single role view still reads the full description, because that view actually shows it.

    The reason this got expensive rather than merely wasteful is that a scraper has been requesting the listing roughly every four seconds, around the clock, for weeks. Nineteen thousand requests a day, each one hauling half a megabyte of text nobody read. A page nobody could cache, fetching data nobody could see.

    What this still will not do: it does not block the scraper, and Pegel has not tried to. Nothing here distinguishes a bot from a person, which is the point, because every visitor gets the faster page and no crawler risks being turned away by mistake. Search engines and AI crawlers are deliberately left alone. The listing is still rendered fresh on every request rather than cached, so the roles you see are still the roles in the database at the moment you asked.

  • data

    A monthly Berlin hiring pulse

    Pegel started a monthly read of what the feed shows about Berlin startup hiring. The first one covers July 2026: 2,843 roles open on the 17th across 316 companies, which sectors and stages are posting, the share that does not require German (about 47 percent), and how rarely pay appears (under 6 percent of roles).

    It also names a counting change found this month. Until July 15 expired roles were not being cleared, so the daily total climbed for weeks without ever counting down. Once the stale postings were removed, the number dropped to a truthful level. The post says so directly instead of reporting a month-over-month fall that never happened in the real market. Every figure in it comes from a query against the live database, and where the data cannot answer a question, the post says unknown rather than guessing.

  • infra

    Classification catches up daily now

    Every role on Pegel carries a German-requirement label when the posting supports one, and the pipeline reads what it can with deterministic rules the moment a role is ingested. The rest was supposed to be filled by a scheduled classification pass. Today it turned out that schedule had never run once; it pointed at a directory from another machine, and macOS quietly refused the rest. 136 roles sat unlabeled because of it.

    The backlog is cleared and the schedule is real now, running in the cloud every morning after the poll. The German requirement and the visa signal get a daily pass. Salary readings and role skills get a weekly one. Each pass reads only the job descriptions the pipeline could not resolve on its own, writes only fields that are still empty, and records that it examined a description so the same unclear posting is not re-read every day. When a description changes, the question reopens.

    What has not changed is the honesty contract. A role with no clear language signal stays unlabeled rather than guessed. Salary appears only when the employer wrote a number in the posting. The schedule makes honest labels arrive faster; it does not make them looser.

  • fix

    Truer visa signals, in both directions

    Pegel found a false positive in the visa classifier. Job ads sometimes carry boilerplate inviting applicants to attach their residence and work permit documents. The German word "Arbeitserlaubnis" in that sentence was enough to mark a role as sponsoring visas, which is backwards: the sentence assumes you already hold a permit. Dozens of warehouse roles at one company were labelled "sponsors visas" on that basis. The classifier now requires actual support wording next to the term, and the affected roles get reclassified on the next poll.

    In the other direction, sixteen more companies now carry the company-level sponsorship signal, n8n among them with thirty current Berlin postings stating plainly that they can sponsor visas. The rest earned it through their own job ads too: each has at least two active Berlin postings whose text offers visa or relocation support. Pegel takes the view that a company repeatedly telling candidates it will help them move counts as evidence about the company, not just about one role.

    What this still will not do: Pegel never infers sponsorship from a company being large or international, a single posting is not enough to flag a whole company, and where the ads say nothing, Pegel says unknown rather than guessing.

  • fix

    The sitemap stopped exaggerating, and the skill moved onto the homepage

    Pegel audited the sitemap end to end, every public page against every entry, in both directions. The good news first: the discipline was mostly there. Closed roles, delisted companies, thin tag pages and operator screens were all correctly kept out.

    Two things were wrong. Three real pages were missing entirely: the salary calculator, the resume matcher, and the API documentation. They are in now. And the sitemap was quietly exaggerating: every static page, every sector page and every landing page claimed it had just changed, every single hour, because the generation time was stamped on them. A crawler that catches you doing that learns to ignore every date you publish. So now a date appears only where it is true. Job pages carry the moment the role was last seen. Company pages carry their last update. Pages whose content is the live inventory carry the freshest observation. Pages whose edit dates Pegel simply does not track carry no date at all, because unknown should look like unknown.

    The Claude skill's install command also moved onto the homepage, one copy button away, under the popular searches. Same command as the tools page, and both are fed from a single constant, so what you read and what lands on your clipboard cannot disagree. The LinkedIn link settled into its final spot in the footer in the same pass.

    What this will not do: a sitemap is a request, not an instruction. Priorities and change frequencies are hints that search engines mostly ignore, and an honest sitemap does not make a thin page rank. What it buys is trust in the dates, which is what gets fresh roles crawled sooner.

  • fix

    Postal codes no longer split the hiring chart's Berlin bar

    Some companies list roles with a German postal code glued to the city, like "10115 Berlin" from Personio feeds. The hiring chart on company pages read that as a different location from plain "Berlin", so one office split across two bars. Pegel now strips a five-digit prefix before it buckets the location, so both forms count as the same city.

    Pegel only strips a prefix of exactly five digits, so a location that starts with a year or another four-digit number stays untouched. A postal code stuck on the end of a location, or a non-German postal code, still falls through unrecognized.

  • fix

    The job list went on a query diet, and a test now guards the door

    Yesterday Pegel fixed the database driver so the site stopped corrupting its own query results. That note also said the job list still asked the database for sixteen separate things on every visit, and that the fix would come carefully rather than quickly. This is that fix.

    The sixteen questions are now two. All thirteen filter counts (the numbers next to Fintech, React, No German required) travel in a single statement, and the job rows carry their own total along with them instead of needing a second and third question. On top of that, the filter counts are now cached for a minute. The job rows themselves are never cached, so a freshly posted role still shows up immediately; only the sidebar numbers can lag by up to sixty seconds, which is roughly how often the numbers change anyway.

    The part worth being transparent about is how this was verified, because a wrong rewrite here would not crash. It would show you a filter count that is quietly wrong, and nobody would ever file a bug about it. So before merging, the old sixteen-query version and the new version ran side by side against the live database, across twelve filter combinations including one designed to return nothing, and the answers had to be byte-for-byte identical. They were. That comparison script is now part of the repository, and any future change to this query has to pass it before it ships.

    The other thing that shipped is a guard. Both database incidents this site has had came from the same mistake: a page asking for more things at once than the connection pool can hold. The first time, the fix was a code comment explaining why not to do it. The mistake came back anyway. So now there is a test that scans the whole codebase for that exact shape and fails the build when it finds one. A comment asks politely; a test refuses.

    What this does not do: under a genuine traffic spike the site can still return errors rather than slowing down forever, because Pegel chose fast honest failure over silent queueing. And the sidebar counts being up to a minute stale is a real trade, not a free lunch. It is the right one for a page where the counts move slowly and the roles matter most.

  • feature

    The Claude skill has a page now

    Two days ago Pegel shipped a free, open-source Claude skill that job-hunts Berlin through Pegel's public API. It lived only in a GitHub repo, which means it effectively did not exist for anyone who was not already looking for it. It has a home on the site now, under Tools.

    The page does three things. It gives you the install command with a copy button (one line, `npx skills add`, works with Claude Code and the other agents the skills CLI supports). It explains the four things the skill actually does: find live roles with honest unknowns, check your CV against a role without inflating the match, tailor a truthful application in the shape German recruiters expect, and prepare you for the interview and the offer. And it states, plainly, the five things the skill will never do, because refusing to auto-apply and refusing to invent facts is the whole point.

    The LinkedIn link also moved into the footer's brand column, next to the tagline, where people actually look for it. It stays in the copyright row too.

    What this page does not do: it does not run the skill in your browser. The skill runs inside an AI coding agent on your machine, and the page is honest about that requirement rather than pretending it is a web app. If you do not use Claude Code or a similar agent, the job list right here on the site is the same data with no install step.

  • fix

    The stats page stopped timing out

    For about a day after launch, `/stats` was broken. Not slow: broken. It either answered in a third of a second or it hung until the request gave up, and which one you got was luck. Every other page was fine, which is what made it confusing.

    The first assumption was that the queries were too slow, and it was wrong. Measured, the six queries behind that page take about 80 milliseconds each, and all six together take about 250 milliseconds. The database was never the problem.

    The problem was how the page asked. It fired all six queries at once, and the connection pool only holds three. The two extra queries waited in line for a connection, and the line never moved. One request per page load, every time, on a page that recomputes on every visit.

    So the page now asks for what it needs one query at a time, and it needs fewer of them: the four headline counts fold into a single pass over the table, and the number of companies is just how many rows the company query already returned. Three queries, in sequence, about 330 milliseconds. It also means a visitor reading the stats page can no longer starve the connection pool for someone reading the job list at the same moment.

    What this does not fix: the underlying sharp edge is still there. If any page ever asks for more database connections at once than the pool has, it can still hang rather than fail with an error, and it should fail loudly. A test now catches the stats page regressing, but none catches this class of mistake everywhere. That is the next thing.

  • fix

    The site was quietly corrupting its own database results

    If Pegel felt unreliable to you over the past six weeks, it was. Pages timed out. The dashboard threw a 500. The job list sometimes just sat there. These had been treated as separate bugs. They were one bug, and it was Pegel's own.

    Pegel talks to its database through a connection pool that holds three connections. When a page asked for more than three things at once, the database library did something reasonable-sounding: it sent the extra queries down connections that were already busy. Pegel's database sits behind a pooler that hands each transaction to a different backend server, so those bundled-up queries got split apart and answered out of order. The connection then had no idea which answer belonged to which question.

    That is worse than a crash. A crash is honest. This handed one query's rows to a different query's reader, which is how a job list ends up trying to call trim on a number. Most of the time it produced an error. There is no way to rule out that it sometimes produced a page that looked fine and was wrong. That possibility is the most uncomfortable part, and it is the reason this note exists rather than a quieter one.

    It was measured before it was fixed, because the first guess had already been wrong once. With three connections and four things asked at once, one query never came back. With sixteen, which is what the job list was asking for on every single visit, **none** came back. The page then sat there until the platform killed it at five minutes. That happened 1,116 times in one day, across nearly every page on the site.

    So Pegel now uses a different database driver. It sends one query per connection, in order, and it takes a number instead of a queue that can grow forever. If it genuinely cannot get a connection in five seconds, it now says so and fails, instead of waiting silently until something else gives up. Same load that used to return zero queries now returns all sixteen in about three-tenths of a second. The dashboard also stopped asking for six things at once, and every heavy page now gives up after fifteen seconds rather than five minutes.

    What this does not fix: the job list still asks the database for sixteen separate things every time you load it, and that is simply too many. It no longer breaks, but it is slow under load, and if enough people arrive at once some of them will now get an honest error instead of a page. Collapsing those sixteen queries into one or two is the next job, and it is happening carefully rather than tonight, because the failure mode of rushing it is a filter count that is quietly wrong, and Pegel would rather be slow than wrong.

  • feature

    A Claude skill for applying to Berlin jobs

    Pegel published a free, open-source Claude skill that uses its live listings to help you prepare an application.

    Pegel is the data layer. The skill is the preparation layer. You apply. It will not submit anything on your behalf, and it will not mass-apply, because mass-applying breaks the terms of every major hiring system and floods the employers you are trying to reach.

    It finds roles through Pegel's public API, reads your CV locally so it never leaves your machine, tells you honestly where you match and where you do not, and helps you tailor a CV and letter that stay true to what you actually did. It knows the things a job tool built for the American market gets wrong about Germany: the tabular Lebenslauf, the photo that is optional rather than expected, when an Anschreiben is genuinely wanted, and the fact that the Chancenkarte lets you look for work rather than start it.

    It will not invent a salary, a visa status, or a language requirement. Where Pegel does not know, the skill says unknown and tells you what to ask the employer.

    The code is at github.com/ElxMaj/pegel-berlin-job-skill.

  • feature

    Salary and tech tags in the public API

    The public API let you filter for roles with a disclosed salary, and then declined to tell you what the salary was. You could ask for the honest ones and still learn nothing from them.

    Job records now carry the salary range, its currency, and its period, along with the tech tags the classifier found. Anything the employer did not disclose comes back as null, and null means the employer did not say. It never means Pegel guessed and it never means zero. That contract is written on the API page now, because anyone building on this data needs to know Pegel will not fill a blank for them.

  • infra

    The 404 log stops absorbing the bot storm

    Going public means getting scanned. Within a day of launch, vulnerability scanners were sweeping the site for the usual things: WordPress admin panels, exposed environment files, forgotten database dumps. None of it exists here, so every probe was a 404.

    Every 404 wrote a row to the database. That was fine in principle, because the write was an upsert keyed on the path, so a bot hitting the same dead URL a thousand times was supposed to increment one counter rather than create a thousand rows.

    It did not hold, for a reason that should have been obvious. The key included the query string, so a scanner appending a random parameter to each request minted a fresh row every time. The table that was meant to stay bounded had grown a 31 MB index and was absorbing a write storm on a small database.

    Two changes. Scanner probes are now dropped before they reach the database at all. And the key is the path alone, because the question this log answers is which links are broken, not which query strings a bot invented. Knowing what is attacking the site is a real question, but it belongs at the edge, not in a table.

  • data

    The visa filter reads negations properly

    The visa classifier had a blind spot. A job description saying "we cannot sponsor work visas" or "visa sponsorship is not available" contains the words "visa" and "sponsorship", and the heuristic was reading the words without reading the sentence.

    Negated phrasings now win over the positive keywords they contain. A role that says it will not sponsor is tagged as not sponsoring, which is what it plainly says.

    This cuts both ways and that is the point. Some roles left the sponsorship filter. The ones that remain are the ones that mean it.

  • fix

    A budget is not a salary

    The salary parser was reading numbers that were never pay. A job description mentioning a marketing budget, a research grant, or the company's last funding round would hand the parser a large euro figure, and the parser took it.

    That is the worst class of bug Pegel can ship. A wrong salary is more damaging than no salary, because it looks like a fact. Someone could have filtered for disclosed pay and found a role advertising a Series B.

    The parser now refuses figures that sit next to budget, grant, funding and round language. Where it is no longer confident, the role shows no salary at all, which is what it should have shown from the start.

  • feature

    Pegel is live

    Pegel is public. The site is open to search engines, the listings are live, and there is no longer a password in front of it.

    It launched with 407 Berlin companies, a daily poll against their public hiring feeds, and the filters it was built for: no German required, visa sponsorship, salary disclosed only when the employer actually disclosed it.

    The last stretch before launch went on things nobody will notice if they were done right. The homepage carries a real heading now instead of a paragraph pretending to be one. Link colours and the "Unknown" label were failing contrast, so they moved to darker tokens. The command palette announces its highlighted result to screen readers. There is a skip-to-content link. The catch-all sector page stopped competing with the pages Pegel actually curates.

    Pegel also named the answer-engine crawlers explicitly in robots.txt: the ones that fetch a page while someone is mid-question in ChatGPT, Claude or Perplexity. If a person asks one of those "which Berlin startups hire without German", the answer should come from a source that checked.

  • feature

    Roles that closed, and saying so

    A role that vanishes from a company's hiring feed for three days is closed. Until now Pegel just quietly stopped showing it. That is the honest outcome but it is not the honest story, because a job you applied to last week deserves to tell you what happened to it.

    Expired roles now keep their page and carry a banner saying the listing closed and when. The apply link comes off. Search engines are told not to index it. The page stays because someone may still arrive from an old link, and a dead end that explains itself beats a 404.

    The stats page also carries a "roles closed this week" tile now. Openings are only half the picture. A board that shows you what opened and never what shut is telling you the flattering half.

  • feature

    EU Blue Card salary labels

    When an employer publishes a salary, Pegel now compares it against the two EU Blue Card thresholds Germany resets every January. For 2026 those are €50,700 a year, and a reduced €45,934.20 for shortage occupations and recent graduates.

    The chip says one thing: the salary this employer published reaches that bar. It does not say the role is Blue Card eligible, and Pegel has no way to know whether it is. A Blue Card also needs a recognised degree and a matching offer.

    Four rules govern the comparison, and each errs toward showing nothing. Pegel reads the bottom of a range and never the top, because the bottom is the number the employer committed to. A salary in another currency gets no chip, because converting it would invent a euro figure nobody published. A monthly or hourly rate gets no chip, because annualising it needs an assumed working year. A salary of exactly €45,934 gets no chip, because the threshold is €45,934.20 and twenty cents short is short.

  • feature

    Company stability signals

    Every company page now carries a stability-signals panel beside the masthead. It names the current CEO and how long they have held the seat. It lists the founders and where they worked before. It shows the most recent funding rounds and the company's Google rating.

    It also shows the total raised. For a while that line said "Unknown" for most companies, because Pegel only stored euro figures and most startups report their funding in dollars. Now the total appears in whatever currency the sources report it in, so "$9.8B" for Stripe and "€400M" for 1KOMMA5 both read the way they were announced. Around 200 of the companies Pegel tracks now carry a total.

    Every figure is cross-checked against at least two independent sources, and Pegel shows "Unknown" rather than guess when it cannot. It never converts a dollar figure into a euro estimate.

  • feature

    Popular searches on the homepage

    The homepage now carries a Popular searches block linking to the landing pages people actually open: no-German engineering, Berlin frontend, Berlin Python, fintech, climate. The same links were already in the footer; surfacing them above the fold meant they got found by visitors who would not have scrolled looking for them.

    If a landing page you would expect to see is missing, the block updates as new ones reach a meaningful job count. The complete map sits at /sectors and on individual tag pages.

  • feature

    Pegel moved to pegel.berlin

    Pegel runs at pegel.berlin now. The old pegel.app domain still resolves and every request lands you on the new host with a permanent redirect, so any bookmark, saved search or digest URL you have continues to work. The change consolidates the link equity that Google has been building up onto a single canonical host and matches the Berlin focus better than the generic .app suffix did.

    If you subscribe to the daily digest, the unsubscribe and manage links inside your existing emails still work; they were always token-based and not host-locked. New emails sent after the switch use pegel.berlin everywhere.

  • fix

    The daily digest stops sending duplicates

    A handful of companies' ATS feeds produce the same role under more than one identifier, usually because the role is posted in two languages or because the feed lists each office location as a separate row. Until last week, both copies could land in the digest if they happened to match the same filter. They do not anymore. The dedupe runs at the ingest layer, so the listing pages, the per-company feeds and the digest all see one row per role.

    If you noticed two of the same job in a recent digest, that should not recur. If it does, replying to the digest email reaches the founder directly.

  • feature

    Filter the companies page like the jobs page

    The page at /companies now carries the same left-sidebar filters the jobs view does. Sector, stage, headcount band, currently-hiring toggle, sort order. The set was already on /jobs and on the landing pages; bringing it across to /companies makes the two browsing surfaces consistent. Touch or tap a filter and the URL updates; share the link and the receiver sees the same view.

    The defaults are unopinionated. No sector preselected, no stage filter, sorted by most-hiring. The page is for browsing across the allowlist, not for pushing you toward any particular cut.

  • feature

    Match your resume without uploading it

    There is a resume matcher now, at /tools/resume-match. Drop in a PDF or paste the text, and it ranks the open Berlin roles by how well they fit. The skills you match and the ones you are missing are spelled out on every result.

    Everything runs in your browser. The file is read on your device and matched on your device. Nothing about your resume reaches Pegel. The only network call fetches the public list of jobs, the same list anyone can see. There is no upload and nothing stored; no copy is kept anywhere. For a document as personal as a resume, that felt like the only honest way to build it.

    The score is a plain keyword overlap, computed in the open, not an AI verdict dressed up as one. Treat it as a guide for where to look. The tool is desktop-only for now, since reading a PDF next to a results list works poorly on a phone.

  • feature

    A Berlin net-salary calculator

    Pegel has a net-salary calculator now, at /tools/net-salary. Type a gross yearly salary and it shows the net that lands in your account each month for tax year 2026. The breakdown splits out income tax and the solidarity surcharge where it still bites. It adds Berlin church tax if you opt in, and lists each social-security contribution on its own line.

    It covers all six tax classes (Steuerklasse 1 through 6), so a single filer and one half of a married 3/5 pairing both get a real figure rather than the class-1 default. The married-pair classes carry a caveat. Their math is the least-certain piece, so the page links out to an authoritative calculator for the exact number. Pegel would rather flag the uncertainty than show a confident wrong figure.

    This is an estimate, and the page says so plainly. Private health insurance and mid-year changes sit outside v1, along with the Saxony care split. The math is pure and tested; the common cases were validated against published figures before any of the UI went on top of it.

  • data

    Companies allowlist passes 250

    The Berlin allowlist now sits at 258 companies, up from 54 a week ago. The latest batch came from a scan of Berlin VC portfolios (Project A, Cherry, Visionaries, plus others), filtered down to companies with a live Berlin role on a supported ATS and not already listed.

    Every entry was verified the same way as before. Pegel hit the ATS feed live and confirmed it returns Berlin roles; the ones that did not were excluded with a written reason on the PR. Verified office addresses also went in for 84 of these companies, parsed from public impressums, so a company page can show where the team actually sits.

    The v1 target was 150 to 200, so the count is comfortably past it. The work now shifts from raw volume to keeping the list clean and the feeds healthy.

  • feature

    A blog, with seven posts to start

    Pegel has a blog now, at /blog. Seven posts to start, on the things a Berlin job seeker actually runs into. What visa sponsorship means, and what an employer's silence about it does and does not tell you. The Probezeit and the short notice that comes with it. How statutory notice periods grow with tenure. The difference between a works council and a union. An honest take on finding a tech job here.

    None of the posts republish a source or invent a number. The one about layoffs deliberately does not repeat the viral figure that prompted it, and it does not name the company, because Pegel could not verify either claim.

    Each post is first-person and runs through the same voice linter as the rest of the site before it ships. Drafts sit in an admin queue until they pass.

  • feature

    Live /status page

    Pegel now has a public status page at /status. It reads from the database on every request and shows two things: the inventory (active jobs, total companies, last successful poll) and the per-slice health of the morning cron.

    The cron runs in eight staggered batches between 04:00 and 05:45 UTC. The status page lists each slice with the timestamp of its last completed run, so if something stops polling, you can see which slice is stuck without having to ask.

    It is not a marketing page. It is the same view the founder checks when the data looks off.

  • infra

    RSS feeds go live

    Pegel has two RSS feeds now. /feed.xml is sitewide, newest 50 roles, refreshed hourly. Each company page also exposes its own feed at /companies/[slug]/feed.xml so you can subscribe to just one employer.

    The feed description is metadata only: company, location, posting date. No job-description text, no LLM summary. Pegel's standing rule is that the JD body is the employer's words, not Pegel's, so the RSS body would be a worse copy of what the source already has.

    Per-filter and per-sector feeds wait on the filter UI and sector pages, both coming up.

  • feature

    Richer structured data on company and job pages

    Each company page now carries an explicit sector label and a numeric employee band in its schema.org JSON-LD. Job pages do the same for the hiring company. Search engines that parse this markup, including Google for Jobs, see the sector ("Fintech", "AI/ML", any of the dozen Pegel categories) and a size range like "11 to 50 employees" without having to guess from the page text.

    Nothing changes on the page itself. The label and band were already shown to readers. This adds them to the structured-data block that crawlers parse first when they pick up the page.

    Where Pegel does not have a value, the field stays out of the markup. A company without a recorded size produces no `numberOfEmployees` entry rather than a guess. Same rule Pegel applies everywhere on the site: omit before you fabricate.

  • fix

    Email tracking flags pinned off

    Every email Pegel sends through Resend now ships with click-tracking and open-tracking flagged off on the request itself. A regression test in the email wrapper fails the build if either flag is missing or flipped.

    Resend's per-message API does not actually honor those keys in 2026; tracking is controlled at the sending-domain level on their dashboard. The defensive marker stays in the code anyway because it locks Pegel-side intent against any future SDK change. Operators get a matching reminder in the pre-launch checklist to flip the dashboard switches off.

    There is a longer story here about why Pegel does not want open-rate analytics on the digest, but it is the same story everyone has heard. Mostly: the digest is the thing, not the funnel.

  • feature

    Company pages get a dashboard, a hero band, and a photo gallery

    The per-company page at /companies/[slug] had been a list of open roles and not much else. It is now structured around three new sections.

    A dashboard summarises the company's current hiring shape across three axes: seniority mix, the German-language requirement, contract type. The numbers come from the open roles on the page, so the sample size is small but honest. When a dimension has no signal, that row simply does not render.

    A hiring-history sparkline runs across the top of the dashboard. It shows how many roles the company posted each month over the last year. The caveat is that for now, "posted" means "first seen by Pegel". Until the daily cron has 12 months of history, the sparkline is mostly a Pegel-coverage view rather than an employer-side view.

    A photo band runs across the top of the page. For companies with a curated hero image, that image renders. For the rest, the page falls back to a tiled stripe pattern in the brand teal. A gallery section under the dashboard supports up to six photos with a lightbox.

    Photos are zero on launch. The build-script and curation pass are next.

  • data

    Companies allowlist passes 50

    The Berlin startup allowlist now sits at 54 companies, up from the 30 it started with. Every new entry was verified by hitting the ATS feed live and confirming it returns Berlin roles. Companies whose feeds returned nothing usable were excluded with a written reason on the PR.

    The v1 launch target is 150 to 200. Half of the gap is curation work that continues, and a chunk of the rest is research on companies that do not publicly disclose their ATS provider. There is a Tavily-driven enrichment script in the repo for the ones that need a public-source pass before they qualify.

    If a Berlin startup you know is missing, the suggestion link in the footer goes straight to the founder.

  • feature

    /about page ships

    The /about page is live. It explains what Pegel is, what it is not, and why the founder built it. The copy is voice-linted via the same script that runs over the rest of the narrative content, so the AI-tell linter catches drift in the about page the same way it would catch it in a blog post.

    If you only read one page on this site to decide whether you trust it, that is the page to read.