Build an Election Results Funding Page
Join US election results and campaign funding in an HTML page.
Introduction
30 Second Summary
Election results tell you who won. They rarely show how much money sat behind each campaign.
In this project, you will use Firecrawl Alexandria to pull House and Senate race results for one state. You will combine those results with campaign funding totals from the FEC.
What You'll Build
You will open a state election page that lets you compare each race result with the money raised by its candidates.
By the end of this project, you'll have:
- A state-wide election results dataset that lets you compare every available House and Senate race for your chosen state.
- A funding-enriched CSV that you can open in a spreadsheet to inspect race results alongside campaign totals.
- An attributed election results page with cards showing each winner, margin, and amount raised. You can sort the cards by money raised to spot races where the bigger spender lost.
- Secret Mission: An optional challenge to push your election data skills further.
Are there any prerequisites?
You need a Firecrawl account with an API key. You also need a terminal with curl plus Python or Node.
Before We Start
Public election records arrive as raw responses. A stable local setup gives those responses somewhere predictable to go.
In this step, the terminal will fetch the data. Python 3 will shape it later.
You'll also create a dedicated workspace. One two-letter state code will anchor every later request.
In this step, get ready to:
- Verify the terminal tools required for public data calls.
- Create the election-results workspace on your Desktop.
- Choose the two-letter state code used throughout the project.
Check your local tools
The curl command sends requests to data providers. Python transforms each response into the files your project needs.
Use Terminal on macOS. Use PowerShell on Windows.
- Press Cmd+Space on macOS or the Windows key on Windows to open system search.
- Enter Terminal on macOS or PowerShell on Windows.
- Press Enter to start the selected terminal.
macOS
- Check both terminal tools by running these commands:
curl --version
python3 --version
What do these checks show?
- The curl line reports the installed command-line client.
- The python3 line reports the available Python interpreter.
Windows
- Check both terminal tools by running these commands:
curl --version
python3 --version
What do these checks show?
- The curl line reports the installed command-line client.
- The python3 line reports the available Python interpreter.
✔️ Both commands return version details
Good. Your terminal can reach both tools needed for the project.
ⓧ Python is unavailable
- Open the official Python setup guide for your operating system.
- Complete the installation steps for your operating system.
- Return to the terminal from earlier.
- Repeat the Python check shown above.
ⓧ curl is unavailable
- Open the official curl download page.
- Choose the package listed for your operating system.
- Complete the installation instructions supplied with that package.
- Return to the terminal from earlier.
- Repeat the curl check shown above.
Still missing a terminal tool?
- Start a fresh terminal session after installing a missing tool.
- Confirm that the command name matches the check shown above.
- Ask for help: Help me diagnose why curl or Python is unavailable in my terminal.
Create the project workspace
A dedicated folder keeps the CSV output in one predictable place. The HTML page will live beside it.
macOS
- Create the Desktop workspace by running these commands:
cd ~/Desktop
mkdir election-results
cd election-results
pwd
What do these commands do?
- The first cd command moves your terminal to the Desktop.
- The mkdir command creates the election-results folder.
- The second cd command enters that folder.
- The pwd command prints the folder's full path.
Windows
- Create the Desktop workspace by running these commands:
Set-Location -Path "$env:USERPROFILE\Desktop"
New-Item -ItemType Directory -Path "election-results"
Set-Location -Path "election-results"
Get-Location
What do these commands do?
- The first Set-Location command moves PowerShell to your Desktop.
- The New-Item command creates the election-results folder.
- The second Set-Location command enters that folder.
- The Get-Location command prints the folder's full path.
You should see an absolute path ending in election-results. This confirms that your terminal is inside the project workspace.
Seeing a folder error?
- Check whether an election-results folder already exists on your Desktop.
- Use the existing election-results folder if you created it during an earlier attempt.
- Confirm that your terminal can access the Desktop path used in your platform tab.
- Ask for help: Help me enter my election-results folder from the terminal.
✔️ My workspace path ends in election-results
That's the workspace ready. Every project file now has a clear home.
ⓧ I see a different path
- Select the platform tab that matches your operating system.
- Repeat the workspace commands in that tab.
- Confirm that the final path ends in election-results.
Prepare your data-review setup
Every results request needs one state. The funding lookup must use that same choice.
Your selected abbreviation becomes the project value named STATE_CODE.
- Choose the uppercase two-letter abbreviation for the state you want to analyse.
- Record your choice here: your two-letter state code.
A spreadsheet application lets you inspect each race as a row. A browser lets you check the finished page.
- Press Cmd+Space on macOS or the Windows key on Windows to open system search.
- Enter the name of your spreadsheet application.
- Press Enter to start the spreadsheet application.
- Create a blank spreadsheet.
- Press Cmd+Space on macOS or the Windows key on Windows to open system search.
- Enter the name of your web browser.
- Press Enter to start the browser.
- Open a blank browser tab.
Your review setup is ready. Both applications can open the file types you create later.
Before you run the final check, do you expect all three lines to confirm the setup you just prepared?
macOS
- Verify the workspace and terminal tools by running:
pwd
curl --version
python3 --version
What does this final check prove?
- The pwd line confirms the terminal's current folder.
- The curl line confirms that the request tool is available.
- The python3 line confirms that the Python interpreter is available.
Windows
- Verify the workspace and terminal tools by running:
Get-Location
curl --version
python3 --version
What does this final check prove?
- The Get-Location line confirms PowerShell's current folder.
- The curl line confirms that the request tool is available.
- The python3 line confirms that the Python interpreter is available.
Your path should end in election-results. The remaining output should identify curl plus Python major version 3.
✔️ Everything is ready
Your local setup now matches the project checkpoint. The workspace is ready for election data.
ⓧ I'd like to double check my setup
- Confirm that the terminal path ends in election-results.
- Confirm that curl returns version details.
- Confirm that Python reports major version 3.
- Confirm that your spreadsheet application opens a blank spreadsheet.
- Confirm that your browser opens a blank tab.
- Confirm that you recorded one uppercase two-letter state code.
Your workspace and local toolkit are ready. Next, you'll create a Firecrawl API key and inspect the election results provider.
Find Alexandria Election Providers
A useful election page depends on knowing exactly where each result comes from. Your local workspace already has STATE_CODE, but it does not yet have a trusted provider contract.
In this step, you'll connect to Firecrawl. Alexandria then shows the civicAPI contract for requests and responses.
In this step, get ready to:
- Set up a securely stored Firecrawl API key.
- Locate the election-results provider in the Public records category.
- Review the results capability's request format and response fields.
Why use Alexandria?
Direct public-data providers expose different interfaces. Alexandria gives this project one catalogue for election sources.
Each capability publishes its own inputs. Its response contract makes later data joins predictable.
Create your Firecrawl API key
An API key authenticates your local requests to Firecrawl. Creating a full-access credential can feel risky, so this flow keeps the value in your password manager.
- Visit the Firecrawl dashboard in your web browser.
- Complete the sign-up or sign-in flow with your preferred account method.
- Navigate to the API Keys page in the dashboard.
- Click Create New API Key.
- Enter election-results-project as the key name.
- Finish the key-creation form to generate the credential.
- Copy the generated key when the dashboard displays it.
- Store the copied key in your password manager under the label FIRECRAWL_API_KEY.
- Before leaving the credential screen, predict what a safe confirmation should show without revealing the key.
- Return to the API Keys page to confirm the election-results-project entry is listed.
That sensitive setup is complete. Your Firecrawl credential now has a safe home under the name FIRECRAWL_API_KEY.
Can't create or find your key?
- Return to the API Keys page if the creation form closes before the named entry appears.
- Create a replacement key if the copied value is incomplete.
- Use the official Firecrawl authentication guide to check your account flow.
- Help me create and securely store my Firecrawl API key.
Find the civicAPI provider
Alexandria groups public data sources into searchable provider pages. The Public records category is long, so finding the correct listing can be fiddly.
- Visit the Alexandria Public records category in your browser.
- Press Cmd+F on macOS or Ctrl+F on Windows to open browser search.
- Search for civicAPI live election results and forecasts.
- Select the matching provider result to load its capability page.
- Read the provider summary to identify the supported US race types.
- Review the provider terms to confirm the attribution requirement.
You have found the source that covers US Senate races, US House races, and governor races. Its provider page now gives you the contract behind those results.
Why does attribution matter?
civicAPI requires attribution when you publish its data. The finished election page needs that credit so readers can trace the result source.
The provider describes election-night counts as unofficial until certified. That status keeps fast-moving totals in context.
Review the results capability
A capability is one operation exposed by a provider. Its contract tells you which values to send before it shows the fields you receive.
- Select Live results on the civicAPI provider page.
- Open the cURL request tab.
- Identify the provider value in the request example.
- Identify the capability value in the request example.
- Inspect the input section to confirm the state value uses two letters.
- Compare the state input with your STATE_CODE value.
- Inspect the response example for candidate names, parties, vote totals, percentages, reporting progress, and winner calls.
- Check the listed execution cost before using this capability in repeated requests.
- Before the final check, predict whether the page contains enough detail to rebuild the call from your terminal.
- Return to the Live results capability to confirm its request inputs are visible.
- Scroll to the response example to confirm the race details and attribution are visible.
You should see a two-letter state input in the request contract. The response example should expose the result details needed to describe each race.
Can't find Live results?
- Use the provider page's example selector to locate the live-results option.
- Refresh the provider page if the capability selector does not load.
- Help me find the civicAPI live-results capability in Firecrawl Alexandria.
- Close every browser tab showing your Firecrawl key before capturing the checkpoint.
Your provider setup is complete. Next, you'll call the results capability for STATE_CODE to inspect its race JSON.
Retrieve State Race Results
You have already found the election-results provider in Alexandria. Your Firecrawl API key is stored safely.
Your results page needs race records before it can compare votes with campaign funding. In this step, you will retrieve the Senate races and House races for your chosen state.
In this step, get ready to:
- Send an authenticated Senate results request for STATE_CODE.
- Send an authenticated House results request for the same state.
- Inspect the returned JSON race records for offices, candidates, vote totals, winners, and margins.
Retrieve the Senate races
The civicAPI provider exposes its state race data through the elections/results capability. The chamber option selects the type of race.
Alexandria lists the execution price in the capability contract. This step makes two requests in total.
- Return to the terminal from the previous step.
- Retrieve the Senate races for your state by running this request:
curl https://api.firecrawl.dev/v2/scrape \
-H "Authorization: Bearer $FIRECRAWL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"alexandria": {
"provider": "civicapi-org",
"capability": "elections/results",
"options": {
"chamber": "senate",
"state": "[[STATE_CODE="your two-letter state code"]]"
}
}
}'
What does this request do?
- The curl command sends the request to the Firecrawl scrape endpoint.
- The Authorization header authenticates the request with your stored FIRECRAWL_API_KEY.
- The provider field selects civicapi-org.
- The capability field selects elections/results.
- The options object limits the response to Senate races in your state.
The terminal should print a JSON response containing the provider data for your state's Senate races.
Request rejected?
- Confirm that FIRECRAWL_API_KEY is available to this terminal session.
- Check that your state code contains two uppercase letters.
- Compare the request with the results capability contract you reviewed earlier.
- Help me diagnose my Firecrawl election-results request.
Retrieve the House races
House results use the same provider capability. The house chamber value returns the district races for your chosen state.
Before you run this request, do you expect your state to return one House race or several?
- Retrieve the House races for the same state by running this request:
curl https://api.firecrawl.dev/v2/scrape \
-H "Authorization: Bearer $FIRECRAWL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"alexandria": {
"provider": "civicapi-org",
"capability": "elections/results",
"options": {
"chamber": "house",
"state": "[[STATE_CODE="your two-letter state code"]]"
}
}
}'
What changed in this request?
- The provider remains civicapi-org.
- The capability remains elections/results.
- The house value requests House race records.
- The state option keeps the response focused on the same state.
You should see a JSON response containing the House races for your state. The number of returned district races settles your prediction.
Missing House races?
- Check that the chamber value is house.
- Confirm that you reused the same two-letter state code from the Senate request.
- Compare the response with the capability's response contract if the race collection appears in an unexpected location.
- Help me find the House race records in my response.
Inspect the race records
Each race record connects an office with its candidates and result. The winner and margin data show who prevailed and how close the contest was.
Before you compare the records, which race do you expect to have the smallest margin?
- Scroll back to the Senate response in your terminal.
- Locate the Senate race records in the returned JSON.
- Return to the House response in your terminal.
- Locate the House race records in the returned JSON.
- Read the office information for one race.
- Compare the candidate vote totals in that race.
- Confirm which candidate is marked as the winner.
- Find the margin recorded for the result.
Your JSON should now let you identify the office, candidates, vote totals, winner, and margin for a race in your state. You can also see whether your smallest-margin prediction matched the returned data.
You have retrieved the first project dataset through an authenticated request. Next, you will turn these race records into a CSV that is easier to scan in a spreadsheet.
Export Race Results to CSV
Your Alexandria request has proved that the election records are available for your state. The JSON response is useful for inspection, but it is difficult to compare across every race.
In this step, you will use Python to retrieve every available House and Senate race for STATE_CODE. You will export the candidate results to a CSV file that is ready for funding data in the next step.
In this step, get ready to:
- Write a Python script that retrieves House and Senate races for your state.
- Convert each candidate result into a structured CSV row.
- Open the completed export in your spreadsheet application.
Build the reusable request
The script needs one reusable function for both chambers. Keeping the request in one place gives the House and Senate records the same input format.
- Create election_results.py inside your existing local project workspace using your code editor's new-file control.
- Paste this request and retrieval code into election_results.py:
import csv
import json
import os
import urllib.request
API_URL = "https://api.firecrawl.dev/v2/scrape"
API_KEY = os.environ["FIRECRAWL_API_KEY"]
STATE_CODE = os.environ["STATE_CODE"]
def fetch_races(chamber):
# Send one Alexandria request for a chamber in the chosen state
payload = json.dumps({
"alexandria": {
"provider": "civicapi-org",
"capability": "elections/live_results",
"options": {"chamber": chamber, "state": STATE_CODE},
}
}).encode("utf-8")
request = urllib.request.Request(
API_URL,
data=payload,
headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
)
with urllib.request.urlopen(request) as response:
return json.load(response)["data"]["races"]
# Retrieve both federal chambers before building the export
races = fetch_races("senate") + fetch_races("house")
print(f"Retrieved {len(races)} races for {STATE_CODE}.")
What does this code do?
- The standard-library imports provide tools for web requests, JSON parsing, environment values, and CSV writing.
- The API_KEY assignment reads your existing credential from the environment without placing its value in the script.
- The fetch_races() function sends the same Alexandria request shape you reviewed earlier.
- The final expression retrieves Senate races first. It adds the House races to the same list.
- Save election_results.py.
Before you run the script, do you expect the combined race count to be greater than zero for your state?
- Retrieve the House and Senate race records by running this command in your existing terminal:
python3 election_results.py
What does this command do?
Python runs election_results.py. The script makes one request for each federal chamber before printing the combined race count.
You should see a message beginning with Retrieved. The number shows how many House and Senate races the script received for STATE_CODE.
No race count in the terminal?
- Confirm that your existing FIRECRAWL_API_KEY value is available in the same terminal session.
- Check that your existing STATE_CODE value contains the same two-letter state abbreviation used in the earlier request.
- Compare the provider name and capability in your script with the code block above.
- Ask for help with the request failure using this debugging prompt.
Write the CSV rows
Each race contains a candidate list. The export turns each candidate into one row while preserving the office, winner flag, and vote margin for the race.
- Add this export code below the final print() line in election_results.py:
# Turn each candidate into one row while keeping the race-level margin
rows = []
for race in races:
candidates = race["candidates"]
vote_totals = sorted((candidate["votes"] for candidate in candidates), reverse=True)
margin_votes = vote_totals[0] - vote_totals[1] if len(vote_totals) > 1 else vote_totals[0]
for candidate in candidates:
rows.append({
"office": race["name"],
"candidate": candidate["name"],
"party": candidate["party"],
"vote_total": candidate["votes"],
"winner": candidate["winner"],
"margin_votes": margin_votes,
})
# Save a stable column order for spreadsheet work
fieldnames = ["office", "candidate", "party", "vote_total", "winner", "margin_votes"]
with open("election_results.csv", "w", newline="", encoding="utf-8") as csv_file:
writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
print(f"Wrote {len(rows)} candidate rows to election_results.csv.")
What does this code do?
- The outer loop visits every retrieved race.
- The sorted vote totals place the largest total first. Subtracting the second total produces the race margin in votes.
- The inner loop creates one row for every candidate. This shape gives the next step a candidate name to match against campaign funding records.
- The csv.DictWriter writes the headers in a stable order before saving every row.
- Save election_results.py.
Before you run the completed script, do you expect the candidate row count to match the race count or exceed it?
- Generate election_results.csv by running the completed script:
python3 election_results.py
What does this command do now?
The script retrieves the current races again. It then writes one row for each candidate to election_results.csv.
You should see the retrieved race count followed by a message beginning with Wrote. The candidate row count is usually larger because most races contain multiple candidates.
CSV file missing or empty?
- Confirm that the terminal is still using your existing project workspace.
- Check that the export code sits below the line that creates races.
- Verify that every line inside the two for loops uses consistent indentation.
- Ask for help with the export using this CSV debugging prompt.
✔️ Awesome, I've got everything!
Your script now retrieves both federal chambers. Double check that you saved election_results.py.
ⓧ I'd like to double check the full code
The complete file should match this reference.
import csv
import json
import os
import urllib.request
API_URL = "https://api.firecrawl.dev/v2/scrape"
API_KEY = os.environ["FIRECRAWL_API_KEY"]
STATE_CODE = os.environ["STATE_CODE"]
def fetch_races(chamber):
# Send one Alexandria request for a chamber in the chosen state
payload = json.dumps({
"alexandria": {
"provider": "civicapi-org",
"capability": "elections/live_results",
"options": {"chamber": chamber, "state": STATE_CODE},
}
}).encode("utf-8")
request = urllib.request.Request(
API_URL,
data=payload,
headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
)
with urllib.request.urlopen(request) as response:
return json.load(response)["data"]["races"]
# Retrieve both federal chambers before building the export
races = fetch_races("senate") + fetch_races("house")
print(f"Retrieved {len(races)} races for {STATE_CODE}.")
# Turn each candidate into one row while keeping the race-level margin
rows = []
for race in races:
candidates = race["candidates"]
vote_totals = sorted((candidate["votes"] for candidate in candidates), reverse=True)
margin_votes = vote_totals[0] - vote_totals[1] if len(vote_totals) > 1 else vote_totals[0]
for candidate in candidates:
rows.append({
"office": race["name"],
"candidate": candidate["name"],
"party": candidate["party"],
"vote_total": candidate["votes"],
"winner": candidate["winner"],
"margin_votes": margin_votes,
})
# Save a stable column order for spreadsheet work
fieldnames = ["office", "candidate", "party", "vote_total", "winner", "margin_votes"]
with open("election_results.csv", "w", newline="", encoding="utf-8") as csv_file:
writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
print(f"Wrote {len(rows)} candidate rows to election_results.csv.")
Inspect the spreadsheet
A spreadsheet makes row boundaries and missing values easy to spot. This check confirms that the JSON fields survived the conversion into a table.
Before you open the export, which candidate from your earlier JSON response do you expect to find first?
- Open election_results.csv from your existing project workspace using your spreadsheet application.
- Confirm that the header row contains the six fields defined by fieldnames.
- Compare one candidate row with the JSON race record you inspected earlier.
- Check that House and Senate offices are both represented in the office column when both are available for your state.
You should see one row per candidate with an office, party, vote total, winner value, and race margin. Each retrieved House or Senate race should appear through its candidate rows.
Strong work. Your state's election results are now a reusable table that you can match with campaign finance records.
Your race results are organized in election_results.csv. Next up, you will add campaign funding totals for the same candidates.
Add Campaign Funding Totals
Your election_results.csv file already shows every House and Senate race for STATE_CODE. You can now connect those results to the money behind each campaign.
Those rows explain who won. The FEC provider in Firecrawl Alexandria adds the money raised by each candidate's principal committee.
In this step, get ready to:
- Locate the FEC candidate search and committee totals capabilities.
- Match each candidate to a principal campaign committee.
- Extend the CSV with money-raised or unavailable funding values.
Match one candidate to a committee
Campaign-finance records use committee IDs to connect candidates to financial filings. The Candidates capability provides each candidate's principal committee.
- Return to the Alexandria public-records category from earlier.
- Locate the FEC provider page in the catalogue.
- Open the records/candidates capability.
- Switch back to election_results.csv in the spreadsheet from earlier.
- Copy the name of the first candidate in the file.
Why use several match fields?
Candidate names can produce multiple records across election cycles. A dependable match agrees with the state plus the office shown in your results.
House matches also use the district. The matching record must list 2026 in its cycles data.
- Adapt the authenticated curl request from earlier to call records/candidates.
- Set query to the candidate's name.
- Set state to STATE_CODE.
- Run the adapted candidate request.
You should receive candidate records containing identity fields plus a principal_committees collection.
- Select the record that matches the candidate's office.
- Confirm that a House record also matches the district.
- Confirm that the record's cycles data includes 2026.
- Record the principal committee_id here: your first principal committee ID.
Committee totals are retrieved with the principal committee ID. The election cycle keeps the financial record aligned with the race result.
- Open the records/committee_totals capability.
- Set committee_id to your first principal committee ID.
- Set cycle to 2026.
- Run the committee totals request.
You should receive a committee record with financial totals for the selected cycle. The response also identifies the committee that produced those totals.
Which total means money raised?
Use the receipts field as the campaign's money-raised total. It represents the committee's incoming funds for that cycle.
A missing candidate match or missing totals record becomes unavailable. That value keeps uncertainty visible in the final page.
Add funding lookups to the script
The manual request proved the complete data path for one candidate. Your script now needs to repeat that path for every unique candidate in the results.
How should the funding join work?
- Each unique candidate gets one records/candidates lookup.
- A candidate match must agree with the race state.
- A candidate match must agree with the race office.
- A House match must also agree with the district.
- The selected principal committee supplies its committee_id.
- The records/committee_totals lookup uses that ID for the 2026 cycle.
- The funding column receives the returned receipts value.
- A missing or ambiguous match receives unavailable.
- Switch back to election_results.py in the local project workspace.
- Implement the candidate-to-committee funding workflow using the capability request shapes you just tested.
- Save election_results.py.
Before you rerun the script, do you expect every candidate to receive a numeric funding total?
- Rerun election_results.py with Python 3.
You should see the script finish and rewrite election_results.csv. Candidates without a dependable committee total remain in the file with unavailable in the funding field.
Seeing too many unavailable values?
- Compare the candidate record's state with STATE_CODE.
- Confirm that the candidate record's office matches the race.
- Check the district when the row represents a House race.
- Confirm that the selected principal committee lists the 2026 cycle.
- Use the response's receipts field for the final funding value.
- Ask for help with the matching logic: Help me debug candidate-to-committee matching in my election results script.
✔️ My script finished
Good progress. Your script now combines election results with available FEC committee receipts.
ⓧ I'd like to double check the full code
Your cumulative election_results.py should preserve the original results export while adding the funding join.
- Confirm that the original Alexandria election-results request still retrieves every House and Senate race for STATE_CODE.
- Confirm that candidate lookups use records/candidates.
- Confirm that the selected match supplies a principal committee_id.
- Confirm that committee lookups use records/committee_totals for the 2026 cycle.
- Confirm that the funding field uses receipts when a total exists.
- Confirm that unresolved funding values become unavailable.
- Confirm that the CSV writer exports the original race fields plus the funding field.
Verify the combined CSV
Before you check the spreadsheet, which candidates do you expect to have an unavailable funding value?
- Return to the spreadsheet application from earlier.
- Reload the updated election_results.csv file.
- Locate the new funding column.
- Scan every candidate row for a money-raised total or unavailable.
You should see a numeric money-raised total for every matched principal committee. Any unresolved row should show unavailable explicitly.
Checkpoint
Your election_results.py script now retrieves election results for STATE_CODE. It matches candidates to FEC committee totals before exporting the combined data.
Your election_results.csv file now contains race results plus money-raised or unavailable funding fields.
That is the core data join working. Each candidate row now carries its election result beside its campaign funding total.
Your combined data is ready. Next, you'll turn those rows into an HTML election results page.
Build the Election Results Page
Your combined CSV already ties election outcomes to committee totals for every matching candidate. A spreadsheet spreads each race across several rows.
This step turns the same records into an HTML page with one card per race. The page will surface the decisive result details plus a visible civicAPI credit.
In this step, get ready to:
- Extend election_results.py with HTML page generation.
- Render one card for every House or Senate race.
- Verify the finished page in your browser.
Generate the race cards
The CSV stores each candidate on a separate row. A race card needs to collect the candidates that belong to the same race.
What should the page generator produce?
- Each distinct House or Senate race renders inside its own visually separated card.
- Each card identifies the race or office.
- Each card shows the result.
- Each card identifies the winner.
- Each card reports the margin.
- Each candidate entry shows the money-raised total or the existing unavailable value.
- A visible credit identifies civicAPI as the election-results source.
- In election_results.py from earlier, add a page-generation section that follows the contract above.
- Save election_results.py.
- Rerun the script with the same Python command from the previous step.
Good progress. Your script now turns the combined records into election_results.html beside the CSV.
Page file missing?
- Confirm that you saved election_results.py before rerunning it.
- Check that the page-generation section runs after the combined race records have been created.
- Confirm that the script writes election_results.html into your existing project workspace.
- Help me debug why my election results script is not generating the HTML file.
✔️ Awesome, I've got everything!
Your cumulative script now produces the combined CSV plus the browser-ready HTML page.
- Confirm that election_results.py is saved.
ⓧ I'd like to double check the full code
Compare your cumulative election_results.py against this checklist.
- Confirm that the script still retrieves election results for STATE_CODE.
- Confirm that the script still matches candidates to committee totals from the FEC provider.
- Confirm that the script still exports combined result plus funding data to election_results.csv.
- Confirm that the script now generates election_results.html with one card per race.
- Confirm that every card includes the result.
- Confirm that every card includes the winner.
- Confirm that every card includes the margin.
- Confirm that every candidate entry includes a money-raised total or an unavailable value.
- Confirm that the generated page includes a visible civicAPI credit.
Open and inspect the page
The browser view is the final proof that the script grouped candidate rows into complete races.
Before you open the page, picture whether its card count should match the number of distinct House and Senate races in election_results.csv.
- Open election_results.html in your web browser by double-clicking it in the file browser for your existing workspace.
What should I see?
- The page contains one card for every distinct House or Senate race in the CSV.
- Each card identifies its race or office.
- Each card displays the result.
- Each card identifies the winner.
- Each card reports the margin.
- Each candidate entry displays a money-raised total or the explicit unavailable value.
- A visible credit names civicAPI as the election-results source.
- Confirm the visible civicAPI credit near the page content.
You pulled it together. The browser now connects each race outcome with the campaign funding behind its candidates.
Seeing missing cards or values?
- Compare the number of cards with the distinct House and Senate races in election_results.csv.
- Check that the page generator receives the full combined dataset.
- Confirm that candidate grouping uses the same race or office field throughout the script.
- Preserve the explicit unavailable value when no matching committee total exists.
- Help me debug missing race cards or funding values in my generated page.
Your election results page is ready to show. Next, you'll sort the races by money raised to spot cases where the bigger spender lost.
Sort Funding and Spot Upsets
Your results page already connects election outcomes with campaign funding. The current race order can hide the funding patterns inside those cards.
In this step, you'll rank race cards by the largest candidate funding total. Then you'll inspect the ordered page for races where the better-funded candidate lost.
In this step, get ready to:
- Rank each race card by its largest candidate funding total.
- Regenerate the results page with the new order.
- Review the cards for bigger-spender losses.
Sort race cards by funding
Your existing HTML page contains several candidate totals inside each race card. A sort key gives every race one comparable number.
How should the race sort work?
- The race-level sort key is the largest numeric money-raised total among that race's candidates.
- Descending order puts the largest sort key first.
- Races with no numeric funding total sit after races with known totals.
- Displayed unavailable values stay unchanged.
- Switch back to election_results.py in the editor from earlier.
- Find the race collection that feeds the HTML card-generation loop.
- Update that collection to apply the descending race-level funding order described above.
- Save election_results.py.
Rebuild the ordered page
Before you rerun the script, which race do you expect to appear first?
- Return to the terminal from earlier.
- Rerun election_results.py with the same command you used in the previous step.
You should see the script finish successfully. The updated election_results.html file should be ready to view.
- Return to the browser tab from earlier.
- Refresh election_results.html in the browser.
You should see race cards ordered from the highest race-level funding value to the lowest. Cards with only unavailable totals should sit at the bottom.
- Confirm the required civicAPI credit remains visible on the page.
Page still in the old order?
- Confirm the sort runs before the HTML card-generation loop in election_results.py.
- Check that numeric funding values are compared as numbers instead of formatted text.
- Verify that your browser refreshed the newly generated election_results.html file.
- Help me debug why my election results page is not sorting by funding.
Spot bigger-spender losses
Before you review the cards, do you expect the best-funded candidate to win every race?
- Start at the first card on the refreshed page.
- Compare the winner's money-raised total with every losing candidate's total.
- Flag the race when a losing candidate has a larger numeric total than the winner.
- Repeat the comparison for every card.
- Confirm each flagged card identifies the lower-funded candidate as the winner.
- Record your findings in the checkpoint below.
You've closed the loop: your ordered page now exposes races where campaign money did not predict the winner.
Secret mission
Audit a Bigger-Spender Loss
Your sorted page can highlight a race where the candidate with more money raised still lost. Can you trace one of those findings back through the CSV and its committee totals before deciding how confidently you can explain it?
Clean Up Your Resources
Clean Up Your Resources
Your finished script does not leave a local process running, so the project creates no ongoing local cost. Your Firecrawl API key remains active until you invalidate it, so choose whether to keep everything, pause your work, or remove the project resources.
Resources you used:
- The election_results.py script in your local project workspace.
- The election_results.csv spreadsheet with the combined race and campaign funding data.
- The election_results.html page with sorted race cards and the required civicAPI credit.
- The active Firecrawl API key referenced as FIRECRAWL_API_KEY in your account and secure local storage.
Keep everything running
No action is needed. Choose this if you plan to run the script again or continue analysing the election results.
- Keep election_results.py, election_results.csv, and election_results.html together in your local project workspace.
- Keep FIRECRAWL_API_KEY in the secure storage location you used earlier.
- Keep the civicAPI credit in election_results.html whenever you reuse or publish the page.
- Review your Firecrawl account usage before making more API requests.
Pause - I'll come back to this later
Close the open project views to pause your work. The script has already finished, so there is no server or background process to stop.
- Close the browser tab showing election_results.html.
- Close election_results.csv in your spreadsheet application.
- Close election_results.py in your code editor if it is open.
- Keep all three files in the local project workspace from earlier.
- Leave FIRECRAWL_API_KEY in secure storage for your next session.
Delete - I don't want to use this again
Remove the project outputs and invalidate the credential when you want a clean break. These actions are permanent, so take a moment to make sure you no longer need the CSV or HTML page.
- Return to the Firecrawl dashboard area where you created FIRECRAWL_API_KEY to locate its key entry.
- Invalidate the old key with the available delete or rotate action.
- Confirm the old key no longer appears as active in the dashboard.
- Remove the old key value from the secure storage location you used earlier.
The old credential can no longer authorize requests. You can now remove the local project outputs.
- Return to the local project workspace from earlier in Finder on macOS or File Explorer on Windows.
- Select election_results.py, election_results.csv, and election_results.html.
- Press Cmd+Delete on macOS or Delete on Windows to remove the selected files.
- Empty the Trash on macOS or the Recycle Bin on Windows.
- Check the local project workspace to confirm that all three filenames are absent.
Nice Work!
Nice Work!
You did it! Your Python script now turns election results plus campaign funding records into a browser-ready page for your chosen state.
You can inspect every House or Senate race in election_results.csv. You can also scan the sorted election_results.html cards to spot where more money did not produce a win.
You've learned how to:
- Use Firecrawl Alexandria to retrieve authenticated House or Senate election records for STATE_CODE. Inspect candidate vote totals in JSON. Confirm each race's winner. Check its margin.
- Match each candidate to an FEC committee total with your Python data pipeline. Export the combined records to election_results.csv for spreadsheet review.
- Generate a sorted election results page in election_results.html with one card per race. Compare each result with the campaign funding totals. Identify races where a bigger spender lost. Preserve the required civicAPI credit.
- Complete the optional Secret Mission to push your election data analysis skills further.
Ready to quiz yourself?