尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗LedgerLens puts a small online business's books on one screen: every sale and every payment of the last six months is one square, placed by day and by amount, with money in above the line and money out below it. An agent then investigates the ledger, pulls competitor prices with an Apify Actor, measures each leak in exact cents, lights up the records each leak rests on, and prepares the fix: a refund request email, a plan review email, or a ready price change. The owner decides one fix at a time. Nothing is ever sent or changed without them, and every decision is written to an audit trail.
Try it: ledgerlens-mu.vercel.app. Click Open the demo, then press p on the overview for a guided tour of every step. Market check shows real retailer prices collected by the custom Apify Actor aidengrails/ledgerlens-competitor-pricing (source in apify/). Built on Convex, Apify, Claude and Next.js.
The 90-day cash forecast beside each decision shows cash as it is, with the fixes approved so far, and with every prepared fix, so an owner sees the size of the opportunity before deciding anything.
The seeded demo company, Acme Outdoor Co., has four leaks for LedgerLens to find:
| Finding | What LedgerLens finds | Recoverable |
|---|---|---|
| Duplicate payment | Summit Packaging invoice INV-20418 paid twice, 5 days apart | $1,058 one-time |
| Margin erosion | Alpine Insulated Bottle 32oz: supplier cost $22 → $29 (+31.8%), price unchanged at $49, margin 55.1% → 40.8%. Competitors list at $59 / $62 / $64; recommend $55 | $156/mo, $1,872/yr |
| Pricing opportunity | Trail Daypack 24L sells at $79; competitors list at $92 / $96 / $99; recommend $86 | $63/mo, $756/yr |
| Subscription creep | Relay Inbox went from $69 to $119 a month | $50/mo, $600/yr |
| Total | $4,286/yr, $269/mo recurring |
These figures are computed by the engine from the seeded records on every run. They are not hardcoded in the UI, and the test suite asserts them.
The investigation above is the slow loop: it reads six months of books, gathers market evidence and writes findings. Beside it runs a fast loop for money as it moves:
lib/watch/screen.ts). Signals: a payee name one or two keystrokes from a known payee (Damerau-Levenshtein, plus lookalike characters such as 1 for l), a receiving account that differs from every earlier payment, an invoice number that was already paid, a duplicate of a recent payment, an amount far above the payee's median, and a large first payment. The demo feed includes an impersonator invoicing as the packaging supplier with new bank details, a second payment of an invoice already paid, and an ad charge 3.7 times the usual.lib/finance/forecast.ts): fixed charges on their own cycle, everything else at its trailing 90-day rate, with approved fixes as a solid line and the rest dashed. Approving a fix moves money from one line to the other.lib/ask/tools.ts): transaction search with exact totals, vendor history, findings, the live feed, the forecast, product economics and category spend. Each tool call appears live as a step. Answers must cite the records behind each claim; citations to records no tool returned are dropped, and every dollar figure in the answer is checked against the tool results and shown as traced or not. Browser Convex deployment
┌──────────────┐ live ┌────────────────────────────────────────────────┐
│ Overview │◄────────►│ queries field.get, dashboard.get, │
│ Finding │ websocket│ investigations.timeline, ... │
│ Audit log │ │ mutations investigations.start, │
│ │ │ recommendations.approve / reject │
└──────▲───────┘ │ │ scheduler.runAfter(0) │
│ SSR preload │ ▼ │
┌──────┴───────┐ │ ┌──── engine.run (Node action) ──────────────┐ │
│ Next.js app │ │ │ CREATED → INGESTING → ANALYZING_INTERNAL_ │ │
│ (Vercel) │ │ │ DATA → GENERATING_HYPOTHESES → GATHERING_ │ │
│ pages, /api, │ │ │ EXTERNAL_EVIDENCE → VALIDATING_FINDINGS → │ │
│ /market │ │ │ CALCULATING_IMPACT → GENERATING_RECOMMEN- │ │
│ no secrets │ │ │ DATIONS → COMPLETED (or FAILED) │ │
└──────────────┘ │ └──┬───────────────┬───────────────┬─────────┘ │
│ ┌──▼─────────┐ ┌───▼────────┐ ┌────▼───────┐ │
│ │ Finance │ │ External │ │ AI layer │ │
│ │ int cents, │ │ intel │ │ (Claude) │ │
│ │ pure TS │ │ Apify or │ │ Zod-checked│ │
│ └────────────┘ │ fixtures │ │ narratives │ │
│ └───┬────────┘ └────────────┘ │
│ tables: 18, indexed by organization │
└────────────────────┼───────────────────────────┘
▼
┌───────────────────────────────┐
│ Apify platform │
│ ledgerlens-competitor-pricing │
│ CheerioCrawler → dataset │
└───────────────────────────────┘
Every engine step is a Convex mutation, so each timeline event, finding and approval reaches every open browser the moment it commits. The browser never polls. The Next.js app only knows the deployment's public URL; the Anthropic and Apify keys live in the Convex deployment environment.
| Path | Responsibility |
|---|---|
lib/finance/ | Money math in integer cents, duplicate detection, recurring-expense detection, margin analysis, pricing recommendations, impact aggregation. Pure functions with no I/O. |
convex/ | Schema, queries, mutations, the engine action and demo reset. convex/_generated is checked in; npx convex dev keeps it current. |
lib/investigation/ | State machine, step events, the storage-agnostic engine (InvestigationStore), carry-forward of earlier decisions, and execution checklists. |
lib/apify/ | Apify client (start, poll, fetch dataset), Zod validation of Actor output, normalization, and the labeled fixture fallback. |
lib/ai/ | Provider interface, Anthropic provider, deterministic provider, prompts, schemas and grounding checks. |
lib/views/ | Pure builders for the dashboard and finding detail shapes, shared by Convex queries and the UI. |
lib/field/ | The overview's field: every record with its day and amount, and the records, cost change and competitor prices each finding rests on. convex/field.ts serves it live; the offline preview builds the same shape. |
components/overview/ | The overview: five beats (books, investigate, decide, screen, ask) over one canvas wall of records (wall.ts, field-canvas.tsx), with a line of the system's parts (system.ts, machine.tsx) that light as they work, fed by live Convex subscriptions or by an offline replay of the engine. |
lib/river/ | Spending streams (product costs, shipping, team, marketing, software, overhead) used by the dashboard query and to tag findings. |
lib/investigation/drafts.ts | Builds the prepared fix for each finding (emails and price changes) from deterministic finding data only. |
lib/watch/ | The payment screen and the demo bank feed. convex/watch.ts runs it on each arriving payment and applies the autopilot policy. |
lib/ask/ | Books tools, answer grounding and the vendor-neutral agent loop. convex/ask.ts stores questions; convex/agent.ts runs Claude's tool loop. |
Money is never calculated by a language model. Every amount is an integer number of cents produced by the finance engine, with explicit rounding rules, and covered by unit tests. The model only does what it is good at: deciding which products are worth researching and writing a plain-English explanation of evidence it was given.
The AI output is constrained:
Open /market-check for the real-data demo. This runs the private aidengrails/ledgerlens-competitor-pricing Actor (r1XmOWedMNF9AKMbr) against curated Gregory Nano 24 product pages at Quest Outdoors, Al's Sporting Goods and Gregory. The page shows the actual captured quotes, stock status, capture time, coverage, extraction method, changes since the previous capture, and links to the Apify run and dataset. The Actor checks expected product-name tokens, handles ProductGroup variants, and records failed pages instead of inventing prices.
The same finance engine turns usable observations into a prepared price test. Preview this price shows only that test's projected effect on the money river; Review the prepared price change opens its evidence, calculations and approval step. Changing a proposed price requires fresh approval. Market checks are kept apart from the overview: the overview and Ask always show the latest full investigation. Acme's daypack is a comparable sample product, not the identical SKU, and the projection assumes unit sales hold. This flow always requests live data and never substitutes fixtures. Failed refreshes preserve the previous capture with its original timestamp. Refreshes are coalesced while running and limited to one start per minute.
See the sponsor submission and opening demo script and the standalone Actor documentation. The Actor may remain private; share its ID with the judges. The separate “Best Actor built with AI Chat” eligibility has not been established.
Open the public market-check demo.
During GATHERING_EXTERNAL_EVIDENCE, LedgerLens selects products with margin erosion or a pricing gap, builds an input of competitor product URLs, and runs the ledgerlens-competitor-pricing Actor:
lib/utils/url-safety.ts) before it is sent, and again inside the Actor with a DNS check, including after redirects.apify-client, polls until it finishes or APIFY_TIMEOUT_MS passes (then aborts the run), and reads the dataset.FAILURES key-value record instead of guessing.capture_mode = live.The Actor extracts prices from JSON-LD (including ProductGroup variants), then meta tags, then microdata, then individual price selectors. Currency must be identifiable; combined sale/list prices are rejected. FAILURES and SUMMARY in the run's key-value store explain extraction coverage. The SDK is installed as a Node external dependency in convex.json so its runtime dependencies work on Convex.
If the token is missing, the run fails, times out, or returns no usable prices, LedgerLens falls back to the fixture dataset, records a warning on the investigation, and labels the evidence "Demo dataset, not live market data". It never presents fixture data as live.
The seeded listings point at LedgerLens's own sandbox storefront pages. A live run against them is a real crawl, but the prices on those pages are set by the demo, so the UI labels that evidence "Demo storefront" and the investigation records sandbox: true. Only evidence from pages other than these sandbox pages is labeled "Live". To collect real market prices, replace the URLs in the competitorProducts table with public product pages from real retailers.
Requirements: Node.js 20 or newer and a free Convex account.
npm install
npx convex dev # log in, create a deployment; writes .env.local and keeps running
In a second terminal, set the server-side keys on the Convex deployment. Each command prompts for the value, so keys never go into a file, the repository or your shell history:
npx convex env set DEMO_MODE true
npx convex env set LLM_API_KEY # optional: Anthropic key for AI narratives
npx convex env set APIFY_TOKEN # optional: live competitor pricing
npx convex env set APIFY_ACTOR_ID # e.g. your-username/ledgerlens-competitor-pricing
npx convex env set EXTERNAL_INTEL_MODE live
npm run demo:seed
npm run dev # http://localhost:3000 (the app at /overview)
You can also enter them in the Convex dashboard under Settings → Environment Variables.
See .env.example for the full annotated list.
Next.js app (.env.local, written by npx convex dev; on Vercel, set by npx convex deploy):
| Variable | Notes |
|---|---|
NEXT_PUBLIC_CONVEX_URL | Address of the Convex deployment. Public by design. |
CONVEX_DEPLOYMENT | Used by the Convex CLI in development. |
CONVEX_DEPLOY_KEY | Vercel only, for npx convex deploy during the build. Secret. |
Convex deployment (npx convex env set NAME):
| Variable | Required | Notes |
|---|---|---|
APP_URL | yes in production | Public URL of the Next.js app. Seeded competitor listings point at <url>/market/.... |
LLM_PROVIDER | no | anthropic (default) or none. |
LLM_API_KEY | no | Anthropic API key. Without it, narratives are rules-based. |
LLM_MODEL | no | Default claude-sonnet-5-5. |
APIFY_TOKEN | no | Needed for live market data. |
APIFY_ACTOR_ID | no | e.g. your-username/ledgerlens-competitor-pricing. |
EXTERNAL_INTEL_MODE | no | fixture (default) or live. |
DEMO_MODE | no | true enables Load demo and Reset demo. |
INVESTIGATION_STEP_DELAY_MS | no | Pause between steps so the timeline is readable. Default 450. |
The secret keys exist only inside Convex functions. npm run check:bundle scans the built client bundle for secret names and key-shaped strings and fails if it finds any.
npm run demo:seed # convex run engine:seedDemo: creates Acme Outdoor Co. if missing
npm run demo:reset # convex run engine:resetDemo: deletes and recreates it, plus a baseline investigation
The seed is deterministic (a fixed-seed PRNG) and anchored to the current date, so the 182 days of sales, transactions and supplier costs always end today. It creates 18 products, 985 sales lines, 202 transactions, 20 supplier cost records, 4 competitors and 8 competitor listings.
The Reset demo button in the header (demo mode only) does the same thing as demo:reset: it removes investigations, findings, evidence, recommendations, audit entries and price observations, restores the seeded records and fixture competitor data, and returns the dashboard to its starting state.
npx convex dev # terminal 1: pushes functions on save
npm run dev # terminal 2
After adding or removing a file in convex/ without the CLI running, npm run convex:codegen refreshes convex/_generated offline. A fixture-mode investigation takes about 5 seconds with the default step delay. To exercise the Actor locally against the sandbox storefront:
cd apify/ledgerlens-competitor-pricing
npm install
mkdir -p storage/key_value_stores/default
echo '{"products":[{"internalProductId":"p1","competitor":"Ridgeline Supply Co.","url":"http://localhost:3000/market/ridgeline-supply/alpine-insulated-bottle"}]}' \
> storage/key_value_stores/default/INPUT.json
LEDGERLENS_ALLOW_PRIVATE_HOSTS=true node src/main.js
LEDGERLENS_ALLOW_PRIVATE_HOSTS only permits localhost and 127.0.0.1, and is ignored when the Actor runs on the Apify platform.
npm run test # Vitest: finance, Apify, AI validation, full investigation
npm run typecheck
npm run lint
npm run verify # typecheck, lint, test, build, check:bundle
The investigation tests run the real Convex schema, queries, mutations and engine against convex-test's in-memory backend, so they need no deployment or network. They seed the demo company, run complete investigations in fixture mode, and assert the exact finding values above, approval and rejection, decisions carrying forward to the next run, and fallbacks when Apify or the model fail.
1. Apify Actor.
npm install -g apify-cli
apify login
cd apify/ledgerlens-competitor-pricing
apify push
Set APIFY_ACTOR_ID to <username>/ledgerlens-competitor-pricing and APIFY_TOKEN to an API token, on the production Convex deployment (npx convex env set --prod ...), along with APP_URL=https://your-app.vercel.app and DEMO_MODE=true.
2. App (Vercel). In the Convex dashboard, generate a production deploy key. Import the repository in Vercel, add CONVEX_DEPLOY_KEY, and set the build command to:
npx convex deploy --cmd 'npm run build'
That pushes the Convex functions and builds Next.js against the production deployment's URL. Then run npx convex run --prod engine:seedDemo (or click Load demo) so the seeded competitor URLs use the deployed APP_URL.
With DEMO_MODE=true (on the Convex deployment) the header shows Reset demo and the demo seed and reset functions run. With it off, they refuse with FORBIDDEN and the button is hidden. The suggested demo:
To record the walkthrough in one take, press p (or open /overview?tour): the overview plays every beat by itself, waiting on the engine, the feed and the agent rather than on a timer, and approves two fixes, blocks the lookalike payment and asks one question along the way. Any key hands control back.
/market-check flow uses real retailer pages and labels the sample business separately. Its curated pages may change, become unavailable or block crawls; partial coverage is shown, and no pricing test is produced without two usable quotes.LLM_API_KEY and APIFY_TOKEN are set on the Convex deployment and read only inside Convex functions; the Next.js app never sees them. npm run check:bundle verifies the client bundle.{ "error": { "code", "message" } }.auditLog table in the same transaction.[redacted].The landing page, fully grown. The roots are the demo company's books, the trunk is the engine, each limb is a finding, and the share of a limb that is orange is proportional to the money it recovers. Scrolling grows the tree; clicking a finding grows twigs out to its evidence:
The same view in light mode:
Past the tree, the blue koi swims the fast loop (each payment screened before it posts) and the orange one the slow loop (an investigation every morning):
Down the river, the story follows the engine's duplicate payment from the leak to the refund. Every figure on the cards comes from an engine run over the demo books at build time:
The overview after an investigation: 1,187 records, one square each and one column per week, with four leaks lit up in orange and labelled with what they cost. These are from the offline replay, which runs on the rules-based narrative and the demo price dataset, so the Apify and Claude parts show as unused (dashed); on the live deployment they light up in their steps:
Deciding a fix: everything else fades, leaving the bottle's sales since the supplier's cost went from $22 to $29, with the price change drafted below:
Screening the bank feed: clear payments land in this week's column, while the lookalike supplier, an invoice already paid and a payment above the approval limit wait at the gate for you:
A leak with its fix already prepared, plus the evidence behind it:
On a phone:
The look is a field notebook: warm paper, black ink, hairline rules, and two pigments. Blue is the books; orange is money the engine found. Type is Geist, with anything measured set in Geist Mono. The landing tree is generated from a fixed seed (lib/landing/tree.ts) and drawn on a pixel grid on a canvas, one frame per scroll position; the drop, ripples and koi that follow it (lib/landing/pond.ts), the river (lib/landing/river.ts), the pond (lib/landing/garden.ts), the fisherman and the chase (lib/landing/fisher.ts, lib/landing/story.ts) and the reef where the story ends (lib/landing/reef.ts) are drawn on the same grid, which goes to the screen as one small image scaled up. The overview's wall is drawn on a canvas too: each square eases to its color on its own clock, so a finding spreads through its records from the one its label points at, and an approval flips them back to blue in a wave. Every figure on the landing page comes from running the engine over the demo books at build time (lib/landing/data.ts). The landing page opens in dark mode and the other pages follow the system setting; a choice made with the switch in the header wins everywhere. The site respects reduced motion.
In development, /preview/overview, /preview/findings/0 and /preview/activity render the app views from an in-memory engine run, without a Convex deployment (?approved=1 on a finding shows the approved state). These routes return 404 in production.
lib/finance/forecast.ts | The 90-day cash forecast. |
lib/demo/ | Demo catalog and the seeded generator for 182 days of records. |
apify/ledgerlens-competitor-pricing/ | The Apify Actor. |
app/market/[competitor]/[product] | Sandbox competitor storefront pages with schema.org JSON-LD. The seeded competitor listings point here, so live mode exercises a real Apify crawl without scraping third-party shops. The prices on these pages are set by the demo, so results from them are labeled "Demo storefront" and never presented as market data. |