# Opportunity Radar (`lovablelynx/opportunity-radar`) Actor

Helps students find scholarships they actually qualify for and flags listings that look suspicious.

- **URL**: https://apify.com/lovablelynx/opportunity-radar.md
- **Developed by:** [Oluwadarasimi Olowe](https://apify.com/lovablelynx) (community)
- **Categories:** Open source
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $30.00 / 1,000 listing processeds

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Opportunity Radar

Find scholarships and grants you actually qualify for, and spot the ones
that look suspicious, before you spend hours applying.

**[Try it live](https://opportunity-radar-by-edubridge.vercel.app)** ·
**[Source](https://github.com/LovableLynx/opportunity-radar)** ·
🏆 **4th place** at the She Code Africa × Apify BuildHER Hackathon

Most scholarship sites just hand you a list and leave you to figure out two
things on your own: do you actually qualify, and is the listing even real.
Opportunity Radar checks both, using real scholarship and grant listings
found live on the web, not a stale database.

- **Do I qualify?** Your deadline, nationality, and education level are
  checked against what each listing actually requires, so you're not left
  guessing based on the title alone. If a listing only mentions something
  vague, like "preference for applicants with research experience," that
  gets flagged too, not silently ignored.
- **Is it real?** Every listing is checked for common warning signs, like
  being asked to pay upfront, pressure to apply immediately, or vague
  requirements, and cross-checked against what else is said about it
  online. A "Low Risk" result means nothing suspicious turned up, not a
  guarantee, so always use your own judgment too.

Under the hood, most scholarship-related Actors on the Apify Store stop at
scraping: they return a raw list and leave the actual matching and trust
judgment to you. Opportunity Radar does that work itself, with evidence
behind both verdicts, not just a badge.

### Who it's for

- Students at high school, Bachelors, Masters, or PhD level looking for
  scholarships or grants abroad, built with African students in mind
- Anyone tired of reading long eligibility text only to find out at the end
  that they don't qualify
- Anyone who wants a second opinion on whether a listing looks legitimate

### How to use it

1. **Get a free Groq API key.** Sign up at
   [console.groq.com/keys](https://console.groq.com/keys) and click
   "Create API Key". It's free and takes about a minute. Opportunity Radar
   uses it to read and judge each listing.
2. **Fill in your profile.** Education level, field of study, your country,
   and whether you need funding. Your GPA and CV are optional, but adding
   them helps with requirements that aren't clear-cut.
3. **Run it**, either on the [website](https://opportunity-radar-by-edubridge.vercel.app)
   or directly here on Apify. A run usually takes 5 to 7 minutes, because it
   checks live listings one by one.
4. **Read your results.** Each listing says whether you're eligible (and if
   not, exactly why), how much time is left before the deadline, and whether
   anything about it looks off.

### How it works

1. A student fills in their profile on the web app: education level, field
   of study, country, and an optional CV.
2. Opportunity Radar scrapes real, current scholarship listings that match
   their education level.
3. Each listing gets checked against the student's profile for eligibility,
   then separately scored for trust risk.
4. Results come back as a plain digest: what you qualify for, what needs a
   closer look, and what to watch out for.

Under the hood, this is an [Apify Actor](https://apify.com), a scraping and
automation program hosted on Apify's platform, paired with a small web
frontend on Vercel that starts a run and shows the results.

The Actor is the entire brain of this product: scraping, eligibility
matching, and trust scoring all happen inside it. The website does not
duplicate or replace any of that logic, it only validates form input before
starting a run, then polls Apify for status and renders whatever the Actor
already computed. Remove the frontend entirely and the Actor still does
everything that matters; it can be run directly from Apify Console or the
API with the same input shape described below.

### Input

| Field | Required | Example |
| --- | --- | --- |
| `groqApiKey` | yes | Free at [console.groq.com/keys](https://console.groq.com/keys). Bring-your-own-key: your AI matching and trust-scoring runs on your own account, not the operator's, so nothing is billed to you beyond your own Groq usage. Not needed with `maintenanceRun`. |
| `educationLevel` | yes | `Bachelors` (one of High school, Bachelors, Masters, PhD) |
| `fieldOfStudy` | yes | `Computer Science` |
| `country` | yes | `Nigeria` |
| `fundingNeeded` | no | `true` |
| `gpaFormat` + `gpaOrGrade` | no | Pick a scale (`cgpa4`, `cgpa5`, `percentage`, `classification`, `waec`, `other`) so the value below is read correctly instead of guessed |
| `cvFile` | no | Upload a CV as PDF (max 5MB), text is extracted automatically |
| `cvText` | no | Plain text of your CV, used if no PDF is uploaded |

### Known Apify Console limitations

Two things worth knowing if you're testing the Actor directly in Apify
Console rather than through the website, both genuine platform constraints,
not gaps in this Actor's own logic:

- **No autocomplete/suggestions on `fieldOfStudy`.** The website's form
  suggests real field names as you type (catching a typo like "Business
  Administartion" before it's submitted); Console's auto-generated Input
  form has no equivalent for a plain text field, so it stays free text
  there. The Actor itself still checks the input isn't obvious gibberish
  (too short or no vowels at all) before starting a real, billed run.
- **`cvFile`'s upload dialog can't be restricted to PDF only.** Apify's
  `fileupload` input editor has no schema-level file-type restriction; any
  file can be selected in Console's upload box regardless of what a field's
  title says. This Actor checks the real file signature (the literal
  `%PDF-` bytes a genuine PDF always starts with, not the filename or
  extension) before trusting it, so a non-PDF upload is safely rejected
  with a clear error rather than silently misread.

### Output

One record per listing. Full field definitions live in
`.actor/output_schema.json`, which also defines the table view Apify Console
renders on the run's Output tab. The fields that matter most:

- `title`, `link`, `deadline`, `urgency`: what the opportunity is and how
  soon it closes.
- `eligibilityMatch`: `Eligible`, `Partial`, or `Not Eligible`, with
  `missingRequirements` and `actionSteps` explaining why and what to do.
- `trustRisk`: `Low Risk`, `Some Concerns`, or `High Risk`, with
  `trustEvidence` listing the actual reasons and `trustConfidence` saying how
  much evidence the verdict rests on.

### Example run

Sample input, a Bachelors student in Nigeria studying Computer Science:

```json
{
  "groqApiKey": "gsk_your_real_key_here",
  "educationLevel": "Bachelors",
  "fieldOfStudy": "Computer Science",
  "country": "Nigeria",
  "fundingNeeded": true
}
```

One real record from an actual run against that profile, trimmed to the
fields that matter most, showing a genuine field-of-study rejection the LLM
caught in the listing's own text:

```json
{
  "title": "Mary Doctor Fine Arts Scholarship",
  "link": "https://www.bachelorsportal.com/scholarships/8918/mary-doctor-fine-arts-scholarship.html",
  "deadline": "19 Mar 2027",
  "eligibilityMatch": "Not Eligible",
  "missingRequirements": [
    "The scholarship requires applicants to plan to pursue an undergraduate degree in an arts discipline (e.g. music, dance, theatre, digital arts, etc.), which does not include Computer Science."
  ],
  "trustRisk": "Some Concerns",
  "trustEvidence": ["No independent web presence found for the sponsoring organization"]
}
```

That restriction is stated in the listing's own eligibility text, not
obvious from the title alone. Every rejection like this comes with the
specific reason, not just a pass/fail flag.

### Technologies and tools used

- **[Apify](https://apify.com)**: the Actor itself, plus its `apify/website-content-crawler`
  Actor called directly from this Actor's own code for the actual page
  fetches, residential proxy, key-value store, and Pay-Per-Event billing.
- **[Groq](https://groq.com)**: the AI provider for eligibility interpretation
  and trust-scoring reasoning, bring-your-own-key (see Input above); Gemini
  and OpenRouter are supported operator-side alternatives.
- **Node.js** (Apify SDK, `@google/generative-ai`, `pdfjs-dist` for server-side
  CV extraction): the Actor's own runtime.
- **Vercel**: hosts the static frontend and its two serverless API routes
  (start a run, poll its status).
- **Plain HTML/CSS/JS**: the frontend, no framework.
- **Node's built-in test runner** (`node --test`) and **Playwright**: backend
  unit tests and frontend end-to-end tests, respectively.

### Project structure

- `src/` holds the Actor itself: scraping, eligibility matching, trust
  scoring.
- `frontend/` holds the web app, a static site plus two small API routes
  that talk to the Actor.
- `test/` has automated tests for the backend logic. Run with `npm test`.
- `eval/` has a curated set of real and synthetic listings, used to check
  the trust-scoring and matching rules stay correct as they get tuned.
- `frontend/tests/` has end-to-end tests for the web app, written with
  Playwright.

### Running it locally

```bash
npm install
```

A real run needs a [Groq](https://console.groq.com/keys) API key, since
that's the default AI provider (bring-your-own-key, see Input above). Pass
it in `storage/key_value_stores/default/INPUT.json` alongside the rest of
your test profile, or set `GROQ_API_KEY` in `.env` for a quick
`maintenanceRun` (that path is the operator/env-var one, not the
student-facing one). Gemini and OpenRouter also work as the underlying
provider; see `.env.example` for how to switch (an operator-side setting,
`LLM_PROVIDER`, not something a student picks). You'll also need the Apify
CLI (`npm install -g apify-cli`). Then:

```bash
apify run
```

Results are written to `storage/datasets/default/`.

To run the web app locally, run `npm run dev` inside `frontend/`. That uses
the Vercel CLI and needs an `APIFY_API_TOKEN` so it can start real Actor
runs. To just look at the interface without a token, click "See a live
example" on the landing page, which loads saved sample results.

### Running the tests

```bash
npm test              # backend logic, no live API calls, safe to run freely
npm run eval:trust     # trust-scoring accuracy against known real/synthetic cases
npm run eval:match     # eligibility-matching accuracy against known cases
```

Inside `frontend/`:

```bash
npm test               # end-to-end browser tests (Playwright)
npm run test:api       # API route validation tests
```

### Monetization

Opportunity Radar uses Apify's Pay-Per-Event pricing, one billable event per
listing that's been fully matched and trust-scored: $0.03 per listing, plus
a tiny $0.00005 start fee per run. A run usually returns 20 to 60 listings,
so it typically costs about $0.60 to $1.80. The AI part runs on your own
free Groq key, so there's no separate AI bill on top.

Other scholarship Actors on the Store charge per scraped result, typically
$0.35–$6.00 per 1,000 listings, for raw data with no eligibility or trust
logic applied. Charging per fully-analyzed listing instead reflects that
what's being billed is a matching and trust verdict, not a scrape.

### Sustainability & future potential

This isn't a one-shot script; the architecture is built to keep getting
better and to grow without a rewrite.

- **It already learns.** Every run that finds a repeating suspicious phrase
  across multiple listings (`cross-listing-patterns.js`) saves it to the
  Actor's key-value store (`learned-patterns.js`), and every future run
  checks new listings against that growing list on top of the fixed
  starting patterns. The trust-scoring model gets sharper with use, without
  anyone retraining anything.
- **Adding a new scholarship source is one object, not a rewrite.**
  `SOURCES` in `src/scrape.js` is a plain map of name, start URL, and
  description; extraction from a scraped page is LLM-driven, not brittle
  CSS selectors tied to one site's markup, so a new source (another
  country's listings, a new provider) is a small, low-risk addition, not a
  new scraper to build from scratch.
- **More education levels and regions are a config change.** The same
  pattern that already splits PhD/Masters/Bachelors listings by source
  (`sourceKeysForEducationLevel`) extends the same way to new
  countries or education systems.
- **A scheduled maintenance mode already exists.** `maintenanceRun` input
  scrapes every source on a schedule with no student attached and no
  billing, catching a source going down before a real student's run would.
  This is the seed of a properly cached, always-warm version of the product.
- **Open source.** The full source lives at
  [github.com/LovableLynx/opportunity-radar](https://github.com/LovableLynx/opportunity-radar),
  so the matching and trust-scoring logic can be reviewed, adapted, or
  built on by anyone, not locked inside a black-box Actor.

### FAQ

**Do I really need a Groq API key?**
Yes, but it's free. Get one at [console.groq.com/keys](https://console.groq.com/keys).
It means the AI part runs on your own account, so the tool doesn't have to
charge you for it.

**What happens to my CV?**
If you add one, its text is used during your run to check requirements that
aren't clear-cut, like "research experience preferred." It's sent to Groq
on your own key for that check, and it isn't copied into your results. Like
any Apify run, your input stays in that run's own storage on your account.

**Which countries does it work for?**
You can pick any country. It was built with African students in mind, but
eligibility checks work the same way for everyone.

**Does "Low Risk" mean a scholarship is definitely real?**
No. It means none of the warning signs we check for turned up. Always
confirm on the provider's official website before applying or sharing any
personal information.

**Why does a run take several minutes?**
It visits live scholarship sites and checks each listing one by one. Some
sites try to block automated visitors, and the tool retries until it gets
through, which takes time.

### Team

| Name | Role |
| --- | --- |
| Oluwadarasimi Olowe | Project lead, backend, QA / test automation |
| Peace Sandy | Frontend Developer |
| Temilade Ajiboye | UI/UX Designer |

### Further reading

Deeper write-ups live at the repo root rather than in this README:

- `Opportunity-Radar-One-Pager.pdf`, the elevator pitch
- `Opportunity-Radar-Build-Plan.pdf`, architecture and technical decisions
- `Opportunity-Radar-PRD-Monetization-Addendum.pdf`, the PPE pricing rationale
- `Opportunity-Radar-Status-Update.pdf` / `Opportunity-Radar-Team-Brief.pdf`,
  project status and team notes

Specific tradeoffs (why a check works the way it does, a bug that shaped a
fix) are documented as comments next to the relevant code, not repeated here.

# Actor input Schema

## `groqApiKey` (type: `string`):

Free at https://console.groq.com/keys. Required so the Actor's AI matching and trust-scoring runs on your own account, not the operator's. Not needed if maintenanceRun is set.

## `educationLevel` (type: `string`):

The user's current or target education level.

## `fieldOfStudy` (type: `string`):

The subject at the education level above (not an earlier degree), e.g. Computer Science, Public Health, Economics. At least 2 letters, must contain a real word (not random consonants).

## `country` (type: `string`):

Your own country, used to check nationality/location-based eligibility. Not the country the scholarship is hosted in.

## `fundingNeeded` (type: `boolean`):

Whether the student needs full or partial funding.

## `gpaFormat` (type: `string`):

Pick the scale your grade below is in. This matches the format picker on the website — picking a scale here means the number below is read correctly instead of guessed at.

## `gpaOrGrade` (type: `string`):

The number or grade itself, matching the format picked above: CGPA → just the number (e.g. 4.5); Percentage → just the number out of 100, no % sign (e.g. 72, not 72% or 0.72); Classification → e.g. First Class, Second Class Upper; WAEC/NECO → comma-separated "Subject: Grade" pairs, e.g. "English: B3, Mathematics: B2, Biology: C4, Chemistry: C5, Physics: C6" (the website requires at least 5 subjects, but this field itself accepts any number); Other → any format, e.g. 8/10.

## `cvFile` (type: `string`):

Upload your CV as a PDF (max 5MB) and its text is extracted automatically. If both this and CV content (text) below are provided, the uploaded PDF takes priority, same as the website. Only PDF files are actually usable — anything else is safely rejected by the Actor itself once uploaded.

## `cvText` (type: `string`):

Plain text of the student's CV, used only if no PDF is uploaded above. When provided, ambiguous eligibility criteria (e.g. 'research experience preferred') are checked against real CV content instead of left unconfirmed.

## `maintenanceRun` (type: `boolean`):

For an Apify Schedule only, not for a real student. Scrapes every education-level source to check they still work, with no profile attached and no PPE billing. When true, educationLevel/fieldOfStudy/country are ignored. Only genuinely works when the run is actually started by a Schedule; setting this directly (Console, API) with no groqApiKey still requires one, since the operator's own key is reserved for real scheduled maintenance checks.

## Actor input object example

```json
{
  "fundingNeeded": true,
  "gpaFormat": "",
  "maintenanceRun": false
}
```

# Actor output Schema

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("lovablelynx/opportunity-radar").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("lovablelynx/opportunity-radar").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call lovablelynx/opportunity-radar --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,lovablelynx/opportunity-radar"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/gP4FHbfhRuuoNWuQO/builds/HwV85RKpajjdkqSUB/openapi.json
