Skip to content

Finance report agent

This page explains how a finance report agent, under delegated user authority, extracts report data from authorized finance or billing backends, validates every figure structurally on the app side (period, currency, source, capture time), writes it to a versioned report DB, and produces a periodic report where every figure traces back to a source page. After reading it, you will understand which permission boundaries read-only financial data needs, why the delegated scope contains no transaction actions, and why the reporting basis and baselines belong in a report DB rather than Memory.

Use case

Analysis teams need to periodically pull data from several finance or billing backends (SaaS billing, payment platforms, bank statement pages) into a periodic report, supplemented with public filings and announcements for context. These backends have no unified export API; signing in to each one and copying figures by hand is slow, error-prone, and leaves no record of which page a number came from.

Typical triggers:

  • Before a monthly or quarterly business report is due, cost and revenue figures must be aggregated across billing backends.
  • A tracked company releases a filing or major announcement, and the analysis summary must be updated.
  • An audit or budget review requires source-page evidence for every figure in the report.

Engineering challenges

  • Basis consistency: the same metric can differ across backends in period, currency, and accounting basis; a report with basis drift is not comparable across periods — "up 12% QoQ" may just mean the basis changed.
  • Figure traceability: every figure in the report must survive the question "where did this number come from." One miscopied figure, or one figure taken from the wrong page version, and the whole report loses its footing in an audit.
  • Backend heterogeneity and login state: every backend differs in report structure, sign-in flow, and MFA policy; extraction must fail loudly when a page structure changes instead of silently emitting wrong numbers.

Module composition

ModuleRoleNotes
GenAuthCoreFinance backend login state is high-risk delegation: interactive authorization, read-only report pages, short-lived revocable credentials, and a delegated scope that excludes all transaction capability at issuance.
Web AgentCoreControlled sessions extract report figures page by page, keeping a source URL, screenshot, and capture time per figure; Profiles reuses login state, and public filings and announcements are supplemented with WebSearch.
GUMemNot usedFigures, the reporting basis, and period baselines are business data — they go to a versioned report DB for cross-period reconciliation and audit. Memory plays no part in this scenario.

Finance report agent architecture

Permission and delegation boundaries

The Agent holds no inherent permissions. The effective authority for each data-pull task is the intersection of three sets: what the user actually holds ∩ what was explicitly delegated ∩ what the enterprise has approved. Applied to this scenario:

  • The delegated scope covers only "read report pages in the specified finance or billing backends" — no transactions, payments, refunds, approvals, or account settings of any kind.
  • Finance backend sign-in is a high-risk operation and must use mode: 'interactive': the user approves in Qoni Console, and the credential is exchanged in a server-side callback — it never reaches the browser.
  • Delegation credentials are short-lived; minute-level validity is recommended for a single data-pull task, and periodic reports rely on scheduled re-issuance.
  • Out-of-scope attempts (for example, a payment or approval page) are rejected and recorded — the audit chain covers all attempts, not just successful actions.

Note: the SDK example requests product-level scopes (such as webagent.do_anything:read). Fine-grained boundaries — backend domains and report-page ranges — are enforced by the GenAuth Agent Profile or your policy layer, not by the task prompt; that configuration is not shown on this page. See Delegate token and attenuation for the full semantics.

Workflow

Finance report agent workflow

  1. The user selects the reporting period and the list of finance backends to pull from.

  2. Your app starts interactive delegation; the user approves in Qoni Console, and the server-side callback exchanges the credential.

  3. Your app loads the current version of the reporting basis (currency, metric definitions, period rules) from the report DB and injects it into the task.

  4. Web Agent opens each finance backend; on first access the user completes sign-in in the controlled session, and later cycles reuse login state through Profiles.

    Checkpoint: When a login wall, CAPTCHA, or risk-control page appears, the Web Agent should escalate to a human instead of silently bypassing it.

  5. Web Agent extracts report figures page by page, recording each figure's source URL, page screenshot, and capture time; public filings and announcements are added as needed.

  6. Your app validates each figure structurally: if any of period, currency, sourceUrl, or capturedAt is missing, the figure is marked pending_verification, never presented as confirmed data.

    Checkpoint: Every figure in the report should trace back to a concrete source page; untraceable figures may only appear in pending-verification state.

  7. Validated figures are written to the report DB together with the basis version; your app produces the periodic report with a figure-to-source table, the pending-verification list, and the audit id.

Example code

The example below wires this scenario in with the official Qoni SDK (@qoniai/qoni): interactive delegation (full callback) → load the versioned reporting basis from the report DB → one doAnything.run() for the read-only pull → app-side structural validation with parseFigures → write to the report DB.

ts
import { Qoni, QoniScopes } from '@qoniai/qoni'

const qoni = new Qoni({
  accessKey: process.env.QONI_ACCESS_KEY!,
  secretKey: process.env.QONI_SECRET_KEY!,
})

// 1. Entry point: start interactive delegation — finance backend sign-in
//    is involved, so the user approves in Qoni Console
export async function startPeriodReport(userId: string, period: string, backendPages: string[]) {
  // Task params go to your app storage; state carries only a task ID,
  // and the callback loads the params back by that ID
  const taskId = await taskStore.save({ period, backendPages })
  const { data: authorization } = await qoni.delegateToken({
    mode: 'interactive',
    agent: 'finance-report',
    scopes: [QoniScopes.DO_ANYTHING_READ, QoniScopes.DO_ANYTHING_MANAGE],
    redirectUri: 'https://app.example.com/qoni/callback',
    state: taskId,
    user: { id: userId },
    expiresIn: 900, // minute-level validity per pull; periodic reports re-issue on schedule
  })
  redirectUserTo(authorization.authorizationUrl)
}

// 2. After the user approves, exchange the delegation token in the
//    server-side callback and continue the pull
export async function handleQoniCallback(request: Request) {
  const query = new URL(request.url).searchParams
  const { data: grant } = await qoni.completeDelegateToken({
    grantId: query.get('grantId')!,
    code: query.get('code')!,
    state: query.get('state')!,
  })
  // Load the task params startPeriodReport stored, keyed by the task ID in state
  const { period, backendPages } = await taskStore.load(query.get('state')!)
  return extractPeriodReport(grant, period, backendPages) // grant.token stays server-side only
}

export async function extractPeriodReport(
  grant: { token: string; auditId: string; grantedScopes: string[] },
  period: string,
  backendPages: string[],
) {
  // 3. Before the pull: load the current reporting basis from YOUR report DB (not Memory)
  const basis = await loadReportingBasis() // e.g. { version: '2026-Q2', currency: 'USD', metrics: [...] }

  // 4. One call runs the read-only pull: extract figures with source & screenshot
  const run = await qoni.doAnything.run({
    token: grant.token,
    prompt: `
      Extract report figures for period ${period} from these finance
      backends: ${backendPages.join(', ')}.
      Return figures as a JSON array of
      { metric, value, period, currency, sourceUrl, capturedAt } objects,
      and keep a page screenshot for every figure. Strictly read only:
      never perform any transaction, payment, refund, approval or
      account-settings action.

      Reporting basis (version ${basis.version}):
      ${JSON.stringify(basis)}
    `,
    capture: { screenshots: true },
  })

  const result = await run.wait({
    // Login walls / MFA / risk-control pages: forward to the user
    onInteraction: (interaction) => notifyUserActionRequired(interaction),
  })

  // 5. App-side structural validation: figures with missing fields are marked
  //    pending_verification, never presented as confirmed data
  const figures = parseFigures(result.output).map((f) =>
    f.period && f.currency && f.sourceUrl && f.capturedAt
      ? { ...f, status: 'verified_source' }
      : { ...f, status: 'pending_verification' },
  )

  // 6. Figures go to the report DB with the basis version — never to Memory
  await reportDb.saveFigures(period, figures, { basisVersion: basis.version })

  return {
    figures,
    pending: figures.filter((f) => f.status === 'pending_verification'),
    artifacts: result.artifacts, // screenshots archived for audit traceability
    basisVersion: basis.version,
    audit: { auditId: grant.auditId, permissionBoundary: grant.grantedScopes },
  }
}

The output structure is a contract set by the task prompt: here it is an array of { metric, value, period, currency, sourceUrl, capturedAt } objects (Figure[]), parsed by parseFigures on the app side; a figure missing any of the four validation fields is marked pending_verification. The SDK itself returns the generic RunResult (runId, status, output, artifacts, and so on).

Data and memory boundaries

This scenario touches four kinds of data; none of them belongs in GUMem:

  • Versioned rules: the reporting basis (currency, metric definitions, period rules) — managed by version in the report DB, referenced by version in every report; a basis change is a version bump.
  • Business state: report figures, figure-to-source mappings, page screenshots, and period baselines — kept in the report DB for cross-period reconciliation, audit, and difference explanation.
  • Audit records: the behavior chain of interactive authorization, every pull, and rejected out-of-scope attempts — maintained by GenAuth.
  • User Memory: this scenario does not use GUMem. Figures and the reporting basis are business data that must reconcile across periods, not user preferences; putting them in Memory loses version reconciliation.

Failure handling

SituationRecommended handling
Finance backend login state expiresSuspend the task, notify the user to sign in again, and resume from the checkpoint.
Report page structure changes break extractionTreat it as a failure and replay the session recording; mark the data point as missing instead of filling in an estimate.
Payment or approval page request outside the delegated scopeReject and record it; the attempted access remains visible in the audit chain.
A figure lacks period, currency, source, or capture timeApp-side validation marks it pending_verification and puts it on the pending list for human review before confirmation.

Production notes

Financial content should preserve sources and dates. Agent output should not be treated as investment advice. The Agent performs no transactions, payments, refunds, or approvals — the delegated scope excludes these capabilities at issuance. When backend figures conflict with public filings, present both sources and the difference side by side as pending verification instead of picking a side; the basis version history in the report DB is the only ground for explaining cross-period differences.

Next steps