Portfolio update in progress. Please be patient while I shine up some images.
The Problem Nobody in the Category Is SolvingTwo Assumptions the Research KilledWhat Directing an AI Build Actually RequiresThe Scope Decision I Made on Day OneA Week Spent Proving a Feature Should Not ExistThe Teleprompter, Built Three TimesA Prompter That Marks the Word You Are About to SayCompliance as a Product, Not a CheckboxThe Rule That Had to Be Taught How to Read EnglishDeleting the Render VendorA Bug That Quietly Disabled Another FeatureFive Platform Approvals, Almost None of It CodeThe Trap That Costs Something PermanentThe Honesty AuditsA Pill That Said Connected on a Row That Could Not PostThe Deal That Wasn'tThe Honest ScorecardWhat I Took Away
← Work

Own the Script

A regulated advertisement, recorded on a phone, with the disclosure burned into the file.

A mortgage loan officer making a short video is a licensed advertiser, and almost nothing built for them treats it that way. I designed and directed an iOS app that lets one write a compliant script, read it off a teleprompter that follows their voice, burn animated captions and a regulatory disclosure card into the video on the phone, and publish it to three platforms in one tap. Behind it sits a web console where a lender writes its own compliance rulebook and the phone enforces it. Ten weeks, one designer, no engineers. AI agents wrote every line of the code and I wrote every line of the direction.

Product design and direction. Product definition, interaction, visual, IA, copy, all platform and legal work, every device testSolo. AI agents wrote the code under continuous directioniOS app and web console2026
Three screens from Own the Script: the home feed with a weekly streak strip and video library, the script builder with a compliance review prompt, and the capture screen with the teleprompter running over the camera.

The Problem Nobody in the Category Is Solving

A mortgage loan officer lives or dies on referral flow, and short-form video is the cheapest referral engine available to them. The evidence that it works is real but narrow. In research I ran across working loan officer communities, one originator posting a short video every day for two years reported qualified leads at $5 to $10 each. Another traced three closed deals to a single September video in their first year. Against that, a thread of thirty-three comments from someone who posted three or four educational videos a week for a year and generated nothing.

What made the category interesting was not that video works. It is that every piece of a loan officer's video is a regulated advertisement, and the tooling pretends otherwise.

One of those videos is governed at the same time by the SAFE Act, which requires the originator's NMLS ID on advertising. By the Fair Housing Act and ECOA, which require the Equal Housing mark and prohibit steering language. By Regulation Z, where quoting an APR or a payment triggers a full disclosure. By the FTC's Mortgage Acts and Practices Rule, which bans guaranteed approvals, debt elimination claims and skip a payment. By RESPA Section 8 on referral arrangements, and by UDAAP generally.

Most of that cannot be handled by a caption, because the caption is not what gets reshared. It has to be inside the rendered video file. That became the product's central technical commitment and its clearest differentiator.

I checked five vendors that a top ten US lender actually contracts with, directly against their own documentation. Not one of them lets a loan officer record vertical video on a phone, burn captions, and publish it to a vertical feed. One states in its own docs that its Instagram support is single images only. Another's video product is a mail merge loan status tool, with real video handled by a third party integration. The only vendor that accepts 9:16 to Reels is upload only, and was acquired in 2023 by a company that now redirects its domain elsewhere.

The home feed with a weekly goal strip and video cards, next to the script builder offering Write with AI, Speak and Scan.
Every screenshot here is a frame from the Meta App Review screencast, recorded from the Release build on a physical iPhone. Nothing is a mockup. Faces are blurred, the interface is not retouched.

Two Assumptions the Research Killed

I started with two working assumptions and the competitive read reversed both of them. Both reversals shaped the product more than any feature decision did.

First, compliance is not a moat. My June deep dive concluded that compliance assistance was the differentiator. A second pass proved that wrong, because both direct competitors already run compliance layers, one of them a 62 rule engine with severity tiers and regulatory citations. What survived scrutiny was narrower and much harder to copy: burning the NMLS ID and the Equal Housing mark into the rendered frame, on every export, with no path around it. Nobody in the category demonstrably does that.

Second, price is not the wedge. An earlier competitor note of mine had recorded one self-serve competitor at $200 to $500 a month, enterprise only, and concluded the individual loan officer was underserved at low price. That pointed at a roughly $40 a month solo app. When I re-verified the live pricing pages in August, that competitor was running a self-serve credit card checkout at $79. The cheap solo lane was not open. It was occupied by two funded incumbents with a $39 product underneath them.

So I priced at $79 for individuals and $59 a seat for teams, matching the market anchor rather than undercutting it, and including everything the nearest competitor gates behind its $149 tier. The pitch became: their price for the basic tier, everything they charge double for.

The research also surfaced a failure mode more useful than any feature request. The most discussed loan officer problem was not "I can't make videos." It was "I have no closings, so I have no confidence to give advice, so my content stays generic." That is a scripts and ideas problem, not a capture problem, and it is why the daily ideas feed and the AI script writer became first class surfaces instead of conveniences.

What Directing an AI Build Actually Requires

The division of labour was absolute. I did product definition, interaction design, visual design, information architecture, copy, all platform and legal work, and every device test. AI agents wrote every line of code. I do not write code.

That arrangement did not remove work from me. It relocated it, and three things became disproportionately important.

Specification became the primary design deliverable. When a build partner implements literally what it is told, ambiguity in a spec surfaces as a defect rather than as a question. So decisions had to be explicit, dated and reasoned. Not "make the prompter smoother" but a stated model of what smooth means, what it is allowed to change, and what must never move. The clearest example became a standing law of the teleprompter screen: colour is the only property that may change. Never weight, never size, never letter spacing. The active line had been rendering at weight 700, and bold glyphs are wider, so every line that became active re-wrapped the paragraph, which invalidated the measured word geometry, which regrouped the lines, which moved the highlight, which caused another reflow. A visible tearing loop, out of one weight change.

Verification had to be designed, not assumed. A build partner that always says the work is done requires me to prove that it isn't. Measure, don't eyeball. Build the ten line thing that makes a silent failure speak: three rounds of reasoning about audio session teardown produced nothing, and a twenty line bare camera self test surfaced the real error on its first run. Run an adversarial pass: a July security review ran seven independent review lenses and then had a separate agent try to refute each finding. Eighty findings raised, twenty-eight survived refutation, fifty-two were disproved, and two of the report's own top five turned out to be stale.

Working rules had to be written down and enforced, because direction that isn't recorded gets re-litigated. Restate every process in full, never "re-run that." Paste runnable content into the conversation, because a file path is not a deliverable. Fewest words when reporting finished work. One compact multiple choice question when a design fork is real, and none when a sensible default exists, because question budget is finite.

The memory problem was structural. Ten weeks across roughly 120 conversations with a partner that has no continuous memory means every session starts from zero and re-derives context, sometimes incorrectly. My answer was a persistent indexed knowledge base of 145 dated notes, each holding one fact, read at the start of every session. It rotted, and the rot was the instructive part. A July audit cross-checked every falsifiable claim against the live repository, database, deployed functions and platform dashboards, and found ten claims that were actively dangerous. One told a future session to send a specific API version that returns a nonexistent version error. Another sat under a heading reading "already correct, do not fix" and was wrong.

The Scope Decision I Made on Day One

The initial wedge was lean: teleprompter capture, AI script, captions, compliance stamp. On 13 June, after the first competitor deep dive, I went all in for v1 instead. Daily headline driven scripts on a three times daily release, scheduled and multi video creation, gamification with leaderboards and streaks, push and email reminders, direct social posting, in-app notifications.

The note I wrote at the time: "This pushes realistic v1 to roughly five to seven months. No longer a lean wedge." I recorded that estimate rather than arguing with it, and it was roughly correct.

I want to state that plainly rather than bury it, because it is the single largest schedule decision in the project. I made it deliberately, on day one, with the cost written down. Everything that came later ran against that clock.

The sequencing decision I made three days after is the one I would keep. On 16 June I split the work into two tracks and refused to let one block the other. The build track was the critical path: environment, scaffold, backend skeleton, vendor lock, walking skeleton, pacing spike. My own track ran in parallel and non-blocking: LLC formation, Apple and Google accounts, Meta and TikTok approvals, with exactly one dependency into the build, which was store accounts for real device testing.

That split is why platform approvals, which consumed more calendar time than any feature in the project, never once blocked a build day.

Between 16 and 17 June the whole spine was proven end to end on both platforms in a single day. Record a 30 second read against a teleprompter, upload, transcribe, render captions, play the finished vertical video back on the phone. Six sub-steps. The highest risk item in the plan, cleared before it could become a schedule question.

A Week Spent Proving a Feature Should Not Exist

Step six of the build plan was a deliberately throwaway one week go or no go. Could a raw read be auto-tightened by cutting dead air, snappy but not choppy? I locked the scope to silence trim only. No filler word removal, no speed ramping, captions off so pacing could be judged on its own.

I wrote the pass bar before running the test, which is the part that mattered. At least six of eight clips rated naturalness four or higher, average cut of ten percent or more on non-clean clips, zero severe word clipping.

I recorded eight selfie reads: two clean, two with natural pauses, two messy, and two repeats of one script. The mechanics worked. One messy clip went from 30.9 seconds to 21.7, a thirty percent cut.

My verdict on 18 June was two words: total no go. Naive pause trimming, meaning hard cuts at silence gaps with no transitions and no word boundary snapping, produces unacceptable results even when it successfully shortens the runtime. Pacing and tone went to phase two. Version one does captions only.

Because the spike was throwaway by design, nothing in the app had to be unwound. A week spent proving a feature should not exist is a week well spent, and that decision has held for two months without being reopened.

The Teleprompter, Built Three Times

The teleprompter is where the most design thinking went, and it is the clearest illustration of the method. I rebuilt it three times and each rebuild was triggered by a different kind of insight.

Generation one was what every teleprompter app does: text scrolling at a rate the user sets, with a speed control. Around it I locked a recording control model that survived all three rebuilds. Stop means pause, not end, so the record button becomes a rounded square with a red arc frozen in place and tapping record again resumes the same file. Retry returns to an armed idle screen rather than auto-restarting, because the previous behaviour felt like an ambush mid-thought. "Done" is the right word, not Finish or Next or Use take, because it is the iOS convention for stop capturing and keep it. Cancel exists twice and is gated, so a stray tap during an active take cannot bin it. Elapsed time is summed from wall clock timestamps rather than per-second ticks, so a busy JavaScript thread cannot drift the clock.

The progress ring is a small decision with a real rationale. It is drawn with plain views and deliberately not SVG, because SVG is a native module and adding one would have forced another full device rebuild. Two clipped rotated half circles, a circle painted only on its top and right borders to give a 315 to 135 degree arc, and a 45 degree rotation to normalise it. I had the geometry verified numerically across eleven progress values and 359 angles rather than checked by eye.

Then on 7 August I removed the recording cap, and the removal cascaded through five separate decisions. The scroll rate stopped being a constant and became derived: rendered text height divided by estimated speaking seconds. The gradient mask fading text at the band edges was deleted. The timer stopped counting down and started counting up, because there is no budget left to spend. The auto-stop became a ten minute runaway guard that banks the take rather than dropping it. Past one minute the timer pill turns amber and says the video is running long for social.

The insight underneath is the part worth keeping. The prompter rate is not a constant to be tuned. The script should travel its own rendered height in exactly the time it takes to say it, so the scroll speed derives from the same words-per-minute constant that drives the duration meter on the script editor. The length the writer was shown and the pace the reader gets are one number, and it self-corrects for font size, band width and script length.

The arithmetic, written down so nobody re-derives it: a 349 point band at 20 point type is roughly 5.2 words per line at a 30 point line height. At 150 words per minute a line takes 2.08 seconds, so 30 divided by 2.08 gives 14.4 pixels per second. The old rate was 30.3, which is 2.1 times too fast, which is exactly what I had reported by eye and could not have proven without the number.

The capture screen with the script panel over the upper frame, a speed pill, a countdown and the record ring.
Generation one, recorded 30 July. Three things in this frame were deleted within a fortnight: the speed pill, the countdown, and the scroll rate itself. What survived is the panel position, the ring, and the pause behaviour.

A Prompter That Marks the Word You Are About to Say

On 12 August a competitor released the feature I had been circling. A phone maker announced a camera suite with a teleprompter in it, and the detail that changed my design was in a hands on report: it highlights words on the camera screen to match your speaking pace.

That is the insight. It does not merely scroll. It marks where you are, word by word. Marking position is the feature, and scrolling is only how the marker stays on screen. The first build of my new engine scrolled without marking, which was a material miss, and I corrected it the same day.

Rather than building it into the capture screen, I put it in a Labs sandbox reachable from Settings, deliberately microphone only at first. Feel first, risky integration second. That sequencing is why the feature could be iterated a dozen times in two days without ever putting the recording path at risk.

The correction that defines the feature was one sentence: the purpose of the highlight is to highlight the text you are supposed to be saying, not the text you already said. The speech aligner's cursor is the last matched word, which by definition has already been spoken. Marking it points at the past, which is useless to a reader, and it makes every millisecond of recogniser lag visible as a marker trailing the voice. The fix was one line of concept. The active word is cursor plus one, and both the highlight and the scroll target use it, so the marker and the page can never disagree.

Two things fell out of that for free. When the cursor is the last word of a line, cursor plus one is the first word of the next line, so the highlight moves the instant a line finishes with no timer and no threshold. And before the first match the active word is word zero, so the opening line is already marked when the screen appears.

The lag I reported next was not an easing problem. The marker only moved when a word was matched, so between the recogniser's callbacks it sat perfectly still and then teleported, and the dead window landed on the line boundary, which is exactly where the eye needs to move. It was not easing badly. It was not moving at all. The standing lesson: never tune the easing before checking whether the thing is moving between updates.

The answer was dead reckoning, the way a GPS navigates between fixes. Advance at the reader's own measured pace between matches and snap to each match when it lands. Prediction is capped at three words, and that cap doubles as the silence detector, because past about three words the reader has stopped rather than sped up, so prediction stalls on its own with no separate timer. Pace is learned live and clamped between 60 and 260 words per minute so a bad early estimate cannot make the marker sprint or stall. And position became a floating point word index, which is the enabling change for everything else, because the fractional part is what makes the colour crossfade and the scroll continuous instead of stepped.

I proposed the colour fade and the two line highlight, and what came out is a ramp rather than four discrete states. The current line is one uniform orange across its whole width. The next line starts already warm at 34 percent toward orange and climbs to 78 percent as the reader crosses the current line. The previous line fades from orange to grey across the new line instead of dropping out at the boundary. The point is that the handoff finishes before the reader arrives at it, so at the moment the lines swap the next line's colour is a small final step rather than a snap, and the eye always has its next target. That was verified numerically, not by eye.

I rejected two things twice each. No background band or highlight fill, because a filled band competes with the video behind the prompter and reads as interface chrome rather than as the script. The colour of the type is the marker. And no word by word darkening inside the live line, which was proposed as a fix for having no feedback until a line boundary. It is a progress bar for the past, and it pulls the eye backwards at the exact moment it should be moving on. There is a comment sitting at the code site now so it does not get added a third time. The only within-line feedback allowed is the next line warming, because that points forward.

Compliance as a Product, Not a Checkbox

Compliance Assist was the feature I conceptualised earliest, held onto longest and pushed furthest. It survived the finding that competitors already do compliance, and it ended up as the most architecturally ambitious thing in the product. It exists at four levels and each was a separate design decision.

Level one is the scan. A deterministic 19 rule engine that reads the script and the caption before anything exports. It started at nine keyword rules and every rule added after that was grounded in a researched CFPB or NMLS citation rather than a guess. Pre-approved and instant approval block under the MAP Rule. Debt elimination blocks under Reg Z. Skip payments, refinance promises, prepayment penalty claims and counselor implication all warn, each against its own section. Fair housing steering warns under ECOA and the FHA. RESPA referral arrangements warn under Section 8.

What is documented as not auto-detected is part of the posture, not an omission. Fixed for variable comparisons, partial term comparisons, foreign language splits, OCR of on-screen text, per-state overlays, and the substantiation truth of any claim. Listing the gaps explicitly in the rule specification is what stops the tool being read as a guarantee. The same reason drives a standing copy rule I applied everywhere, including the marketing site: say compliance-checked, never compliant. The engine has no attorney sign off, and "compliant" reads as a guarantee about the loan officer's own regulatory standing, which cannot be made.

Level two is the burn, and it is the claim the whole product rests on. The original stamp was a persistent navy pill at the top of the video. On 6 July I replaced it with a three second compliance end card carrying company, NMLS, disclosure text and the Equal Housing mark on the brand colour. I flagged the compliance implication rather than burying it, because always-visible to end-card-only is a posture change and an attorney should confirm it. There is a documented residual risk, which is that a mid-clip reshare loses the disclosure, and I chose intro and outro only knowing that.

The architectural guarantee is what makes it true. No export path may produce a video without a compliance card, because the renderer injects one into any specification lacking it. That is a choke point, not a convention. Nobody can hand roll an end card at a call site and nobody can bypass the renderer.

For a period the end card never rendered at all, and the cause is worth recording because it is a failure that looks like nothing. AVFoundation silently drops a trailing empty edit, so inserting an empty time range at the end of the composition left the duration equal to the footage duration and the three seconds simply vanished. No error, no warning, correct-looking code, and a regulated disclosure missing from every export. The card needs its own opaque layer to exist.

Level three is the rulebook a lender writes and a phone enforces. An organisation authors its own rules in the web console, and the phone fetches, merges and enforces them, offline included. The safety property is the important half: the baseline is a floor that can only be added to. Offline, or before any policy has ever been fetched, the phone falls back to the hardcoded baseline and never to something less safe. A cached policy stays authoritative until a newer fetch replaces it. Signing out clears both caches so a shared device cannot leak the previous organisation's locked branding.

Level four is the record. Every compliance event is written to an append-only log stamped with the ruleset version that produced it, exportable to CSV or JSON. I researched retention rather than copying a competitor: 24 months is the federal floor under the MAP Rule and Reg Z. A competitor's stated three years is more conservative than required.

The pre-publish compliance screen showing a passing scan, the loan officer's editable text, and the list of disclosures the export will carry. Beside it, the caption composer.
Nothing exports until this passes. The caption is scanned by the same engine as the script, because it posts with the video and it is advertising too.

The Rule That Had to Be Taught How to Read English

A live false positive on 30 July is the most instructive single defect in the compliance system. The caption "Thinking about waiting for the perfect moment to refinance? Perfect rarely announces itself." was hard blocked by the superlatives rule with no way through. The Post button simply disabled and read "Fix the blocking phrases above before posting."

The rule blocks best, lowest, cheapest, number one, unbeatable, perfect. But "the perfect moment" and "perfect rarely announces itself" are idiomatic and temporal. They are not claims about the product, the rate or the terms, which is what the MAP Rule actually targets. The mechanism to fix it already existed, because the "free" rule carries a guard that skips "feel free" and "free to reach out." The same shape was needed for "perfect," plus an audit of every overridable term for the same class of idiom.

The more interesting outcome is that the false positive produced a design question rather than a patch. My position was that a user who has been informed and accepts the risk should be able to publish. I agreed that with tiering and governance, and then deliberately did not build it, because building it meant redeploying the exact server function a platform reviewer was about to watch.

Tier one is overridable with a logged acknowledgement: superlatives, free, urgency, comparative, qualify, prepay, counselor and all three trigger term rules. These are substantiation judgments. A licensed loan officer can reasonably decide the claim is supportable in their context, and the tool's job is to inform, not to make a legal determination.

Tier two is never overridable: guarantee, approval, credit-income, debt elimination, government affiliation, fair housing and RESPA referral. These are false representations about how lending works, not stylistic risk. No professional judgment makes them true, and fair lending and RESPA exposure is not the loan officer's to waive.

Four hard constraints make the model real rather than decorative, and I established them by reading the live code before any design work started. The block is enforced server side, because a client side "post anyway" button alone means anyone can bypass it by crafting a request and the audit trail becomes a lie. A new distinct audit event is required, so an override is never counted as a clean publish. The acknowledgement must be recorded, not merely displayed, because "we warned them" is only a defence if it is evidenced. And acknowledgement is per flag rather than one blanket checkbox, so the member has to see each claim they are standing behind.

Governance sits on top: override is a capability, not a default. A solo user may override, because they are their own compliance officer. A team member may override only if the organisation permits it, because a member silently bypassing a company compliance rule is precisely what the org admin is paying to prevent.

Deleting the Render Vendor

The version one architecture was a cloud pipeline. Record on the phone, upload the raw video, transcribe it with one vendor, render captions with another, download the finished file. It worked end to end by 17 June, cost roughly 20 to 41 cents per finished video, and meant every video a loan officer made passed through two vendors' servers.

Between 3 and 6 July I replaced all of it. My brief was one line: captions produced with no outside subscription.

The rebuild took a week. A bundled speech model transcribed a sample file with word timings on a real iPhone, proving on-device speech to text. A hand written native module extracted audio on both platforms, producing 16kHz mono WAV, and a real recording gave twenty words in 192 milliseconds. Then the first on-device animated burn: a Swift module decoding a declarative caption specification into a composition with a Core Animation overlay, producing a real MP4 on my own phone with a bold uppercase caption composited into the actual recording and the NMLS and Equal Housing stamp on it. All twelve presets followed, device verified. Android built and ran but produced black video and bad audio, because the Android emulator cannot encode video and I had emulator only. Android caption burn is still a 55 line stub today, and I say so.

The rewrite is usually described as a cost decision. The more interesting half is a product integrity decision, and it shows up in the caption architecture.

Captions are stored as data and burned only at export. Transcribe, then a live styled overlay preview that is fully adjustable at zero re-render, then burn on export. Captions live as text plus start and end timestamps, and the style is a declarative specification of animation primitives: line animation, per-character mode, dim-unspoken, active colour, underline, palette, line background, tilt, border. The same primitives drive both the interactive preview and the native renderer.

That is what makes the preview honest. One vocabulary, two renderers, no translation layer that can drift. Under the previous architecture the vendor's burns were static, so there was no karaoke reveal, no typewriter, no wave, and the preview and the export never matched. That mismatch had been accepted as a known limit. Removing it was the real win, and it is worth more than the cost saving.

What exists today is four hand written Swift modules, 1,675 lines of native code, twelve animated caption presets all rendered natively, and zero marginal cost per render. Record, on-device speech to text with word timings, caption studio with a live animated preview on the real timings, compliance scan, native burn with music mixed and ducked under the voice, three second compliance end card in the brand colour, save to Photos. No upload of the raw take, no vendor, no per-video cost.

The elimination is not total and the record says so. The cloud render functions remain deployed but dormant as a fallback until the Android burn exists, reachable only from a developer path. Four functions belonging to the retired generative b-roll pipeline are also still deployed. B-roll itself is inert by design: nine clips are uploaded but the manifest file does not exist, so the library fetch fails and the feature silently skips. Double gated, and it can never block a burn.

The caption studio previewing an animated preset on real word timings, next to the finished burned video with the caption composited natively.
One caption vocabulary, two renderers. The preview runs on the same word timings the burn does, which is the only reason the preview can be trusted.

A Bug That Quietly Disabled Another Feature

On 9 July I sent a screenshot of a compliance screen showing this caption text: "Waiting for rates and drop before you buy?. Here 's a math... the prices. rise while you wait, . refin refinancing... Marry ry the house"

The root cause is genuinely subtle. The speech model run in single word mode emits one segment per token, not per word. Word-initial tokens carry a leading space, and sub-word continuations, clitics and punctuation do not. The word assembler called trim on every segment, which destroyed the only signal distinguishing a new word from a continuation, and then joined with spaces. So " refin" plus "ancing" became refin ancing, " Mar" plus "ry" became Mar ry, and every comma became its own caption word.

The expensive part is what it did quietly. The mangling disabled the script correction feature, which aligns heard words one to one against the script and snaps near misses back to what the loan officer actually wrote. It could never match a script word split across two heard words, because "refin" scores 0.45 similarity against "refinancing," just under the 0.5 threshold, so it passed through untouched.

Fixing the token reassembly reactivated a feature nobody knew was broken. And that feature is a marketing claim on the live website: misheard words snap back to your script, homeboy becomes homebuyer.

A second defect in the same area hid for a month and produced a rule I now apply everywhere. On 31 July on-device transcription failed, but only in Release builds. The speech model was loaded through a bundler asset handle passed straight to the native module. In Debug the development server serves assets over HTTP, so the handle resolves to a URL the native module can fetch. In Release there is no development server, the model is copied into the app bundle, the handle no longer resolves, and initialisation throws.

It hid because every build before 30 July was a Debug build. The caption pipeline looked device verified when the actual path had never once been exercised. Any bundled binary asset handed to a native module has to be resolved to a real filesystem path first, and every such path has to be tested in a Release build before you believe it.

Five Platform Approvals, Almost None of It Code

Record once, post everywhere is one line on a marketing page. Behind it are five independent corporate approval processes, each with its own vocabulary, its own irreversible decisions, and its own way of failing. This was the most calendar-expensive part of the project.

The first decision was made in one line, on 27 July: I'm not paying $780 a month. So the app does real OAuth per platform, our own developer apps, our name on every consent screen, zero recurring cost. I evaluated a second aggregator properly in July after an outside suggestion, and rejected it on the merits rather than on price. Self-hosting it is single tenant on credentials, multi-tenant token injection is an open unanswered feature request, self-hosting does not skip the platform reviews anyway, and it is strictly worse for the stated threat, because it puts the same tokens in someone else's database with the key in an environment variable.

On 28 July I corrected a premise that would otherwise have cost weeks. "Nothing has blocked the reviews" was true only for one platform's business verification. In reality three of the four reviews need the working integration built first. One rejects mock screencasts. One wants a recording of the real post sheet. One wants the actual OAuth grant on video with working demo credentials. So the adapter work moved ahead of the reviews rather than after them, and business verification had to complete before app review, which is the single most common sequencing mistake in this space.

The most careful work went into the six scope measurement. I was initially advised to strip a business management scope for least privilege. That advice was wrong and I proved it wrong by measurement rather than by argument: same user, same Page, explicit full access role. With six scopes the API returns the Page and the linked Instagram account. With the same five minus that one, it returns an empty array. A Page owned by a business portfolio is invisible without it, and that is not an edge case in mortgage, because branch and team Pages are routinely business owned. Those loan officers would have been told they administer no Pages with no way forward.

Four dead ends cost real time and none of them were explained by the platform. A whitelisting error that turned out to be a redirect recorded incorrectly in my own notes, where the OAuth return leg is a different endpoint from the authenticated action endpoint. An "unexpected error" that turned out to be a browser signed into the platform as a Page profile rather than a personal profile, which is a real product problem rather than a setup quirk, because loan officers commonly browse as their business Page and the platform tells them nothing. An empty Page list, which was the missing scope. And a development mode restriction that would have failed silently.

The screencast needed two resets, and the first is counter-intuitive. Removing the app from the platform side is necessary but not sufficient, and on its own it ruins the take, because the app has no deauthorise callback so it never learns the grant was removed. The connection row survives, the screen still says Connected, and there is no Connect button left to tap on camera. The correct reset is the in-app disconnect, which revokes at the platform, deletes both rows and clears the profile flags in one action. The platform then becomes the verification step rather than the action.

I verified the finished recording frame by frame by extracting stills rather than by watching it. Cold launch from the home screen with the real icon. Settings without scrolling far enough to reveal developer-only rows. The permission grant listing all six by name, including the one most at risk of a requested-but-never-demonstrated rejection. The Page picker showing all three Pages and their Instagram status. A full create to publish run ending with the video live on both destinations.

All six permissions were approved on 13 August, eight days ahead of my estimate, and the access verification tied to it confirmed on 18 August.

The connect screen with LinkedIn, TikTok, Instagram, Facebook and YouTube rows, a second state where Facebook needs a Page chosen, and the platform consent screen listing the permissions.
One consent screen produced both the Facebook row and the Instagram row. Greyed platforms are unconnected and cannot be selected by accident.

The Trap That Costs Something Permanent

The video platform track produced the single most consequential piece of research in the platform work, and it is entirely about not pressing a button.

Publishing an unverified app with a sensitive scope subjects the project to a 100 new user cap that applies over the entire lifetime of the project and cannot be reset or changed. The planned beta is 20 to 50 loan officers. Publishing early would have spent those slots permanently on people who also see an unverified app warning screen. There is no way to get them back, not by verifying later and not by support request.

A second separate trap sits next to it. A project in Testing status issues refresh tokens that expire in seven days, and the scope this product needs is not in the exempt subset. Every connection would die weekly. Fine for development, fatal for beta users. That is exactly why the adapter for that platform is written, committed, and deliberately not registered. Registering it would let a member make a connection that silently breaks within the week.

One measurement went the right way and protected the business model. The upload scope was classified by the platform's own console as sensitive rather than restricted, which means a three to five business day review rather than a third party security assessment taking several weeks, repeated annually, and normally a paid engagement. A restricted classification would have broken the zero-cost direct integration route outright.

Two design decisions inside that adapter are worth keeping. Resumable upload was chosen for certainty, not for file size, because a resumable session can be interrogated: an empty request naming only the total returns either an incomplete range response or a terminal response carrying the finished video. So a lost response on the final chunk becomes a definite answer rather than the double-post ambiguity the other platforms leave you with. And "posted" must not be a lie, because unaudited projects have every upload forced private. The design requirement is to read the privacy status back after publishing and surface a non-public result, since telling a loan officer their video is live when nobody can see it is the worst possible bug for a product selling audit integrity.

The Honesty Audits

The most valuable design work in the last month was not adding anything. It was finding every place the interface claimed something that was not true.

By August the product had roughly thirty screens, three live publishing integrations, an admin console and a compliance engine. It also had a set of accumulated lies. Not deliberate ones, but the ordinary kind a fast moving product grows: a control that promises an outcome the backend cannot deliver, a status pill that says a thing works when it does not, a disclosure list naming something that is never added. For a product whose entire proposition is audit integrity, those are not cosmetic defects. They are category-defining ones. I ran three separate audits.

The first audited the live marketing site claim by claim against the actual implementation. Ten problems: six false claims and four misleading ones. The site claimed an NMLS Consumer Access link that was never added. It claimed five publishing platforms when three were live. It said iOS and Android when Android is not shippable. It described filler word removal and smoothed pacing, which was the spike I killed in June. It described a brand kit with a logo and a lower third, neither of which is rendered. It promised a feed refreshing three times a day when the scheduled refresh was never built. I fixed the sites the same day and re-verified every fix live.

Twelve claims verified as accurate, which mattered as much, because the instinct after finding six false ones is to start deleting. The 19 rule engine, transcription on the phone, the script correction feature, music ducked under the voice at twelve percent with one second ramps, the Equal Housing mark genuinely burned by the renderer, the rulebook enforced server side, overrides logged, and disconnecting removing tokens immediately, which revokes first and then deletes even when the stored value cannot be decrypted. And one thing that is correct by omission: the site makes no scheduling claim, which is right, because nothing fires scheduled posts.

The method finding is the one I would carry to any project. Every finding came from reading code, not from memory. Three claims I suspected were broken proved correct. Three I did not suspect at all proved false, and those three only surfaced because I re-fetched the pages for exact wording rather than working from an earlier summary. Suspicion is not a finding, and a summary is not the source text.

The second audit turned the same lens on the interface. Five problems, and I led the backlog with them rather than with new features. A full scheduling drawer with a month calendar, a twelve hour picker and a confirmation, with no worker behind it to fire the rows, which lets a loan officer schedule a post that will never go out. The compliance screen listing a consumer verification link under auto-added disclosures that was never attached. A green Connected pill on a row that could not post. A row stating a problem in the tone of a resolution. And a composer that did not gate on whether a destination had been chosen.

The third found three rows in Settings with no action attached. They were not greyed out. Every one had a press target, a button role, a chevron and a pressed state animation. They looked live, animated under a thumb, and did nothing. The file's own header admitted it: rows are visual for now. The fix was a Legal screen, a wired support row, and removing the Subscription row entirely until billing exists. Then the part that matters more: I had the row component's press handler changed from optional to required, so a future dead row is a compile error rather than a silent defect, and a clean type check proves no other one survives.

A Pill That Said Connected on a Row That Could Not Post

The third honesty problem is the one I would show someone to explain what I mean by structural design.

The question was genuinely open. Does grant healthy, destination unchosen deserve its own row state? The existing vocabulary was Connected, Reconnect, Coming soon. I added a fifth state.

An amber pill in the slot the green one had, not a second line beneath it, which was the runner up. Burying it leaves the contradiction on screen and merely annotates it. That needed a new colour token, because the existing warning amber measures 2.9 to 1 on white and fails AA as text, since it was built as a fill and icon colour. I added a stronger amber at the same hue at 5.0 to 1, mirroring the existing success and success-strong split exactly, and fixed the composer's own reason text, which had been using the failing token.

The pill has identical geometry to the green one, same height, radius and border, so nothing reflows when the member picks. A row that jumps on a successful action reads as something having gone wrong. Disconnect is the casualty, because it lives on the pill this replaces, and I accepted that knowingly: the state is transient and self clearing, and someone who has just authorised a platform is trying to finish, not to leave. And the signal is a sentinel from the server, never the display string, because the display string is member-facing copy that will be reworded and deriving from it would break the row silently.

I found the bug my own fix introduced on a device the same day. Amber pill, sheet opens, tap the Page, sheet closes, and the row still says no Page chosen. The handler opened with a nothing-changed-skip-the-write guard. That guard is right for a settled row and wrong for a row that needs a destination, where re-picking the displayed Page is exactly how the state resolves. A nothing-changed guard becomes a dead end the moment the same value can mean two different states.

There is a second lesson underneath it that I nearly missed. A real user would never have hit that bug, because their pending row has no stored Page at all so the guard cannot fire. It surfaced only because the SQL I used to reset the state for testing was incomplete. It flipped one field to the sentinel and left the other pointing at the real Page, producing a sheet showing a tick that a genuine pending row would never show. An incomplete test fixture manufactured a bug report for a state that cannot occur.

The rule that came out of the screen is locked: never auto-select a destination when there is more than one. Three independent reasons and any one is sufficient. Compliance, because the API returns Pages in the platform's order and defaulting means a loan officer can publish a licensed mortgage advertisement to their brokerage's corporate Page while believing it went to their personal brand Page, which is a filing problem rather than an interface annoyance. Consistency with our own filing, because the permission justification says we cannot assume a destination and must not guess one, and an auto-selecting app would be filmed doing the exact thing its own justification denies. And it is stronger on camera, because unselected to picker to selection demonstrates the permission doing visible work while a pre-filled value demonstrates nothing. Auto-select at exactly one candidate is correct and stays.

One defect from that screen is still open and I am not going to pretend otherwise. The big green Connected pill disconnects, while the actual destination control is small grey text with a chevron. A loan officer will tap the big green thing and destroy a working integration. It is recorded as a design defect and the row rebuild that fixes it is scoped and not yet built.

A Facebook row showing a green Connected pill directly above secondary text reading Page, 3 Pages, choose one.
Two statements on the same card that contradict each other. The grant is healthy, the destination is unchosen, the row cannot publish anything, and the one element whose entire job is to answer can this post says it can.

The Deal That Wasn't

I started Own the Script after a family member, a head of marketing at one of the largest US mortgage companies, said the company would buy it for $500,000. Interest cooled over the following two months.

The honest read, which I wrote down in August: there was never a deal. No letter of intent, no term sheet, no pilot, no procurement contact. A marketing head at a lender that size cannot unilaterally authorise a $500,000 acquisition, because legal, information security and vendor management all sit in that path.

What changed is not his interest. It is that I now know what he actually controls. Cost of learning it was two months rather than twelve.

There is a related caution I am including because it is the kind of thing that is easy not to say. Detailed product requirements had been flowing to someone actively engaged with two competing vendors. Worth limiting what goes that way.

I built a full strategic framework around the question of which door to go through, direct to loan officer, enterprise, or white label, with explicit proof-it's-open and proof-it's-shut criteria for each, a validation kit, and a quit criterion agreed in advance: if across ten conversations nobody gives money and nobody offers an introduction, that is a complete answer and the project stops. I named the specific failure mode to guard against, which is rounding a "send me the link" up to a maybe.

Then on 4 August I parked the entire framework in one line. It was preserved verbatim behind a banner, silenced completely, and the build freeze it implied was lifted. Being able to switch off your own strategy without deleting it is what stops strategy becoming a ratchet, and the ability to cleanly park a direction is a design skill I did not have five years ago.

Price was decided twice. Locked at $49 on 4 August, re-locked at $79 on 16 August, and the interesting part is that it went up. Willingness to pay in the research was bimodal: a $30 to $80 do-it-yourself band and a $500 plus professional band. But $79 is the market anchor, it is 47 percent cheaper than the tier a competitor gates publishing behind, it sits above the $39 budget option so it signals quality rather than bargain bin, and it captures the top of the do-it-yourself band rather than the middle.

The Honest Scorecard

The core creation flow is built and device verified end to end, from record to published video. Compliance in the frame is built and architecturally guaranteed at a single choke point, which is the clearest defensible claim in the product. Platform standing is real: six permissions approved, tech provider status confirmed, one platform self-serve, two more in progress. Governance and admin are built and proven with a live team policy on a device.

The audit trail is partial, and the gap is the one that bothers me most, because it is the differentiator. Twice in July I hand deleted posts after testing and in both cases the audit trail still recorded them as published. It is structural rather than incidental, because every test cycle where the operator cleans up their own feed creates another false record. And on one platform it cannot self heal, because the granted scope has no read side so the system cannot ask whether a post still exists. Only the member can say. The fix is a member-initiated retract action writing a distinct event. Designed, not built.

There is no billing implementation at all. No in-app purchase, no payment processor. The model is locked on paper and the product cannot take a dollar. Android is not shippable, because the caption burn is a 55 line stub blocked on hardware since 3 July. Scheduling has a table and an interface and no worker, so the tile is now honestly disabled. Campaigns, the strongest differentiation play on the backlog, is still a coming soon tab. Leaderboards were named in pricing copy and have zero lines of code, so I removed the copy. And there is no attorney sign off on the compliance rules, which is required before any launch and has been named as such since version one.

Market validation has not been done. The validation framework was built and parked. No loan officer outside me has used this product.

That last line is the most useful thing on this page. A product that has cleared a major platform's app review, holds tech provider status, publishes real video to three platforms, and enforces an organisation's compliance policy on a phone, and has still never been used by a single customer, is a specific and common kind of project. Naming it plainly is worth more than any feature list.

The finished video playing in the Instagram Reels feed with the burned caption visible.
The end of the loop, recorded on a device. Captured, compliance checked, burned and published from one tap.

What I Took Away

The interface may not claim something the system cannot do. Not scheduling that never fires, not a Connected pill on a row that cannot post, not a disclosure listed as auto-added that is never added. For a product selling audit integrity those are category-defining defects rather than polish.

Guarantee the important thing structurally, not by convention. "Always include the disclosure" is a promise. A renderer that injects the card into any specification lacking one is a guarantee. Making a press handler required turns a whole class of dead-control defect into a compile error.

Measure before you tune. The prompter was not easing badly, it was not moving at all between callbacks. The scroll was not jumpy from a bad curve, it was starting at 638 pixels per second from a standstill. Both fixes required changing the model and neither was visible without a number.

When something fails silently, build the ten line thing that makes it speak.

Only one property may change on a moving surface. The reader's eye is tracking a position, so nothing may move except the page.

Point forward, never backward. The highlight marks what you are about to say. Word by word darkening of what was read is a progress bar for the past.

Never guess a destination on a user's behalf when the stakes are regulatory.

Read the vendor's own documentation before theorising about your account. I proposed three separate causes from inference in one session and the answer was one sentence on a help page the whole time. And when the documentation and the dashboard disagree, the dashboard wins.

Fetch the source, don't trust the summary.

Test the shipping path, not the convenient one.

Park decisions cleanly, and write down what you decided and why.

The one underneath all of them: a partner that will build exactly what it is told needs me to be exceptionally good at knowing what to tell it, and even better at proving it did not. The agents wrote thirty-nine thousand lines of TypeScript, sixteen hundred lines of Swift and forty-two server functions. They also confidently reported work as finished that was not, produced a security defect that put live posting credentials in plaintext, wrote a code comment claiming the opposite, recorded a dangerous API version in project memory, and got the direction of the prompter highlight backwards. I caught every one of those by running a verification process I designed on purpose: device tests, adversarial review passes, numerically verified geometry, claim by claim audits against source, and a dated written record with a correction ledger for when the record itself rots. That process is the actual design work in an AI-built product, and it is the part that does not transfer to the next tool.

Tags

Solo ProjectProduct DesignComplianceiOSDesign SystemsAI-Directed Build