# Freelancer.com Scraper - Projects, Parsed Budgets & Skills (`techforce.global/freelancer`) Actor

Scrape live Freelancer.com projects by keyword with budget, skills & poster info. Filter by budget, export to CSV/JSON/Excel, or deliver leads to Notion, Slack & Jira via MCP connectors.

- **URL**: https://apify.com/techforce.global/freelancer.md
- **Developed by:** [Techforce Global](https://apify.com/techforce.global) (community)
- **Categories:** Agents, Jobs, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Freelancer.com Project Scraper - Public Projects by Keyword, with Parsed Budget Ranges, Skill Tags and Per-Row Keyword Attribution

[![Apify Actor](https://img.shields.io/badge/Apify-Actor-orange?logo=apify)](https://apify.com/techforce.global/freelancer)
[![MCP Ready](https://img.shields.io/badge/MCP-Compatible-blue?logo=anthropic)](https://mcp.apify.com?tools=techforce.global/freelancer)
[![Pricing](https://img.shields.io/badge/Pricing-Usage--based-green)](https://apify.com/techforce.global/freelancer)
[![Scope](https://img.shields.io/badge/Scope-active%20fixed--price%20projects-brightgreen)](#-input-contract)
[![Output](https://img.shields.io/badge/Output-11%20fields%20per%20project-darkgreen)](#-output-contract--data-structure)
[![Keys](https://img.shields.io/badge/API%20keys-none%20required-informational)](#-input-contract)

> **Keyword in, live gigs out — with the budget as a number, not a string.** Searches
> **active fixed-price projects** on Freelancer.com and returns **title, numeric budget range,
> currency symbol, tagged skills, posting time and project URL**, one row per project, each tagged
> with the keyword that found it. **No Freelancer account, no login, no API key.** Optional
> delivery into Notion, Slack, Linear, Jira, GitHub or Airtable over MCP.

#### 🔐 Trust & scope at a glance

|                                            |                                                                                                                                                                 |
| :----------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Credentials**                            | None required. No Freelancer login, no OAuth, no API key. Delivery runs through the Apify MCP Proxy, so the Actor never sees your connector tokens.             |
| **Data scope**                             | **Active, fixed-price projects only.** Hourly projects, closed projects, bids and proposals are all out of scope.                                               |
| **Personal data**                          | Minimal — and mostly absent. The `poster` block exists but **commonly comes back all `"N/A"`**; treat any populated value as personal data about a real client. |
| **⚠️ Currency**                            | `currency` is a **symbol**, not an ISO code, and `$` is ambiguous across USD/AUD/CAD/NZD/SGD/HKD. Never aggregate budgets across symbols.                       |
| **⚠️ The `budget` string is display-only** | It is always rendered with a literal `$`, even when `currency` says otherwise. **Use `budgetMin`/`budgetMax` + `currency` for anything numeric.**               |

Full detail: **[Compliance & Data Privacy](#-compliance--data-privacy)**.

***

#### Freelancer.com's own API vs. this Actor

Freelancer publishes a developer API, but it is built around participating in the marketplace
rather than analysing it:

| Feature / Capability          | Freelancer developer API                                       | Freelancer.com Project Scraper (Apify)                    |
| :---------------------------- | :------------------------------------------------------------- | :-------------------------------------------------------- |
| **Access**                    | ❌ Register an app, obtain an OAuth token, manage its lifetime | ✅ Apify account, nothing else                            |
| **Multi-keyword sweeps**      | ❌ You orchestrate the loop, paging and merging                | ✅ A keyword list in, one dataset out, each row tagged    |
| **Budget as a number**        | ⚠️ Available, but you normalise it yourself                    | ✅ `budgetMin` / `budgetMax` ready to sort and filter     |
| **Server-side budget filter** | ⚠️ Your code, your pagination                                  | ✅ `minBudget` / `maxBudget` passed to the search itself  |
| **Delivery into your tools**  | ❌ Build the integration                                       | ✅ Notion, Slack, Linear, Jira, GitHub, Airtable over MCP |
| **Scheduling and storage**    | ❌ Your infrastructure                                         | ✅ Apify schedules, dataset, API, CSV/Excel export        |
| **AI agent / MCP native**     | ❌ Requires a custom tool wrapper                              | ✅ Native MCP tool                                        |
| **Officially sanctioned**     | ✅ Yes                                                         | ⚠️ No — see [Compliance](#-compliance--data-privacy)      |

**Use Freelancer's own API if you're bidding, messaging or managing projects.** Use this Actor when
you want the market as data — sourcing, pricing research or a lead feed.

> Freelancer's API terms and endpoint availability are theirs to change — check their current
> developer documentation if this comparison is load-bearing for your decision.

***

### 🤖 AI Discovery & MCP Metadata

Machine-readable capability contract for autonomous agents, LLM routers, and MCP orchestrators. Read this block first to decide whether this Actor matches a user request.

```yaml
ai_discovery:
    slug: "techforce.global/freelancer"
    one_line: >
        Searches ACTIVE FIXED-PRICE projects on Freelancer.com for one or more keywords and returns
        title, numeric budget range, currency symbol, tagged skills, posting time and project URL -
        one row per project, each tagged with the keyword that produced it. No Freelancer account or
        API key. Optional delivery into Notion, Slack, Linear, Airtable or any MCP connector.
    primary_purpose: "extract"
    secondary_purpose: "deliver"
    entity: "Freelancer.com project listing"
    category: "freelance-marketplace / lead-generation / pricing-research / gig-sourcing"
    supported_platforms: ["freelancer.com"]
    supported_tasks:
        - "search-freelancer-projects-by-keyword"
        - "filter-projects-by-budget-range"
        - "monitor-new-projects-on-a-schedule"
        - "benchmark-what-clients-pay-for-a-skill"
        - "find-in-demand-skills-from-project-tags"
        - "deliver-projects-to-mcp-connector"
    unsupported_tasks:
        - "scrape-hourly-projects (only fixed-price projects are requested)"
        - "scrape-closed-completed-or-awarded-projects (active listings only)"
        - "scrape-bids-proposals-or-bid-counts"
        - "scrape-client-contact-details-or-messages"
        - "scrape-freelancer-profiles-or-portfolios"
        - "place-a-bid-or-any-write-action"
        - "scrape-contest-listings"
    search_keywords:
        # First entry is the H1 / meta-description target. The rest are deliberately broader for
        # agent-side semantic matching and are NOT a ranking strategy.
        - "freelancer.com scraper"
        - "freelance project scraper"
        - "freelancer api alternative"
        - "gig marketplace data"
        - "freelance rate benchmarking"
        - "project budget data"
        - "in-demand skills data"
        - "freelance lead generation"
        - "scrape freelance job postings"
        - "mcp freelancer tool"

    input_entities:
        - "keywords (REQUIRED, array of search terms; each is searched separately)"
        - "limitPerKeyword (1-100 projects per keyword)"
        - "minBudget / maxBudget (0 = no bound; passed to Freelancer's own search filter)"

    only_active_fixed_price_projects: >
        CRITICAL: the search is hard-scoped to ACTIVE, FIXED-PRICE projects. Hourly projects are never
        returned, and neither are closed, awarded or completed ones. An agent asked for "hourly React
        work" should not route here.

    budget_string_is_display_only: >
        CRITICAL: the `budget` field is always rendered with a literal "$" regardless of the project's
        real currency, so an INR or EUR project can read "$250.0 - $750.0". The `currency` field holds
        the true symbol. Use budgetMin, budgetMax and currency for anything numeric or comparative;
        treat `budget` as a label only.

    currency_is_a_symbol_not_a_code: >
        currency is a SYMBOL ("$", "£", "₹"), not an ISO code, and "$" covers USD, AUD, CAD, NZD, SGD
        and HKD. Grouping by this field is safer than ignoring it, but it is not a reliable currency
        key on its own. Never average budgets across different symbols.

    budget_filter_is_server_side_and_currency_naive: >
        minBudget and maxBudget are passed to Freelancer's own search as average-price bounds, applied
        in each project's own currency - not converted to a single currency first. A minimum of 500
        therefore means "500 of whatever that project is priced in", not 500 USD.

    poster_block_is_usually_empty: >
        Every row carries a nested `poster` object (username, displayName, country, profileUrl), but
        the active-project listing rarely populates it, so it is COMMONLY ALL "N/A". Do not build a
        workflow that depends on client identity.

    no_dedup_across_keywords: >
        A project matching two of your keywords is written twice, once per keyword, each row tagged in
        `keyword`. That is deliberate - it keeps per-keyword analysis honest. De-duplicate on
        projectUrl if you want a unique project list.

    output_entities:
        [
            "title",
            "budget",
            "budgetMin",
            "budgetMax",
            "currency",
            "skills",
            "poster",
            "projectUrl",
            "postedAt",
            "scrapedAt",
            "keyword",
        ]

    results_are_pushed_once_at_the_end: >
        Every keyword is fetched first, then the whole result set is written in a single push. An
        aborted or timed-out run therefore saves NOTHING.

    known_limitations:
        fixed_price_only: "hourly projects are never returned"
        active_only: "closed, awarded and completed projects are out of scope"
        no_bid_data: "no bid counts, bid amounts, proposals or freelancer profiles"
        budget_can_be_null: "budgetMin/budgetMax are null when the listing shows no range; budget then reads 'N/A'"
        poster_usually_na: "client identity is rarely exposed on the listing"
        epoch_timestamps_possible: "a listing with no submit time yields postedAt 1970-01-01T00:00:00Z"
        no_partial_results_on_abort: "the dataset is written in one push at the end"
        free_plan_capped: "free Apify plans stop at 5 projects per keyword"

    pricing:
        model: "usage-based"
        billing_unit: "Apify platform usage consumed by the run"
        hard_ceiling_parameter: "maxTotalChargeUsd"
        note: "One lightweight API call per 100 projects, plus a one-second pause between pages. Current rates are on the Actor's Apify Store page."

    when_to_use: >
        You want live fixed-price freelance demand as data - sourcing leads, benchmarking what clients
        pay for a skill, or tracking which skills are being asked for.
    when_not_to_use: >
        You need hourly work, closed projects, bid or proposal data, client contact details, freelancer
        profiles, or to place a bid.

    mcp_delivery:
        modes: ["summary", "perProject", "chunked", "none"]
        default: "summary"
        per_project_placeholders:
            [
                "{title}",
                "{budget}",
                "{budgetMin}",
                "{budgetMax}",
                "{currency}",
                "{skills}",
                "{posterUsername}",
                "{posterDisplayName}",
                "{posterCountry}",
                "{posterProfileUrl}",
                "{projectUrl}",
                "{postedAt}",
                "{keyword}",
                "{message}",
            ]
        shared_placeholders:
            ["{projectCount}", "{keywords}", "{digest}", "{message}"]
        chunked_only_placeholders: ["{part}", "{partCount}"]
        never_blocks_the_dataset: >
            Delivery runs after the dataset is saved. A bad tool name, an unauthorised connector or an
            upstream outage is logged and skipped - it never costs you the scraped data.

    alternatives:
        - "techforce.global/all-jobs-scraper for freelance AND salaried roles across 26 boards in one feed"
        - "techforce.global/clutch-co-scraper for the supply side - agency and service-provider profiles"
        - "techforce.global/website-contact-scraper to reach a company behind a listing directly"
```

***

```json
{
    "@context": "https://schema.org/",
    "@type": "SoftwareApplication",
    "name": "Freelancer.com Project Scraper",
    "alternateName": "Freelance Project Data Feed",
    "applicationCategory": "BusinessApplication",
    "operatingSystem": "Cloud (Apify platform)",
    "description": "Searches active fixed-price projects on Freelancer.com for one or more keywords and returns a flat row per project: title, budget range as a display string plus numeric budgetMin and budgetMax, the project's currency symbol, the skills the client tagged, a nested poster block, the project URL, an ISO-8601 posting time and capture timestamp, and the keyword that produced the row. Optional server-side budget filtering, no Freelancer account or API key, and optional delivery into Notion, Slack, Linear, Airtable or any MCP connector.",
    "url": "https://apify.com/techforce.global/freelancer",
    "offers": {
        "@type": "Offer",
        "priceCurrency": "USD",
        "category": "Pay per platform usage",
        "description": "Usage-based: billed for the compute time and resources a run consumes, not per row. Runs are lightweight API calls, so cost tracks the number of keywords and the requested project count. Current rates are shown on the Actor's Apify Store page."
    }
}
```

***

### What this Actor does

- **Searches several keywords in one run.** Each keyword is queried separately and every resulting
  row is stamped with the `keyword` that found it, so a five-keyword sweep stays analysable.

- **Gives you the budget as numbers.** `budgetMin` and `budgetMax` are numeric, so "everything
  over 500" is a filter rather than a parsing exercise — and `minBudget` / `maxBudget` push that
  filter into Freelancer's own search so you don't pay to fetch what you'll discard.

- **Returns the skills the client actually tagged.** `skills` is an array straight off the
  listing — the cleanest signal of what a project really needs.

- **Scoped to live, biddable work.** Only **active, fixed-price** projects are requested, so the
  dataset is work you could still win rather than a historical archive.

- **Timestamps you can sort on.** `postedAt` is ISO-8601 UTC from the listing's own submit time,
  and `scrapedAt` records when you captured it — necessary, because budgets and availability move.

- **Honest about what the listing doesn't say.** Budgets come back `null` (and `budget` reads
  `"N/A"`) when the client published no range, and the `poster` block is present but usually
  empty — documented rather than quietly implied.

- **No credentials at all.** No Freelancer account, no OAuth app, no API key.

- **Optionally delivers into your tools.** Notion, Slack, Linear, Jira, GitHub, Airtable or
  anything else you've authorised, in four delivery shapes.

***

### When to use it — and when not to

| Scenario                                    | Fit                                                               |
| ------------------------------------------- | ----------------------------------------------------------------- |
| Finding live fixed-price work for a skill   | ✅ Strong — the core use case                                     |
| Budget-floor sourcing ("nothing under 500") | ✅ Strong — `minBudget` filters at the source                     |
| Benchmarking what clients pay for a skill   | ✅ Strong — but group by `currency` first                         |
| Tracking which skills clients ask for       | ✅ Strong — aggregate the `skills` array                          |
| New-project alerts on a schedule            | ✅ Strong — schedule it, de-duplicate on `projectUrl`             |
| Comparing demand across several keywords    | ✅ Strong — every row carries its `keyword`                       |
| Results in Notion / Slack / Airtable        | ✅ Strong — native MCP delivery                                   |
| A unique project list                       | ⚠️ Rows repeat across keywords by design — dedupe on `projectUrl` |
| Cross-currency budget comparison            | ⚠️ `currency` is a symbol and `$` is ambiguous                    |
| Knowing who posted a project                | ⚠️ The `poster` block is usually all `"N/A"`                      |
| **Hourly projects**                         | ❌ Only fixed-price projects are requested                        |
| **Closed, awarded or completed projects**   | ❌ Active listings only                                           |
| **Bids, proposals or bid counts**           | ❌ Not collected                                                  |
| **Client contact details or messages**      | ❌ Never accessible                                               |
| **Freelancer profiles or portfolios**       | ❌ Out of scope                                                   |
| Contest listings                            | ❌ Not requested                                                  |
| Placing a bid or any write action           | ❌ Read-only                                                      |

***

### ⚡ Execution Rules (Avoid Common Agent Failures)

1. **`keywords` is required and is an array.** Each entry is searched separately, so five keywords
   is five sweeps. Every output row carries the `keyword` that produced it.

2. **Only active, fixed-price projects are returned.** Hourly work is never included — that is a
   hard scope decision in the search, not a filter you can switch off. An agent asked for hourly
   work should route elsewhere.

3. **Do not do arithmetic on `budget`.** It is a display string and it is **always rendered with a
   literal `$`**, even when the project is priced in rupees or euros. `budgetMin`, `budgetMax` and
   `currency` are the fields to trust.

4. **`currency` is a symbol, not an ISO code.** `$` covers USD, AUD, CAD, NZD, SGD and HKD.
   Grouping by it is better than ignoring it, but it is not a reliable currency key —
   **never average across symbols**.

5. **`minBudget` / `maxBudget` are applied by Freelancer, in the project's own currency.** They
   bound the project's *average* price at the source, so `minBudget: 500` means "500 of whatever
   that project is priced in", not 500 USD. Both default to `0`, meaning no bound.

6. **`budgetMin` / `budgetMax` can be `null`.** When the listing publishes no range, both are
   null and `budget` reads `"N/A"`. Null is not zero — exclude those rows from averages.

7. **The `poster` block is usually all `"N/A"`.** The listing endpoint rarely exposes client
   identity. Don't build a workflow that depends on it.

8. **Rows repeat across keywords, deliberately.** A project matching two keywords appears twice,
   once per keyword. De-duplicate on `projectUrl` if you want a unique list.

9. **`limitPerKeyword` caps at 100.** Larger values are rejected by the schema; the search pages
   through in batches with a one-second pause between them.

10. **A keyword that errors is skipped, not fatal.** The failure is printed and the run moves on
    to the next keyword, still finishing successfully.

11. **`postedAt` can be the Unix epoch.** A listing with no submit time yields
    `1970-01-01T00:00:00Z`. Treat pre-2000 dates as missing, not as very old projects.

12. **Nothing is saved until the end.** All keywords are fetched, then the whole set is written in
    **one push** — an aborted or timed-out run saves **nothing**.

13. **`deliveryMode` defaults to `summary`.** `perProject` sends one connector call per project; a
    300-project run is 300 calls.

14. **Free Apify plans stop at 5 projects per keyword.** A smoke test, not a data pull.

```yaml
dependencies:
    - rule: "keywords is required and must contain at least one non-empty term"
      on_violation: "nothing is searched and the run finishes with an empty dataset"
    - rule: "budget is a display string with a hard-coded $ - budgetMin/budgetMax/currency are authoritative"
      note: "the only field-level trap in this Actor"
    - rule: "minBudget/maxBudget bound the average price in the project's own currency"
      note: "they are not converted to a single currency first"
    - rule: "delivery requires mcpConnector AND mcpTool"
      on_violation: "delivery is skipped with a warning; every dataset row is still written"
    - rule: "mcpArguments must reference {message} or {digest} for the rendered text to appear"
      on_violation: "the tool is called with an empty body"
    - rule: "mcpConnector only resolves when the Actor runs on the Apify platform"
      on_violation: "local runs skip delivery; the dataset is unaffected"
authentication:
    actor_input: none
    platform: "Apify API token required for API / SDK / MCP invocation"
    target_site: "no Freelancer.com account, OAuth app or API key required (public listings only)"
```

***

### 📥 Input Contract

`.actor/input_schema.json` is authoritative — if this page disagrees with the schema files,
**the schema files win**.

| Parameter         | Type                | Required   | Default                      | Notes                                                        |
| ----------------- | ------------------- | ---------- | ---------------------------- | ------------------------------------------------------------ |
| `keywords`        | array\<string> | ✅ **Yes** | `["python", "web scraping"]` | Each term is searched separately and stamped on its rows     |
| `limitPerKeyword` | integer             | ⬜ No      | `20`                         | `1`–`100` projects **per keyword**                           |
| `minBudget`       | integer             | ⬜ No      | `0`                          | Lower bound on the project's average price. `0` = no minimum |
| `maxBudget`       | integer             | ⬜ No      | `0`                          | Upper bound. `0` = no maximum                                |

> **What the budget bounds really do.** They are handed to Freelancer's own search as
> average-price limits and applied **in each project's own currency** — they are not converted to
> a common currency first. On a multi-currency result set, treat them as a coarse filter and do
> the precise work yourself on `budgetMin` / `budgetMax` + `currency`.

#### Delivery (optional)

| Parameter            | Type          | Default     | Description                                                                                                                                                              |
| -------------------- | ------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `mcpConnector`       | string        | `""`        | A connector you've authorised on your Apify account — Notion, Slack, Linear, Jira, GitHub, Airtable, or any MCP-compatible connector. Leave empty for dataset-only runs. |
| `deliveryMode`       | string (enum) | `"summary"` | `summary` (one digest) · `perProject` (one call per project) · `chunked` (digest split across calls) · `none`.                                                           |
| `mcpTool`            | string        | `""`        | Tool to call — `create-page`, `send-message`, `create_issue`. **Required** when a connector is set.                                                                      |
| `mcpArguments`       | object        | `{}`        | Arguments for the tool. String values support `{placeholder}` tokens.                                                                                                    |
| `mcpMessageTemplate` | string        | `""`        | Template rendered and exposed to `mcpArguments` as `{message}`.                                                                                                          |

**Placeholders by mode:**

| Mode         | Placeholders                                                                                                                                                                                                                        |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `summary`    | `{projectCount}` `{keywords}` `{digest}` (every project, formatted) `{message}`                                                                                                                                                     |
| `chunked`    | the same, plus `{part}` `{partCount}` — `{digest}` holds one part                                                                                                                                                                   |
| `perProject` | the shared three, plus `{title}` `{budget}` `{budgetMin}` `{budgetMax}` `{currency}` `{skills}` `{posterUsername}` `{posterDisplayName}` `{posterCountry}` `{posterProfileUrl}` `{projectUrl}` `{postedAt}` `{keyword}` `{message}` |

`chunked` groups the digest so no single call exceeds roughly 72,000 characters, with each project
block packed to about 1,800 characters — which keeps Notion's block and timeout limits happy.

***

### 📤 Output Contract & Data Structure

**One dataset row per project per keyword.** Every row carries the same 11 keys.

| Field        | Type   | Nullable | Meaning                                                                                                                |
| ------------ | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------- |
| `title`      | string | yes      | Project title as posted. `"N/A"` when the listing exposed none                                                         |
| `budget`     | string | yes      | **Display only.** Always formatted with a literal `$`, whatever the real currency. `"N/A"` when no range was published |
| `budgetMin`  | number | **yes**  | Lower bound, numeric. **Use this for filtering and arithmetic**                                                        |
| `budgetMax`  | number | **yes**  | Upper bound, numeric                                                                                                   |
| `currency`   | string | yes      | The project's currency **symbol** (`$`, `£`, `₹`) — not an ISO code, and `$` is ambiguous                              |
| `skills`     | array  | no       | Skills the client tagged. `[]` when none were tagged                                                                   |
| `poster`     | object | no       | `{username, displayName, country, profileUrl}` — **commonly all `"N/A"`**                                              |
| `projectUrl` | string | yes      | Link to the project page. **The de-duplication key**                                                                   |
| `postedAt`   | string | yes      | ISO-8601 UTC posting time. `1970-01-01T00:00:00Z` when the listing had none                                            |
| `scrapedAt`  | string | yes      | ISO-8601 UTC capture time — **budgets and availability move, so this is the row's shelf life**                         |
| `keyword`    | string | yes      | Which of your input keywords produced this row                                                                         |

> **The one trap worth repeating:** `budget` is a label, `budgetMin`/`budgetMax`/`currency` are the
> data. A project priced at ₹20,000–₹50,000 will show `budget: "$20000.0 - $50000.0"` with
> `currency: "₹"`. Sorting or averaging on the string will quietly mislead you.

#### Example output object

```json
{
    "title": "Build a Python scraper for a real-estate portal",
    "budget": "$250.0 - $750.0",
    "budgetMin": 250.0,
    "budgetMax": 750.0,
    "currency": "$",
    "skills": ["Python", "Web Scraping", "Data Mining", "BeautifulSoup"],
    "poster": {
        "username": "N/A",
        "displayName": "N/A",
        "country": "N/A",
        "profileUrl": "N/A"
    },
    "projectUrl": "https://www.freelancer.com/projects/python/build-python-scraper-real-estate",
    "postedAt": "2026-08-31T14:07:12Z",
    "scrapedAt": "2026-09-01T09:14:02Z",
    "keyword": "web scraping"
}
```

> Values illustrate shape and formatting — the project is invented. The all-`"N/A"` `poster` block
> is the normal case, not an error.

#### What a successful run looks like

- **Status:** `SUCCEEDED`
- **Dataset:** one row per project per keyword, up to `limitPerKeyword` for each
- **Log:** a header echoing keywords, limit and budget bounds, then
  `Searching: '<keyword>'` and `Fetched N/M projects...` per keyword, then the push count
- **Zero results is also a success.** A keyword with no live fixed-price matches, or budget bounds
  nothing satisfies, returns an empty dataset with `SUCCEEDED`. The log shows
  `No more results for '<keyword>'`.
- **A keyword that errored is skipped**, logged as `Request failed`, and the run continues.
- **Fewer rows than `keywords × limitPerKeyword` is normal** — most keywords simply don't have
  that many live projects.

#### Dataset views

| View                  | Shows                                                                                |
| --------------------- | ------------------------------------------------------------------------------------ |
| **Projects**          | Title, budget string and numeric range, currency, skills, keyword, posted time, link |
| **Budgets**           | Numeric range, currency, budget string, title, skills, posted time, link             |
| **Keyword & Capture** | Keyword, title, posted and captured timestamps, poster block, link                   |

***

### ▶️ Invocation & Integration

Authenticate every call with `Authorization: Bearer <APIFY_TOKEN>`.

#### Apify Console

Open the [Actor page](https://apify.com/techforce.global/freelancer) → **Input** tab → add your
keywords → set `limitPerKeyword` → optionally set a budget floor or ceiling → **Start**.

#### Model Context Protocol (MCP)

```bash
claude mcp add --transport http apify \
  "https://mcp.apify.com/?tools=techforce.global/freelancer"
```

Then ask your agent for *"live Python scraping gigs paying over $500"*.

#### Python (apify-client)

```python
import os
from collections import Counter, defaultdict
from statistics import median
from apify_client import ApifyClient

client = ApifyClient(os.getenv("APIFY_TOKEN"))

run = client.actor("techforce.global/freelancer").call(run_input={
    "keywords": ["web scraping", "data engineering"],
    "limitPerKeyword": 50,
    "minBudget": 250,
    "maxBudget": 0,          # 0 = no ceiling
})

projects = list(client.dataset(run["defaultDatasetId"]).iterate_items())
if not projects:
    raise SystemExit("Nothing matched — try broader keywords or a lower minBudget.")

## Which keyword is actually finding the good work?
print(Counter(p["keyword"] for p in projects))

## currency is a SYMBOL and "$" is ambiguous — never aggregate across symbols.
by_symbol = defaultdict(list)
for p in projects:
    if p.get("budgetMin") is not None:
        by_symbol[p.get("currency") or "?"].append(p["budgetMin"])
for symbol, values in by_symbol.items():
    print(f"{symbol}  n={len(values):>3}  median floor {median(values):,.0f}")

## The same project can appear once per matching keyword — dedupe on projectUrl.
unique = {p["projectUrl"]: p for p in projects}
print(f"{len(unique)} unique projects out of {len(projects)} rows")

## What skills are clients actually asking for?
skills = Counter(s for p in unique.values() for s in (p.get("skills") or []))
print(skills.most_common(10))
```

#### JavaScript / TypeScript (apify-client)

```typescript
import { ApifyClient } from "apify-client";

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });

const run = await client.actor("techforce.global/freelancer").call({
    keywords: ["react", "next.js"],
    limitPerKeyword: 40,
    minBudget: 500,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();

// Never sort on `budget` — it is a display string with a hard-coded "$".
const priced = items.filter(
    (p) => p.budgetMin !== null && p.budgetMax !== null,
);
priced.sort((a, b) => b.budgetMax - a.budgetMax);

for (const p of priced.slice(0, 10)) {
    // Rebuild the label from the fields that are actually correct.
    const label = `${p.currency}${p.budgetMin} - ${p.currency}${p.budgetMax}`;
    console.log(
        `${label.padEnd(22)} ${(p.skills ?? []).slice(0, 3).join(", ").padEnd(34)} ${p.title}`,
    );
}

// postedAt can legitimately be the Unix epoch when the listing had no submit time.
const dated = items.filter((p) => p.postedAt && !p.postedAt.startsWith("1970"));
console.log(`${dated.length}/${items.length} rows have a usable postedAt`);
```

#### cURL — synchronous (waits, returns the dataset)

```bash
curl -X POST \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' \
  -d '{ "keywords": ["python"], "limitPerKeyword": 20, "minBudget": 0, "maxBudget": 0 }' \
  'https://api.apify.com/v2/acts/techforce.global~freelancer/run-sync-get-dataset-items'
```

#### cURL — asynchronous with a spend cap (recommended for large runs)

```bash
## 1. Start with a hard spend limit
curl -X POST -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' \
  -d '{ "keywords": ["python","react","logo design","copywriting"], "limitPerKeyword": 100 }' \
  'https://api.apify.com/v2/acts/techforce.global~freelancer/runs?maxTotalChargeUsd=5'

## 2. Poll, then 3. fetch the dataset when SUCCEEDED
curl -H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' 'https://api.apify.com/v2/actor-runs/%3CrunId%3E'
curl -H 'Authorization: Bearer <YOUR_APIFY_TOKEN>' \
  'https://api.apify.com/v2/datasets/%3CDATASET_ID%3E/items?clean=true&format=json'
```

#### Budget-floor sourcing

```json
{
    "keywords": ["web scraping", "data extraction", "crawler"],
    "limitPerKeyword": 100,
    "minBudget": 500,
    "maxBudget": 0
}
```

Pushing the floor into the search means you don't pay to fetch the £30 jobs and throw them away.
Remember the bound is applied in each project's own currency — verify with `budgetMin` +
`currency` afterwards if precision matters.

#### Skill-demand research

```json
{
    "keywords": ["machine learning"],
    "limitPerKeyword": 100,
    "minBudget": 0,
    "maxBudget": 0
}
```

One broad keyword, no budget bounds, then aggregate the `skills` arrays downstream — that's the
cleanest read on what clients are pairing with a skill right now.

#### With MCP delivery — new projects into Slack

```json
{
    "keywords": ["web scraping"],
    "limitPerKeyword": 20,
    "minBudget": 250,
    "mcpConnector": "<your-authorized-slack-connector>",
    "mcpTool": "send-message",
    "deliveryMode": "summary",
    "mcpMessageTemplate": "{projectCount} new Freelancer projects for {keywords}:\n\n{digest}",
    "mcpArguments": { "channel": "#gig-alerts", "text": "{message}" }
}
```

Use `summary` for a digest and `chunked` when the list is long. `perProject` is one message per
project — fine for 20, noisy for 300.

#### Scheduling for new-project alerts

Put a fixed input on a [schedule](https://docs.apify.com/platform/schedules), keep
`limitPerKeyword` modest, and de-duplicate against your own store on `projectUrl`. Anything you
haven't seen before is new work — and `postedAt` tells you how fresh it is.

***

### ⚠️ Autonomous Agent Error Handling & Resolution Matrix

**Read this first:** nothing about the *listings* fails the run. An empty keyword list, a failing
request and a budget filter nothing satisfies all end `SUCCEEDED` while the log carries the
reason. An agent that checks run status alone will read an empty dataset as success-with-data —
**check the row count and the log**.

The `CONDITION` labels are documentation handles for observable states; this Actor writes no error
code into the dataset.

| Condition                      | What you observe                                                       | Cause                                                | Deterministic agent action                                                   |
| ------------------------------ | ---------------------------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------- |
| `NO_KEYWORDS`                  | Empty dataset, no `Searching:` lines in the log                        | `keywords` empty                                     | **Modify Input:** supply at least one keyword.                               |
| `NO_RESULTS_FOR_KEYWORD`       | Log: `No more results for '<keyword>'`                                 | No live fixed-price projects match                   | **Expected.** Broaden the keyword or lower `minBudget`.                      |
| `REQUEST_FAILED`               | Log: `Request failed: …`, that keyword contributes nothing             | Freelancer's endpoint errored or timed out           | **Retry later.** Other keywords are unaffected; the run still succeeds.      |
| `HOURLY_WORK_MISSING`          | No hourly projects anywhere in the output                              | Only fixed-price projects are requested              | **Fix expectation.** Route hourly enquiries elsewhere.                       |
| `CLOSED_PROJECTS_MISSING`      | No completed or awarded projects                                       | Active listings only                                 | **Fix expectation.** Historical data is out of scope.                        |
| `BUDGET_STRING_WRONG_CURRENCY` | `budget` shows `$` but `currency` says `₹`                             | The display string is hard-coded with `$`            | **Fix consumer:** rebuild the label from `budgetMin`/`budgetMax`/`currency`. |
| `MIXED_CURRENCY_AGGREGATE`     | Budget averages look impossible                                        | `$` covers several currencies and symbols are mixed  | **Fix consumer:** group by `currency`, and treat `$` as ambiguous.           |
| `BUDGET_FILTER_SURPRISE`       | Rows below your `minBudget` in another currency                        | The bound is applied per project in its own currency | **Expected.** Re-filter on `budgetMin` + `currency` downstream.              |
| `NULL_BUDGET`                  | `budgetMin`/`budgetMax` null, `budget: "N/A"`                          | The listing published no range                       | **Expected.** Exclude from averages; null ≠ zero.                            |
| `POSTER_ALL_NA`                | Every `poster` field is `"N/A"`                                        | The listing endpoint rarely exposes client identity  | **Expected.** Don't depend on client identity.                               |
| `DUPLICATE_PROJECTS`           | The same `projectUrl` appears twice                                    | It matched two keywords; each row is tagged          | **Expected.** Dedupe on `projectUrl` for a unique list.                      |
| `EPOCH_POSTED_AT`              | `postedAt` is `1970-01-01T00:00:00Z`                                   | The listing had no submit time                       | **Fix consumer:** treat pre-2000 dates as missing.                           |
| `FEWER_ROWS_THAN_REQUESTED`    | Rows < `keywords × limitPerKeyword`                                    | That many live projects don't exist                  | **Expected.** Add keywords rather than raising the limit.                    |
| `NO_DATA_AFTER_ABORT`          | An aborted run left an empty dataset                                   | Results are written in one push at the end           | **Expected.** Lower `limitPerKeyword` so the run completes.                  |
| `FREE_PLAN_CAPPED`             | 5 projects per keyword; log says so                                    | Free Apify plan                                      | **Upgrade the plan.**                                                        |
| `CONNECTOR_CALL_STORM`         | Hundreds of connector calls                                            | `perProject` sends one call per project              | **Modify Input:** switch to `summary` or `chunked`.                          |
| `CONNECTOR_TOOL_NOT_FOUND`     | Log: `Tool 'x' is not available on this connector. Available tools: …` | Wrong `mcpTool` name (empty is logged separately)    | **Modify Input:** copy an exact name from that log line. Dataset unaffected. |
| `CONNECTOR_UNREACHABLE`        | Log: `Could not reach the connector: …`                                | Connector not authorised, or running locally         | **Authorise it**, or set `deliveryMode: "none"`. Data already saved.         |
| `AUTH_INVALID`                 | HTTP `401`                                                             | Missing or malformed `APIFY_TOKEN`                   | **Abort & prompt user** for a valid token.                                   |
| `RATE_LIMITED_APIFY`           | HTTP `429`                                                             | Apify API rate / concurrency limits                  | **Retry:** exponential backoff (2s, 4s, 8s).                                 |
| `SYNC_TIMEOUT`                 | HTTP `408` on the sync endpoint                                        | Run exceeded the synchronous ceiling                 | **Reconfigure:** async `POST /runs` + polling.                               |

> **Provenance:** every log line, status and dataset condition above is taken from this Actor's own
> source and verified against it. The three HTTP rows describe the Apify **API transport layer**,
> not this Actor — confirm them against the [Apify API reference](https://docs.apify.com/api/v2)
> before wiring them into automated retry logic.

***

### 🗣️ Natural Language → Actor Mapping

| A user says…                             | Intent              | Constructed Actor input                                                      |
| ---------------------------------------- | ------------------- | ---------------------------------------------------------------------------- |
| "Find Python gigs on Freelancer"         | Core search         | `{"keywords":["python"],"limitPerKeyword":20}`                               |
| "Web scraping jobs paying over $500"     | Budget floor        | `{"keywords":["web scraping"],"minBudget":500}`                              |
| "Small jobs under $200"                  | Budget ceiling      | `{"keywords":["<skill>"],"maxBudget":200}`                                   |
| "Projects between $500 and $2000"        | Budget band         | `{"keywords":["<skill>"],"minBudget":500,"maxBudget":2000}`                  |
| "Compare demand for React vs Vue"        | Multi-keyword       | `{"keywords":["react","vue"],"limitPerKeyword":100}` then count by `keyword` |
| "What do clients pay for logo design?"   | Rate benchmarking   | Scrape, group by `currency`, aggregate `budgetMin`/`budgetMax`               |
| "Which skills go with machine learning?" | Skill co-occurrence | One broad keyword, then aggregate the `skills` arrays                        |
| "Alert me to new scraping projects"      | Monitoring          | Fixed input on a schedule; dedupe on `projectUrl`                            |
| "The biggest projects available"         | Ranking             | Scrape, then sort on `budgetMax` excluding nulls                             |
| "Just a quick sample"                    | Cheap probe         | `{"keywords":["<skill>"],"limitPerKeyword":5}`                               |
| "Send new gigs to Slack"                 | Scrape + deliver    | Above, plus `mcpConnector`, `mcpTool`, `deliveryMode: "summary"`             |
| "It returned nothing"                    | Empty result        | Broaden the keyword, lower `minBudget`, check the log                        |
| "Show me hourly contracts"               | Hourly work         | ❌ Fixed-price only — out of scope                                           |
| "Who posted this project?"               | Client identity     | ⚠️ `poster` is usually `"N/A"`                                               |
| "How many bids does it have?"            | Bid data            | ❌ Not collected                                                             |
| "Bid on this for me"                     | Write action        | ❌ Read-only                                                                 |

**Should NOT route here:** **hourly projects** · **closed, awarded or completed projects** ·
**bids, proposals or bid counts** · **client contact details or messages** · **freelancer profiles
or portfolios** · **contest listings** · placing a bid or any write action · other marketplaces —
for freelance *and* salaried roles across many boards, use
[All Jobs Scraper](https://apify.com/techforce.global/all-jobs-scraper).

***

### 🧭 Agent Execution & Routing Logic

```text
Does the user want LIVE FREELANCE PROJECT listings?
├── NO → wrong Actor.
│        freelance + salaried roles, many boards → All Jobs Scraper
│        agencies / service providers            → Clutch.co Scraper
│        a company's own contact details         → Website Contact Scraper
│
└── YES
    │
    ├── Is the work HOURLY?  → ❌ out of scope: only fixed-price projects are requested
    ├── Do they want CLOSED / historical projects? → ❌ active listings only
    ├── Do they want BIDS or CLIENT identity?      → ❌ not collected (poster is usually "N/A")
    │
    ├── keywords — REQUIRED, an ARRAY. Each term is its own sweep.
    │     comparing terms? → pass them together; every row is tagged with `keyword`
    │
    ├── Size the run
    │     ├── limitPerKeyword → 1-100 PER KEYWORD (total = keywords × limit)
    │     └── nothing is saved until the end → size it so the run finishes
    │
    ├── Budget bounds?
    │     ├── minBudget / maxBudget → applied by Freelancer, in the PROJECT'S OWN currency
    │     ├── 0 means "no bound" on either side
    │     └── need precision? filter downstream on budgetMin + currency
    │
    ├── What is the actual question?
    │     ├── best-paid       → sort budgetMax, EXCLUDING nulls
    │     ├── rate benchmark  → GROUP BY currency first ("$" is ambiguous)
    │     ├── skill demand    → aggregate the skills arrays
    │     ├── keyword compare → count rows per `keyword`
    │     └── what's new      → SCHEDULE it; dedupe on projectUrl, read postedAt
    │
    └── Should results land in another app?
          ├── yes → mcpConnector + mcpTool + {message} or {digest} in mcpArguments
          │           digest → summary (default)  ·  long list → chunked  ·  each → perProject
          └── no  → deliveryMode "none", or leave mcpConnector empty

[Execute Actor]
    ├──► SUCCEEDED with rows → dedupe on projectUrl, then analyse (never on `budget`)
    ├──► SUCCEEDED, 0 rows   → READ THE LOG: no keywords / no matches / request failed
    └──► 5 rows per keyword  → free plan cap, not a data pull
```

***

### 💰 Cost Control & Pricing Transparency

**Billing model: usage-based.** This Actor bills for **platform usage** — the compute time and
resources a run consumes — not per row. Current rates are on the
[Actor's Apify Store page](https://apify.com/techforce.global/freelancer); the Console shows the
live total mid-run.

This is one of the cheaper Actors in the catalogue: it talks to a JSON endpoint rather than
rendering pages, so a run is a handful of HTTP calls plus a one-second pause between pages.

| Choice                    | Effect on cost                                                         |
| ------------------------- | ---------------------------------------------------------------------- |
| Number of `keywords`      | **The main multiplier** — each is a separate sweep.                    |
| `limitPerKeyword`         | One request per 100 projects, so 20 and 100 differ by one call.        |
| `minBudget` / `maxBudget` | **Reduce cost**: filtering at the source means fewer projects fetched. |
| Delivery mode             | `perProject` adds one connector call per project; `summary` adds one.  |

#### Worked examples — relative run shape

| Scenario                                      | Keywords | Requests | Rows      | Notes                                                 |
| :-------------------------------------------- | :------- | :------- | :-------- | :---------------------------------------------------- |
| Probe — 1 keyword, `limitPerKeyword: 5`       | 1        | 1        | ≤ 5       | **Baseline.** Confirms the keyword returns live work. |
| Standard — 2 keywords × 50                    | 2        | 2        | ≤ 100     | The common sourcing run.                              |
| Broad sweep — 4 keywords × 100                | 4        | 4        | ≤ 400     | Still only four API calls.                            |
| Filtered — 4 keywords × 100, `minBudget: 500` | 4        | 4        | far fewer | Cheapest per *useful* row.                            |
| Monitoring — 1 keyword × 20, hourly schedule  | 1        | 1        | ≤ 20      | Very cheap; dedupe on `projectUrl`.                   |

**Keeping runs predictable:**

1. **First run: one keyword, `limitPerKeyword: 5`.** Confirms the search returns live projects.
2. **Use `minBudget` rather than filtering afterwards** — it is the one lever that reduces both
   cost and noise.
3. **Add keywords for coverage** — they're the multiplier, but each is only a request or two.
4. **Set `maxTotalChargeUsd`** on large scheduled runs, as a matter of habit.
5. **Schedule small and often** for alerts rather than one big sweep.
6. **Free Apify plans get 5 projects per keyword** — a connectivity check, not a data pull.

***

### 🛠️ Troubleshooting

Most problems are **scope expectations** — hourly work, closed projects, client identity — or the
`budget` string. Very few are scraper faults.

| #   | Symptom you see                                           | Most likely cause                                                 | Fix                                                                                                         |
| :-- | :-------------------------------------------------------- | :---------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------- |
| 1   | Empty dataset                                             | No keywords supplied, or nothing matched                          | Check the log for `Searching:` lines and `No more results for …`. Broaden the keyword or lower `minBudget`. |
| 2   | A keyword returned nothing while others worked            | No live fixed-price projects for that term, or its request failed | Look for `Request failed` in the log; retry later.                                                          |
| 3   | No hourly projects anywhere                               | Only fixed-price projects are requested                           | Out of scope — this is a hard scope decision, not a filter.                                                 |
| 4   | No closed or completed projects                           | Active listings only                                              | Out of scope.                                                                                               |
| 5   | `budget` shows `$` but the project is in another currency | The display string is hard-coded with `$`                         | Rebuild the label from `budgetMin`/`budgetMax` + `currency`. Never sort on `budget`.                        |
| 6   | Budget averages look wrong                                | Symbols mixed, and `$` covers several currencies                  | Group by `currency`; treat `$` as ambiguous rather than as USD.                                             |
| 7   | Rows below my `minBudget`                                 | The bound is applied per project in its own currency              | Re-filter downstream on `budgetMin` + `currency`.                                                           |
| 8   | `budgetMin` / `budgetMax` are null                        | The listing published no range                                    | Expected — exclude those rows rather than treating them as 0.                                               |
| 9   | Every `poster` field is `"N/A"`                           | The listing endpoint rarely exposes client identity               | Expected. Don't build on client identity.                                                                   |
| 10  | The same project appears twice                            | It matched two keywords; each row is tagged                       | Dedupe on `projectUrl`.                                                                                     |
| 11  | `postedAt` is `1970-01-01T00:00:00Z`                      | The listing had no submit time                                    | Treat pre-2000 dates as missing.                                                                            |
| 12  | Fewer rows than `keywords × limitPerKeyword`              | That many live projects don't exist                               | Expected. Add keywords rather than raising the limit.                                                       |
| 13  | `limitPerKeyword` above 100 rejected                      | The schema caps it at 100                                         | Split across more keywords.                                                                                 |
| 14  | An aborted run saved nothing                              | Results are written in one push at the end                        | Lower `limitPerKeyword` so the run completes.                                                               |
| 15  | Only 5 projects per keyword                               | Free plan cap                                                     | Upgrade; the log says so.                                                                                   |
| 16  | Hundreds of connector messages                            | `perProject` sends one call per project                           | Switch to `summary` or `chunked`.                                                                           |
| 17  | Slack / Notion received an empty message                  | `mcpArguments` didn't reference `{message}` or `{digest}`         | Map one of them in `mcpArguments`.                                                                          |
| 18  | Slack / Notion received nothing at all                    | Delivery needs **both** `mcpConnector` and `mcpTool`              | Set both. Every dataset row is still saved.                                                                 |
| 19  | `HTTP 401` / `403` from the Apify API                     | Missing, expired or malformed `APIFY_TOKEN`                       | Regenerate in **Console → Settings → API & Integrations**.                                                  |
| 20  | `HTTP 429` / `408` from the Apify API                     | Apify rate limits / the sync endpoint's time ceiling              | Back off (2s → 4s → 8s), or switch to async `POST /runs` + polling.                                         |

#### Diagnostic checklist before opening an issue

1. **Read the log header** — it echoes the keywords, limit and budget bounds actually in effect.
2. **Check for `Request failed`** — that's an upstream problem, not a parsing one.
3. **Baseline run:** one keyword, `limitPerKeyword: 5`, `minBudget: 0`, `maxBudget: 0`.
4. **Search the same keyword on freelancer.com** — if there's no live fixed-price work there,
   there's nothing to return.
5. **Check `currency` before trusting any budget figure**, and never sort on `budget`.
6. **Confirm your expectation is in scope** — hourly, closed, bids and client identity are all
   deliberately absent.

If the issue survives all six, open an **Issues** ticket (or email support) with the **run ID**,
the **input you used**, and what you expected.

***

### ❓ FAQ

**Do I need a Freelancer.com account or API key?**

No. The Actor reads publicly visible project listings. You need an **Apify** account and API token; you need no Freelancer credentials, OAuth app or API key.

**Is this the official Freelancer API?**

No. This is an independent Actor and is not affiliated with, endorsed by, or sponsored by Freelancer Technology Pty Limited. Freelancer's own API exists for participating in the marketplace — bidding, messaging, managing projects — which is a different job from analysing it.

**Can I plug it into Claude, Cursor, or a LangChain agent?**

Yes — it is a native MCP tool: `claude mcp add --transport http apify "https://mcp.apify.com/?tools=techforce.global/freelancer"`

**Does it return hourly projects?**

No. The search is scoped to **fixed-price** projects only, and that isn't a switch you can flip in the input. If you need hourly contracts, this isn't the right Actor.

**Does it return closed or completed projects?**

No — active listings only. The point is work you can still bid on, not a historical archive.

**How do the keywords work?**

Each keyword in the array is searched separately, up to `limitPerKeyword` projects each, and every row records which keyword produced it. Two keywords at 50 is up to 100 rows.

**Why does the same project appear twice?**

Because it matched two of your keywords, and each row is tagged with its own `keyword` so per-keyword analysis stays honest. De-duplicate on `projectUrl` when you want a unique project list.

**Why does `budget` show `$` when the project is in another currency?**

Because `budget` is a display string that is always formatted with a literal `$`. The `currency` field carries the project's real symbol. **Use `budgetMin`, `budgetMax` and `currency` for anything numeric** — sorting or averaging the string will mislead you.

**Is `currency` an ISO code?**

No, it's a **symbol** — and `$` is ambiguous across USD, AUD, CAD, NZD, SGD and HKD. Group by it rather than ignoring it, but don't treat it as a reliable currency key, and never average budgets across different symbols.

**How do `minBudget` and `maxBudget` actually filter?**

They're passed to Freelancer's own search as average-price bounds and applied **in each project's own currency** — they are not converted first. So `minBudget: 500` means "500 of whatever that project is priced in". Use them as a coarse, cost-saving filter, then refine on `budgetMin` + `currency`.

**Why are `budgetMin` and `budgetMax` sometimes null?**

Because the client published no budget range. `budget` then reads `"N/A"`. Null is not zero — exclude those rows from averages rather than coercing them.

**Why is the `poster` block always `"N/A"`?**

Because the active-project listing rarely exposes client identity. The block is present for shape consistency, but you should assume it's empty and not build a workflow around it.

**Why is `postedAt` sometimes 1970?**

That's the Unix epoch, produced when a listing carries no submit time. Treat any pre-2000 `postedAt` as missing rather than as a very old project.

**How many projects can I get per keyword?**

Up to 100 — that's the schema's ceiling. For more coverage, add keywords rather than trying to raise the limit.

**Can I get bid counts, proposals or freelancer profiles?**

No. This Actor returns the project listing itself: what the work is, what it pays and what skills it needs. Bids, proposals and profiles are out of scope.

**Does an aborted run keep partial results?**

No. All keywords are fetched first and the whole set is written in a single push, so an aborted or timed-out run saves nothing. Keep `limitPerKeyword` sized so the run completes.

**Which delivery mode should I use?**

`summary` (the default) for one digest, `chunked` when the digest is long enough to trouble Notion's limits, `perProject` only when you want one record each — a 300-project run means 300 calls — and `none` for dataset-only runs.

**What does it cost, and what do I get on the free plan?**

Usage-based: you pay for the platform usage a run consumes, not per row. It's a lightweight JSON-endpoint scrape — roughly one request per 100 projects per keyword — so the keyword count is the main multiplier. Free Apify plans return 5 projects per keyword. Pass `maxTotalChargeUsd` for a hard per-run ceiling.

***

### 🔍 Companion machine-readable files

| File                                                       | What it defines                                                    |
| ---------------------------------------------------------- | ------------------------------------------------------------------ |
| [`.actor/input_schema.json`](.actor/input_schema.json)     | Every input, its type, default and bounds, plus the delivery enum  |
| [`.actor/dataset_schema.json`](.actor/dataset_schema.json) | All 11 output fields with nullability, and the three Console views |
| [`.actor/output_schema.json`](.actor/output_schema.json)   | Where the run's output is exposed                                  |
| [`.actor/actor.json`](.actor/actor.json)                   | Actor identity and storage wiring                                  |

> **If this page and the schema files disagree, the schema files win.** They are what the platform
> validates against at run time; this page is documentation and can lag a release.

***

### Related Actors by Techforce Global

| Actor                                                                                 | Best for                                                                 |
| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| **This Actor**                                                                        | **Freelancer.com** projects with numeric budget ranges and tagged skills |
| [All Jobs Scraper](https://apify.com/techforce.global/all-jobs-scraper)               | Freelance **and** salaried roles across 26 boards in one normalised feed |
| [Clutch.co Scraper](https://apify.com/techforce.global/clutch-co-scraper)             | Agency and service-provider profiles — the supply side                   |
| [Website Contact Scraper](https://apify.com/techforce.global/website-contact-scraper) | Business contact details from a company's own site                       |
| [Website Content Crawler](https://apify.com/techforce.global/website-crawler)         | Whole-site text, markdown or HTML for RAG and search                     |

Use **All Jobs Scraper** when you want freelance work in the same feed as employed roles; use
**this** Actor when Freelancer's budget and skills detail is the point.

> Browse the full catalogue at **[scraper.techforce.global](https://scraper.techforce.global/)**.

***

### 🔐 Compliance & Data Privacy

**Trademark & affiliation.** This is an independent Actor. It is **not** affiliated with, endorsed
by, or sponsored by Freelancer Technology Pty Limited. Freelancer.com™ is their trademark.

- **Public project listings only.** No logins, no bids or proposals, no private messages, no
  client contact details, no freelancer profiles or portfolios.

- **Very little personal data — and mostly absent in practice.** The `poster` block *can* carry a
  username, display name and country, which would be personal data about a real client. In
  practice the listing endpoint rarely populates it, so those fields are usually `"N/A"`. **If
  they are populated and you keep them, you are processing personal data** — drop the block at
  ingest when your question is about budgets and skills, which it usually is.

- **Project descriptions are the client's content.** Titles and skill tags are short factual
  labels; internal analysis, sourcing and benchmarking sit on far safer ground than republishing
  listings as your own job board.

- **Budgets are a snapshot, and a noisy one.** They are what the client *proposed*, not what the
  work sold for — and the mixed-symbol currency field makes naive aggregation actively
  misleading. Publishing "the average price of X on Freelancer" without stating the currency, the
  sample size and the capture date produces a number that looks authoritative and isn't.

- **Listings expire.** A project awarded or withdrawn after your run stays in your dataset until
  you re-scrape. Anything freelancer-facing needs a refresh cadence and a visible `scrapedAt`.

- **Freelancer.com's Terms of Use restrict automated access and data extraction.** **This Actor
  does not remove that risk — it is a risk you accept by running it.** Keep volumes proportionate
  to the question you're answering.

- **Don't use it to spam clients.** Aggregating live projects to pitch at scale is exactly the
  behaviour marketplaces police, and the listing gives you no sanctioned contact channel anyway.
  Bid through the platform.

- **Attribution over mirroring.** Every row carries `projectUrl`. Link back to the project rather
  than presenting Freelancer's marketplace as your own listings.

***

### 🆘 Support & Custom Pipeline Engineering

Need automated multi-city scheduled feeds, webhook enrichment, or enterprise data warehouse syncs?

- **Email**: bhavin.shah@techforceglobal.com
- **Custom Enterprise Integrations**: [Book a 15-Minute Technical Consultation](https://calendly.com/techforce-global/intro-meeting)
- **Maintained by**: [Techforce Global](https://www.techforceglobal.com) — Specialists in High-Performance Web Scrapers and Agentic Workflows.

***

<sub>Built and maintained by **Techforce Global**. Scrapes public Freelancer.com project listings
only — no logins, no bids or proposals, no client contact details, no freelancer profiles. Scope
is active, fixed-price projects; hourly and closed projects are never returned. The `poster` block
is present but commonly empty. The `budget` string is display-only and always formatted with `$` —
use `budgetMin`, `budgetMax` and `currency` for anything numeric, and never average across
currency symbols. Budgets and availability are true only as of `scrapedAt`. You are responsible
for complying with Freelancer.com's Terms of Use and with the law of your jurisdiction.</sub>

***

### 🏷️ Structured data for search & AI discovery

`SoftwareApplication` describes the Actor; `FAQPage` wraps the FAQ above so those answers are
eligible for rich results, matching it question-for-question.

```json
{
    "@context": "https://schema.org/",
    "@type": "SoftwareApplication",
    "name": "Freelancer.com Project Scraper",
    "alternateName": "Freelance Project Data Feed",
    "applicationCategory": "BusinessApplication",
    "applicationSubCategory": "Web Scraping / Freelance Marketplace & Rate Research",
    "operatingSystem": "Cloud (Apify platform)",
    "description": "Searches active fixed-price projects on Freelancer.com for one or more keywords and returns a flat row per project: title, budget range as a display string plus numeric budgetMin and budgetMax, the project's currency symbol, the skills the client tagged, a nested poster block, the project URL, an ISO-8601 posting time and capture timestamp, and the keyword that produced the row. Optional server-side budget filtering, no Freelancer account or API key, and optional delivery into Notion, Slack, Linear, Airtable or any MCP connector.",
    "url": "https://apify.com/techforce.global/freelancer",
    "author": {
        "@type": "Organization",
        "name": "Techforce Global",
        "url": "https://www.techforceglobal.com/"
    },
    "provider": {
        "@type": "Organization",
        "name": "Apify",
        "url": "https://apify.com/"
    },
    "offers": {
        "@type": "Offer",
        "priceCurrency": "USD",
        "category": "Pay per platform usage",
        "description": "Usage-based: billed for the compute time and resources a run consumes, not per row. Runs are lightweight API calls, so cost tracks the number of keywords and the requested project count. Current rates are shown on the Actor's Apify Store page.",
        "url": "https://apify.com/techforce.global/freelancer"
    },
    "featureList": [
        "No Freelancer.com account, OAuth app or API key required",
        "Multiple search keywords in one run, each row tagged with the keyword that found it",
        "Numeric budgetMin and budgetMax alongside the display string",
        "Server-side budget filtering via minBudget and maxBudget",
        "Client-tagged skills as a clean array",
        "Scoped to active, fixed-price projects - live work you can still bid on",
        "ISO-8601 posting time and capture timestamp on every row",
        "Up to 100 projects per keyword",
        "Lightweight JSON-endpoint scrape rather than page rendering",
        "Optional MCP delivery in summary, chunked or per-project shapes",
        "Three Console dataset views covering projects, budgets and provenance"
    ],
    "isAccessibleForFree": false,
    "softwareRequirements": "Apify account and API token",
    "keywords": "Freelancer.com scraper, freelance project scraper, gig marketplace data, freelance rate benchmarking, project budget data, in-demand skills, freelance lead generation, scrape freelance job postings, MCP connector"
}
```

```json
{
    "@context": "https://schema.org/",
    "@type": "FAQPage",
    "mainEntity": [
        {
            "@type": "Question",
            "name": "Do I need a Freelancer.com account or API key?",
            "acceptedAnswer": {
                "@type": "Answer",
                "text": "No. The Actor reads publicly visible project listings. You need an Apify account and API token; you need no Freelancer credentials, OAuth app or API key."
            }
        },
        {
            "@type": "Question",
            "name": "Is this the official Freelancer API?",
            "acceptedAnswer": {
                "@type": "Answer",
                "text": "No. This is an independent Actor and is not affiliated with, endorsed by, or sponsored by Freelancer Technology Pty Limited. Freelancer's own API exists for participating in the marketplace — bidding, messaging, managing projects — which is a different job from analysing it."
            }
        },
        {
            "@type": "Question",
            "name": "Can I plug it into Claude, Cursor, or a LangChain agent?",
            "acceptedAnswer": {
                "@type": "Answer",
                "text": "Yes — it is a native MCP tool: claude mcp add --transport http apify \"https://mcp.apify.com/?tools=techforce.global/freelancer\""
            }
        },
        {
            "@type": "Question",
            "name": "Does it return hourly projects?",
            "acceptedAnswer": {
                "@type": "Answer",
                "text": "No. The search is scoped to fixed-price projects only, and that isn't a switch you can flip in the input. If you need hourly contracts, this isn't the right Actor."
            }
        },
        {
            "@type": "Question",
            "name": "Does it return closed or completed projects?",
            "acceptedAnswer": {
                "@type": "Answer",
                "text": "No — active listings only. The point is work you can still bid on, not a historical archive."
            }
        },
        {
            "@type": "Question",
            "name": "How do the keywords work?",
            "acceptedAnswer": {
                "@type": "Answer",
                "text": "Each keyword in the array is searched separately, up to limitPerKeyword projects each, and every row records which keyword produced it. Two keywords at 50 is up to 100 rows."
            }
        },
        {
            "@type": "Question",
            "name": "Why does the same project appear twice?",
            "acceptedAnswer": {
                "@type": "Answer",
                "text": "Because it matched two of your keywords, and each row is tagged with its own keyword so per-keyword analysis stays honest. De-duplicate on projectUrl when you want a unique project list."
            }
        },
        {
            "@type": "Question",
            "name": "Why does budget show $ when the project is in another currency?",
            "acceptedAnswer": {
                "@type": "Answer",
                "text": "Because budget is a display string that is always formatted with a literal $. The currency field carries the project's real symbol. Use budgetMin, budgetMax and currency for anything numeric — sorting or averaging the string will mislead you."
            }
        },
        {
            "@type": "Question",
            "name": "Is currency an ISO code?",
            "acceptedAnswer": {
                "@type": "Answer",
                "text": "No, it's a symbol — and $ is ambiguous across USD, AUD, CAD, NZD, SGD and HKD. Group by it rather than ignoring it, but don't treat it as a reliable currency key, and never average budgets across different symbols."
            }
        },
        {
            "@type": "Question",
            "name": "How do minBudget and maxBudget actually filter?",
            "acceptedAnswer": {
                "@type": "Answer",
                "text": "They're passed to Freelancer's own search as average-price bounds and applied in each project's own currency — they are not converted first. So minBudget: 500 means \"500 of whatever that project is priced in\". Use them as a coarse, cost-saving filter, then refine on budgetMin + currency."
            }
        },
        {
            "@type": "Question",
            "name": "Why are budgetMin and budgetMax sometimes null?",
            "acceptedAnswer": {
                "@type": "Answer",
                "text": "Because the client published no budget range. budget then reads \"N/A\". Null is not zero — exclude those rows from averages rather than coercing them."
            }
        },
        {
            "@type": "Question",
            "name": "Why is the poster block always \"N/A\"?",
            "acceptedAnswer": {
                "@type": "Answer",
                "text": "Because the active-project listing rarely exposes client identity. The block is present for shape consistency, but you should assume it's empty and not build a workflow around it."
            }
        },
        {
            "@type": "Question",
            "name": "Why is postedAt sometimes 1970?",
            "acceptedAnswer": {
                "@type": "Answer",
                "text": "That's the Unix epoch, produced when a listing carries no submit time. Treat any pre-2000 postedAt as missing rather than as a very old project."
            }
        },
        {
            "@type": "Question",
            "name": "How many projects can I get per keyword?",
            "acceptedAnswer": {
                "@type": "Answer",
                "text": "Up to 100 — that's the schema's ceiling. For more coverage, add keywords rather than trying to raise the limit."
            }
        },
        {
            "@type": "Question",
            "name": "Can I get bid counts, proposals or freelancer profiles?",
            "acceptedAnswer": {
                "@type": "Answer",
                "text": "No. This Actor returns the project listing itself: what the work is, what it pays and what skills it needs. Bids, proposals and profiles are out of scope."
            }
        },
        {
            "@type": "Question",
            "name": "Does an aborted run keep partial results?",
            "acceptedAnswer": {
                "@type": "Answer",
                "text": "No. All keywords are fetched first and the whole set is written in a single push, so an aborted or timed-out run saves nothing. Keep limitPerKeyword sized so the run completes."
            }
        },
        {
            "@type": "Question",
            "name": "Which delivery mode should I use?",
            "acceptedAnswer": {
                "@type": "Answer",
                "text": "summary (the default) for one digest, chunked when the digest is long enough to trouble Notion's limits, perProject only when you want one record each — a 300-project run means 300 calls — and none for dataset-only runs."
            }
        },
        {
            "@type": "Question",
            "name": "What does it cost, and what do I get on the free plan?",
            "acceptedAnswer": {
                "@type": "Answer",
                "text": "Usage-based: you pay for the platform usage a run consumes, not per row. It's a lightweight JSON-endpoint scrape — roughly one request per 100 projects per keyword — so the keyword count is the main multiplier. Free Apify plans return 5 projects per keyword. Pass maxTotalChargeUsd for a hard per-run ceiling."
            }
        }
    ]
}
```

# Actor input Schema

## `keywords` (type: `array`):

Keywords to search for on Freelancer.com (e.g. python, web scraping, react)

## `limitPerKeyword` (type: `integer`):

How many projects to fetch per keyword (max 100)

## `minBudget` (type: `integer`):

Filter projects with budget above this amount (0 = no minimum)

## `maxBudget` (type: `integer`):

Filter projects with budget below this amount (0 = no maximum)

## `mcpConnector` (type: `string`):

Optionally deliver scraped projects into a connector you have authorized - Notion, Slack, Linear, Airtable, Jira, GitHub, or any MCP-compatible connector. Leave empty to only save results to the dataset.

## `deliveryMode` (type: `string`):

How to package projects when sending them to the connector.

## `mcpTool` (type: `string`):

Name of the tool to call on the selected MCP connector (e.g. create-page, send-message).

## `mcpArguments` (type: `object`):

JSON object of arguments to pass to the tool. Use {placeholder} tokens (e.g. {title}, {message}, {digest}, {projectUrl}) which get filled in per delivery.

## `mcpMessageTemplate` (type: `string`):

Template used to build the {message} placeholder available in Tool Arguments. Supports {title}, {budget}, {skills}, {projectUrl}, {digest}, {projectCount}, {keywords}, {part}, {partCount}.

## Actor input object example

```json
{
  "keywords": [
    "python",
    "web scraping"
  ],
  "limitPerKeyword": 20,
  "minBudget": 0,
  "maxBudget": 0,
  "deliveryMode": "summary",
  "mcpTool": "",
  "mcpArguments": {},
  "mcpMessageTemplate": ""
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# 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("techforce.global/freelancer").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("techforce.global/freelancer").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 techforce.global/freelancer --silent --output-dataset

```

## MCP server setup

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

```

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/rsbRdEWzGcH6Ayb4E/builds/5b0MV54W3egrJYai7/openapi.json
