Simple charity validation for volunteers - Help families find food assistance by keeping charity information accurate and up-to-date.
Ready to help in 5 minutes:
# 1. **Install dependencies:**
pip install -r requirements.txt
# 2. Get API token from team lead, create .env file:
echo "AI_SCRAPING_TOKEN=your_token_here" > .env
echo "ENVIRONMENT=dev" >> .env
# 3. Test it works:
python -c "from src.tackle_hunger.graphql_client import TackleHungerClient; print('✅ Ready!')"
# 4. Run tests (optional):
python -m pytest tests/
# 5. Start validating charities:📋 Read the How to Validate Charities Guide - everything you need to know!
Charity Validation Tasks:
- ✅ Verify charity addresses, phone numbers, websites
- ✅ Update missing or incorrect information
- ✅ Add new charities to help more families
- ✅ Ensure data quality for food distribution network
Impact: Your work helps hungry families find food assistance faster!
Choose your preferred setup:
- Local Python (Simplest) - Follow quick start above
- GitHub Codespaces - Cloud development environment (Guide)
- Docker - Containerized environment (Guide)
Target Deliverables:
- Update Missing Info - Fill in blank charity details
- Verify Current Data - Check addresses, phones, websites work
- Add New Charities - Expand the network to help more families
- Ensure Data Quality - Mark reliable sources, validate information
- Never commit API keys - they're in
.env(git-ignored) - Use dev environment for testing your changes
- Always include
modifiedBy: "AI_Copilot_Assistant"when updating - Test your changes with
python -m pytest tests/
- Getting Started: How to Validate Charities Guide
- API Reference: GraphQL playground at https://devapi.sboc.us/graphql
- Network Issues: Firewall Setup Guide
- Questions: Ask in the project channel
- Language: Python 3.13
- API: GraphQL for charity data operations
- Focus: Simple, volunteer-friendly code - no enterprise complexity!
🎯 Ready to make a difference? Start with the How to Validate Charities Guide!
In the Tackle Hunger Map app, Charities are stored as an Organization & a Site (service location). Charities can have more than 1 service location so Org--<Site is a 1-to-Many relation (however, the vast majority have just 1 Site). Most information is on Sites - charities that haven't interacted with the app yet typically have mostly blank Organization fields.
The only other relevant relations (EligiblePopulations, PlaceTypes, & ServiceTypes) are internally handled - just reference the Schema's Scalar definition for each.
The full Schema is visible on the Staging API GraphQL Playground.
Relevant Sites fields to fetch are defined in the GraphQL Schema type SiteForAI. Request the appropriate ones for your usage in sitesForAI queries.
Here they are ordered for clarity & explained:
type SiteForAI {
### CORE (IDs & Important Direct Joined Relations)
#
id: ID! # string, primary key, exclude from web searches to avoid false-positive matches
organizationId: ID! # string, foreign key, exclude from web searches to avoid false-positive matches
organization: OrganizationForAI # M-to-1
###
### LOCATION DETAILS (Food Pickup/Dropoff/Distribution Address, avoid PO Boxes)
#
# Any of these 4 changed triggers internal Location-Standardizing
streetAddress: String # required for creation
city: String # required for creation
state: String # required for creation
zip: String # required for creation
#
name: String # required for creation, only updated on first Location-Standardizing triggered
addressLine2: String # only updated if blank when Location-Standardizing is triggered
country: Country # see scalar options, default can be 'US', updated if Location-Standardizing triggered
county: String # updated if Location-Standardizing triggered, not very important
neighborhood: String # updated if Location-Standardizing triggered, not very important
lat: Float # updated if Location-Standardizing triggered
lng: Float # updated if Location-Standardizing triggered
###
### CONTACT DETAILS
#
publicEmail: String # validated: (isEmail: true, isLowercase: true, notEmpty: false), allows Null
publicPhone: String
socialMedia: String
website: String
publicContactMethod: String # which public method is preferred? ('Email','Phone','Social Media','Website')
#
# Direct Internal Contact fields, not public, less likely found by AI
contactEmail: String # validated: (isEmail: true, isLowercase: true, notEmpty: false), allows Null
contactName: String
contactPhone: String
#
# Direct Finance Manager Contact fields, not public, less likely found by AI, really valuable if available
financialControllerEmail: String # validated: (isEmail: true, isLowercase: true, notEmpty: false), allows Null
financialControllerPhone: String
###
### SITE SERVICE DETAILS
#
status: BusinessStatus # see scalar options, updated if Location-Standardizing triggered
ein: String # US Employer Identification Number (EIN), Government Tax ID if outside USA
efroid: String # New York State specific, not very important
acceptsFoodDonations: YesNoEnum # see scalar options
#
eligiblePopulations: [EligiblePopulation!]! # see scalar options, M-to-M
placeTypes: [PlaceType!]! # see scalar options, updated if Location-Standardizing triggered, M-to-M
serviceTypes: [ServiceType!]! # see scalar options, M-to-M
#
# Multiline Text fields
description: String # max 500 char, public & donor-facing summary, ideally around 50 words
serviceArea: String # max 250 char, Do you have a specific service area? (counties, neighborhoods, zip codes, etc.)
requiredDocuments: String # max 250 char, Are first-time clients required to bring any documents? (State ID, Proof of Residence, etc.)
hoursText: String # max 250 char, When is food available at your site?
accessInfoText: String # max 500 char, Any other specifics someone seeking assistance at this site needs to know? (like a separate entrance) do not repeat info in other fields
#
banner: String # large rectangular image at top of page, unnecessary but nice to have
logo: String # small square-ish icon, unnecessary but nice to have
#
# Food-Availability fields, unlikely found by AI, maintained by Charity Representatives
disasterPrepared: YesNoEnum # prepared to distribute food during a disaster, see scalar options
stockStatus: Int # (0, 1, 2, 3, 4), current food-availability
###
### BACKEND FIELDS (system provenance & workflow)
#
pendingStatus: PendingStatus # approval workflow state, see scalar options
lastUserModifiedAt: Date # when a Charity Representative last modified this site
staffConfirmedAt: Date # when a Tackle Hunger Staff member last confirmed this site is valid
#
# Human-Readable Partner/Service/Run Identifiers if AI/API/ETL Operation, consistent per partner/service/run
createdMethod: String # required when AI/API/ETL-created, AI Programs Must Push This When Creating
modifiedBy: String # required when AI/API/ETL-modified, AI Programs Must Push This When Updating
#
dataSource: String # URI or human-readable identifier if found on foreign source list/table/DB with IDs
dataSourceId: ID # id from the foreign source (if from one with IDs)
#
createdAt: Date!
updatedAt: Date!
###
}Relevant Organization fields to fetch are defined in the GraphQL Schema type OrganizationForAI. Request the appropriate ones for your usage in organizationsForAI queries.
Here they are ordered for clarity & explained:
type OrganizationForAI {
### CORE (IDs & Important Direct Joined Relations)
#
id: ID! # string, primary key, exclude from web searches to avoid false-positive matches
sites: [SiteForAI!]! # each Organization must have at least 1 Site, 1-to-M
###
### LOCATION DETAILS
#
# Parent Organization Mailing Address, PO box is fine, public but not shown on the Map
name: String # Charity's Registered Name
streetAddress: String
addressLine2: String
city: String
state: String
zip: String
###
### CONTACT DETAILS
#
publicEmail: String
publicPhone: String
#
# Direct Internal Contact fields, not public, less likely found by AI
email: String
phone: String
###
### PARENT CHARITY ORGANIZATION DETAILS
#
isFeedingAmericaAffiliate: YesNoEnum # Does this Charity Org or any of its Sites: (1) exist in Feeding America's Network? or (2) collaborate with FA? or (3) receive support from FA? or (4) exist in the network of a Food Bank or collaborate with or receive support from a Food Bank that does (1), (2), or (3)?
#
# Especially include these if they differ from its Site(s)
description: String # multiline text, max 500 char, public & donor-facing summary, ideally around 50 words
ein: String # US Employer Identification Number (EIN), Government Tax ID if outside USA
banner: String # large rectangular image at top of page, unnecessary but nice to have
logo: String # small square-ish icon, unnecessary but nice to have
###
### BACKEND FIELDS (Timestamps)
#
updatedAt: Date!
createdAt: Date!
###
}Relevant fields to push when creating a Site are defined in the GraphQL Schema input siteInputForAI, a variable used in the addCharityFromAI mutation.
siteId: String is a separate variable required for the addCharityFromAI mutation.
Most siteInputForAI fields can be omitted if not intending to set with a value. The 3 Scalar arrays can be omitted, but if included they can't be empty. Only the 5 String fields marked with ! are required.
Here they are ordered for clarity & explained:
input siteInputForAI {
### LOCATION DETAILS (Food Pickup/Dropoff/Distribution Address, avoid PO Boxes)
#
# Any of these 4 changed triggers internal Location-Standardizing
streetAddress: String! # required for creation
city: String! # required for creation
state: String! # required for creation
zip: String! # required for creation
#
name: String! # required for creation, only updated on first Location-Standardizing triggered
addressLine2: String # only updated if blank when Location-Standardizing is triggered
country: Country # see scalar options, default can be 'US', updated if Location-Standardizing triggered
county: String # updated if Location-Standardizing triggered, not very important
neighborhood: String # updated if Location-Standardizing triggered, not very important
lat: Float # updated if Location-Standardizing triggered
lng: Float # updated if Location-Standardizing triggered
###
### CONTACT DETAILS
#
publicEmail: String # validated: (isEmail: true, isLowercase: true, notEmpty: false), allows Null
publicPhone: String
socialMedia: String
website: String
publicContactMethod: String # which public method is preferred? ('Email','Phone','Social Media','Website')
#
# Direct Internal Contact fields, not public, less likely found by AI
contactEmail: String # validated: (isEmail: true, isLowercase: true, notEmpty: false), allows Null
contactName: String
contactPhone: String
#
# Direct Finance Manager Contact fields, not public, less likely found by AI, really valuable if available
financialControllerEmail: String # validated: (isEmail: true, isLowercase: true, notEmpty: false), allows Null
financialControllerPhone: String
###
### SITE SERVICE DETAILS
#
status: BusinessStatus # see scalar options, updated if Location-Standardizing triggered
ein: String # US Employer Identification Number (EIN), Government Tax ID if outside USA
efroid: String # New York State specific, not very important
acceptsFoodDonations: YesNoEnum # see scalar options
#
eligiblePopulations: [EligiblePopulation!] # see scalar options, M-to-M
placeTypes: [PlaceType!] # see scalar options, updated if Location-Standardizing triggered, M-to-M
serviceTypes: [ServiceType!] # see scalar options, M-to-M
#
# Multiline Text fields
description: String # max 500 char, public & donor-facing summary, ideally around 50 words
serviceArea: String # max 250 char, Do you have a specific service area? (counties, neighborhoods, zip codes, etc.)
requiredDocuments: String # max 250 char, Are first-time clients required to bring any documents? (State ID, Proof of Residence, etc.)
hoursText: String # max 250 char, When is food available at your site?
accessInfoText: String # max 500 char, Any other specifics someone seeking assistance at this site needs to know? (like a separate entrance) do not repeat info in other fields
#
banner: String # large rectangular image at top of page, unnecessary but nice to have
logo: String # small square-ish icon, unnecessary but nice to have
#
# Food-Availability fields, unlikely found by AI, maintained by Charity Representatives
disasterPrepared: YesNoEnum # prepared to distribute food during a disaster, see scalar options
stockStatus: Int # (0, 1, 2, 3, 4), current food-availability
###
### BACKEND FIELDS (system provenance & workflow)
#
organizationId: ID # string, foreign key, only set if grouping this Site w/ others on a preexisting Organization
pendingStatus: PendingStatus # approval workflow state, see scalar options
#
# Human-Readable Partner/Service/Run Identifiers if AI/API/ETL Operation, consistent per partner/service/run
createdMethod: String! # required when AI/API/ETL-created, AI Programs Must Push This
modifiedBy: String # required when AI/API/ETL-modified
#
dataSource: String # URI or human-readable identifier if found on foreign source list/table/DB with IDs
dataSourceId: ID # id from the foreign source (if from one with IDs)
###
}Relevant fields to push when updating a Site are defined in the GraphQL Schema input siteInputForAIUpdate, a variable used in the updateSiteFromAI mutation.
siteId: String is a separate variable required for the updateSiteFromAI mutation.
Only a few Organization fields really need to be updated. When a Financial Controller gets invited (which is automatic upon update of site.financialControllerEmail), then most Org fields get filled by copying them from its Site. All fields below are welcome if they differ from their Site(s), the Charity is being newly created, or the fields were previously blank.
Any fields not intended to modify can be omitted.
Here they are ordered for clarity & explained:
input siteInputForAIUpdate {
### LOCATION DETAILS (Food Pickup/Dropoff/Distribution Address, avoid PO Boxes)
#
# Any of these 4 changed triggers internal Location-Standardizing
streetAddress: String # required for creation
city: String # required for creation
state: String # required for creation
zip: String # required for creation
#
name: String # required for creation, only updated on first Location-Standardizing triggered
addressLine2: String # only updated if blank when Location-Standardizing is triggered
country: Country # see scalar options, default can be 'US', updated if Location-Standardizing triggered
county: String # updated if Location-Standardizing triggered, not very important
neighborhood: String # updated if Location-Standardizing triggered, not very important
lat: Float # updated if Location-Standardizing triggered
lng: Float # updated if Location-Standardizing triggered
###
### CONTACT DETAILS
#
publicEmail: String # validated: (isEmail: true, isLowercase: true, notEmpty: false), allows Null
publicPhone: String
socialMedia: String
website: String
publicContactMethod: String # which public method is preferred? ('Email','Phone','Social Media','Website')
#
# Direct Internal Contact fields, not public, less likely found by AI
contactEmail: String # validated: (isEmail: true, isLowercase: true, notEmpty: false), allows Null
contactName: String
contactPhone: String
#
# Direct Finance Manager Contact fields, not public, less likely found by AI, really valuable if available
financialControllerEmail: String # validated: (isEmail: true, isLowercase: true, notEmpty: false), allows Null
financialControllerPhone: String
###
### SITE SERVICE DETAILS
#
status: BusinessStatus # see scalar options, updated if Location-Standardizing triggered
ein: String # US Employer Identification Number (EIN), Government Tax ID if outside USA
efroid: String # New York State specific, not very important
acceptsFoodDonations: YesNoEnum # see scalar options
#
eligiblePopulations: [EligiblePopulation!] # see scalar options, M-to-M
placeTypes: [PlaceType!] # see scalar options, updated if Location-Standardizing triggered, M-to-M
serviceTypes: [ServiceType!] # see scalar options, M-to-M
#
# Multiline Text fields
description: String # max 500 char, public & donor-facing summary, ideally around 50 words
serviceArea: String # max 250 char, Do you have a specific service area? (counties, neighborhoods, zip codes, etc.)
requiredDocuments: String # max 250 char, Are first-time clients required to bring any documents? (State ID, Proof of Residence, etc.)
hoursText: String # max 250 char, When is food available at your site?
accessInfoText: String # max 500 char, Any other specifics someone seeking assistance at this site needs to know? (like a separate entrance) do not repeat info in other fields
#
banner: String # large rectangular image at top of page, unnecessary but nice to have
logo: String # small square-ish icon, unnecessary but nice to have
#
# Food-Availability fields, unlikely found by AI, maintained by Charity Representatives
disasterPrepared: YesNoEnum # prepared to distribute food during a disaster, see scalar options
stockStatus: Int # (0, 1, 2, 3, 4), current food-availability
###
### BACKEND FIELDS (system provenance & workflow)
#
organizationId: ID # string, foreign key, only modify if grouping this Site w/ others on a different Organization
pendingStatus: PendingStatus # approval workflow state, see scalar options
#
# Human-Readable Partner/Service/Run Identifiers if AI/API/ETL Operation, consistent per partner/service/run
modifiedBy: String! # required when AI/API/ETL-modified, AI Programs Must Push This
#
dataSource: String # URI or human-readable identifier if found on foreign source list/table/DB with IDs
dataSourceId: ID # id from the foreign source (if from one with IDs)
###
}Each Organization must have at least 1 Site to prevent them from getting orphaned (so ignore the createOrganization mutation - it is deprecated, do not try to use it).
A new blank Organization is automatically created for each new Site that doesn't specify a preexisting site.organizationId.
To add fields to a newly created Organization, first create the Site (addCharityFromAI mutation) & request organizationId in the response, then use that to update Organization fields via the updateOrganizationFromAI mutation explained below.
Relevant fields to push when updating an Organization are defined in the GraphQL Schema input organizationInputUpdate, a variable used in the updateOrganizationFromAI mutation.
organizationId: String is a separate variable required for the updateOrganizationFromAI mutation.
Any fields not intended to modify can be omitted.
Here they are ordered for clarity & explained:
input organizationInputUpdate {
### LOCATION DETAILS
#
# Parent Organization Mailing Address, PO box is fine, public but not shown on the Map
name: String! # Charity's Registered Name, required when updating (can't be blank)
streetAddress: String
addressLine2: String
city: String
state: String
zip: String
###
### CONTACT DETAILS
#
publicEmail: String
publicPhone: String
#
# Direct Internal Contact fields, not public, less likely found by AI
email: String
phone: String
###
### PARENT CHARITY ORGANIZATION DETAILS
#
isFeedingAmericaAffiliate: YesNoEnum # Does this Charity Org or any of its Sites: (1) exist in Feeding America's Network? or (2) collaborate with FA? or (3) receive support from FA? or (4) exist in the network of a Food Bank or collaborate with or receive support from a Food Bank that does (1), (2), or (3)?
#
# Especially include these if they differ from its Site(s)
description: String # multiline text, max 500 char, public & donor-facing summary, ideally around 50 words
ein: String # US Employer Identification Number (EIN), Government Tax ID if outside USA
banner: String # large rectangular image at top of page, unnecessary but nice to have
logo: String # small square-ish icon, unnecessary but nice to have
###
}-
Common to each:
- store mailing address separately if it differs from the food pickup/dropoff address
- a non-food-service address like a PO box goes on the related Organization, as opposed to the Site (see schemas)
- ensure they're open year-round (as opposed to seasonal, like school or summer programs)
- potentially have the AI give a data-reliability score for each result
- i.e. higher if verified from multiple sources, favored sources, conforms to key phrases
- in our current setup, new Charities submitted are marked Pending until a human does a quick check - this could help with that review
- could also potentially push very good ones to our API to immediately go live (i.e. not Pending)
- automate it - deliver some amount of infrastructure-as-code that'd run it automatically (i.e. github actions, crons, webhook triggers, etc.)
- store mailing address separately if it differs from the food pickup/dropoff address
-
Find Info that our Current Charities are Missing & Verify their Operational Status
- i.e. blank, missing, or really poor quality field values
- push fields that were previously empty to our API (
mutation updateSiteFromAI) - if you think they're good enough we can save them immediately - store a copy of our original data & the new recommended data at that moment in time, as well as links to our Site/Org & to the new source
- in a new storage bucket or DB for this project
-
Scrape Facebook for Charities (added to any of these other projects)
- would probably be best as a helper-function to run within any of these other automated-scraping goals
- many of our Charities don't have main websites and use social media as their only up-to-date source on hours, status, & contact info
- we don't have experience automating this yet, would love help
-
Find New Charities that we don't currently have
- requires serious & careful deduplication - it's counter-productive if we have to manually deal with multiples of the same charity
- find trustworthy values for as many fields as possible & fill them in
- push them to our API (
mutation addCharityFromAI)- new additions are Pending until a member of the Tackle Hunger team approves them
- potentially, if the AI gives a data-reliability score, push very good ones to our API to immediately go live (i.e. not Pending)
- would require an additional endpoint/mutation or changes to the current one - TBD
-
Re-check our Current Charities to Find Newer Data & Flag ones that might have changed
- store a copy of our original data & the new recommended data at that moment in time, as well as links to our Site/Org & to the new source
- in a new storage bucket or DB for this project
- potentially, if the AI gives a data-reliability score, push very good ones to our API to immediately go live (
mutation updateSiteFromAI)
- store a copy of our original data & the new recommended data at that moment in time, as well as links to our Site/Org & to the new source