DEV Community

Jaeyoung Yun
Jaeyoung Yun

Posted on • Originally published at github.com

Why bug bounty income is harder than it looks: the New Hacker trial cap and six compound mistakes that wasted a full day

Why bug bounty income is harder than it looks: the New Hacker trial cap and six compound mistakes that wasted a full day

A field report on what actually fails when you try to make money from bug bounties as a new researcher, the program-tier landscape that nobody documents, and an original pre-submit methodology that prevents the most common reputation-burning closures.


TL;DR (operator-callable summary)

If you are a new bug bounty researcher trying to convert real technical skill into real money, the conversion rate is much worse than the public narrative suggests. This piece documents a single day that produced six HackerOne submissions, around thirty research commits, and zero dollars, then dissects exactly which decisions caused the zero. The result is a three-stage pre-submit gate (variant-race timing check, design-context filter, payment-likely trust-model check) that takes about ninety seconds per submission and eliminates the dominant failure modes: defense-in-depth findings that compile but never pay, misrouted reports to the wrong team handle, programs whose offers_bounties flag is true but whose per-scope eligible_for_bounty is uniformly false, and findings that get out-raced by other reporters because they sat in a queue waiting for an account trial cap to expire. The reader walks away with a checklist, a shell script template, and a calibrated expectation of where their first hundred submission-hours should and should not be spent.


1. The shape of the problem

Most public writing about bug bounties is written by researchers who already have a track record. They received an invitation, they have a multiplier, their reputation score buys them the benefit of the doubt at triage. The advice that flows from that position is structurally optimistic because it skips the part where the new researcher pays the real cost: low signal, no multiplier, a per-account trial cap that limits how many open reports you can have at once, and a market that has already absorbed all the easy findings from public-disclosure waves.

The pattern looked like this for one full working day:

  • Six HackerOne submissions across three programs.
  • Four came back Informative. One was a Duplicate. One was N/A (the submitter mistakenly targeted the wrong team handle).
  • Around thirty research commits across a private monorepo.
  • Nine background research agents dispatched to scan source repositories for sibling-variant patterns.
  • Roughly twelve hours of focused researcher time.
  • Net income: zero.

Every one of those submissions describes a real, technically correct observation about the code in question. None of them were fabricated. The bugs exist. The conversion failure is not about whether the bugs are real, it is about whether the program's threat model maps the finding to a paid severity. Those are different questions and conflating them is the most expensive mistake a new researcher can make.

2. The New Hacker trial cap (the timing problem nobody warns you about)

HackerOne enforces an account-wide trial cap on new accounts. The exact number varies by account state, but functionally: a new researcher can have only a small number of open reports at a time, and once they hit the cap they wait approximately thirty days before they can submit again. The cap is not per-program. It is per-account. So one Informative closure on a low-value program freezes submission against every other program you could have targeted.

The naive response to this is to use the thirty-day wait as a queue-build period: keep researching, keep finding things, batch up a portfolio of high-quality findings, then submit them all the moment the cap lifts. This feels disciplined. It is wrong, and the reason is not obvious until you have watched it fail.

Bug bounty findings have a shelf life. Every day a finding sits in a queue:

  • The vendor might push a fix that closes the variant, in which case the variant becomes invalid because the public commit shows the path is patched.
  • Another researcher might submit the same finding, in which case the new researcher who has been holding it gets Duplicate.
  • The vendor might update their docs to mark the behavior as known-limitation, which moves it into the Informative bucket.
  • A coordinated disclosure window might expire and the issue becomes public, also closing it as Informative or Duplicate.

HackerOne pays the first reporter. Sitting on a finding while a thirty-day clock counts down is competing against everyone else doing the same variant-race research, plus the vendor's own internal sweep, plus any automated triage tooling on the vendor side. The expected value after the delay penalty is often negative.

The correct frame is: any finding worth submitting is worth submitting now. If now requires a non-HackerOne channel because the cap is active, pivot to that channel. Do not queue. Concretely, the alternative channels that actually pay are smaller than people think:

  • Microsoft MSRC accepts .NET, ASP.NET Core, Roslyn, and the OSS dotnet repos through msrc.microsoft.com directly, not through HackerOne.
  • Apple Security Bounty operates entirely outside HackerOne.
  • Google VRP for Chrome and the GCP surface operates through its own intake, not HackerOne.
  • Mozilla operates two separate programs: client-side bugs (Firefox memory safety, JIT, sandbox) go to bugzilla.mozilla.org/form.client.bounty; web-side bugs (AMO, MDN, Sync) go to HackerOne. Cross-channeling either way will get bounced.
  • Immunefi handles most crypto and DeFi protocols, again outside HackerOne.
  • GitHub Security Lab is parallel to GitHub's HackerOne program for certain CodeQL-class findings, but only for the explicit scope they publish.
  • Many vendors maintain security@vendor.com direct-email intake that bypasses HackerOne entirely, but with one critical exception covered below.

The thirty-day wait should be spent re-routing to those channels, not stockpiling.

3. The six compound mistakes

The day above produced zero income not because of one big mistake but because of six smaller ones that compounded. Each one wastes hours individually. Falling into multiple in the same day wastes the day. Documented here in priority order:

Mistake 1: the queue-build trap

Covered above. Findings have shelf life; the time cost of waiting is paid in lost first-reporter status, not in zero.

Mistake 2: serial agent dispatch

Background research agents are slow. A single agent takes five to fifteen minutes of wall-clock time to scan one repository for sibling-variant patterns. The natural impulse is to dispatch one, wait for it to return, look at the results, dispatch the next. Three agents serially is forty-five minutes. Three agents in parallel is fifteen. When the work decomposes into independent sub-tasks (different programs to research, different repositories to audit, different submission channels to verify) it is always parallelizable and the serial version always wastes time. The fix is structural: any time the next three to five steps are independent, dispatch them as parallel agents in a single message, not one at a time waiting for completion.

Mistake 3: trusting the offers_bounties program flag

HackerOne exposes a program-level offers_bounties boolean and per-scope eligible_for_bounty booleans. The two do not have to agree. A program can show offers_bounties: true (which is what shows up in directory listings) while every single structured_scope under it has eligible_for_bounty: false. This happens when programs migrate from paid to VDP (vulnerability disclosure program, recognition-only) but never clean up the program-level flag.

Both Ruby and Django are documented examples. Both show offers_bounties: true. Both have zero scopes with eligible_for_bounty: true. Functionally they are VDPs with no payout.

The fix is a one-line GraphQL query before any research time is spent:

query {
  team(handle: "ruby") {
    offers_bounties
    structured_scopes(eligible_for_bounty: true) {
      total_count
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

If total_count is zero, the program does not pay, full stop. Skip.

Mistake 4: not checking submission_state

A program can be in submission_state: "paused" or submission_state: "disabled" while still appearing in the public directory and still showing a bounty table. Submissions made against a paused program are dropped silently or returned with a non-actionable error after hours of preparation work.

Square Open Source on HackerOne is the example. It looks perfect from the public directory: signal threshold off, $500 base, ten resolved reports. It is disabled. Discovering this at submission time after the commit-history mining is done wastes the entire research budget for that target. A single GraphQL call upfront would have caught it.

query {
  team(handle: "square-open-source") {
    submission_state
    state
  }
}
Enter fullscreen mode Exit fullscreen mode

If submission_state is anything other than open, do not start research on that program.

Mistake 5: methodology must match scope type

HackerOne scopes are typed: SOURCE_CODE, URL, WILDCARD, APPLE_STORE_APP_ID, GOOGLE_PLAY_APP_ID, OTHER, DOWNLOADABLE_EXECUTABLES, HARDWARE, SMART_CONTRACT. Each type rewards a different kind of work:

  • SOURCE_CODE scope rewards variant-race against recent commits. Read the security-flavored commits in the last ninety days, find the sibling unfixed call sites that the patch missed, submit the variant. This is the cheapest income loop because the diff tells you exactly what bug class the vendor pays for.
  • URL and WILDCARD reward authenticated interactive testing. You need an account, often multiple accounts, and a multi-day engagement to chain IDOR, broken access control, server-side request forgery, and similar web-app classes.
  • APPLE_STORE_APP_ID and GOOGLE_PLAY_APP_ID reward reverse engineering. APK and IPA extraction, runtime instrumentation, Frida hooks, often a physical device.
  • SMART_CONTRACT rewards Solidity or Move auditing, often with formal-verification tooling.

Applying variant-race methodology to a web-only program produces zero findings no matter how much time you spend, because there is no source code to read. Snapchat is the canonical example: forty-six in-scope assets, all URL/mobile/hardware, no source code. Passive recon and HTTP-header analysis on Snapchat finds nothing because Snapchat does not pay for HTTP-header analysis on Snapchat.

The fix is to verify scope type before deciding methodology:

curl -s -u "$H1_USER:$H1_TOKEN" \
  "https://api.hackerone.com/v1/hackers/programs/<handle>" \
  | jq '.relationships.structured_scopes.data[].attributes | {asset_type, asset_identifier, eligible_for_bounty}'
Enter fullscreen mode Exit fullscreen mode

If the output is mostly URL or WILDCARD and you do not have an authenticated-IDOR workflow set up, do not spend agent budget on that target.

Mistake 6: out-of-band communication is forbidden when a vendor has an active HackerOne program

There is a tempting shortcut when HackerOne blocks your account: just email the vendor's security@ address directly. The vendor wants to hear about real bugs, the bug is real, what is the problem?

The problem is in HackerOne's terms. When a vendor has an active HackerOne program, that program is the channel. HackerOne's signal-block dialog quotes it verbatim: "Any attempts to bypass this restriction (such as: account collusion, out of band communication, off topic comments) are not permitted and will result in further restrictions."

So the direct-email shortcut is not a shortcut, it is an account-burning move. The vendor will sometimes forward the email to HackerOne staff, who will see it as policy violation, and the new researcher's account gets further restricted.

The only legitimate parallel channels are ones the vendor explicitly publishes:

  • GitHub Security Lab is published as a parallel channel for CodeQL-class findings on certain GitHub assets.
  • Some Stripe OSS findings are accepted at security@stripe.com for components that are documented as out of scope for the HackerOne program (smokescreen, munkisrv historically, before they were added to HackerOne scope).
  • A handful of vendors maintain explicit dual-intake.

The default assumption must be: if the vendor has a HackerOne program, that is the only channel. Period. Verify the parallel-channel option in writing from the vendor's own policy doc before using it.

4. The original methodology: variant-race plus design-context filter plus payment-likely check

The above six mistakes describe what to avoid. The positive methodology that converts research time into actual income has three stages, run in this order. The cost is about ninety seconds per candidate finding. The yield is a dramatically lower rate of Informative closures.

Stage 1: variant-race

The setup: vendor X publishes a security-flavored fix commit. The fix patches one call site of a bug class. Other call sites in the same codebase often have the same bug class because the original author did not sweep the codebase, they patched the one site that was reported.

The work: read the fix commit. Identify the bug class. Use grep, ast-grep, or a semantic search tool to find every other call site in the same repository that matches the bug-class pattern. Verify each candidate against the fix's actual change (some will be already-defended, some will be in dead code, some will have a downstream check that compensates). The remaining set is the variant pool.

The reason it works: the fix commit is public information from the moment the vendor merges it to a public branch. Every researcher with a tooling pipeline can see the fix. The window for "find a variant that the vendor's sweep missed" is small, often twenty-four to seventy-two hours, sometimes a week. The race is real and the new researcher's advantage is responsiveness, not depth.

Concrete example from the field: WooCommerce shipped a fix on April 15 for /wp-json/wc/v3/orders/{order_id}/fulfillments IDOR. The race window was April 15 through approximately May 10, during which a researcher checking sibling endpoints (shipments, refunds, notes, similar) for the same guest-order pattern could have submitted variants. By May 12, the vendor's follow-up PR closed all of them. The window had closed.

Stage 2: design-context filter

The variant-race produces candidates. Not all candidates are real findings. A pattern match against a bug class is necessary but not sufficient because the codebase may have defensive logic upstream or downstream of the matched site that closes the attack path.

The filter: for each variant candidate, read the surrounding context and answer two questions:

  • Is there an upstream check that would reject the malicious input before it reaches the call site?
  • Is there a downstream check that would catch the corrupted state before it has effect?

Specific examples documented in the field:

  • A WooCommerce sibling IDOR endpoint had checkUserHasViewAccess applied at the routing layer for unauthenticated requests. The variant matched the pattern but was already defended.
  • A Matomo CoreUpdater variant had the relevant token scope-limited at the auth middleware. The variant matched but the token-scope check made exploitation impossible.
  • A PayPal SDK proto-walk variant had the script URL hardcoded to a first-party paypal.com origin. The variant matched but there was no attacker-controlled input flowing into the walk.
  • A Tor Project off-by-one variant routed through trunnel-generated parsers with auto-bounds-checking. The pattern matched but the bounds check was a generator-level invariant.

In all four cases, a thirty-second read of the upstream and downstream context closed the candidate. Without the filter, the researcher submits a finding that compiles but does not exploit, and the triager closes it as Informative or N/A.

Stage 3: payment-likely trust-model check

A finding that survives stages 1 and 2 is a real bug. But real bugs do not always pay. The payment-likely check is the six-point self-test that the program will actually map the finding to a paid severity rather than an Informative classification. This is the most expensive lesson because it is the one that gets you all the way to submission before failing.

The six points:

  1. Can I name the program's defined trust boundary in one sentence? If you cannot, you do not understand the program well enough to submit. The trust boundary is the most important thing in the policy doc and it is usually not on the bounty page, it is in the program description or the threat-model section.
  2. Does my finding cross that boundary, or does it just touch defense-in-depth? "Defense in depth" is not a trust-boundary crossing. Programs frequently accept fixes for defense-in-depth gaps without paying. A trust-boundary crossing is: external network attacker reaching internal trust zone, low-privilege user gaining admin, untrusted contributor gaining repo write, and similar.
  3. Have I checked the program's hacktivity for prior Informative classifications of this bug class? Sixty seconds of search saves the submission.
  4. Does my severity claim survive the program's published severity-calculation methodology? Not just CVSS, but the program's own published bounty table. Some programs pay only for High-impact and above; Medium goes Informative by policy.
  5. If I read the program's "out of scope" section, does my finding clearly avoid those buckets? Many programs explicitly call out classes like "self-XSS", "missing security headers", "click-jacking on non-sensitive endpoints" as out of scope. The match is often subtle.
  6. Income-margin: do I have at least 50% confidence this lands at the bounty tier I am claiming? If unsure, downgrade the severity claim or defer the submission.

If any of the six is unchecked, the finding does not get submitted. It goes back into the research queue with a note about which check failed, and the researcher works on the next candidate.

Concrete example from the field, the one that taught the lesson: a finding against an OSS GitHub Action where a control character could be injected into $GITHUB_PATH via a with: input. The bug was technically real. The fix commit (6cad158) was prefixed with security:. The CVSS estimate was 7.5 High. The researcher submitted with that severity claim. Triage came back Informative ($0). The vendor's reasoning: "The path_to_*_executable parameters are with: inputs set by the workflow author. In the GitHub Actions trust model, composite-action with: inputs are trusted configuration. An actor who can set these values already controls the workflow file and can add arbitrary steps directly. The newline behavior therefore does not grant any capability beyond what control of the input value already provides."

The trust boundary in GitHub Actions composite-action design is: "the workflow author trusts the inputs they pass." The finding did not cross that boundary because the attacker who controls the input already controls the workflow. It was a defense-in-depth gap, patched for hygiene, not for payment. A sixty-second hacktivity search would have surfaced a prior Informative closure of the same bug class. The researcher would have deferred. The submission cost was paid for nothing.

5. The program-tier landscape (what to research vs. what to skip)

The published HackerOne directory does not sort programs by EV for a new researcher. Here is a tier breakdown built from one day of scouting, useful as a calibration anchor for the reader's own list.

Tier A: paid plus OSS in scope plus favorable threat model. Spend time here. Examples include: GitHub (cli/cli source code under DOWNLOADABLE_EXECUTABLES asset, Medium tier $4K-$7.5K), Cloudflare (workers-sdk and smokescreen in scope, $1K-$5K Medium and $5K-$50K High), Stripe (newly-added OSS scopes including smokescreen and munkisrv, $5K-$25K Critical), Matomo ($10K base, multiple critical-severity OSS scopes), Mozilla client-side via Bugzilla (memory-safety bounties $5K-$20K with explicit static-analyzer-checker tier $500-$5K), PayPal ($1K Low through $30K Critical with explicit SDK scope), Tor Project (IBB-funded critical tier), Brave (LLM/AI Agent Security doubled to $20K Crit).

Tier B: paid plus OSS in scope but threat model unfavorable. Skip unless the threat model framing changes. Stainless-template SDK code-gen parity findings close Informative across every vendor that ships SDKs from the shared Stainless template; Kubernetes NodeRestriction admission plugin is too well-hardened for variant work; GitLab Runner S3 IAM-injection is patched comprehensively in siblings; curl had honest negative on OCSP deepdive.

Tier C: verified not paid or paused or scope-mismatch. Do not re-scout. Includes HashiCorp (recognition-only program), Docker (swag-only), Supabase (VDP), Internet Bug Bounty (paused), DigitalOcean (archived), Atlassian (HackerOne is VDP, the Bugcrowd engagement is paid but invite-only), Nextcloud (temporarily suspended due to AI-generated report volume), Sentry (invitation-only), Discord (Bugcrowd not HackerOne, invitation-only, SaaS scope only), and similar.

The cost of having this landscape pre-built is significant. Without it, every new researcher re-scouts the same dead programs from scratch. With it, the first sixty minutes of any research session are spent confirming program state in the Tier A list (programs change), not in re-discovering that HashiCorp does not pay.

6. The pre-submit gate, as code

The above checks are operationalized in a shell function that runs immediately before every submission. The full version is around two hundred lines; the core looks like this:

presubmit_gate() {
  local team_handle="$1"
  local scope_id="$2"
  local title="$3"
  local body_path="$4"
  local token="$5"
  local fail=0

  # 1. Body quality: not a placeholder, real finding length
  local body_size
  body_size="$(wc -c < "$body_path" | tr -d '[:space:]')"
  if [ "$body_size" -lt 500 ]; then
    echo "PRE-SUBMIT FAIL: body is $body_size bytes (<500)" >&2
    fail=1
  fi
  if grep -qiE 'minimal test|test body|placeholder|do not submit|debug only|TODO body|For operator:|operator quickstart' "$body_path"; then
    echo "PRE-SUBMIT FAIL: body contains placeholder phrase" >&2
    fail=1
  fi

  # 2. Title length: HackerOne returns HTTP 500 above 128
  if [ ${#title} -gt 128 ]; then
    echo "PRE-SUBMIT FAIL: title length ${#title} > 128" >&2
    fail=1
  fi

  # 3. Generator-tells and em-dash (load patterns from external file)
  if grep -qF -f "$PRESUBMIT_BANNED_PHRASES_FILE" "$body_path"; then
    echo "PRE-SUBMIT FAIL: body contains generator-tell phrase" >&2
    fail=1
  fi
  if grep -qP "\x{2014}" "$body_path"; then
    echo "PRE-SUBMIT FAIL: body contains em-dash (U+2014)" >&2
    fail=1
  fi

  # 4. Team handle: not HackerOne's own internal handles
  case "$team_handle" in
    security|hackerone|hacker_one)
      echo "PRE-SUBMIT FAIL: $team_handle is HackerOne's own program" >&2
      fail=1
      ;;
  esac

  # 5. Scope exists and is bounty-eligible (GraphQL)
  local scope_query='{ "query": "query($handle:String!){team(handle:$handle){submission_state structured_scopes(eligible_for_bounty:true){total_count edges{node{id}}}}}", "variables": {"handle":"'"$team_handle"'"} }'
  local scope_result
  scope_result="$(curl -s -X POST -H "Authorization: Bearer $token" -H "Content-Type: application/json" -d "$scope_query" https://api.hackerone.com/graphql)"
  local sub_state
  sub_state="$(echo "$scope_result" | jq -r '.data.team.submission_state')"
  if [ "$sub_state" != "open" ]; then
    echo "PRE-SUBMIT FAIL: submission_state=$sub_state (not open)" >&2
    fail=1
  fi
  local elig_count
  elig_count="$(echo "$scope_result" | jq -r '.data.team.structured_scopes.total_count')"
  if [ "$elig_count" = "0" ] || [ "$elig_count" = "null" ]; then
    echo "PRE-SUBMIT FAIL: zero bounty-eligible scopes" >&2
    fail=1
  fi

  return $fail
}
Enter fullscreen mode Exit fullscreen mode

This gate by itself would have prevented four of the six closures from the day above:

  • The fifty-character placeholder body that closed as Informative would have failed body-quality.
  • The 134-character title that returned HTTP 500 from HackerOne's API would have failed title-length.
  • The misrouted submission to team_handle security (which is HackerOne's own internal handle, not the vendor's) would have failed team-handle.
  • The submission against a scope with eligible_for_bounty: false would have failed bounty-eligible.

The remaining two closures were defense-in-depth findings that needed the payment-likely trust-model check from stage 3 of the methodology, which is harder to automate but takes about ninety seconds to run by hand.

7. Calibrated expectations for the first hundred hours

What does a new researcher actually earn in the first hundred hours of bug bounty work, applying the above methodology rigorously? The honest range, based on the calibration data:

  • The bottom decile (p10) is zero dollars. This happens when the trial cap traps the researcher and the methodology has not been built yet.
  • The median (p50) is somewhere between three hundred and two thousand dollars, conditional on finding one to three Medium-tier accepted findings during the trial period.
  • The top decile (p90) is in the five to fifteen thousand dollar range, conditional on landing one High or Critical at a Tier A program (GitHub gh-cli ANSI injection at Medium is around $5K; Cloudflare workers-sdk path traversal at High is in the $5K-$15K range; a sandbox-escape at Mozilla client is $20K).
  • The top percentile (p99) is fifty thousand dollars or more, conditional on a coordinated-disclosure event opening a unique submission window (a fresh CVE wave at a high-paying program where you happen to be the second-reporter on the variant).

Most public writing about bug bounties anchors on the p90 outcome. The reader should anchor on the p50 with explicit awareness that the p10 is real and that the methodology above is what moves the distribution rightward.

8. What you can build on top of this

The methodology is reproducible. The pre-submit gate is portable to any bug bounty platform with a query API (HackerOne, Bugcrowd, Intigriti, YesWeHack all expose enough metadata to run it). The Tier A/B/C landscape is a living document that gets stale fast (programs change weekly) but the framework for building it is durable: scope-eligibility check, submission-state check, threat-model framing, hacktivity search.

The next compounding layer is a daily variant-race feed: a cron job that watches the security-flavored commits on the Tier A programs and surfaces fresh fix commits within minutes of merge. This is the high-EV play because the race window is small and most researchers are not running this feed. The implementation is a few hundred lines of shell plus a list of repositories to watch and a regex pattern for security-class commits. Combined with the pre-submit gate, the conversion rate from research hour to dollar is dramatically different from the unconditional rate.

The hardest thing about bug bounty income is not the technical work. It is the meta-work: knowing which programs pay, which scopes pay, which bug classes pay at which programs, and which timing windows are open. That meta-work is what this piece tries to compress into a checklist. Everything else is just careful reading.


This post draws from a single intensive research session that produced six submissions, zero income, and the methodology that prevents that outcome in subsequent sessions. The shell scripts and program-tier landscape referenced here are part of an ongoing open-source publication effort. If this saved you a day of wasted submissions, the corresponding income capability charters that funded the research time are linked from the author's profile.

Top comments (0)