Skip to content

intenxe-ops/grant-terminal

Repository files navigation

Grant Warfare Terminal

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.


Live


System architecture

High-level

┌─────────────────────────────────────────────────────────────────────┐
│                         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           │
        └───────────────────────────────────────────────────┘

Data flow: analysis execution

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.

Intelligence layer (lib/agents)

Two sequential steps; both use Anthropic SDK, no external agent framework.

Psychology (lib/agents/psychology.ts)

  • Input: title, description, dao
  • Output: PsychologyResult
    • urgency, urgencyReason
    • desperation, desperationReason
    • flexibility, flexibilityReason
    • triggers: string[]
    • fullAnalysis: string
  • Model: ANTHROPIC_MODEL env or claude-sonnet-4-20250514
  • Implementation: Single Messages API call; response parsed as JSON and normalized.

Edge (lib/agents/edge.ts)

  • Input: title, description, psychologyAnalysis (PsychologyResult)
  • Output: EdgeResult
    • skillMatch, 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.

Opportunity score

Computed in API route:

(edge.skillMatch * 0.4 + edge.winProbability * 0.6) / 10, rounded. Displayed as 0–10 in the brief.


API endpoints

POST /api/analyze

  • 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.

POST /api/mcp

  • Same pipeline as /api/analyze; response includes source: 'mcp'.
  • Body: Same.
  • Used by: Clients that call this route (e.g. lib/api-client-mcp.ts).

Data layer (Supabase)

  • Table: proposal_analyses
  • Usage:
    • getCachedAnalysis(proposalId): select by proposal_id, order by created_at desc, maybeSingle().
    • saveAnalysis(record): insert full analysis (proposal_id, proposal_title, dao, opportunity_score, psychology, edge, recommendation).
  • Types: AnalysisRecord in lib/supabase.ts (matches psychology + edge + recommendation shape).

No other tables or caches are used in the codebase.


Frontend

  • Proposals: Loaded via lib/snapshot.tsfetchActiveProposals(filter) calls Snapshot Hub GraphQL; filter by DAO type (all / defi / gaming / other).
  • Analysis: ProposalScanner calls /api/analyze with 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.

Tech stack

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)

Project structure

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

Installation and local run

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:3000

Scripts: dev, build, start, lint, typecheck (see package.json).


Environment variables

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)

Usage

  1. Load proposals — App fetches active proposals from Snapshot (FilterBar: all / defi / gaming / other).
  2. Filter — Switch DAO filter; proposals reload.
  3. Analyze — Click ANALYZE TARGET on a proposal; frontend POSTs to /api/analyze; loading state; on success TacticalBriefPanel shows the brief.
  4. 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).
  5. Export — Export brief as markdown or JSON (export-brief.ts).


Performance notes

  • 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_id served from Supabase; no second LLM run.
  • Timeout: api-client uses 90s timeout and retries (fetchWithRetry).

License

MIT. See LICENSE.


Author

intenxe-ops

About

AI-Powered Tactical Intelligence for DAO Governance Proposals | Real-Time Streaming |

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors