# USPTO Patent Maintenance-Fee Lapse Monitor — FTO Tracker (`malonestar/uspto-patent-lapse-fto-monitor`) Actor

Expired patent tracker: US patents lapsed for non-payment of maintenance fees — now public domain. Weekly USPTO bulk events filtered to lapse/reinstatement, with grant date, patent age, entity size and Google Patents links.

- **URL**: https://apify.com/malonestar/uspto-patent-lapse-fto-monitor.md
- **Developed by:** [Kyle Maloney](https://apify.com/malonestar) (community)
- **Categories:** Lead generation, Developer tools, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.40 / 1,000 results

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

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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

## USPTO Patent Maintenance-Fee Lapse Monitor — FTO Tracker

Expired patent tracker: US patents lapsed for non-payment of maintenance fees, sourced
directly from USPTO's official bulk **maintenance-fee event** product (`PTMNFEE2`) on
the USPTO Open Data Portal (`api.uspto.gov` / data.uspto.gov — USPTO retired the old
`bulkdata.uspto.gov` host in 2026). Every US utility patent owner must
pay a maintenance fee at 4, 8, and 12 years after grant or the patent **expires and
enters the public domain**. This actor surfaces exactly those lapse events — a
freedom-to-operate (FTO) signal that is otherwise buried in a raw government text
dump nobody parses into a searchable feed.

**It also checks whether the patent came back.** A lapse is not permanent: under
37 CFR 1.378 the owner can petition to accept a delayed payment, and USPTO records
the revival as further events on the same patent. Measured on the 2026-05-26
snapshot, **1.51% of the patents that lapsed two years earlier had since been
revived** (204 of 13,519), and 0.13% were already revived inside the very same
weekly file. Every lapse row therefore carries `revived_after_lapse`, and
`fto_flag` is only `true` once that check has actually run.

### Who this is for

- **IP / freedom-to-operate counsel** — screening whether a blocking patent has quietly
  lapsed before advising a client to design around it or license it.
- **Competitive intelligence teams** — tracking when a competitor (or anyone) lets a
  patent lapse, signaling abandoned R\&D or a deprioritized product line.
- **Patent brokers & licensing shops** — recently-lapsed, recently-granted patents can
  still be re-filed as a continuation or reveal licensing opportunities before the
  underlying tech is picked up by someone else.
- **R\&D / product strategy** — mining the public domain for technology that was
  patented and then abandoned, now free to build on.

### What it does

1. Resolves the newest weekly USPTO `PTMNFEE2` snapshot through the Open Data Portal.
   Identifying it is free (product metadata sits in ODP's 5,000,000-calls-per-week
   bucket), so every run knows which week it is in before deciding whether to spend a
   download. If the same question has already been answered against that snapshot, the
   cached answer is replayed and **nothing is downloaded** — see *Download quota* below.
2. Streams the whole ~195 MB zip — **the entire file, every run** — parsing USPTO's
   fixed-width 57-character event layout by byte offset. The full file is never held
   in memory.
3. Classifies each event against **USPTO's own code dictionary, which ships inside
   the same zip** (163 documented codes), so `event_code_description` is USPTO's
   wording, not ours, and an unknown code fails the run instead of being quietly
   bucketed as "other".
4. Groups every event by patent (USPTO lists a patent's events contiguously) and, for
   each lapse, looks for a **post-expiration revival** — `EXPX`, a granted petition
   (`PMFG`/`PTGR`), a delayed-payment petition (`M1558`/`M2560`/…) or an accepted
   maintenance-fee payment.
5. Returns a normalized, filterable feed with a direct Google Patents link, plus the
   honest denominator: `matches_in_window_total` is how many rows matched in the whole
   snapshot, and `results_truncated` says whether `maxResults` cut the answer short.

### Example input

```json
{
  "mode": "lapsed",
  "lookbackWeeks": 4,
  "grantYearMin": 2018,
  "maxResults": 500
}
```

- `mode` — `"lapsed"` (default) returns only expiration events; `"reinstated"` returns
  patents that came back from expiration (`EXPX`); `"all_events"` returns everything
  including routine fee payments (useful for building a full per-patent fee history).
- `lookbackWeeks` — how far back to include events (default 4 ≈ 1 month). Applied as
  an event-date window against the snapshot that actually answered.
- `grantYearMin` / `grantYearMax` — optional. Recently-granted patents that already
  lapsed tend to be the most actionable FTO signal (an abandoned patent from 3 years
  ago vs. one that expired at the natural end of its 20-year term).
- `maxResults` — cap on rows returned (default 500). A 4-week window typically holds
  **~18,000 lapse events**, so the default is a slice: the run returns the most recent
  ones and reports the true total in `matches_in_window_total`.
- `odpApiKey` — optional. Your own FREE USPTO Open Data Portal API key (register at
  data.uspto.gov, ~2 min). If left blank the actor uses its built-in default key.
  USPTO scopes its download allowance to the API key, so supplying your own gives you
  an entirely separate budget (and the actor tracks it separately too).
- `allowFullFile` — legacy switch from the retired bulkdata.uspto.gov host; ignored on
  the ODP path (the snapshot is always streamed, never fully held in memory).

### Output fields

| Field | Description |
|---|---|
| `patent_number` | US patent number, zero padding stripped (`11324169`, `RE45927`, `D1000000`) |
| `patent_kind` | `utility`, `reissue`, `design`, `plant`, `sir` or `defensive_publication` |
| `application_number` | Associated USPTO application number |
| `event_code` | Raw USPTO event code (`EXP.`, `EXPX`, `M1551`, `PMFG`, …); `null` on the rare row USPTO ships with a blank code |
| `event_code_description` | USPTO's own description of that code, read from the dictionary shipped inside the same zip |
| `event_type` | Normalized: `lapsed`, `reinstated`, `fee_paid`, or `other` |
| `event_date` | Date of the maintenance-fee event (`YYYY-MM-DD`) |
| `grant_date` | Patent grant date (`YYYY-MM-DD`) |
| `filing_date` | Original application filing date (`YYYY-MM-DD`) |
| `patent_age_years_at_event` | Years between grant and event (1 decimal place) |
| `entity_size` | `large` / `small` / `micro` — from the fee code, else the entity-indicator column |
| `entity_indicator_raw` | Raw USPTO entity column: `N` (undiscounted), `Y` (small), `M` (micro) |
| `revived_after_lapse` | Lapse rows only. `true` = USPTO recorded a post-expiration revival for this patent; `false` = the patent's full event history was checked and there is none; `null` = not checked |
| `revival_event_code` | Code of the earliest revival event (e.g. `PMFG`, `M1558`, `EXPX`), else `null` |
| `revival_event_date` | Date of that revival event, else `null` |
| `post_lapse_event_codes` | Every event recorded after the lapse, as `CODE@YYYY-MM-DD`, comma separated — including non-revival signals such as a petition merely *filed* (`PMFP`) |
| `fto_flag` | `true` only when this is a lapse **and** no revival was found; `false` when the patent was revived or the row is not a lapse; `null` when the revival check could not run |
| `patent_event_count` | How many maintenance-fee events the snapshot holds for this patent |
| `latest_event_code` / `latest_event_date` | The most recent event on this patent in the snapshot |
| `google_patents_url` | Direct link to the patent on Google Patents |
| `source_file` | The USPTO snapshot zip this event was parsed from |
| `snapshot_date` | Date of the weekly USPTO cumulative snapshot this row was read from |
| `snapshot_max_event_date` | Newest event date found anywhere in that snapshot — its true data end |
| `window_start` / `window_end` | The event-date window this run covered |
| `snapshot_is_newest_available` | `true` if answered from USPTO's newest weekly file, `false` if an older one had to be used, `null` if not checked |
| `newest_available_snapshot` | Newest weekly snapshot USPTO offered at run time |
| `snapshot_lag_days` | Days between the snapshot that answered and the newest one (`0` = fully current) |
| `matches_in_window_total` | Total rows matching your query in the whole snapshot, **before** `maxResults` |
| `results_truncated` | `true` when `matches_in_window_total` exceeded `maxResults` |
| `scan_complete` | `true` when the entire snapshot was streamed and counted |
| `source_lines_scanned` / `source_lines_unparsed` | Lines read, and lines that did not parse as an event |
| `served_from_cache` | `true` when this answer was replayed from the actor's cached copy of the same snapshot and the same query, so no USPTO download was spent; `false` when the snapshot was downloaded and parsed on this run |
| `answer_computed_at` | UTC timestamp of the run that actually streamed and parsed the snapshot. On a replay this is earlier than the current run, so freshness is never overstated |
| `downloads_used_this_run` | Requests this run made against USPTO's per-file download allowance (`0` on a cache hit) |
| `download_budget_remaining` | Downloads the actor will still allow itself against the snapshot it served, under a self-imposed cap set below USPTO's documented 20. A conservative internal count — USPTO exposes no remaining-quota counter |

#### Download quota, and why this actor caches

USPTO documents the constraint plainly on
[data.uspto.gov/apis/api-rate-limits](https://data.uspto.gov/apis/api-rate-limits):

> "For the same file downloaded using the Bulk Datasets Downloads API, you are limited
> to **20 downloads per year using your API key**"
> "You can download up to **5 files per 10 seconds** from the same IP address."

The allowance is scoped to the **API key and the file**, not to the caller's IP — the
same counter continues across different networks, so a proxy changes nothing. Because
USPTO publishes a new file every Tuesday, the practical ceiling is *20 runs during the
week a given snapshot is the newest one*.

This actor therefore keeps a **named cache** keyed by (snapshot date, query). The first
run of the week against a given question streams and parses the 195 MB file; every
repeat of the **same question against the same weekly snapshot answers from cache and
downloads nothing**. A new USPTO release invalidates the cache automatically, so
freshness always wins when it is available. It also keeps a per-key ledger of download
attempts, so a file USPTO has reported exhausted is never probed again — a rejected
request still increments USPTO's counter.

Steady-state cost is therefore **about one download per week per distinct question**,
roughly 5% of the allowance, and every row states what it cost
(`served_from_cache`, `downloads_used_this_run`, `download_budget_remaining`).

When the newest snapshot genuinely cannot be obtained, the actor falls back to the
next-newest — so the answer can be a week or two behind.

#### How the lookback window works

`lookbackWeeks` is measured **back from the snapshot that actually
answered**, not from today. `window_start` / `window_end` state that period
exactly on every row, and `snapshot_lag_days` states how far behind USPTO's
newest file it is. If no obtainable snapshot reaches the requested window, the
run **fails loudly with 0 rows billed** rather than reporting "no lapses found".

#### What makes a run fail instead of returning a thin answer

Every gate below runs **before a single row is pushed**, so a failure bills nothing:

- the downloaded byte count must match the `Content-Length` of the snapshot (a
  truncated body otherwise ends the stream normally and looks like a complete answer);
- at least 20,000,000 event lines must be read from a cumulative snapshot, and at
  least 95% of them must parse (measured healthy: 26,889,079 lines, 100.00% parsed);
- USPTO's shipped code dictionary must be present and must still define `EXP.` and
  `EXPX`, at least 100,000 `EXP.` events must be present (measured: 3,590,629), and no
  more than 1% of events may carry a code absent from that dictionary (measured: 0);
- each patent's events must still be listed contiguously, because the revival check
  depends on it (measured: 8,497,655 patents, 0 violations);
- a 0-row result is only returned when the snapshot genuinely reaches into the
  requested window.

### Use as an MCP tool

This actor is Pay-Per-Event and discoverable via [mcp.apify.com](https://mcp.apify.com) —
an AI agent (Claude, Cursor, etc.) can call it directly as a tool to answer "which
patents just expired for non-payment" style questions, chaining the `google_patents_url`
output into a follow-up full-text fetch. Billing is unchanged when called as a tool, and
a run that cannot answer fails without billing.

### FAQ

**Which patents just expired for non-payment?**
Run with default `mode: "lapsed"` — every row is a confirmed `EXP.` event, and
`fto_flag: true` additionally means no revival was recorded for that patent.

**Can a "lapsed" patent come back?**
Yes, and that is why `revived_after_lapse` exists. USPTO grants petitions to accept
delayed maintenance-fee payments under 37 CFR 1.378 — roughly 1–1.5% of lapses are
reversed over the following two years. Rows where that has already happened are
returned with `fto_flag: false` and the revival code and date attached.

**I asked for 4 weeks and got 500 rows — is that everything?**
Check `matches_in_window_total` and `results_truncated`. A 4-week window typically
holds ~18,000 lapses; raise `maxResults` (up to 50,000) or narrow with
`grantYearMin` / `grantYearMax` to get the subset you care about.

**Does this include design or plant patents?**
Design and plant patents do not pay maintenance fees, so they have no lapse events —
but their other maintenance-fee-file events are parsed, and **reissue patents (`RE…`)
do pay maintenance fees and are included** (3,478 reissue lapse events in the
2026-05-26 snapshot).

**Is there a patent maintenance fee lapse list I can search?**
Yes — this actor turns USPTO's raw bulk file into exactly that: a queryable,
date-ranged, entity-size-tagged lapse list with direct Google Patents links.

**How do I find public domain patents from 2026?**
Set `mode: "lapsed"` and narrow with `grantYearMin` / `grantYearMax` to focus on
recently-granted patents that have already lapsed — the most actionable FTO subset.

### Notes on the source format

USPTO's weekly `PTMNFEE2` zip contains two text files: the event file
(`MaintFeeEvents_YYYYMMDD.txt`, ~26.9 million fixed-width 57-character rows) and
`MaintFeeEventsDesc_YYYYMMDD.txt`, USPTO's own dictionary of every event code. This
actor reads **both** — the layout is parsed by byte offset and the vocabulary is closed
against USPTO's dictionary rather than a hard-coded list, so a renamed or new code is
detected on the run it appears rather than silently becoming "other".

This is a screening tool built on USPTO's published maintenance-fee record, not a legal
determination. Confirm any freedom-to-operate conclusion against Patent Center / PAIR
before acting on it.

### Pricing

Pay-Per-Event: **$8 per 1,000 result rows** (`Result` dataset-item event only — no
double-charging). Runs that cannot answer fail loudly and bill nothing.

# Actor input Schema

## `mode` (type: `string`):

Which maintenance-fee events to return: 'lapsed' = patents that EXPIRED for non-payment (public-domain/FTO signal), 'reinstated' = patents reinstated after an earlier expiration, 'all\_events' = every event including fee payments (no filtering by event type).

## `lookbackWeeks` (type: `integer`):

Size of the event-date window to return, in weeks. Measured back from the USPTO snapshot that actually answered (published weekly; may be a week or two behind today if the newest file's download quota is exhausted), NOT from today. Every row carries window\_start, window\_end and snapshot\_lag\_days so the covered period is explicit.

## `grantYearMin` (type: `integer`):

Only include patents granted in or after this year. Recently granted patents that already lapsed are usually the most valuable FTO signal. Leave blank for no lower bound.

## `grantYearMax` (type: `integer`):

Only include patents granted in or before this year. Leave blank for no upper bound.

## `maxResults` (type: `integer`):

Maximum number of maintenance-fee event rows to return across all files scanned.

## `allowFullFile` (type: `boolean`):

If no weekly delta files can be resolved, fall back to USPTO's single cumulative MaintFeeEvents.zip covering ALL history. This file is hundreds of MB and can take several minutes to stream-process even though it is never fully loaded into memory. Off by default -- most use cases only need recent weekly deltas.

## `odpApiKey` (type: `string`):

FREE API key for api.uspto.gov (register at data.uspto.gov, ~2 min). Required since USPTO retired bulkdata.uspto.gov in 2026. Leave blank to use the actor default key if configured by the owner.

## Actor input object example

```json
{
  "mode": "lapsed",
  "lookbackWeeks": 4,
  "maxResults": 500,
  "allowFullFile": false
}
```

# Actor output Schema

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

The default dataset.

# 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 = {
    "mode": "lapsed",
    "lookbackWeeks": 4,
    "maxResults": 500,
    "allowFullFile": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("malonestar/uspto-patent-lapse-fto-monitor").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 = {
    "mode": "lapsed",
    "lookbackWeeks": 4,
    "maxResults": 500,
    "allowFullFile": False,
}

# Run the Actor and wait for it to finish
run = client.actor("malonestar/uspto-patent-lapse-fto-monitor").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 '{
  "mode": "lapsed",
  "lookbackWeeks": 4,
  "maxResults": 500,
  "allowFullFile": false
}' |
apify call malonestar/uspto-patent-lapse-fto-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,malonestar/uspto-patent-lapse-fto-monitor"
        }
    }
}

```

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/sNJ3c0qv1gnn2AsGn/builds/0oxz0oZKizSDEXOeG/openapi.json
