Three years ago, “fake it until you make it” meant slapping a “Coming soon” label on a button you hadn’t built. A user clicked, you saw the intent in your analytics, and you went and built the thing over the next sprint. It worked. The loop was measured in days.

The loop I run today is measured in hours, and most of it happens while I’m doing something else.

I now ship POCs and MVPs for startups under one running assumption: I will build the surface first, and let real users tell me where the depth has to go. The trick is no longer a UX placeholder. It is an agent pipeline that takes a user complaint and produces a pull request I can review with my morning coffee.

Why the old pattern is finally productizable

Three things lined up that weren’t there a year ago.

PostHog ships a session replay and an in-app survey widget that cost almost nothing on a low-traffic MVP. The replay shows me what the user actually did, which removes 80% of the back and forth on a typical bug ticket.

Linear’s API and triage workflow are stable enough that I can pipe events in from anywhere, label them, route them, and treat the inbox as a queue instead of a board. Crucially, an issue can carry rich context (URL, user, replay link, screenshot) without me typing any of it.

And Claude Code, used as a coding agent over the GitHub API, can take a well-scoped issue and produce a real PR. Not a hallucinated diff: a branch, a passing build, a description, and the right files touched. The quality is good enough that the bottleneck has shifted to me reviewing it, not the agent writing it.

Once those three pieces existed, the old “fake it” advice stopped being a marketing trick and became a way to compress the product loop.

The loop

flowchart LR
    A[User clicks<br/>Feedback button] --> B[PostHog<br/>survey + replay]
    B -->|webhook| C[Linear issue<br/>with user, URL, replay]
    C --> D[Triage agent<br/>label + scope]
    D -->|tiny / clear| E[Coding agent<br/>opens PR]
    D -->|larger / fuzzy| F[Stays in my queue]
    E --> G[I review,<br/>merge, deploy]

Each arrow is either a webhook or an agent invocation. There is no human step until the very last box, and that’s intentional. The point is not to remove me from the loop, it is to remove me from the parts of the loop where I add no judgment.

A walkthrough: the export button I never built properly

Here is the kind of scenario this was built for, and which I now hit several times a week.

A B2B SaaS I maintain has an “Export to CSV” button on a data table. The first version exports six columns, which covers the obvious case. I shipped it in an afternoon and moved on, knowing full well that real users would want different columns, filters, formats. The old me would have built a configuration UI up front, spent a week on it, and shipped four features nobody asked for.

The new version of me ships the dumb button and a Send feedback link next to it.

A user clicks export, opens the CSV, sees the client_reference column is missing, and clicks the feedback link. PostHog opens a small survey: “What’s missing?”. They type one sentence. The survey closes.

What happens next, without me touching anything: a Linear issue is created with their email, the URL they were on, a link to the session replay (so I can see exactly which view they exported from), and their message. The triage agent reads it, tags it missing-data and scope:tiny, and assigns it to the coding agent. The coding agent opens the export module, finds the column mapping, adds the missing field, runs the test suite, and opens a PR with a description that quotes the user’s original message.

I get a Slack ping. I look at the diff, which is six lines. I merge it. The user has the column on their next export, often before they email me to follow up.

Total elapsed: usually under two hours. My time spent: about ten minutes, most of it reviewing the diff.

The three pieces of glue

The whole thing is held together by three small pieces of code. None of them are clever. That’s the point.

1. The feedback widget. PostHog’s survey API is enough on its own, you just trigger it from your “Send feedback” button instead of letting it auto-open.

import posthog from 'posthog-js';

posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
  api_host: 'https://eu.i.posthog.com',
  session_recording: { maskAllInputs: false },
});

export function openFeedback() {
  posthog.surveys.getActiveMatchingSurveys((surveys) => {
    const inApp = surveys.find((s) => s.name === 'In-app feedback');
    if (inApp) posthog.renderSurvey(inApp.id, '#feedback-root');
  });
}

2. The PostHog to Linear bridge. A single webhook handler. PostHog posts to it on survey sent, and it creates a Linear issue with the context already attached.

import { LinearClient } from '@linear/sdk';

export async function POST(req: Request) {
  const event = await req.json();
  if (event.event !== 'survey sent') return new Response('ignored');

  const { distinct_id, properties } = event;
  const replay = `https://eu.posthog.com/replay/${properties.$session_id}`;
  const linear = new LinearClient({ apiKey: process.env.LINEAR_API_KEY! });

  await linear.createIssue({
    teamId: process.env.LINEAR_TEAM_ID!,
    title: properties.$survey_response.slice(0, 80),
    description: [
      `**From:** ${distinct_id}`,
      `**Page:** ${properties.$current_url}`,
      `**Replay:** ${replay}`,
      '',
      properties.$survey_response,
    ].join('\n'),
    labelIds: [process.env.LINEAR_LABEL_TRIAGE!],
  });

  return new Response('ok');
}

3. The triage agent prompt. This is the only piece of “AI” in the pipeline that makes a judgment call. It runs on every issue with the triage label, and its job is to decide what becomes work and what becomes noise.

You triage the Linear inbox.

For each issue tagged `triage`:
1. Read the user message and skim the replay summary.
2. Classify it: bug, missing-data, feature-request, ux-friction, noise.
3. Estimate scope: tiny (<2h), small (<1d), medium, large, unknown.
4. Apply the matching labels and move it to the right project.
5. If `tiny` + (`bug` or `missing-data`), assign the coding agent and
   leave a one-line implementation hint as a comment.
6. If `noise`, close with a short, polite reply.

Never assign anything above `small` to an agent. Never edit code yourself.

The coding agent itself is a stock Claude Code setup pointed at the repo, with a system prompt that tells it to stay within the labels the triage agent left, open a PR against main, and quote the originating issue in the description.

Why this matters for an MVP

The reason this isn’t just “neat automation” is that it changes what I’m allowed to ship.

I used to feel obligated to build configurable, generic, “future-proof” features because every shipped surface was expensive to revisit. A roadmap was a series of bets I had to hedge.

With this loop in place, shipping a dumb v1 of a feature is cheap because the loop will tell me which dimension to grow it on, and that dimension will be addressed before the user has time to be annoyed about it. The roadmap stops being a stack of guesses and becomes a queue of revealed preferences.

For a solo CTO running multiple POCs in parallel, this is the difference between feeling underwater and feeling fast. I can hold three or four products in my head because most of the day-to-day work is delegated to a pipeline that knows the difference between “user is confused” and “user found a real gap”.

What it does not replace

It does not replace product judgment. The agent will happily ship the next missing CSV column. It will not tell you that you should have shipped an API endpoint instead of the export feature in the first place. Strategy and scope stay human work, and they should.

It also does not replace code review. The agent’s PRs are good. They are not infallible. Every single one gets a human pass before it touches production, and a non-trivial fraction get sent back. The loop is fast because the boring parts are automated, not because the careful parts are skipped.

The pattern is still “fake it until you make it”. The only thing that changed is how short the gap between the two has become.