Instagram Competitor Monitor avatar

Instagram Competitor Monitor

Pricing

Pay per event

Go to Apify Store
Instagram Competitor Monitor

Instagram Competitor Monitor

Track up to 20 Instagram accounts and get one row each with what changed since the last run: followers, following and posts gained or lost, bio and verification changes, and the exact window the change covers.

Pricing

Pay per event

Rating

0.0

(0)

Developer

Dami's Studio

Dami's Studio

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Share

Give it up to 20 Instagram accounts and get one row each showing what changed since the last time it ran: followers gained or lost, posts published, whether the bio or the verification badge moved, and how many hours the window covered. It remembers between runs, so from the second run onwards you get a change report rather than another snapshot. No account, no cookies, no login, no browser.

  • Every run is compared against the previous run, so you read movement instead of raw numbers.
  • The first check of an account returns nulls, never zeros — a brand-new baseline is not the same thing as a flat week, and the rows say which one you are looking at.
  • One row per account per run, whatever the account size: a hundred-million-follower account costs exactly the same to check as a small one.
  • The saved baseline only moves forward behind a row that reached your dataset, so an interrupted run cannot quietly swallow a week of change.
  • Runs with empty input return a labelled sample row, free, so you can see the shape first.

Price

$5.00 per 1,000 competitors ($0.005 each), plus a $0.001 start fee per run. The start fee is billed per gigabyte of run memory with a minimum of one event, and this Actor runs at 512 MB, so a run start is always exactly $0.001.

This is a flat rate on every plan, free or paid. There are no volume tiers, no minimum spend, no subscription and no add-on fees. What you read here is what you pay on day one and on day four hundred.

CompetitorsTotal cost
1$0.006
5$0.026
10$0.051
20$0.101

Every line in that table is one run. This Actor checks up to 20 accounts in a single run, so all four volumes fit in one run and carry exactly one start fee each. Watching more than 20 accounts means more than one run — split the list across runs or schedules — and every run carries its own start fee, so 100 accounts is 5 runs and 5 start fees. The per-account rate never changes.

What is actually charged

  • One competitor-checked event per competitor row written to the dataset. Nothing else is metered per row.
  • Free: the sample row an empty run returns, and every diagnostic row — a blocked target, a dead URL, a search that matched nothing. Those rows all carry "charged": false.
  • The same account written three ways — nasa, @nasa and its profile link — is one account. It is checked once and charged once.
  • An account that cannot be found, has been renamed or has been removed is reported in an uncharged row.
  • A run that finds nothing costs the start fee and nothing more.
  • Rows never leave the dataset without a charge, and are never charged without a row. The billed event is a named one, competitor-checked, so the run's cost breakdown lists exactly what you paid for and nothing is metered under a generic dataset-item charge.

Input

{
"competitors": [
"nasa",
"@natgeo",
"https://www.instagram.com/spacex/"
]
}
FieldWhat it does
competitorsThe accounts to watch. A bare handle (nasa), an @handle or a full profile link all work and are treated as the same account, so duplicates cost nothing. Between 1 and 20 per run. Leave it empty and the run returns one free sample row instead.

Run it with empty input and you get one clearly labelled sample row, free, so you can see the output shape before you spend anything.

Output

One row per competitor. A real row from a real run:

{
"ok": true,
"charged": true,
"recordType": "competitor",
"username": "nasa",
"fullName": "NASA",
"isVerified": true,
"isPrivate": false,
"isBusiness": true,
"profileUrl": "https://www.instagram.com/nasa/",
"followers": 104417769,
"following": 92,
"posts": 4887,
"biography": "Making the seemingly impossible, possible. ✨",
"externalUrl": "https://www.nasa.gov",
"followersDelta": 18432,
"followingDelta": 0,
"postsDelta": 3,
"biographyChanged": false,
"verifiedChanged": false,
"isFirstRun": false,
"hoursSinceLastCheck": 168,
"previousCheckedAt": "2026-08-12T13:42:49.307Z",
"checkedAt": "2026-08-19T13:42:49.537Z"
}

Field notes

  • followers — the follower count at the moment of this check.
  • followersDelta — the change since the previous run: positive for growth, negative for a loss, 0 when nothing moved, and null on the first check of that account, when there is nothing to compare against.
  • postsDelta — how many posts appeared between the two checks. A negative number means posts were deleted or archived.
  • biographyChangedtrue when the bio text differs from the previous check — a launch, a rebrand or a new campaign line usually lands here first. null on a first check.
  • verifiedChangedtrue when the blue badge was gained or lost since the previous check. null on a first check.
  • isFirstRuntrue only for the very first check of that account. Every change field on that row is null, and the run after it carries the first real comparison.
  • hoursSinceLastCheck — the length of the window this row describes, to two decimal places, so a schedule that slipped is visible rather than hidden. null on a first check.
  • previousCheckedAt — when the account was last checked. Together with checkedAt it tells you exactly which window a delta belongs to.
  • externalUrl — the link in the bio. Worth diffing between rows — it changes before a launch more often than the bio text does.

Every real row carries "charged": true. Sample rows carry "_sample": true and diagnostic rows carry "_diagnostic": true with an errorCode you can filter on, and neither is ever billed.

How it works

  • Each account is read once per run from a licensed data source — one request per account, no page rendering, no headless browser, no login and no cookies.
  • After a row has been written to your dataset, the numbers behind it are saved to a named storage that survives between runs. The next run reads that saved check back and reports the difference against it.
  • The saved baseline moves forward only after the row is actually delivered. If a run stops halfway, the accounts it never reached keep their old baseline, and the next run reports the whole window instead of losing it.
  • The saved history belongs to your own account and is keyed by handle, so two people running this never see or affect each other's baselines.
  • Reading the history for an account happens before anything is fetched for it, so a storage problem can never turn into a row that claims to be a first check and then overwrites good history.

What people use it for

  • A weekly growth report: schedule the same list every Monday and read followersDelta down the column instead of copying numbers off profiles by hand.
  • Spotting a change of strategy the week it happens — postsDelta jumping from 2 to 14 is a campaign starting, and you see it without watching a feed.
  • Catching a bio or link change on the day: biographyChanged and externalUrl move before the announcement does.
  • Benchmarking your own account against the set you track, on identical timing and identical definitions, so the comparison is fair.
  • Feeding a dashboard or a spreadsheet with a flat, typed table where every row already carries its own window (previousCheckedAt, checkedAt, hoursSinceLastCheck).

How the memory works

This is the part that makes it a monitor rather than a scheduled scrape, so it is worth being precise about.

  • First run for an account. There is no previous check, so isFirstRun is true and every change field — followersDelta, followingDelta, postsDelta, biographyChanged, verifiedChanged, hoursSinceLastCheck, previousCheckedAt — is null. It is deliberately not 0: zero would mean "nothing moved", which is a claim about a week nobody watched.
  • Every run after that. You get the current numbers and the movement since the previous run, plus the exact window those deltas belong to.
  • The window is your schedule. Run it daily and the deltas are daily; run it every Monday and they are weekly. Nothing is normalised for you, because hoursSinceLastCheck lets you do that yourself and know you did it right.
  • Adding an account later costs one first-run row, and the run after that starts reporting its changes. You can grow the list whenever you like.
  • Removing an account from the input simply stops checking it. Its saved history stays, so putting it back later resumes the comparison — with a longer window, which hoursSinceLastCheck will show.

Reading the output

Every run writes three kinds of row, and they are easy to tell apart:

  • Real rows carry "charged": true and "recordType": "competitor". One billed event each, one per account per run.
  • The sample row carries "_sample": true and "charged": false. There is exactly one, it only appears when the input had no accounts in it, and it exists so you can see the output shape before spending anything.
  • Diagnostic rows carry "_diagnostic": true, "charged": false and an errorCode you can switch on: BAD_INPUT for an entry that is not a handle or a profile link, NOT_FOUND for an account that does not exist any more, RATE_LIMITED, SERVER_ERROR or NETWORK when the data source could not answer for one account within the time allowed, TIME_BUDGET when the run ran out of time before reaching the rest of the list, PARTIAL when the run stopped early and had to leave part of the list unchecked — including when it reached the maximum cost you set for the run — STATE_UNAVAILABLE when the saved history for an account could not be read (it is skipped rather than reported as a first check), STATE_NOT_SAVED when a delivered row could not be saved as the new baseline, and CHARGE_ERROR when a charge could not be recorded (see below).

If you only want the data, filter on charged == true. Normally the count of those rows is exactly the number of events you were billed for, so the dataset doubles as your invoice. There is one exception, and it runs in your favour: if the platform refuses to record a charge, the row has already been written, so it stays in your dataset marked "charged": true while nothing was billed for it. That is logged as an error and summarised in a CHARGE_ERROR diagnostic row at the end of the run, with the delivered and charged counts on it. A charge is never recorded without a row.

Limitations

  • Profile-level numbers only: followers, following, posts, bio, link, name, verification and account type. There are no per-post metrics, no engagement rates, no stories and no audience breakdown.
  • The first check of an account produces a row with no comparison in it. That row is charged, because it is the baseline everything afterwards is measured against, and it is clearly marked with isFirstRun: true.
  • A delta is the difference between two checks, not a per-day figure. If you skip a week, the delta covers a fortnight; hoursSinceLastCheck is there so you can scale it yourself.
  • History is keyed by handle. If an account renames itself, the old history stays under the old handle and the new one starts with a fresh first check.
  • The counts are whatever the account reports at that moment. Instagram updates them with some lag, so a check taken immediately after a post can trail reality by a few minutes.
  • A private account is still readable at profile level in most cases, but nothing behind the privacy wall is — and an account that goes private can stop reporting counts entirely, which shows up as null values rather than a drop to zero.
  • 20 accounts per run, one check per account per run. For a bigger list, split it across several runs or several schedules, and remember that each run carries its own start fee.
  • Run one schedule per account list. The saved history is shared by every run under your account and there is no lock on it, so two runs covering the same account at the same time both read the same saved check, both are charged, and the second row repeats the changes the first one already reported. Give overlapping lists their own schedules on different times, and avoid starting a manual run for accounts a schedule is currently checking.
  • Deleting the saved history storage resets the baseline: the next run reports a first check for every account again, and charges for it.

Questions

What do I get on the very first run?

One row per account with the current numbers, isFirstRun: true, and every change field set to null — there is no previous check to compare against yet. The next run is where the comparison starts. Those first rows are charged, because they are the baseline the rest of the reporting is built on.

How should I schedule it?

Pick the window you actually want to read: daily for a fast-moving set, weekly for a growth report. Use the same account list each time. The window between two runs is the window in the deltas, and every row carries it in hoursSinceLastCheck, so an occasional missed run is visible instead of silently distorting a chart.

What happens if a run is aborted or something breaks halfway?

The accounts already delivered are in your dataset and their baseline has moved on. The ones that were not reached keep the baseline they had, so the next run reports the full window rather than losing the part nobody saw. Nothing is charged for an account that was not checked.

Can I add or remove accounts later?

Yes. A new account costs one first-run row and starts reporting changes from the run after that. Removing one just stops checking it; its history is kept, so adding it back resumes the comparison over the longer window.

Will the run fail if something goes wrong?

No. A missing account, a data-source problem or a storage problem produces an uncharged diagnostic row explaining what happened, and the run still finishes as succeeded. A failed run would still bill you the start fee, which would mean paying to be told something went wrong.

How do I get exactly the rows I paid for?

Filter the dataset on "charged": true. Sample and diagnostic rows are always false, and normally the number of charged rows is exactly the number of billed events. The one way they differ is if the platform refuses a charge: the row is already written, so you keep it — marked "charged": true — and were not billed for it. The run logs that as an error and adds a CHARGE_ERROR row carrying both counts, so the gap is always visible and always in your favour.