← Home

// project

Grant Writing Agent

September 2025
Technologies
PythonspaCyNLTKTransformersOpenAI APIPostgreSQLFastAPIReact

The system analyzes successful grant applications to extract the structural patterns that won them — section ordering, keyword density, sentiment arc, readability targets — then generates new proposal sections that match those patterns for a specific funder and a specific organization. It also tracks 10,000+ funding opportunities and matches organizations to grants using semantic similarity.

For the nonprofits that used it, proposal prep time dropped about 60%. The grants themselves had to be written by humans; the system removed the parts of grant-writing that are essentially formatting and matching.

Three problems, three subsystems

1. Find the right grants. Most nonprofits miss funding opportunities they’re eligible for because nobody has time to read every RFP on grants.gov and every foundation portal. The system embeds organization profiles and matches against embedded grant opportunities — cosine similarity on a TF-IDF vectorization, ranked by historical success rate for similar organizations.

class GrantMatcher:
    def __init__(self, organization_profile):
        self.profile = organization_profile
        self.vectorizer = TfidfVectorizer(max_features=1000)

    def match_opportunities(self, grant_database):
        """Match organization to relevant grants"""
        profile_vector = self.vectorize_profile()
        grant_vectors = self.vectorize_grants(grant_database)

        similarities = cosine_similarity(profile_vector, grant_vectors)
        return self.rank_opportunities(similarities)

2. Understand what wins. Given a corpus of past winning proposals, what do they have in common that losing proposals don’t? The analyzer extracts structural features and surfaces the patterns:

class ProposalAnalyzer:
    def __init__(self):
        self.nlp = spacy.load('en_core_web_lg')

    def extract_successful_patterns(self, winning_proposals):
        """Identify patterns in successful grants"""
        patterns = {
            'structure': self.analyze_structure(winning_proposals),
            'keywords': self.extract_keywords(winning_proposals),
            'sentiment': self.analyze_sentiment(winning_proposals),
            'readability': self.calculate_readability(winning_proposals)
        }
        return patterns

    def score_proposal(self, proposal_text, requirements):
        """Evaluate proposal against requirements"""
        return {
            'completeness': self.check_requirements(proposal_text, requirements),
            'clarity': self.assess_clarity(proposal_text),
            'impact': self.measure_impact_language(proposal_text),
            'compliance': self.verify_compliance(proposal_text)
        }

The output is descriptive, not prescriptive — here’s what winning proposals do, not do these things and you’ll win. The framing matters: grant reviewers are humans, and a proposal that obviously gamed the analyzer would feel hollow.

3. Generate compliant boilerplate. The repetitive parts of grant writing — executive summary, budget narrative, impact statement, the same standard text adapted to a new funder — get drafted from templates that take the organization profile and grant requirements as input.

class ContentGenerator:
    def generate_section(self, section_type, context):
        if section_type == 'executive_summary':
            return self.generate_summary(context)
        elif section_type == 'budget_narrative':
            return self.generate_budget_narrative(context)
        elif section_type == 'impact_statement':
            return self.generate_impact(context)

    def adapt_boilerplate(self, template, specifics):
        """Customize standard text for specific grant"""
        # Named-entity replacement, context-aware modification, tone adjustment
        ...

The point is to do the formatting work, not the thinking. The substantive sections — methodology, theory of change, why this organization can deliver — those still have to be written by someone who knows the organization.

Predicting success

A classifier on extracted features estimates the probability that a draft proposal will be funded. It runs as a sanity check before submission — not a guarantee, just a heuristic that catches proposals that obviously fall short on a feature dimension (too short, too low-readability, missing required sections).

class SuccessPredictor:
    def __init__(self):
        self.model = XGBClassifier(
            n_estimators=100,
            max_depth=5,
            learning_rate=0.01
        )

    def extract_features(self, proposal):
        return {
            'word_count': len(proposal.split()),
            'readability_score': self.calculate_flesch_score(proposal),
            'keyword_density': self.calculate_keyword_density(proposal),
            'section_completeness': self.check_sections(proposal),
            'budget_clarity': self.assess_budget(proposal),
        }

What the API looks like

@app.post("/analyze-rfp")
async def analyze_rfp(document: UploadFile):
    """Extract requirements from an RFP document"""
    text = extract_text(document)
    requirements = parse_requirements(text)
    return {
        "requirements": requirements,
        "deadlines": extract_deadlines(text)
    }

@app.post("/generate-section")
async def generate_section(
    section: str,
    context: dict,
    word_limit: int
):
    """Generate a specific proposal section"""
    content = generator.create_section(section, context, word_limit)
    return {"content": content, "word_count": len(content.split())}

A FastAPI service, structured around the workflow: ingest the RFP, parse requirements, generate sections, check the result, track the deadline.

Storage

  • PostgreSQL for the structured records — grants, organizations, proposals, deadlines, submissions, outcomes.
  • MongoDB for the raw documents — PDFs of RFPs, scraped funding-portal pages, prior proposal text.
  • A vector store for embeddings used in semantic matching.

Three stores, each suited to its access pattern. The relational store handles the queries the team actually runs (when is this due, what’s the status); the document store keeps everything for later analysis; the vector store powers the matcher.

Results

  • 10,000+ funding opportunities tracked
  • 500+ reusable template components
  • <5 seconds per page on RFP processing
  • 90% accuracy on requirement extraction
  • 60% reduction in proposal-prep time
  • Better consistency across submissions — the system enforces compliance and formatting that humans miss when they’re tired

Specific client details and proprietary algorithms have been omitted.