Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions public/demos/group.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*
* Spotlight: a group of companies, consolidated. The worksheet fills company by
* company, the drafted adjustments are named one at a time, the consolidated
* column lands, and a person closes the group's month. Driftline's real August
* 2026 worksheet (consolidation-worksheet), rounded to the dollar: Harbor Café
* is 70% owned, Pier Market Kiosk 40% on the equity method.
*/
import {
appChrome,
CURSOR,
dip,
eo,
pageHeader,
PHONE_APP_CSS,
pointer,
push,
rise,
seg,
steps,
swap,
} from './kit.js'

const COLS = [
['Roasters', 'mc'],
['Café LLC', 'mc'],
['Harbor Café 70%', 'mc'],
['Combined', ''],
['Adjustments', 'adj'],
['Consolidated', 'con'],
]
// Costs read positive and the outside owners' share is deducted, as on the statement.
const ROWS = [
['Revenues', [135830, 21400, 19600, 176830, -1230, 175600]],
['Cost of revenue', [60000, 5200, 6100, 71300, 0, 71300]],
['Operating expenses', [61738, 9442, 8488, 79668, -1230, 78438]],
['Interest, net', [25, -25, 0, 0, 0, 0]],
['Share of Pier Market 40%', [0, 0, 0, 0, 1080, 1080]],
["Outside owners' share", [0, 0, 0, 0, 1504, 1504]],
['Net income', [14117, 6733, 5012, 25862, -424, 25438], 1],
]
const fmt = (v) =>
v === 0
? '–'
: v < 0
? `(${(-v).toLocaleString('en-US')})`
: v.toLocaleString('en-US')

// What each drafted adjustment is, ringed on its cell in turn: [row, note].
const NOTES = [
[
0,
'Business the companies did with each other: $1,230 out of revenue and expense',
],
[
4,
"Pier Market Kiosk, held 40%: the group's share of its $2,700, not its revenue",
],
[
5,
"Harbor Café's outside owners: 30% of its $5,012, as noncontrolling interest",
],
]
const CHECKS = [
'4 of 4 companies closed',
'Entries drafted',
'Nothing unexplained',
'Statements tie',
]

// Column reveal times: the three companies, combined, the adjustments, consolidated.
const REVEAL = [1.5, 1.8, 2.1, 2.6, 3.3, 7.3]
const TOTAL = 12.5

const main = `${pageHeader('Live Statements', 'Driftline Coffee Roasters group · August 2026')}
<div class="tabs" id="tabs"><span class="tab">This entity</span><span class="tab">Combined</span><span class="tab" id="tc">Consolidated</span><span class="tab" id="tw">Worksheet</span>
<span class="badge b-mute" id="dr" style="margin-left:auto">Adjustments</span></div>
<div class="ws" id="ws"><div class="hl" id="hl"></div>
<table><tr><th>Income statement</th>${COLS.map((c, i) => `<th class="n ${c[1]}"><span class="v" id="h${i}">${c[0]}</span></th>`).join('')}</tr>
${ROWS.map(
(r, ri) =>
`<tr${r[2] ? ' class="tot"' : ''}><td>${r[0]}</td>${r[1].map((v, ci) => `<td class="n ${COLS[ci][1]}" id="c${ri}_${ci}"><span class="v" id="v${ri}_${ci}">${fmt(v)}</span></td>`).join('')}</tr>`
).join('')}
</table></div>
<div class="slot">
<div class="card note" id="nt"><span class="badge b-v">Drafted by your AI</span><span id="ntx"></span></div>
<div class="card gclose" id="gc">
<div class="gl"><b>Close the group · August 2026</b>
<div class="chk">${CHECKS.map((c, i) => `<span class="badge b-mute" id="k${i}">${c}</span>`).join('')}</div></div>
<span class="btn go" id="cp">Close the group</span>
</div>
</div>`

const css = `
.ws { position: relative; }
.ws th { font-size: 12px; padding: 10px 10px; line-height: 1.25; }
.ws td { padding: 10px 10px; font-size: 16px; }
.ws td.n { font-size: 15px; white-space: nowrap; }
.ws .v { display: inline-block; }
.ws th:first-child, .ws td:first-child { width: 228px; }
.ws td.adj, .ws th.adj { color: var(--v300); }
.ws tr.tot td.con { color: var(--f400); }
.slot { position: relative; height: 96px; margin-top: 16px; }
.slot .card { position: absolute; inset: 0; padding: 16px 20px; display: flex; align-items: center; gap: 18px; opacity: 0; }
.note span:last-child { font-size: 18px; }
.gl b { font-size: 19px; display: block; margin-bottom: 10px; }
.chk { display: flex; gap: 8px; flex-wrap: wrap; } .chk .badge { font-size: 13px; padding: 5px 10px; }
.gclose .btn { margin-left: auto; white-space: nowrap; }
`

function setup(ctx) {
const { $ } = ctx
ctx.nav('statements')
// the figures fade in, not the cells, so the table keeps its rows while it fills
const cells = (ci) => [$('h' + ci), ...ROWS.map((_, ri) => $(`v${ri}_${ci}`))]
return (t) => {
rise($('tabs'), eo(seg(t, 0.1, 0.5)), 12)
pointer(ctx, $('cur'), $('tw'), t, 0.6, 1.3)
swap($('tc'), t, 1.3, null, null, ['tab on', 'tab'])
swap($('tw'), t, 1.3, null, null, ['tab', 'tab on'])
REVEAL.forEach((at, ci) => {
const p = eo(seg(t, at, at + 0.45))
cells(ci).forEach((el) => rise(el, p, 10))
})
steps(
$('dr'),
t,
[3.3],
['Adjustments', 'Adjustments drafted'],
['badge b-mute', 'badge b-v']
)
push($('main'), t, TOTAL, 640, 300)

// The drafted adjustments, one at a time, then the note gives way to the close.
const k =
t < 3.8 ? 0 : Math.min(NOTES.length - 1, Math.floor((t - 3.8) / 1.15))
const at = 3.8 + k * 1.15
$('ntx').textContent = NOTES[k][1]
$('ntx').style.opacity = String(k > 0 ? dip(t, at) : 1)
const noteOn = eo(seg(t, 3.6, 4.0)) * (1 - seg(t, 7.3, 7.7))
rise($('nt'), noteOn, 12)
ctx.ring($('hl'), $(`c${NOTES[k][0]}_4`), t > 3.8 && t < 7.3 ? noteOn : 0)

rise($('gc'), eo(seg(t, 7.8, 8.2)), 12)
CHECKS.forEach((_, i) =>
steps($('k' + i), t, [8.4 + i * 0.3], null, [
'badge b-mute',
'badge b-good',
])
)
pointer(ctx, $('cur2'), $('cp'), t, 9.7, 10.3, { from: [-220, 120] })
swap($('cp'), t, 10.4, 'Close the group', 'Closed ✓')
$('cp').style.opacity = (0.4 + 0.6 * eo(seg(t, 9.3, 9.6))) * dip(t, 10.4)
}
}

// Phone layout: the three companies give way to combined, adjustments and consolidated.
const phoneCss = `
.ws .mc { display: none; }
.ws th:first-child, .ws td:first-child { width: auto; }
.ws td { font-size: 15px; } .ws td.n { font-size: 14px; }
.tabs { flex-wrap: wrap; gap: 8px; } .tab { font-size: 13px; padding: 7px 10px; }
#dr { display: none; }
.slot { height: 150px; }
.slot .card { flex-wrap: wrap; gap: 10px; padding: 14px 16px; }
.note span:last-child { font-size: 16px; }
.gl b { font-size: 16px; }
.gclose .btn { margin-left: 0; font-size: 15px; padding: 8px 14px; }
`

export default {
width: 1200,
height: 750,
total: TOTAL,
poster: 11.5,
css,
html:
appChrome({ active: 'statements', main }) +
CURSOR.replace('class="cursor"', 'class="cursor" id="cur"') +
CURSOR.replace('class="cursor"', 'class="cursor" id="cur2"'),
setup,
mobile: { width: 720, height: 740, css: PHONE_APP_CSS + phoneCss },
}
1 change: 1 addition & 0 deletions public/demos/render.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
'console',
'close',
'inbox',
'group',
]
const name = new URLSearchParams(location.search).get('demo') || 'hero'
if (!DEMOS.includes(name)) {
Expand Down
8 changes: 6 additions & 2 deletions src/app/(landing)/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import type { Metadata } from 'next'
export const landingMetadata: Metadata = {
title: SITE_TITLE,
description: SITE_DESCRIPTION,
// The long tail behind the head terms Intuit owns (2026-09-02 SERP sample); the close
// terms left with the old hero.
// The long tail behind the head terms Intuit owns (2026-09-02 SERP sample), then the group
// terms with search volume (gtm/seo/keywords.md, 2026-10-07; the holding-company and LLC
// phrasings read blank); the close terms left with the old hero.
keywords: [
'AI-native accounting',
'QuickBooks MCP server',
Expand All @@ -15,6 +16,9 @@ export const landingMetadata: Metadata = {
'analyze QuickBooks data with AI',
'benchmark small business against public companies',
'QuickBooks scenario planning AI',
'multi entity accounting software',
'multi-entity accounting',
'combined vs consolidated financial statements',
'financial knowledge graph',
'XBRL reporting',
],
Expand Down
2 changes: 1 addition & 1 deletion src/app/llms.txt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export async function GET() {
'',
`> ${SITE_DESCRIPTION}`,
'',
'RoboLedger is a finance layer on top of QuickBooks, not a replacement ledger. It syncs the books into a knowledge graph that Claude, ChatGPT, or any MCP client can query at https://api.robosystems.ai/v1/mcp/roboledger: financial statements, forecasts that roll off actuals, comparisons with public companies from their SEC filings, and a month-end close that is drafted for review. Nothing is written back to QuickBooks until a person posts it. It is built on RoboSystems (https://robosystems.ai), open source under Apache 2.0, by RFS LLC.',
"RoboLedger keeps the books of a company or a whole group of companies (a holding company and its LLCs, a franchise operator, a founder with several companies), each with its own chart of accounts and close, and builds the group's consolidated statements from them: the assistant drafts the consolidating entries (eliminations, intercompany balances, noncontrolling interest, equity-method results) and a person posts them. It syncs the group parent's QuickBooks and takes bank feeds for the other companies, into a knowledge graph that Claude, ChatGPT, or any MCP client can query at https://api.robosystems.ai/v1/mcp/roboledger: financial statements, forecasts that roll off actuals, comparisons with public companies from their SEC filings, and a month-end close that is drafted for review. Nothing is written back to QuickBooks until a person posts it. It costs $99 a month per graph on the RoboSystems Standard plan (https://robosystems.ai/pricing). It is built on RoboSystems (https://robosystems.ai), open source under Apache 2.0, by RFS LLC.",
'',
'## Product',
...PRODUCT_PAGES.map((page) =>
Expand Down
2 changes: 1 addition & 1 deletion src/app/opengraph-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ export default function Image() {
eyebrow: 'RoboLedger',
title: 'Connect your books. Ask your AI.',
subtitle:
'Sync QuickBooks into a graph Claude, ChatGPT, or any MCP client can query.',
'One company or a whole group, in Claude, ChatGPT, or any MCP client.',
})
}
26 changes: 15 additions & 11 deletions src/components/landing/AgentSurface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import FloatingElementsVariant from './FloatingElementsVariant'
* ship as a screen.
*
* Every tool named here is a real, registered MCP tool on a writable
* roboledger graph — verified against the backend's tool registry, not
* roboledger graph, verified against the backend's tool registry, not
* aspirational. Write operations declared as `OperationSpec`s are
* auto-generated into MCP tools by the registrar, which is *why* the agent
* surface runs ahead of the UI: a new operation is callable from any MCP
Expand All @@ -16,7 +16,7 @@ import FloatingElementsVariant from './FloatingElementsVariant'
* Keep this list honest. Before adding a row, confirm the tool is actually
* registered (an `OperationSpec(name=...)`, or a hand-written tool in
* `middleware/mcp/tools/`) and that the app genuinely has no UI input for
* it — several report operations, for instance, are plain REST routes and
* it: several report operations, for instance, are plain REST routes and
* are NOT MCP tools, and schedules ARE authorable in the Closing Book.
*/

Expand All @@ -33,10 +33,12 @@ const capabilities: { title: string; blurb: string; tools: string[] }[] = [
'Declare a metric block and its operands once; the Block Explorer computes and charts its standing time series from then on.',
tools: ['create-information-block', 'compute-metrics'],
},
// Posting Rules (2026-10-21) reviews, edits, approves and switches rules on and off, but
// a new rule is still written over MCP, so the card stays and names where it lands.
{
title: 'Write an event-handler rule',
blurb:
'Teach the ledger how a new kind of business event should post — the same DSL rows the Inbox reads when it pre-classifies a transaction.',
'Teach the ledger how a new kind of business event should post. The rule your AI writes waits in Posting Rules, booking nothing until a person approves it there.',
tools: ['create-event-handler', 'update-event-handler'],
},
{
Expand Down Expand Up @@ -76,7 +78,7 @@ export default function AgentSurface() {
</h2>
<p className="mx-auto max-w-3xl text-base text-gray-300 sm:text-lg md:text-xl">
Every write operation on the platform becomes an MCP tool the day it
ships. Screens follow where they earn it — so there are things your
ships. Screens follow where they earn it, so there are things your
AI assistant can do today that don&apos;t have a button yet.
</p>
</div>
Expand All @@ -89,8 +91,9 @@ export default function AgentSurface() {
label="One AI chat with three companies' books connected, reading each and answering which client needs attention this month."
/>
<p className="mt-4 text-center text-sm text-gray-400">
One connector per set of books, side by side in the same chat. Your
AI reads each one and answers across all of them.
One connector per set of books, side by side in the same chat. A
group&apos;s companies share one; each client gets its own. Your AI
reads each one and answers across all of them.
</p>
</div>

Expand Down Expand Up @@ -131,11 +134,12 @@ export default function AgentSurface() {
</div>
<p className="text-sm leading-relaxed text-gray-400">
Add this address as a connector in Claude, ChatGPT, or any MCP
client, sign in, and choose the company. Add it again for each
company you look after. It can read your books and drive these
tools directly — with you approving each write. No install, no
key to paste. The same tools back the in-app AI Console, so
nothing is locked behind a desktop app.
client, sign in, and choose the books. One connector covers a
whole group; add it again for each client you look after. It can
read your books and drive these tools directly, with you
approving each write. No install, no key to paste. The same
tools back the in-app AI Console, so nothing is locked behind a
desktop app.
</p>
</div>
<div className="w-full shrink-0 lg:w-auto">
Expand Down
Loading
Loading