AI-powered tactical intelligence for DAO grant proposals.
Analyzes grant proposal text (psychology + competitive edge), caches results in Supabase, and surfaces a tactical brief with opportunity score, strategy, and export.
┌─────────────────────────────────────────────────────────────────────┐
│ GRANT WARFARE TERMINAL │
│ Next.js 16 · App Router · React 19 │
└─────────────────────────────────────────────────────────────────────┘
│
┌──────────────┴──────────────┐
▼ ▼
┌───────────────────────┐ ┌───────────────────────┐
│ FRONTEND │ │ API ROUTES │
├───────────────────────┤ ├───────────────────────┤
│ • ProposalScanner │ │ POST /api/analyze │
│ • TacticalBriefPanel │ │ POST /api/mcp │
│ • FilterBar │ │ (same pipeline) │
│ • GlyphNavigation │ │ │
│ • CommandHeader │ │ Cache check → agents │
└───────────┬────────────┘ └───────────┬────────────┘
│ │
│ POST body: │
│ proposalId, title, │
│ description, dao │
└─────────────┬───────────────┘
▼
┌───────────────────────────────────────────────────┐
│ lib/supabase.ts │
│ getCachedAnalysis(proposalId) │
│ Table: proposal_analyses │
│ Hit → return cached. Miss → continue. │
└───────────────┬───────────────────────────────────┘
│ cache miss
▼
┌───────────────────────────────────────────────────┐
│ lib/agents/ │
├───────────────────────────────────────────────────┤
│ runPsychologyAnalysis({ title, description, dao }) │
│ → Anthropic Messages API │
│ → urgency, desperation, flexibility, triggers, │
│ fullAnalysis │
└───────────────┬───────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────┐
│ runEdgeAnalysis({ title, description, │
│ psychologyAnalysis }) │
│ → Anthropic Messages API │
│ → skillMatch, winProbability, advantages, gaps, │
│ mainStrategy, emphasize, avoid, openingLine │
└───────────────┬───────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────┐
│ Opportunity score: │
│ (skillMatch × 0.4 + winProbability × 0.6) / 10 │
│ saveAnalysis(record) → proposal_analyses │
└───────────────────────────────────────────────────┘
USER: Click "ANALYZE TARGET" on a proposal
│
▼
Frontend (ProposalScanner) POST /api/analyze
body: { proposalId, title, description, dao }
│
▼
1. getCachedAnalysis(proposalId) → Supabase proposal_analyses
• Found → return { success, cached: true, data }
• Not found → continue
│
▼
2. runPsychologyAnalysis({ title, description, dao })
• Anthropic Messages API (model from ANTHROPIC_MODEL or default)
• Prompt: grant psychology analyst, JSON schema for urgency, desperation,
flexibility, triggers, fullAnalysis
• Returns: PsychologyResult
│
▼
3. runEdgeAnalysis({ title, description, psychologyAnalysis })
• Anthropic Messages API
• Prompt: grant tactical analyst, psychology summary as context
• Returns: EdgeResult (skillMatch, winProbability, advantages, gaps,
mainStrategy, emphasize, avoid, openingLine)
│
▼
4. opportunityScore = round((skillMatch×0.4 + winProbability×0.6) / 10)
│
▼
5. saveAnalysis(record) → Supabase proposal_analyses insert
│
▼
6. Response: { success, cached: false, data: analysisData }
Frontend shows TacticalBriefPanel with brief; export via export-brief.
Two sequential steps; both use Anthropic SDK, no external agent framework.
- Input:
title,description,dao - Output:
PsychologyResulturgency,urgencyReasondesperation,desperationReasonflexibility,flexibilityReasontriggers: string[]fullAnalysis: string
- Model:
ANTHROPIC_MODELenv orclaude-sonnet-4-20250514 - Implementation: Single Messages API call; response parsed as JSON and normalized.
- Input:
title,description,psychologyAnalysis(PsychologyResult) - Output:
EdgeResultskillMatch,winProbability(0–100, clamped)advantages: string[],gaps: string[]mainStrategy,emphasize: string[],avoid: string[],openingLine
- Model: Same as psychology.
- Implementation: Psychology summary passed in prompt; single Messages API call; JSON parsed and normalized.
Computed in API route:
(edge.skillMatch * 0.4 + edge.winProbability * 0.6) / 10, rounded. Displayed as 0–10 in the brief.
- Used by: ProposalScanner (fetchWithRetry from
lib/api-client.ts). - Body:
{ proposalId, title, description?, dao } - Response:
{ success, cached?, data?: TacticalBrief, error?, details? } - Flow: Cache → psychology agent → edge agent → score → save → return.
- Same pipeline as
/api/analyze; response includessource: 'mcp'. - Body: Same.
- Used by: Clients that call this route (e.g.
lib/api-client-mcp.ts).
- Table:
proposal_analyses - Usage:
getCachedAnalysis(proposalId): select byproposal_id, order bycreated_atdesc,maybeSingle().saveAnalysis(record): insert full analysis (proposal_id, proposal_title, dao, opportunity_score, psychology, edge, recommendation).
- Types:
AnalysisRecordinlib/supabase.ts(matches psychology + edge + recommendation shape).
No other tables or caches are used in the codebase.
- Proposals: Loaded via
lib/snapshot.ts—fetchActiveProposals(filter)calls Snapshot Hub GraphQL; filter by DAO type (all / defi / gaming / other). - Analysis: ProposalScanner calls
/api/analyzewith proposal id, title, body, dao; shows loading then TacticalBriefPanel with TacticalBrief. - Brief: Opportunity score, psychology (urgency, desperation, flexibility, triggers, fullAnalysis), edge (skillMatch, winProbability, advantages, gaps), recommendation (mainStrategy, emphasize, avoid, openingLine).
- Export:
lib/export-brief.ts— markdown or JSON download (exportBriefAsMarkdown, exportBriefAsJSON, downloadFile, exportTacticalBrief). - UI: Tailwind, Framer Motion, Lucide, Radix (tooltip, slot); glyphs (Triangle, Diamond, Hexagon, Pentagon); CommandHeader, GlyphNavigation, FilterBar, ProposalCard, TacticalBriefPanel, TacticalLogo, TacticalNavTrigger, ErrorBoundary, SidebarContext, HomeLanding.
| Layer | Stack |
|---|---|
| Runtime | Next.js 16 (App Router), Node, React 19 |
| LLM | @anthropic-ai/sdk (Claude); model via ANTHROPIC_MODEL or default |
| Data | Supabase (PostgreSQL); table proposal_analyses |
| Proposals | Snapshot Hub GraphQL (lib/snapshot.ts) |
| UI | Tailwind CSS, Framer Motion, Lucide React, Radix UI (tooltip, slot), class-variance-authority, clsx, tailwind-merge, tailwindcss-animate |
| Validation | Zod (used where applicable) |
| Package mgr | pnpm 10; packageManager in package.json |
| Dev | TypeScript 5.2, ESLint, eslint-config-next, baseline-browser-mapping (devDep) |
GrantTerminal/
├── app/
│ ├── api/
│ │ ├── analyze/
│ │ │ └── route.ts # POST; cache → lib/agents → save
│ │ └── mcp/
│ │ └── route.ts # POST; same pipeline, source: mcp
│ ├── layout.tsx
│ ├── page.tsx
│ └── globals.css
├── components/
│ ├── scanner/
│ │ ├── ProposalScanner.tsx # Load proposals, analyze, show brief
│ │ ├── ProposalCard.tsx
│ │ ├── FilterBar.tsx
│ │ └── ScannerStates.tsx # EmptyState, LoadingState, ErrorState
│ ├── tactical-brief/
│ │ └── TacticalBriefPanel.tsx
│ ├── glyphs/
│ │ ├── TriangleGlyph.tsx
│ │ ├── DiamondGlyph.tsx
│ │ ├── HexagonGlyph.tsx
│ │ └── PentagonGlyph.tsx
│ ├── ui/
│ │ └── tooltip.tsx
│ ├── CommandHeader.tsx
│ ├── GlyphNavigation.tsx
│ ├── TacticalLogo.tsx
│ ├── TacticalNavTrigger.tsx
│ ├── ErrorBoundary.tsx
│ ├── SidebarContext.tsx
│ └── HomeLanding.tsx
├── lib/
│ ├── agents/
│ │ ├── psychology.ts # runPsychologyAnalysis
│ │ ├── edge.ts # runEdgeAnalysis
│ │ └── index.ts
│ ├── supabase.ts # client, getCachedAnalysis, saveAnalysis, AnalysisRecord
│ ├── api-client.ts # fetchWithRetry, APIError
│ ├── api-client-mcp.ts # calls /api/mcp
│ ├── snapshot.ts # fetchActiveProposals, formatTimeRemaining, etc.
│ ├── export-brief.ts # markdown/JSON export
│ ├── utils.ts
│ └── snapshot.ts # Snapshot Hub fetch, formatTimeRemaining, etc.
├── types/
│ ├── tactical-brief.ts # TacticalBrief, PanelState
│ └── snapshot.ts # Proposal, SnapshotProposal, DAOFilter, etc.
├── next.config.js
├── package.json
├── pnpm-lock.yaml
├── LICENSE
└── README.md
git clone https://github.com/intenxe-ops/grant-terminal.git
cd grant-terminal
pnpm install
cp .env.example .env
# Set:
# ANTHROPIC_API_KEY=
# NEXT_PUBLIC_SUPABASE_URL=
# NEXT_PUBLIC_SUPABASE_ANON_KEY=
pnpm dev
# Open http://localhost:3000Scripts: dev, build, start, lint, typecheck (see package.json).
| Variable | Required | Description |
|---|---|---|
| ANTHROPIC_API_KEY | Yes | Anthropic API key for lib/agents |
| NEXT_PUBLIC_SUPABASE_URL | Yes | Supabase project URL |
| NEXT_PUBLIC_SUPABASE_ANON_KEY | Yes | Supabase anon key |
| ANTHROPIC_MODEL | No | Model id (default: claude-sonnet-4-20250514) |
- Load proposals — App fetches active proposals from Snapshot (FilterBar: all / defi / gaming / other).
- Filter — Switch DAO filter; proposals reload.
- Analyze — Click ANALYZE TARGET on a proposal; frontend POSTs to
/api/analyze; loading state; on success TacticalBriefPanel shows the brief. - Brief — Opportunity score (0–10), psychology block (urgency, desperation, flexibility, triggers, full analysis), edge block (skill match %, win probability %, advantages, gaps), recommendation (main strategy, emphasize, avoid, opening line).
- Export — Export brief as markdown or JSON (export-brief.ts).
- Analysis: Two sequential LLM calls (psychology then edge); typical total in the tens of seconds depending on model and network.
- Cache: Repeat analyses for same
proposal_idserved from Supabase; no second LLM run. - Timeout: api-client uses 90s timeout and retries (fetchWithRetry).
MIT. See LICENSE.
intenxe-ops