I Spent 120M Tokens Building a Figma Clone With an AI Agent

What I learned about feedback loops, tests, skills, taste, and product decisions while building a Figma clone with Cursor CLI and Composer 2.5.

Cursor's usage dashboard recorded 119.3M Composer 2.5 Fast tokens on May 20.

I wanted to see how far Cursor CLI could go if token usage was not the main constraint. Instead of asking it to build another dashboard, I gave it a more difficult problem: build a Figma clone from an empty Next.js repository. I used Cursor's Composer 2.5 model in Fast mode.

Experiment at a glance

Tokens
~120M
Duration
~10 hours
Chats/Sessions
13
Tracked files (Next.js full-stack)
215
Automated tests (105 unit + 88 Playwright)
193
Git commits
19

I wasn't trying to benchmark Cursor or score the result as a pass or fail. It was an open-ended experiment. The useful part was seeing what allowed the agent to continue on its own, where it got false confidence, and which decisions still required a human intervention.

What the agent built

Calling it a Figma clone needs some explanation. This was a narrow Figma clone focused on the core editor workflow, not Figma's full feature set.

A user entered a workspace and project name without creating an account. Entering an existing pair reopened the saved project. Opening a project displayed a Figma-like editor with a canvas in the center and multiple artboards on it. An artboard is an individual design frame placed on the larger canvas.

Name-based access in two steps: choose a workspace, then enter a project name. Existing names open the same project; new names create it.
The final editor with a mobile design file open. The canvas sits between the nested Layers panel and the selected text element's properties.

The editor included:

  • A toolbar with shapes and a text tool.
  • Multiple artboards on the same canvas.
  • Drawing and placing elements on an artboard.
  • Moving elements inside an artboard, with moving an image layer between artboards included in the intended scope.
  • A left panel containing the nested tree of elements on each artboard.
  • A right panel for modifying the selected element's properties.
  • Exporting the active artboard as PNG or JPEG at 1× or 2× scale.
  • Multiple projects inside a workspace.
  • SQLite storage for workspaces, projects, and design data.

I chose Next.js for the application. The editor rendered artboards and design objects with SVG inside the React application, while CSS transforms handled canvas pan and zoom. Export happened on the server through a Next.js route using Sharp, which combined the active artboard, image layer, and SVG objects into a flat PNG or JPEG.

The custom export dialog supported PNG or JPEG, 1× or 2× output, and JPEG quality control.

Multiplayer editing and comments were outside the V1 scope. V1 means the smallest version of the product that proves the main workflow. Leaving those features out mattered because a "Figma clone" with real-time multiplayer is a very different engineering problem from an editor that saves concurrent changes with last write wins.

Defining V1 took real work

I did not begin with one prompt saying, "Build Figma."

Before the implementation, I used Matt Pocock's grill-with-docs skill. A skill is a reusable instruction file that gives an agent a specific workflow or set of rules. In this case, the workflow kept asking questions until the product and its domain were more clearly defined.

It asked around 40 questions about what I wanted in V1. That process helped narrow the features before the agent started producing a large amount of code. Decisions such as which tools belonged in the toolbar, how export should work, and which parts of the interface could be simplified had to be discussed first.

This part is easy to miss when looking at the final token number. Extra tokens bought more implementation time. I still had to decide what the product was and refine those decisions as the work progressed. The quality of the work still depended on what had been defined before and during the sessions.

The repository contains the output of that planning. The V1 PRD is 235 lines and defines 64 user stories. Four architecture decision records document choices such as name-based access, Next.js with SQLite, deferred view-only sharing, and fixed device frames. The work was then split into 39 implementation issues, each with its own acceptance criteria.

Later, I expanded the Figma clone from screenshot annotation to designing screens on phone, tablet, and desktop frames. The planning questions turned that change into eight implementation issues. Cursor completed seven feature issues, then used Playwright to test the complete workflow.

Those roughly 40 questions produced the PRDs, architecture decisions, implementation issues, and acceptance criteria that guided Cursor's work. Writing the decisions down kept Cursor from asking me the same product questions again.

Playwright made the agent more independent

The biggest improvement came after I added Playwright dependencies to the repository. Playwright is a browser automation tool. It can open the application, click buttons, fill forms, inspect elements, and reproduce user flows.

Before Playwright, the loop looked something like this:

Prompt

Write code

Assume the result works

After Playwright, the agent had a feedback loop:

V1 specification

Composer edits the code

Run the application

Playwright uses the feature

Inspect the UI, errors, and test result

Composer repairs the problem

Run the flow again

That last part changed the experiment. The agent could see some consequences of its own code instead of stopping after the code compiled.

One concrete example came from blank device frames. The Playwright flow created a Phone design file without uploading an image, drew a rectangle, exported a 390 by 844 PNG, and then added a Tablet frame beside it.

Create a blank Phone design file

Verify that no image layer exists

Draw a rectangle on the frame

Export and verify 390 × 844 pixels

Add a Tablet frame and verify both frames persist
The completed browser flow: a rectangle drawn on a blank 390 × 844 Phone frame, with a 768 × 1024 Tablet frame beside it.

The original editor could draw only over an uploaded image, so drawing did not work correctly on blank frames. Cursor fixed that, but the blank frame still inherited 48 pixels of padding from the image layout. A follow-up fix removed that padding, making the entire frame drawable.

The Git history records three explicit repair commits immediately after the screen-design Playwright work: restoring a missing database migration, recovering from failed database initialization while showing useful API errors, and fixing the blank-frame drawing area. Most of the earlier work arrived in the first commit, so the repository cannot tell me the total number of autonomous repairs.

Passing tests did not mean the feature worked

Playwright improved the output, but it also exposed a different problem. The agent became good at satisfying the path it could observe.

One test was named select tool drags image from one artboard to another. After performing the drag, it checked 3 conditions:

  1. The document still had two artboards
  2. Exactly one artboard contained an image
  3. One artboard was blank.

All three conditions were already true before the drag. The test never checked that the original image reached the target artboard or that the source artboard became blank. It could pass even if the drag did nothing.

The test ended with this entire assertion block:

const boards = data.document.artboards;
expect(boards).toHaveLength(2);
expect(boards.filter((a) => a.imageId).length).toBe(1);
expect(boards.find((a) => !a.imageId)?.imageId ?? null).toBeNull();

The transfer code also cleared the object stacks on both the source and target artboards. The test did not define whether those objects should move, stay, or be cleared. This is a more precise example of the problem: the test exercised the gesture, but its assertions did not describe the result a user cared about.

Grouping elements produced another example. The agent implemented the group operation, but it did not handle the ordering of elements inside the group correctly. In a canvas editor, the z-index is the order that decides which element appears in front of another. After grouping, I still needed to move an item to the foreground or background. The happy-path test did not cover that behavior.

The agent did not intentionally ignore these cases. It followed the success conditions it was given:

Tested behavior: Perform a cross-artboard image drag
Asserted behavior: One artboard still has an image
Actual behavior: The target receives the original image, the source becomes blank, and objects follow a defined retention rule

Tested behavior: Group multiple elements
Actual behavior: Group elements and continue changing their visual order

This made green tests feel more dangerous than no tests in some cases. A failing test tells you something is wrong. A passing test can make the agent stop even when the test describes only a small part of normal usage.

The agent's reliability depended on what I made observable. When my test setup covered only the happy path, the agent learned to satisfy only that path.

Drag and drop showed the limit of browser feedback

Drag and drop was one of the hardest parts of the experiment. Playwright could exercise a basic image drag between artboards, but the original assertions did not give the agent enough feedback to prove that the transfer happened correctly or without losing other objects.

This is different from testing a normal form. A form can often be described as a sequence of clicks and expected text. A canvas interaction depends on pointer movement, coordinates, artboard boundaries, element size, visual placement, and sometimes timing.

The code could report that a drag completed while the result still felt broken when I used it. The image layer might remain attached to the old artboard, land at the wrong canvas coordinate, or fail to snap where I expected.

This was one place where more attempts did not automatically make the agent better. The missing part was a reliable way for the agent to judge the final interaction.

Skills could provide rules, but not taste

I also gave the agent Vercel's React best-practices skill. This type of skill puts framework guidance into the agent's context so I do not need to repeat the same React rules in every prompt.

The final repository contains several patterns that match that guidance. The editor is loaded with next/dynamic because it is a large client-only component. Individual artboards are memoized to reduce work while the pointer moves, and server-side repository reads use React's cache helper to avoid repeated reads during one render. These examples show that the practices exist in the output, but they do not prove that the skill caused them. I do not have a controlled before-and-after comparison.

What was easier to observe was the gap between technical correctness and visual taste.

The first toolbar was vertical and placed on the left. The left side already contained the nested tree of elements inside the artboard, so the toolbar and layer panel competed for the same space. The controls worked, but the editor felt confusing. I asked the agent to move the toolbar to the top.

The right sidebar, where you edit the selected object, also felt incomplete. A text object stored its font size and color, and I could resize it using handles on the canvas. But the sidebar only offered controls for its content, bold, italic, background, and alignment. There was no direct way to change the font size or text color, and the available controls felt poorly organized.

An agent can check that a button exists, that clicking it changes state, and that the page does not crash. It is much harder for it to decide whether the button belongs there in the first place.

The Git history does not preserve the earlier vertical-toolbar interface, so I cannot show an authentic before-and-after comparison. The final arrangement puts the drawing tools above the canvas, keeps Layers on the left, and places the selected object's controls on the right.

The final control layout after moving the toolbar above the canvas. The selected heading remains visible between the Layers tree and its text properties.

The human decisions moved to a different level

I still made the product decisions. The agent changed which ones consumed my time.

I still had to decide that the application would use Next.js, what belonged in V1, where the toolbar should go, which tools to include, how exporting should work, and which parts of the interface needed to be simplified.

The agent could provide options. One example was browser IndexedDB versus SQLite for storage. IndexedDB is a database built into the browser, while SQLite stores data in a local database file used by the application. The final project used SQLite.

Providing options was useful, but an option list was not the same as a decision. The agent did not know which tradeoff mattered most for my version of the product unless I explained it.

Those 120 million tokens bought more implementation time once I clarified the constraints. I spent more of my time defining the product and judging the result.

Why I stopped

I stopped when the planned V1 scope was mostly done and the product reached a quality plateau. The experiment had already served its purpose.

The repository contains a lot of code, but the final application is not used in production. Continuing to spend tokens would have produced more fixes and features. It would not automatically solve incomplete tests, visual judgment, or unclear product decisions.

The main things I learned were:

  • A coding agent works longer on its own when it can test and observe its changes.
  • Passing tests can create false confidence when the tests cover only the happy path.
  • Browser feedback is easier for discrete actions than continuous canvas interactions.
  • Skills can make guidance available to the agent, although their effect still needs evidence.
  • Correct UI behavior and good UI placement are separate problems.
  • More tokens let the agent build more, but I still had to define V1, choose the tradeoffs, and decide when the product felt right.

Thanks for reading.