DEV Community

yan yan
yan yan

Posted on

I Built an AI Proposal Writer in 2 Weeks. Here's the Exact Prompt Architecture That Made It Work.

I'm a freelance Next.js developer. Last month I built ProposalAI — a tool
that reads Upwork job posts and generates tailored proposals. Here's the
technical breakdown of what I learned.

The Problem With Existing AI Proposal Tools

Most tools on the market use a single large prompt:
"Write a proposal for this job post: [POST]. Skills: [SKILLS]. Make it professional."

Plain Text

The result? Generic garbage. It starts with "Hi, I'm interested," talks
about your skills, and ends with "I look forward to hearing from you."
90% of freelancers on Upwork write exactly this.

My First Attempt (And Why It Failed)

I started with the same single-prompt approach. The output was indistinguishable
from every other AI tool. I almost gave up until I had an insight:

The problem isn't the model. It's the prompt architecture.

The Solution: Two-Stage Prompting

Instead of one big prompt, I split it into two stages: Extraction and Generation.

Stage 1: Signal Extraction

const extractionPrompt = ` You are analyzing a freelance job posting. Extract ONLY these fields as JSON:

{ "client_company": "string | null", "specific_problem": "string - the exact pain point described", "desired_outcome": "string - what success looks like", "tone": "formal | casual | urgent", "budget_signal": "string | null - any rate mentioned" }

Job Post: ${jobPost} `;


Plain Text

This pass just pulls structured data. No generation. No creativity. Just facts.

**Stage 2: Proposal Generation**
Enter fullscreen mode Exit fullscreen mode


typescript
const proposalPrompt = ` Write an Upwork proposal using this EXACT structure:

OPEN with: "${extracted.specific_problem || 'Your project sounds interesting'}"
Reference their SPECIFIC problem, not your skills
Do NOT start with "Hi" or "I read your post"
PROVE with one quantified result
Example format: "I helped a client [do X] which resulted in [Y]"
Keep it specific, not generic
MATCH their tone: ${extracted.tone}
END with a specific question about their current setup
Example: "Are you currently using [tool]?"
Keep total word count under 200
NEVER start sentences with "I am," "I have," or "I am a"
Client: ${extracted.client_company || 'not mentioned'} `;

Plain Text

Why This Works

  • The extraction pass forces the model to read and understand the job post before generating output.
  • The generation pass has no choice but to reference the client's specific problem, because it's the first thing in the prompt.
  • The "NEVER" rules prevent the model from defaulting to generic phrases.

The Tech Stack

  • Next.js 14 (App Router) + Tailwind CSS + shadcn/ui
  • Supabase (Postgres, Auth, Edge Functions for secure OpenAI calls)
  • GPT-4o (best reasoning for extraction)
  • Creem (payments - Stripe alternative with lower fees)

What I'd Do Differently

  1. Stream from day one. Non-streaming responses feel broken to users.
  2. Cache extraction. I re-extract on every edit. Should key by job post hash.
  3. Add a "regenerate" button early. Users want alternatives instantly.

Conclusion
The quality difference between a single-prompt tool and a two-stage extraction

  • generation tool is night and day. Same model, different architecture.

If you're building something that needs to produce specific, tailored output
from unstructured input, the two-stage pattern is the single biggest lever
I found.

Check out the live tool at proposalai.top (free tier available).

Top comments (0)