Search Fiverr for “pdf to excel” or “data entry” and you’ll get thousands of gigs. Most of them are someone retyping supplier invoices, price lists, bank-statement tables or scanned forms into a spreadsheet by hand. Buyers keep ordering because their accounting software can’t read the PDF, the free online converters mangle the tables, and they don’t want to spend a Saturday doing it themselves.
You can sell the same result but do the work differently. Gemini reads the PDF and fills in the table. It does this twice, independently, and any cell where the two runs disagree gets highlighted for you to check by hand. You spend your time reviewing the yellow cells instead of typing every row. On clean digital PDFs, a 40-page job that takes a manual typist hours can take you roughly 20–40 minutes including review, and the AI bill is well under a dollar. Bad scans take much longer.
In this guide:
- what these gigs pay, and what Fiverr and Upwork take from you
- a Python script that splits the PDF, extracts each batch twice with Gemini 3.5 Flash-Lite, compares the two runs, converts numbers, and writes an
.xlsxwith flagged cells and a review sheet - how to set up the gig, quote jobs, and deliver
- costs, realistic earnings, and the risks: account bans, client data, and the jobs you should turn down
What the market pays
Data entry is one of the most crowded, cheapest categories on both platforms. Be realistic about that from the start:
- On Fiverr (Data → Data Entry), entry-level “PDF to Excel” gigs commonly start at $5–$15 for a handful of pages. Established sellers charge $30–$100+ for bigger batches, and they sell 24-hour delivery as a paid extra. Look at the current listings yourself before you set prices, since they change every week.
- On Upwork, clients post fixed-price jobs (“convert 200 PDF invoices to Excel”) with budgets from $20 to a few hundred dollars, or hourly work, often at $5–$15/hour for data entry.
The money is in the mid-size, messy jobs: 30–300 pages, tables that run across pages, a mix of scanned and digital PDFs. These are too boring for the client to do and too slow for a $5 typist. The job is the same whether you type it or not, but you can take three of these jobs a day and a manual typist can’t.
Platform fees (check before you rely on them):
| Platform | What you pay |
|---|---|
| Fiverr | 20% of every order, including tips. Earnings clear 14 days after the order is completed (7 days for Top Rated sellers). Withdrawal fees depend on the method. |
| Upwork | Connects to apply for jobs, $0.15 each, and most proposals cost several Connects. Upwork’s service fee was a flat 10% for years and became variable (0–15% per contract) in 2025. The exact rate is shown before you submit a proposal or accept a contract. |
What you need
| Item | Cost | Notes |
|---|---|---|
| Python 3.10+ | $0 | Any laptop |
google-genai, pydantic, pypdf, openpyxl | $0 | pip install |
| Gemini API key with billing on | Pennies per job | aistudio.google.com. Paid tier is required for client documents (see Risks). |
| Fiverr and/or Upwork seller account | Fees above | One account per person. Verify your ID early. |
| Excel or LibreOffice Calc | $0 | For the review pass |
Gemini pricing (October 2026, check ai.google.dev/pricing): Gemini 3.5 Flash-Lite (gemini-3.5-flash-lite, paid tier) is $0.30 per 1M input tokens and $2.50 per 1M output tokens. Gemini’s docs count each PDF page as about 258 input tokens (more or less depending on the media resolution setting). Thinking tokens are billed as output, which is why the script sets thinking to minimal. The output is the expensive part: a dense page with 30 table rows can be 1,000–2,000 output tokens.
Cost per job, worked out
A 100-page job of dense invoice tables, extracted twice:
- Input: 2 passes × 100 pages × ~350 tokens (page + share of the prompt) = 70,000 tokens × $0.30/1M = $0.02
- Output: 2 passes × 100 pages × ~1,500 tokens = 300,000 tokens × $2.50/1M = $0.75
- Total: about $0.80 per 100 pages, and much less for sparse pages
The script prints the real token count after every run, so you’ll know your actual cost after the first job.
Step 1: Set up the project
mkdir pdf2xlsx && cd pdf2xlsx
python3 -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install google-genai pydantic pypdf openpyxl
export GEMINI_API_KEY="paste-your-paid-tier-key" # PowerShell: $env:GEMINI_API_KEY="..."
Step 2: Why two passes
If you ask a model to read a table once, it’ll get most cells right and a few wrong, and you won’t know which ones. Checking every cell yourself is the manual job you were trying to avoid.
So the script reads every batch of pages twice, independently. When both runs give the same value for a cell, it’s very likely correct, because random misreads rarely repeat in exactly the same way. When they disagree, or the model marks a cell as hard to read, or a number won’t parse, that cell turns yellow or red. In clean digital PDFs you’ll usually see only a handful of flags per hundred rows. In bad scans you’ll see many, which tells you to quote a higher price.
This doesn’t catch everything. If both runs make the same mistake (a “6” that really looks like an “8” on a smudged scan), it gets through unflagged. That’s why there’s also a totals check, and why you still spot-check (see Step 5).
Step 3: The script
Save this as pdf2xlsx.py:
import argparse, os, re, sys, time
from io import BytesIO
from pathlib import Path
from google import genai
from google.genai import errors, types
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill
from pydantic import BaseModel
from pypdf import PdfReader, PdfWriter
MODEL = os.environ.get("EXTRACT_MODEL", "gemini-3.5-flash-lite")
PRICE_IN, PRICE_OUT = 0.30, 2.50 # USD per 1M tokens, Gemini 3.5 Flash-Lite paid tier
YELLOW = PatternFill("solid", fgColor="FFF2A8")
RED = PatternFill("solid", fgColor="F8B4B4")
class Row(BaseModel):
page: int
cells: list[str]
unsure: list[int]
class Batch(BaseModel):
rows: list[Row]
printed_totals: list[str]
PROMPT = """Extract every data row from these PDF pages into one table.
Columns, in this exact order: {cols}
Rules:
- One output row per row in the document. Never merge, skip, or invent rows.
- "cells" must contain exactly {n} strings in the column order above. Use "" when the document has no value.
- Copy values exactly as printed. Do not translate, round, or reformat numbers or dates.
- "page" is the page number inside this batch, starting at 1.
- "unsure": 0-based indexes of cells that are blurry, cut off, or ambiguous.
- Skip column headers, page headers, footers, and subtotal lines.
- Put any printed totals (e.g. "Total: 1.250.000") in "printed_totals", copied exactly.
{notes}"""
usage = [0, 0]
def split_pdf(path, size):
reader = PdfReader(path)
for start in range(0, len(reader.pages), size):
writer = PdfWriter()
for page in reader.pages[start:start + size]:
writer.add_page(page)
buf = BytesIO()
writer.write(buf)
yield start, buf.getvalue()
def extract(client, pdf_bytes, prompt):
for attempt in range(5):
try:
r = client.models.generate_content(
model=MODEL,
contents=[types.Part.from_bytes(data=pdf_bytes, mime_type="application/pdf"), prompt],
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=Batch,
thinking_config=types.ThinkingConfig(thinking_level="minimal"),
),
)
u = r.usage_metadata
usage[0] += u.prompt_token_count or 0
usage[1] += (u.candidates_token_count or 0) + (u.thoughts_token_count or 0)
if r.parsed is None:
raise ValueError("reply was not valid JSON")
return r.parsed
except errors.APIError as e:
if e.code not in (429, 500, 503) or attempt == 4:
raise
time.sleep(5 * 2 ** attempt)
except ValueError:
if attempt == 4:
raise
raise RuntimeError("unreachable")
def to_number(s):
t = re.sub(r"[^\d,.\-()]", "", s)
neg = t.startswith("-") or (t.startswith("(") and t.endswith(")"))
t = t.strip("()-")
if not t:
return None
if "," in t and "." in t: # last separator is the decimal one
dec = "," if t.rfind(",") > t.rfind(".") else "."
t = t.replace("." if dec == "," else ",", "").replace(dec, ".")
elif t.count(",") == 1 and len(t.split(",")[1]) != 3:
t = t.replace(",", ".") # 12,5 -> 12.5
elif t.count(".") == 1 and len(t.split(".")[1]) != 3:
pass # 12.50 stays
else:
t = t.replace(",", "").replace(".", "") # 1.250.000 or 1,250 -> thousands
try:
v = float(t)
except ValueError:
return None
return -v if neg else v
def fit(cells, n):
return (cells + [""] * n)[:n]
def main():
ap = argparse.ArgumentParser()
ap.add_argument("pdfs", nargs="+")
ap.add_argument("--cols", required=True, help='e.g. "Date,Invoice No,Description,Qty,Amount"')
ap.add_argument("--numeric", default="", help="columns to convert to numbers, comma-separated")
ap.add_argument("--notes", default="", help="extra instructions for this client")
ap.add_argument("--pages", type=int, default=4, help="pages per request")
ap.add_argument("--single-pass", action="store_true", help="cheaper, no cross-check")
ap.add_argument("--out", default="output.xlsx")
a = ap.parse_args()
cols = [c.strip() for c in a.cols.split(",")]
numeric = {cols.index(c.strip()) for c in a.numeric.split(",") if c.strip()}
prompt = PROMPT.format(cols=", ".join(cols), n=len(cols), notes=a.notes)
client = genai.Client()
wb = Workbook()
data = wb.active
data.title = "Data"
data.append(["Source file", "Page"] + cols)
review = wb.create_sheet("Review")
review.append(["Source file", "Page", "Excel row", "Column", "Issue", "Pass 1", "Pass 2"])
for cell in data[1] + review[1]:
cell.font = Font(bold=True)
sums = {i: 0.0 for i in numeric}
printed, flags = [], 0
for pdf in a.pdfs:
name = Path(pdf).name
for start, chunk in split_pdf(pdf, a.pages):
p1 = extract(client, chunk, prompt)
p2 = None if a.single_pass else extract(client, chunk, prompt)
printed += [(name, t) for t in p1.printed_totals]
if p2 and len(p2.rows) != len(p1.rows):
review.append([name, start + 1, "", "", f"Row count differs: {len(p1.rows)} vs {len(p2.rows)}. "
f"Check pages {start + 1}-{start + a.pages} by hand.", "", ""])
flags += 1
p2 = None
for k, row in enumerate(p1.rows):
page = start + row.page
c1 = fit(row.cells, len(cols))
c2 = fit(p2.rows[k].cells, len(cols)) if p2 else c1
unsure = set(row.unsure) | (set(p2.rows[k].unsure) if p2 else set())
values = []
for i, v in enumerate(c1):
num = to_number(v) if i in numeric and v else None
values.append(num if num is not None else v)
if num is not None:
sums[i] += num
data.append([name, page] + values)
r = data.max_row
for i, v in enumerate(c1):
issue = None
if v.strip() != c2[i].strip():
issue, fill = "Passes disagree", YELLOW
elif i in unsure:
issue, fill = "Model unsure", YELLOW
if i in numeric and v and to_number(v) is None:
issue, fill = "Not a number", RED
if issue:
data.cell(row=r, column=i + 3).fill = fill
review.append([name, page, r, cols[i], issue, v, c2[i]])
flags += 1
print(f"{name} pages {start + 1}+: {len(p1.rows)} rows", file=sys.stderr)
review.append([])
for i, total in sums.items():
review.append(["", "", "", cols[i], "Sum of extracted values", total, ""])
for name, t in printed:
review.append([name, "", "", "", "Total printed in document", t, ""])
wb.save(a.out)
cost = usage[0] / 1e6 * PRICE_IN + usage[1] / 1e6 * PRICE_OUT
print(f"Saved {a.out}: {data.max_row - 1} rows, {flags} flags. "
f"Tokens in {usage[0]:,}, out {usage[1]:,}. Cost ~${cost:.4f}", file=sys.stderr)
if __name__ == "__main__":
main()
What it does, briefly:
- Splits the PDF into 4-page batches. Shorter requests mean fewer skipped rows on long documents and stay well under Gemini’s inline request size limit. If a batch fails, you only rerun that batch, not the whole file.
- Uses a fixed column order with structured output (
response_schema). The model can’t rename columns or add new ones, and short or long rows are padded or cut to the right length. - Runs each batch twice and compares the results cell by cell. If the two runs return different numbers of rows, it doesn’t try to line them up. It flags the page range for you to check by hand, because a matching mistake there would be worse than a flag.
- Converts numbers in the columns you list as numeric, so the client gets real numbers they can add up, not text. It handles both
1.250.000,50(Indonesian/European) and1,250,000.50(US). - Writes a Review sheet listing every flag, plus the sum of each numeric column next to any totals printed in the documents.
Step 4: Run it on a sample
Always ask for a sample before you quote. Then run:
python pdf2xlsx.py sample.pdf \
--cols "Date,Invoice No,Item,Qty,Unit Price,Amount" \
--numeric "Qty,Unit Price,Amount" \
--notes "Dates are DD/MM/YYYY. Ignore the stamp and signature area." \
--out sample.xlsx
For a whole folder of invoices:
python pdf2xlsx.py invoices/*.pdf --cols "Supplier,Invoice No,Date,Total" --numeric "Total" --out invoices.xlsx
The terminal output looks like this (your numbers will differ):
sample.pdf pages 1+: 27 rows
sample.pdf pages 5+: 31 rows
Saved sample.xlsx: 58 rows, 4 flags. Tokens in 7,912, out 9,340. Cost ~$0.0257
Use the --notes flag for anything client-specific: date format, “one row per line item, not per invoice”, “copy SKU codes exactly including leading zeros”.
Step 5: Review, then deliver
This is the part you’re actually paid for:
- Open the Review sheet. Go through each flag, look at that page in the PDF, and fix the cell in Data. Then clear the highlight.
- Compare Sum of extracted values with Total printed in document. The sum covers everything in one run, so to check invoice by invoice, run one file at a time (or add the totals up yourself). If the invoice says Rp 12.450.000 and your column adds up to Rp 12.405.000, a row is wrong or missing. Find it before the client does.
- Spot-check 5% of unflagged rows at random. If you find mistakes, the document is harder than the flags suggest, so check more.
- Delete the Review sheet, format the columns (dates, thousand separators), freeze the header row, and deliver.
Time per job: for clean digital PDFs, roughly 15–20 seconds of review per page. Bad scans can take a minute or more per page.
Step 6: Set up the gig and quote properly
Fiverr gig (Data → Data Entry):
- Title: “I will convert your PDF tables, invoices or scans to clean Excel, human-verified”
- Packages: Basic $15 up to 10 pages, Standard $40 up to 50 pages, Premium $90 up to 150 pages. Add 24-hour delivery as an extra (+$10 to +$25).
- Requirements: ask for the files, the columns they want, and one example of a finished row. Without that, you’ll redo the job.
- Description: say plainly that you use AI-assisted extraction and check every flagged cell by hand. “Human-verified” is the selling point, and it’s also true.
Upwork: search “pdf to excel”, “invoice data entry”, “extract table from PDF”. Apply only to jobs with a verified payment method and a clear page count. Your proposal should start with: “Send me 2 pages and I’ll return them in Excel within the hour, free.” Then actually do it, because the script makes it easy.
Quoting formula: (pages × your review minutes per page ÷ 60) × your target hourly rate, rounded up. For bad scans, handwriting, or tables split across pages, charge more, or say no.
Realistic earnings
- Month 1: a new Fiverr gig with no reviews often gets zero to three orders. Upwork is faster if you send good proposals, but every proposal costs Connects. Plenty of sellers make $0 in the first month. A few small orders ($50–$200 total) is a good start, not a baseline. Remember Fiverr holds earnings for 14 days.
- Month 2–3, with 10+ good reviews: some sellers reach 1–2 mid-size orders a day, but it isn’t guaranteed in a category this crowded. Treat this as a target, not a forecast. At an average of $35 per order after a 20% Fiverr cut ($28 net) and about 40 minutes of work, that works out to roughly $40/hour of actual work on the jobs you get, at about $0.30 in Gemini costs per order. Time spent on proposals, messages and revisions is unpaid and brings the real hourly rate down.
- The ceiling: repeat clients. An accounting firm or a small importer that sends 300 supplier invoices every month is worth more than 20 one-off buyers. Once the platform relationship is established, offer them a monthly package through the platform.
Risks: bans, data, and jobs to refuse
Fiverr/Upwork account bans. These are the rules that actually get sellers banned:
- Taking payment or contact details off-platform before a contract exists. Both platforms scan messages for emails, phone numbers and “pay me via PayPal”. One warning, then suspension, and your pending earnings can be held.
- Multiple accounts. One person, one seller account. Fiverr links accounts by ID, device and payment method.
- Fake or swapped reviews. “Review exchange” groups on Telegram and Facebook get whole circles of accounts banned at once.
- Misrepresenting the work. Using AI is allowed. Fiverr has entire AI service categories. Getting into trouble comes from claiming “100% manual typing” and then delivering something that obviously wasn’t. Say “AI-assisted, human-verified” and you’re covered.
Client data. Invoices, bank statements and forms contain personal and financial data.
- Use a paid-tier Gemini key. Under the Gemini API terms, content sent on the unpaid tier can be used to improve Google’s products and may be seen by human reviewers. The paid tier doesn’t do this. Using the free tier for client documents isn’t something you can defend.
- In Indonesia, UU 27/2022 on Personal Data Protection (UU PDP) applies when you process someone’s personal data. You’re acting as a processor for your client. Delete the files after delivery (say so in your gig, e.g. “files deleted 7 days after delivery”) and don’t reuse the data.
- If a client sends medical records or ID cards (KTP) in bulk, or asks you to sign an NDA that forbids third-party processing, either do that job manually or decline it. Sending it to an API would breach what you agreed to.
Jobs to refuse:
- “Copy data from behind this website’s login into Excel” or “solve these captchas”. That’s scraping or captcha-farming dressed up as data entry, and it can violate someone else’s terms or worse.
- “Data entry job” offers that ask you to pay for training, software or a “registration fee”. These are a well-known scam. Real clients pay you.
- Anything that looks like you’re building a lead list from scraped personal data for spam.
Chargebacks and disputes. Fiverr buyers can open a dispute or ask Fiverr support to cancel an order even after delivery. Your protection is the sample + clear requirements + the order message history. Don’t start large orders until the client confirms the columns and format in writing.
Reality check
What doesn’t work:
- Single-pass extraction delivered without review. It looks perfect until the client’s accountant finds one wrong total, and then you get a one-star review that ends a new gig.
- $5 for 100 pages. You’ll get orders and no profit, and you’ll attract clients who dispute everything. Compete on speed and accuracy, not on price.
- Handwritten forms and bad phone photos of documents. Flags pile up, both passes make the same mistakes, and your review time explodes. Quote them by the hour or decline.
- Expecting passive income. The AI does the typing. Selling, quoting, reviewing and client messages are still your work. This is a fast service business, not a bot.
Common mistakes:
- Not asking for a sample row, then delivering the wrong columns
- Forgetting
--numeric, so the client gets numbers stored as text that won’t add up - Ambiguous numbers:
1.250could be 1,250 or 1.25. The script assumes thousands when there are exactly three digits after the separator. Check the first page and use--notesto tell the model the format - Long batches (
--pages 20) to save time, which leads to skipped rows. Keep it at 3–5 - Running client files on a free-tier key
Model names change. If gemini-3.5-flash-lite is retired, set EXTRACT_MODEL to the current Flash-Lite model and update PRICE_IN/PRICE_OUT. For documents Flash-Lite struggles with, try a Flash model for that one job and compare the flag count.
If you want to go deeper on the extraction itself, see receipts to spreadsheet with Gemini and categorizing bank statements.
Final checklist
- Paid-tier Gemini key, cost printed after the first test
- Script tested on three of your own PDFs: one digital, one scan, one multi-page table
- Fiverr gig live with three packages, a fast-delivery extra, and an “AI-assisted, human-verified” description
- Upwork profile with a free 2-page sample offer in every proposal
- Data deletion policy written in the gig
- No off-platform contact, no review swaps, one account
Start today: download any public PDF with a table (a company annual report or a government price list), run it through the script, and turn the result into the before/after images for your gig. That’s your portfolio, and the gig can be live tonight.