Documentation

Troubleshooting

Symptoms, causes and fixes for the failures people hit.

Symptom, cause, fix. Every case below is a real failure path in the running system, not a hypothetical one. Where the correct fix is “nothing — that is the design”, the page says so, because half of what looks like a fault in this API is a refusal doing its job.

Before working through a specific case, three checks resolve a large share of problems:

  1. Are you keeping the cookie? Without a persisted ff_session every request is a new anonymous visitor.
  2. Are you reading reason? Most refusals arrive with HTTP 200.
  3. Does the code exist in the registry? GET /api/reasons will tell you what it means, in the server’s own words.

Session and identity

Every call returns not_claimed, even though I claimed a product a moment ago.

Cause. Your client is not persisting the ff_session cookie. The server issues a new session to any request without one, so every call is arriving as a brand-new visitor.

Fix. Use a cookie-aware client — curl -b jar -c jar, a requests.Session(), an Agent with a jar. Confirm by making two calls and checking that the second sends the same ff_session value the first returned.

The same account behaves differently in two of my processes.

Cause. Two sessions bound to one account. That is legitimate — the account is the durable subject and both are signed in — but session-scoped state such as an active hold is per-cookie.

Fix. Share one cookie jar for one logical client, or accept that holds are not shared. Claims, revenue and the usage meter are account-scoped and will agree.

POST /api/account returns 409 for an address I own.

Cause. The address belongs to a verified account, and a verified address is never bound from a typed form. Before that rule existed, anyone could type a founder’s email and adopt their account.

Fix. Open the sign-in link that the 409 response just caused to be sent to that address. The response’s email_sent field tells you whether the send was accepted.

The sign-in link says invalid_link and I only clicked it once.

Cause. Three causes share one answer, on purpose: the token expired (15 minutes), it was already spent, or it never existed. A mail client that pre-fetches links will spend a token before you click it.

Fix. Request a fresh link and open it promptly. If it happens every time, your mail provider is pre-fetching URLs — open the link by pasting it rather than clicking.

Signing out did not remove the ff_session cookie.

Cause. It is not meant to. POST /api/logout unbinds the browser from the account; the cookie survives as an anonymous session.

Fix. Nothing. Verify with GET /api/account, which should now report account: null.

Claims and candidates

Claiming returns 409 claimed_by_another_founder, and retrying never succeeds.

Cause. Exclusivity. Another founder won the race, permanently. There is no queue.

Fix. Treat it as terminal for that id and move to another candidate — the refusal says as much: “your other candidates are still yours to take”. A retry loop on this code will never terminate successfully.

Claiming returns not_shown for an id I definitely have.

Cause. You may only claim a candidate this session was shown. An id copied from another session, from a log, or constructed by hand is refused.

Fix. Claim only from ids returned to the current cookie by /api/find or /api/feed.

The find endpoint returns nothing and I cannot tell why.

Cause. Four distinct states are deliberately not collapsed: all_held, passed_out, catalog_exhausted and claim_limit_reached.

Fix. Read reason. Only claim_limit_reached has an exit — it is your own allowance, and one share lifts it. Rendering all four as “nothing available” hides the one action that works.

Store generation and publishing

The store status keeps changing and never settles.

Cause. Generation is asynchronous and reports each stage: researching generating checking_domains verifying ready or failed.

Fix. Poll and render the stage names. Only ready and failed are terminal; a second build request during any other stage is refused with build_in_progress.

The store reports ready but a page 404s.

Cause. Readiness requires the manifest to exist and its browsable path to resolve, so this should not happen — if it does, the manifest is out of date relative to what is on disk.

Fix. Call POST /api/store/publish. Publishing regenerates a stale store, re-crawls it, and confirms what a visitor actually receives. It reapplies your applied intents and never touches your custom CSS.

Publish returns 409 not_started.

Cause. There is no manifest, so nothing has been built to publish.

Fix. Build the store first.

Publish returns 500 verify_failed.

Cause. The re-crawl could not be completed, so we will not call the store published. The pages may well be fine; the confirmation is what failed.

Fix. Retry. The response carries a truncated diagnostic string — include it if you report the problem.

Intents and editing

My change request comes back refused with not_an_intent.

Cause. The text contained a dimension or a colour — px, pt, em, rem, % or a six-digit hex. A pixel value cannot be justified by an evidence row, which is the test every intent has to pass.

Fix. Say what you want it to do. “Make the hero feel more urgent” resolves; “make the hero 48px” does not.

no_row_justifies_this, for a change that seems obviously reasonable.

Cause. No dated evidence row on this product supports it. The system declines rather than guessing at your buyer.

Fix. This is working correctly. Either request something the evidence supports, or add the evidence. Inspect what exists — every product carries its rows, each with a URL and a date.

Applying a proposal returns stale_proposal.

Cause. The evidence moved between the proposal being served and being accepted.

Fix. Request a fresh proposal and show the new because and source_row before applying. Do not re-send the old one; that is the case this code exists to prevent.

The AI editor answers cli_unavailable.

Cause. The editing model is not available on that deployment. Nothing was changed and nothing was spent.

Fix. Environmental, not a code problem. Note that it is deliberately distinct from billing_unconfigured — an unconfigured payment key must never read as a broken editor.

Limits, absence and degraded answers

429 on one feature while the rest of the product works.

Cause. Per-feature buckets. Exhausting the website editor leaves the find engine untouched — that is the design, not a partial outage.

Fix. Read unaffected in the body and say which features still work. Back off until resets_at. See Rate limits.

An empty support desk. Did the endpoint fail?

Cause. No. A ticket exists only where a real customer asked something the product record could not answer.

Fix. An empty desk means the agent handled everything, not that nothing happened. The endpoint says so in its note — read it from GET /api/controls rather than writing your own copy.

The supplier search returned zero suppliers.

Cause. Supplier discovery parses a public search page rather than an API, and that source can refuse or change at any time.

Fix. When it fails you get zero suppliers and a statement of which — never a plausible factory. Retry later. A founder emails these companies, so an invented one would be worse than none.

The 3D model control does nothing.

Cause. no_worker — the mesh worker is not running, so the product stays a photograph.

Fix. Do not render the control as working. The refusal is the instruction.

An answer came back abstained rather than answered.

Cause. The curriculum layer found no passage that meets its threshold (no_passage_meets_threshold), or the question carried no usable terms.

Fix. Show the abstention. It is an honest answer, and presenting it as a failure trains people to distrust the one behaviour that protects them from a confabulated one.

The top-up control is visible but nothing happens.

Cause. Payments are not configured on that deployment. GET /api/adbalance serves can_top_up precisely so this is knowable before the control renders.

Fix. Gate the control on can_top_up. Absence is stated, not hidden.

Transport, caching and 404s

A 404 with a nine-byte text/plain body.

Cause. A path that resolves to no route and no file. It is served as plain text, not JSON — your parser will fail before your error handler sees it.

Fix. Check the path against the API reference, and check the response Content-Type before parsing JSON.

GET /preview/<id> returns 404.

Cause. The product has no live offer, and a preview with an invented price is worse than no preview.

Fix. Do not construct this path. The product object serves preview_available and preview_url; render the control only when the first is true, and follow the second.

Stale data through my own proxy or CDN.

Cause. Something is caching /api/. The origin sends private, no-store, must-revalidate on every /api/ path and on /login exactly because a cached copy would be served to the wrong account.

Fix. Remove the intermediary, or configure it to honour the origin headers.

An HTML error page instead of JSON from an /api/ route.

Cause. This should not occur. One dispatch guard wraps every route so a crash answers {"status":"error","reason":"internal"} as JSON. HTML from an /api/ path means something in front of the origin answered instead.

Fix. Check what sits between you and https://flowfinds.ai — a proxy, a captive portal, a firewall interstitial.

Collecting a useful report

If none of the above applies, send us the request and the response, with the cookie value removed:

curl -i -b ff.jar -X POST https://flowfinds.ai/api/<path> \
  -H 'Content-Type: application/json' \
  -d '<your body>' 2>&1 | sed 's/ff_session=[^;]*/ff_session=REDACTED/g'

Include the UTC time, the path, the full response body, and the reason if there was one. Send it to [email protected]. If the problem is a security one, use [email protected] and the responsible disclosure process instead.

Next: Errors · Glossary · Changelog