---
title: "Build it"
url: https://services.dev/docs/examples/tax/build
captured: [tax-build.txt]
---
# Build it

URL: https://services.dev/docs/examples/tax/build

> The tax brand's folder, one file at a time. Your code first, then its words, its rules table and its Pool, then ten Services, with check after each one and the plan each file needs.

*tax's words: return is a job, taxpayer is a subject, review is a task, preparer is a member (examples/tax/brand.ts).*

The brand is a folder. Write its files in this order and run `check` after each. It counts lines of code only, and prints under each file the services.dev plan its keys need, with the line that sets it.

| File | What it adds | Plan |
|---|---|---|
| [`lib/tax.ts`](#libtaxts) | your code: the calendar, each jurisdiction's rules, the request list, the schedules, the tentative tax, the estimates, the notice match, and a simulated e-file system a live brand swaps for its transmitter | none |
| [`brand.ts`](#brandts) | the words, four credentials the brand holds, and the calendar: [Brand()](https://services.dev/docs/service/brand.md) | Launch, for the domain |
| [`rules.ts`](#rulests) | who may perform each act, per Service, each row illustrative with its position: [the rules table](https://services.dev/docs/examples/tax/rules.md) | Free: the floor |
| [`pools/preparers.ts`](#poolspreparersts) | three credentialed roles, a door of its own, protections and standby: [the Pool](https://services.dev/docs/examples/tax/pool.md) | Team |
| [Four small Services](#four-small-services) | the estimate, the question, the notice response and the amendment, which the plan orders | Launch |
| [`services/carryforward-watch.ts`](#servicescarryforward-watchts) | the steward a return names, before the return that imports it | Launch |
| [`services/annual-return.ts`](#servicesannual-returnts) | the return: the §7216 consent, kept facts, the organizer, a signature for the brand's firm, a filing per jurisdiction, five clocks and a guarantee: [Annual return](https://services.dev/docs/examples/tax/annual-return.md) | Team |
| [`services/information-returns.ts`](#servicesinformation-returnsts) | the year's 1099s, under the brand's own codes: [Information returns](https://services.dev/docs/examples/tax/information-returns.md) | Launch |
| [`services/tax-plan.ts`, `services/transcript.ts`](#the-plan-and-the-transcript) | the year that orders the rest, and lender records, the table's last name | Team, and Free |
| [`services/year-end-close.ts`](#servicesyear-end-closets) | the books, closed by AI, with an accountant's approval sold on top, from a Pool another operator runs | Launch |

## lib/tax.ts

```bash
cat lib/tax.ts
```

```txt title="captured/tax-build.txt · magic values: none"
export type ByJurisdiction = Record<string, number>
export const irs: Calendar = { zone: 'America/New_York', weekends: ['saturday', 'sunday'], closed: holidays, closes: '23:59', rings_at: '09:00', roll: 'next open day' }
export function holidays(year: number): string[] {
export const SCENARIOS: Readonly<Record<string, Readonly<Record<string, string>>>> = {
export const formOf = (entity: string, at: string) => FORMS[entity]?.[at] ?? 'the state return'
export const authorization = (job: AnyView) => (job.input.entity === 'partnership' ? 'Form 8879-PE' : 'Form 8879-CORP')
export const JURISDICTIONS: Readonly<Record<string, Rule>> = {
export async function efile({ ein, tax_year, entity }: Year, job: Filing): Promise<Answer> {
export async function extension({ tax_year }: Year, job: Filing): Promise<Answer> {
export const fileDue = (job: AnyView, j: string) => later(statutory(job, j), String(job.input.extended_to ?? ''))
export const extendedDue = (job: AnyView, j: string) => fifteenth(statutory(job, j), j === 'US' && june(job) ? 7 : JURISDICTIONS[j].extend.months)
export const payDue = (job: AnyView) => statutory(job, 'US')
export const assessmentCloses = (job: AnyView) => {
export const refundCloses = (job: AnyView) => {
export const claimsRefund = (job: AnyView) => Number((job.output.tax as ByJurisdiction | undefined)?.US ?? 0) < Number(job.subject.fact<ByJurisdiction>('tax', { period: String(job.input.tax_year) })?.US ?? 0)
export const keptUntil = (job: AnyView) => {
export const lateCost = (job: AnyView) => Math.round(unpaid(job) * 0.005)
export const lateFiling = (job: AnyView) => (job.input.entity === 'C corp' ? Math.round(unpaid(job) * 0.05) : 245 * ((job.input.owners as unknown[] | undefined)?.length ?? 1))
export function requestList(job: AnyView): AskItem[] {
export const reask = {
export interface Carryforward { kind: 'loss' | 'credit'; vintage: string; jurisdiction: string; amount: number; expires?: string }
export const carryforwardList: StandardSchemaV1<Carryforward[]> = {
export async function schedules({ tax_year, entity, jurisdictions }: Year & { jurisdictions: readonly string[] }, job: AnyView) {
export async function tentative({ tax_year, entity, jurisdictions }: Year & { jurisdictions: readonly string[] }, job: AnyView) {
export const estimateDue = (job: AnyView) => fifteenth(job.period.start, [3, 5, 8, 11][Number(job.period.name.slice(-1)) - 1])
export async function estimateFor({ quarter, jurisdictions }: { quarter: string; jurisdictions: readonly string[] }, job: AnyView) {
export const underpaymentCost = (job: AnyView) => Math.round(Number((job.output.amount as ByJurisdiction | undefined)?.US ?? 0) * 0.07 / 4)
export function form1084(years: readonly { year: number; net_profit: number; depreciation: number; business_use_of_home: number }[]) {
export async function selfEmployedIncome(_: unknown, job: AnyView) {
export async function classify(_: unknown, job: AnyView) {
export async function matchPayments({ tax_years }: { tax_years: readonly string[] }, job: AnyView) {
export async function payees({ year }: { year: string }, job: AnyView) {
export const earliestExpiry = (job: AnyView) => ((job.output.carryforwards as Carryforward[] | undefined) ?? job.subject.fact<Carryforward[]>('carryforwards') ?? []).flatMap(c => (c.expires ? [c.expires] : [])).sort()[0] ?? ''
```

## brand.ts

```bash
cat brand.ts
```

```txt title="captured/tax-build.txt · magic values: none"
import { Brand } from 'services.dev'
import { irs } from './lib/tax.ts'

// A tax brand shaped like the .tax family, proving Time.
// Its words render every surface its buyers, agents and preparers read: /returns, return.ordered, get_return. The brand holds four credentials of its own: the e-file
// number every return is filed under, the transmitter code its 1099s go under, TIN matching, and the transcript program's participant status.
// Its dates fall on the agency's calendar: Eastern time, closing at 23:59, a weekend or a legal holiday rolling to the next business day.
export default Brand({
  domain: 'apis.tax',
  words: { job: 'return', subject: 'taxpayer', task: 'review', member: 'preparer' },
  credentials: {
    efin: { verify: 'https://verify.example.test/efin', fresh: '30d', via: 'the e-file application' },
    iris: { verify: 'https://verify.example.test/iris', fresh: '30d', via: 'the information returns transmitter code' },
    tin_matching: { verify: 'https://verify.example.test/tin-matching', fresh: '30d', via: 'the TIN matching program' },
    ives: { verify: 'https://verify.example.test/ives', fresh: '30d', via: 'the transcript participant program' },
  },
  calendar: irs,
})
```

## rules.ts

```bash
cat rules.ts
```

```txt title="captured/tax-build.txt · magic values: none"
import { Rules } from 'services.dev'

// Rows per Service, frozen and versioned, each illustrative: a brand's counsel writes its real table, and every row
// records what the brand does and why. A paid preparer holds a PTIN; on a California leg, a CTEC registrant, a CPA, an
// enrolled agent or an attorney. A return is signed by one who also holds a CPA licence or an enrolled agent's card, as the
// paid preparer for the brand's firm, and e-filed under the brand's own EFIN. Return information goes only where the
// taxpayer's §7216 consent sends it. No fee rides on a return as filed; a notice's abated penalty may carry one. The
// transcript door orders under the brand's participant status, and refuses the three acts that belong to people.
const preparer = 'ptin'
const signer = { all: ['ptin'], any: ['cpa', 'enrolled agent'] }
const filing = {
  prepare: { reserved: preparer, where: '*', in: { 'US-CA': { all: ['ptin'], any: ['ctec', 'cpa', 'enrolled agent', 'attorney'] } }, position: 'A paid preparer holds a PTIN; on a California leg the brand staffs a CTEC registrant, a CPA, an enrolled agent or an attorney.' },
  sign: { reserved: signer, where: '*', position: 'The signer holds a PTIN, and a CPA licence or an enrolled agent\'s card, and signs as the paid preparer for the brand\'s firm.' },
  file: { reserved: preparer, and: { brand: 'efin' }, where: '*', position: 'E-filed under the brand\'s own EFIN, the signer\'s PTIN in the paid-preparer block.' },
  disclose: { reserved: { consent: '7216' }, position: '§7216: return information goes only where the taxpayer\'s signed consent names, and abroad only when it says so.' },
  price_outcome: { refused: true, position: 'Circular 230 §10.27: no fee is contingent on a return as prepared or amended.' },
} as const

export default Rules({
  version: '2026-10',
  services: {
    'annual-return': {
      ...filing,
      extend: { open: true, position: 'An extension asks for time, not a return: the brand files it, or answers that a state needs none.' },
      pay: { open: true, position: 'Paying what is owed with the extension is a payment, not a return: the brand sends it.' },
    },
    'amended-return': filing,
    'notice-response': {
      represent: { reserved: ['cpa', 'enrolled agent', 'attorney'], where: '*', self: true, position: 'Representing a taxpayer needs one of these and a Form 2848 that names the representative; a taxpayer may answer for themselves.' },
      send: { open: true, position: 'Sending a signed response under a live authorization is a delivery act.' },
      price_outcome: { open: { on: 'abated_usd' }, position: 'Circular 230 §10.27(b): a fee on a penalty the agency abates is permitted.' },
    },
    question: {
      advise: { reserved: signer, where: '*', kind: 'advice', position: 'An answer that tells a taxpayer what to do is a signer\'s advice, adopted in their name.' },
    },
    'information-returns': {
      match: { open: true, and: { brand: 'tin_matching' }, position: 'Each payee\'s name and TIN are matched under the brand\'s own TIN matching.' },
      file: { open: true, and: { brand: 'iris' }, position: 'A 1099 reports a payment, not a return: the brand files it under its own transmitter code.' },
    },
    transcript: {
      order: { reserved: { brand: 'ives' }, position: 'Transcripts are ordered through the brand\'s participant status.' },
      disclose: filing.disclose,
      prepare_return: { refused: true, route: 'https://apis.tax/annual-return', position: 'A paid preparer with a PTIN prepares a return: the transcript door prepares none.' },
      represent_taxpayer: { refused: true, route: 'https://apis.tax/notice-response', position: 'A CPA, enrolled agent or attorney represents a taxpayer: the transcript door represents none.' },
      sign_consent: { refused: true, position: 'The taxpayer signs the consent: nothing here signs for them.' },
    },
  },
})
```

## pools/preparers.ts

```bash
cat pools/preparers.ts
```

```txt title="captured/tax-build.txt · magic values: none"
import { Pool } from 'services.dev'
import { keptUntil } from '../lib/tax.ts'

// One Pool for every tax Service, on gigs.tax. The verifier checks each credential and renews the PTIN by 31 December; a
// signer never signs their own preparation. A PTIN or an enrolled agent's card counts on every leg; a CPA's licence or a
// bar admission federally, and in the state that granted it. Each act is covered, tailed to the year's assessment date;
// the signer of record is offered next year first; a return its consent keeps in the US goes to members who work there.
// Pay is payable two days after the act, paid that evening. Standby covers the spring season and the October crunch.
export const preparers = Pool({
  name: 'practitioners',
  door: 'gigs.tax',
  roles: {
    preparer: { credential: 'ptin', where: '*' },
    signer: { credential: { all: ['ptin'], any: ['cpa', 'enrolled agent'] }, where: '*', reviews: 'preparer' },
    representative: { credential: ['cpa', 'enrolled agent', 'attorney'], where: '*' },
  },
  scopes: { ptin: '*', 'enrolled agent': '*', cpa: { federal: '*', state: 'region' }, attorney: { federal: '*', state: 'region' }, ctec: 'US-CA' },
  join: {
    name: 'text: your name as credentialed', credential: ['ptin', 'ctec', 'cpa', 'enrolled agent', 'attorney'],
    number: 'text: your PTIN, licence or enrollment number', caf: 'text?: your CAF number, to represent', regions: 'jurisdiction[]: every place you are licensed',
    located: 'jurisdiction: where you work from', areas: ['returns', 'amendments', 'notices', 'questions'], hours: 'number: spare hours a week',
    turnaround: ['same day', '24h', '48h'], min_fee: 'money: the least you would take per review',
  },
  verify: { url: 'https://verify.example.test/preparers', fresh: { ptin: '12-31' } },
  located: { abroad: 'disclose: a preparer outside the US' },
  coverage: { per_act: 500_000, by: 'pool', tail: 'assessment' },
  sees: { teaser: ['act', 'fee_usd', 'jurisdictions', 'signals'] },
  clear: { attest: 'I have no conflict with this taxpayer, or I disclosed it and they consented in writing', checks: ['business', 'owner'] },
  firms: true,
  affinity: '14d',
  retains: { signed: keptUntil },
  standby: [
    { window: 'Feb 1 to Apr 15', hours: 20, retainer_usd: 400 },
    { window: 'Sep 1 to Oct 15', hours: 15, retainer_usd: 300 },
  ],
  payouts: 'same-day',
})
```

With no Service yet, `check` has nothing to sell:

```bash
npx services.dev check   # after pools/preparers.ts
```

```txt title="magic values: none"
No Services here.
  next    npx services.dev init
```

## Four small Services

The estimate is AI and code, with the officer deciding. Now `check` reads the whole folder:

```bash
cat services/estimate.ts
```

```txt title="captured/tax-build.txt · magic values: none"
import { Service } from 'services.dev'
import { estimateDue, estimateFor, underpaymentCost } from '../lib/tax.ts'

// A quarter's estimated payments, the owner deciding. The quarter is its period on the taxpayer, so a second estimate for
// the same quarter answers period_taken. Your code figures each jurisdiction's installment: what the year requires by
// this quarter on last year's tax, less what is paid, so a quarter figured short catches up. The owner pays it, pays a
// different amount themselves, or skips it in writing. Paid, each payment goes under a cap, its receipt satisfies the
// clock the buyer answers for, and what was paid is kept on the taxpayer, in its quarter, for the year's return to credit.
// The estimate is earned once it is figured and decided, so it is charged on delivery, whatever the owner chose.
const decided = (job: { record: readonly { step?: string; decided?: string }[] }) => job.record.findLast(e => e.step === 'approve' && e.decided)?.decided

export default Service({
  name: 'Estimated Payment',
  description: "A quarter's estimated tax, figured, decided and paid.",
  input: { owner: 'party: the officer who decides', ein: 'text: such as 12-3456789', quarter: 'text: such as 2027-Q1', jurisdictions: 'jurisdiction[]: such as US, US-CA' },
  output: { amount: 'money{}', basis: 'text', voucher: 'file', receipt: 'text[]?', paid: 'money{}?' },
  subject: { kind: 'taxpayer', key: 'ein', period: 'quarter', keeps: { paid: 'paid' } },
  deliver: {
    compute: { by: estimateFor, cite: 'prior-year safe harbor, cumulative by quarter' },
    approve: { ask: 'owner', to: 'decide', choices: ['pay it', 'pay a different amount', 'skip, in writing'] },
    pay: {
      submit: 'https://treasury.example.test/payments', to: 'Treasury and states', receipt: 'receipt', each: { of: job => Object.keys(job.output.amount) }, when: job => decided(job) === 'pay it',
      guard: { amount: v => v <= 50_000 || 'over the single-payment cap: the owner pays directly' },
    },
    record: { by: async (_, job) => ({ paid: job.output.amount }), when: job => decided(job) === 'pay it' },
  },
  clocks: { pay: { due: estimateDue, owner: 'buyer', rungs: ['notice 10d', 'escalate 2d'], exposure: underpaymentCost } },
  price: { usd: 79, release: 'delivery' },
})
```

```bash
npx services.dev check   # after services/estimate.ts
```

```txt title="captured/tax-build.txt · magic values: none"
  plan    Launch (services.dev) · Brand.domain at :9
  plan    Team (services.dev) · Pool.door at :11, gigs.tax, a door your account operates · Pool.firms at :29 · Pool.affinity at :30 · Pool.standby at :32
services/estimate.ts · 17 lines · $79 per job, within 48 hours, charged on delivery.
  plan    Launch (services.dev) · subject at :17, and the payment it keeps
```

The question names a rules row, so `check` holds the brand at [`rule_unread`](https://services.dev/docs/reference/check/rule_unread.md) until the table's other Services are written:

```bash
cat services/question.ts
```

```txt title="captured/tax-build.txt · magic values: none"
import { Service } from 'services.dev'
import { preparers } from '../pools/preparers.ts'
import rules from '../rules.ts'

// The AI answers a taxpayer's question in the job's thread, citing what it rests on. An answer that tells them what to do
// is advice the rules reserve: it waits until a signer from the Pool adopts it as written, revises it, or refuses it,
// and what the client reads goes out in the signer's name.
export default Service({
  name: 'Tax Question',
  description: 'A tax question, answered with its sources.',
  input: { owner: 'party: who asks', ein: 'text: such as 12-3456789', question: 'text: such as Can we deduct the van?' },
  output: { answer: 'text' },
  rules,
  subject: { kind: 'taxpayer', key: 'ein' },
  roles: { signer: { pool: preparers, pay: 25 } },
  talk: { with: 'owner', adopt: { by: 'signer', may: ['adopt', 'revise', 'refuse'], checklist: ['the authority cited says what the answer says', 'the facts it assumes are the facts in the thread'] } },
  deliver: { answer: { by: 'ai', cite: 'the code, its regulations and the agency guidance it rests on' } },
  price: 49,
})
```

```bash
npx services.dev check   # after services/question.ts
```

```txt title="captured/tax-build.txt · magic values: none"
services/question.ts · 10 lines · $49 per job, within 48 hours, charged on delivery.
  plan    Launch (services.dev) · subject at :14 · roles.signer.pool at :15
  ✗ The rules table binds a name nothing reads: a key that names no Service file, or a row no step performs that is neither an advice class nor refused. Rename it to what it means, or take it out. rules.services has a key 'annual-return', and no Service file is services/annual-return.ts. rules.services has a key 'amended-return', and no Service file is services/amended-return.ts. rules.services has a key 'notice-response', and no Service file is services/notice-response.ts. rules.services has a key 'information-returns', and no Service file is services/information-returns.ts. rules.services has a key 'transcript', and no Service file is services/transcript.ts.
✗ 1 to fix
```

[The notice response](https://services.dev/docs/examples/tax/notice-response.md), then [the amended return](https://services.dev/docs/examples/tax/amended-return.md):

```bash
cat services/notice-response.ts
```

```txt title="captured/tax-build.txt · magic values: none"
import { Service } from 'services.dev'
import { classify, matchPayments } from '../lib/tax.ts'
import { preparers } from '../pools/preparers.ts'
import rules from '../rules.ts'

// A letter from the agency, answered. The AI reads it, your code matches each payment the taxpayer kept against the
// periods the letter shows them posted to, and sorts it: pay what is owed, ask for a misapplied payment to be moved, ask
// for a penalty to be abated, or reply. A representative claims it, and only then is the officer asked for a Form 2848
// that names them by CAF number, for this form and these periods, for a year, withdrawn once the reply is accepted. The
// reply date is the representative's to answer for. For $99 the officer answers it themselves, in their own name. On an
// abatement, a quarter of the penalty taken off is the fee, $99 at least. A letter asking for more time to assess gets
// the officer's signature on a Form 872, and the date it sets is kept on the taxpayer, so the year's clocks move to it.
export default Service({
  name: 'Notice Response',
  description: 'A notice, answered.',
  input: { owner: 'party: the officer who authorizes', ein: 'text: such as 12-3456789', notice: 'file: the letter', reply_by: 'date: such as 2027-12-14', tax_years: 'text[]: the periods it names, such as 2026' },
  output: { code: 'text', kind: ['agree', 'misapplied', 'penalty', 'extend', 'respond'], payments: 'json', response: 'file', receipt: 'text', abated_usd: 'money?', statute_extended_to: 'json?: by year, the date a signed Form 872 sets' },
  rules,
  subject: { kind: 'taxpayer', key: 'ein', keeps: { statute_extended_to: 'statute_extended_to' } },
  roles: { representative: { pool: preparers, pay: 90 } },
  deliver: {
    read: { by: 'ai', cite: 'source' },
    match: { by: matchPayments, cite: "each payment's receipt, and the period it was for" },
    classify: { by: classify, routes: { agree: 'pay', misapplied: 'respond', penalty: 'respond', '*': 'respond' } },
    respond: { by: 'representative', act: 'represent', needs: 'poa', stamp: true, est: '1h', drafts: { misapplied: 'a transfer request citing the receipt', penalty: 'a request to abate', extend: 'a Form 872 limited to the issues and a date' } },
    extend_statute: { ask: 'owner', to: 'sign', what: 'Form 872', after: 'respond', when: job => job.output.kind === 'extend', how: 'esign', capacity: 'officer' },
    poa: { ask: 'owner', to: 'grant', after: 'respond.claim', what: 'Form 2848', names: { claimant: 'caf' }, scope: job => ({ acts: ['represent', 'send'], matters: [{ tax: 'income', form: '1120', periods: job.input.tax_years }], for: '365d' }), how: 'esign', capacity: 'officer', withdrawn: ['accepted', 'for'] },
    send: { submit: 'https://correspondence.example.test/replies', to: 'the agency', receipt: 'receipt', needs: 'poa' },
    pay: { routed: true, submit: 'https://treasury.example.test/payments', to: 'Treasury', receipt: 'receipt' },
  },
  clocks: { reply: { due: job => job.input.reply_by, owner: 'representative', rungs: ['notice 10d', 'request time 5d', 'ack daily 3d'] } },
  price: { usd: 299, self: 99, outcome: { on: 'abated_usd', share: 0.25, min: 99, when: job => job.output.kind === 'penalty' } },
})
```

```bash
npx services.dev check   # after services/notice-response.ts
```

```txt title="captured/tax-build.txt · magic values: none"
services/notice-response.ts · 19 lines · $299 per job, or $99 if you answer it yourself, or 25% of a penalty abated, $99 at least, within 48 hours, charged on send's receipt.
  self    The owner may answer it themselves, in their own name, for $99.
  ✗ The rules table binds a name nothing reads: a key that names no Service file, or a row no step performs that is neither an advice class nor refused. Rename it to what it means, or take it out. rules.services has a key 'annual-return', and no Service file is services/annual-return.ts. rules.services has a key 'amended-return', and no Service file is services/amended-return.ts. rules.services has a key 'information-returns', and no Service file is services/information-returns.ts. rules.services has a key 'transcript', and no Service file is services/transcript.ts.
✗ 1 to fix
```

```bash
cat services/amended-return.ts
```

```txt title="captured/tax-build.txt · magic values: none"
import { Service } from 'services.dev'
import { authorization, carryforwardList, efile, schedules } from '../lib/tax.ts'
import { preparers } from '../pools/preparers.ts'
import rules from '../rules.ts'

// A year already filed, filed again. A second order for a tax year the annual return holds answers 409 period_taken, and
// its next is this Service, ordered from the accepted return with the same input: the schedules figured again, prepared,
// signed for the brand's firm, authorized and e-filed, the federal return first. A partnership under the centralized
// audit regime doesn't amend: it files an administrative adjustment request, and each owner gets an adjusted statement.
export default Service({
  name: 'Amended Return',
  description: 'Amended returns.',
  input: {
    business: 'party', owner: 'party: the officer who signs', ein: 'text: such as 12-3456789', tax_year: 'text: such as 2026',
    entity: ['C corp', 'S corp', 'partnership'], jurisdictions: 'jurisdiction[]: such as US, US-CA', books: 'url: a read-only link to the ledger, such as https://books.example/acme',
    owners: 'party[]?: each owner of a partnership or an S corp', change: 'text?: what changed since the return was filed',
  },
  output: { return: 'file: the return packet, every jurisdiction', returns: 'file[]', workpapers: 'file', statements: 'file[]?: each owner\'s adjusted statement', tax: 'money{}', balance_due: 'money{}', carryforwards: carryforwardList, receipts: 'text[]' },
  rules,
  subject: { kind: 'taxpayer', key: 'ein', period: { of: 'tax_year', many: true }, keeps: { carryforwards: 'carryforwards', tax: 'tax' } },
  roles: { preparer: { pool: preparers, pay: job => 90 + 15 * job.input.jurisdictions.length }, signer: { pool: preparers, pay: job => 45 + 15 * job.input.jurisdictions.length } },
  deliver: {
    compute: { by: schedules, cite: 'depreciation and carryforward schedules' },
    draft: { by: 'ai', cite: 'the workpapers and the records the organizer took' },
    check: { review: 'adversarial', lenses: ['what changed', 'numeric tie-out'] },
    prepare: { by: 'preparer', est: '1h', checklist: ['every changed line traces to a source'] },
    sign: { by: 'signer', stamp: { block: 'paid preparer', firm: 'brand' }, est: '20min' },
    authorize: { ask: 'owner', to: 'sign', what: authorization, how: 'id-verified', capacity: 'officer', void: { tolerance: { balance_due: 1 } } },
    file: { submit: efile, to: 'IRS and states', receipt: 'receipts', each: { of: 'jurisdictions', lead: j => j === 'US' }, rejected: { '*': 'prepare' }, when: job => job.input.entity !== 'partnership' },
    aar: { submit: efile, to: 'IRS', form: '8082', receipt: 'receipts', when: job => job.input.entity === 'partnership', emit: { 'k1.adjusted': job => job.input.owners ?? [] } },
  },
  price: { usd: 900, within: '30d', release: ['file.accepted', 'aar.accepted'] },
})
```

```bash
npx services.dev check   # after services/amended-return.ts
```

```txt title="captured/tax-build.txt · magic values: none"
services/amended-return.ts · 22 lines · $900 per job, within 30 days, charged when file or aar is accepted.
  plan    Launch (services.dev) · subject at :20 · roles.preparer.pool at :21
  ✗ The rules table binds a name nothing reads: a key that names no Service file, or a row no step performs that is neither an advice class nor refused. Rename it to what it means, or take it out. rules.services has a key 'annual-return', and no Service file is services/annual-return.ts. rules.services has a key 'information-returns', and no Service file is services/information-returns.ts. rules.services has a key 'transcript', and no Service file is services/transcript.ts.
✗ 1 to fix
```

## services/carryforward-watch.ts

```bash
cat services/carryforward-watch.ts
```

```txt title="captured/tax-build.txt · magic values: none"
import { Service } from 'services.dev'
import { earliestExpiry } from '../lib/tax.ts'

// The steward a taxpayer keeps once no plan runs: it answers for the dates a return leaves on the subject. The brand
// owns the carryforwards' expiry, gives notice 180 days out, and sends a letter each year of what is still open.
export default Service({
  name: 'Carryforward Watch',
  description: 'Carryforwards, watched until they expire.',
  input: { owner: 'party: who hears the notice', ein: 'text: such as 12-3456789' },
  output: { letter: 'file: what is still open, and when it expires' },
  subject: { kind: 'taxpayer', key: 'ein' },
  deliver: { letter: { by: 'ai', cite: 'the carryforwards the taxpayer keeps' } },
  clocks: { expiring: { due: earliestExpiry, owner: 'brand', lives_on: 'subject', rungs: ['notice 180d'] } },
  price: { usd: 9, every: 'year' },
})
```

```bash
npx services.dev check   # after services/carryforward-watch.ts
```

```txt title="captured/tax-build.txt · magic values: none"
services/carryforward-watch.ts · 8 lines · $9 per year, charged at the start of each year.
  clocks  expiring · a date your code decides · the brand · on the taxpayer
  plan    Launch (services.dev) · subject at :11 · clocks.lives_on: 'subject' at :13 · price.every at :14
  ✗ The rules table binds a name nothing reads: a key that names no Service file, or a row no step performs that is neither an advice class nor refused. Rename it to what it means, or take it out. rules.services has a key 'annual-return', and no Service file is services/annual-return.ts. rules.services has a key 'information-returns', and no Service file is services/information-returns.ts. rules.services has a key 'transcript', and no Service file is services/transcript.ts.
✗ 1 to fix
```

## services/annual-return.ts

```bash
cat services/annual-return.ts
```

```txt title="captured/tax-build.txt · magic values: none"
import { Service } from 'services.dev'
import { assessmentCloses, authorization, carryforwardList, claimsRefund, earliestExpiry, efile, extendedDue, extension, fileDue, lateCost, lateFiling, payDue, reask, refundCloses, requestList, schedules, tentative } from '../lib/tax.ts'
import { preparers } from '../pools/preparers.ts'
import rules from '../rules.ts'
import amendedReturn from './amended-return.ts'
import carryforwardWatch from './carryforward-watch.ts'

// The taxpayer keeps last year's carryforwards by value, each jurisdiction's tax and each payment, so the request list,
// the schedules and next year's estimates start from them. The officer signs a §7216 consent naming where the return may
// go. A signer reads last year's return beside this one and signs as the paid preparer for the brand's firm; the officer
// signs the e-file authorization, voided by a $1 change; the federal return leads and each state follows on its own date.
// Three days out, unfiled, the clock deposits the tentative tax and files each extension its state's rule needs: the date
// to file moves as far as that rule says, the date to pay never does. The signer stays of record to the assessment date.
// A missed date's penalty and its interest are paid from the brand's line once a notice response asks to take them off.
export default Service({
  name: 'Annual Return',
  description: 'Returns, e-filed.',
  input: {
    business: 'party', owner: 'party: the officer who signs', ein: 'text: such as 12-3456789', tax_year: 'text: such as 2026',
    entity: ['C corp', 'S corp', 'partnership'], jurisdictions: 'jurisdiction[]: the federal return first, then each state, such as US, US-CA',
    books: 'url: a read-only link to the ledger, such as https://books.example/acme', owners: 'party[]?: each owner of a partnership or an S corp, with their share',
    year_end: 'text?: MM-DD, 12-31 unless set', extended_to: 'date?: an extension already filed',
  },
  output: {
    return: 'file: the return packet, every jurisdiction', returns: 'file[]', workpapers: 'file', k1s: 'file[]?: one for each owner', tax: 'money{}', balance_due: 'money{}',
    carried_in: carryforwardList, carryforwards: carryforwardList, tentative_due: 'money{}?', payment_usd: 'money{}?', first_installment: 'money{}?', receipts: 'text[]', extensions: 'text[]', payment: 'text[]?',
  },
  rules,
  subject: {
    kind: 'taxpayer', key: 'ein', period: { of: 'tax_year', ends: job => job.input.year_end ?? '12-31' },
    of_record: { role: 'signer', named_at: 'sign', custodian: ['signer', 'brand'], until: 'assessment' },
    keeps: { carryforwards: 'carryforwards', tax: 'tax', paid: 'payment_usd', first_installment: 'first_installment' }, steward: carryforwardWatch,
  },
  roles: { preparer: { pool: preparers, pay: job => 90 + 15 * job.input.jurisdictions.length }, signer: { pool: preparers, pay: job => 45 + 15 * job.input.jurisdictions.length } },
  deliver: {
    consent: { ask: 'owner', to: 'consent', what: '§7216: use and disclosure of return information', uses: ['prepare'], may: ['disclose: a preparer outside the US', 'disclose: a lender you name'], expires: '1y', capacity: 'officer' },
    organizer: { ask: 'owner', to: 'upload', items: requestList, expires: '180d' },
    compute: { by: schedules, after: 'organizer.books', cite: 'depreciation and carryforward schedules' },
    draft: { by: 'ai', cite: 'the workpapers and the records the organizer took' },
    check: { review: 'adversarial', lenses: ['reviewer', 'compliance', 'numeric tie-out'] },
    sign: { by: 'signer', stamp: { block: 'paid preparer', firm: 'brand' }, compare: 'prior', est: '30min', checklist: ['every line traces to a source', 'due diligence answered', "the officer's e-file authorization shows this return's figures"] },
    authorize: { ask: 'owner', to: 'sign', what: authorization, how: 'id-verified', capacity: 'officer', void: { tolerance: { balance_due: 1 } } },
    file: {
      submit: efile, to: 'IRS and states', receipt: 'receipts', each: { of: 'jurisdictions', lead: j => j === 'US' },
      rejected: { ein_mismatch: { to: 'organizer', items: reask.ein_mismatch }, schedule_missing: { to: 'organizer', items: reask.schedule_missing }, math: 'draft', '*': 'prepare' }, perfect: '10d',
    },
    prepare: { by: 'preparer', routed: true, est: '2h', checklist: ['the rejection is answered'], next: () => 'sign' },
    tentative: { by: tentative, routed: true, cite: 'the year as the ledger stands, less the estimates paid', next: () => 'pay' },
    pay: { routed: true, submit: 'https://treasury.example.test/payments', to: 'Treasury and states', receipt: 'payment', each: { of: 'jurisdictions' }, next: () => 'extend' },
    extend: { routed: true, submit: extension, to: 'IRS and states', receipt: 'extensions', each: { of: 'jurisdictions', lead: j => j === 'US' } },
  },
  clocks: {
    file: { due: fileDue, each: { of: 'jurisdictions' }, owner: 'of_record', rungs: ['notice 90d', 'notice 30d', 'run tentative 3d', 'ack daily 5d'], moves: { extend: extendedDue }, exposure: lateFiling, guarantee: { covers: ['penalties', 'interest on them'], up_to: 5_000, after: 'notice-response', excludes: 'held' } },
    pay: { due: payDue, owner: 'buyer', hard: true, rungs: ['notice 14d', 'escalate 3d'], satisfied_by: ['pay', 'file'], exposure: lateCost },
    assessment: { due: assessmentCloses, owner: 'brand', lives_on: 'subject', rungs: ['notice 30d'] },
    refund: { due: refundCloses, owner: 'of_record', lives_on: 'subject', hard: { bars: [{ step: 'amended-return.file', when: claimsRefund }] } },
    expiring: { due: earliestExpiry, owner: 'brand', lives_on: 'subject', rungs: ['notice 180d'] },
  },
  ends: { accepted: { follow: [amendedReturn], emit: { 'k1.issued': job => job.input.owners ?? [] } } },
  price: { usd: 1_800, within: '5d before file', late: '20%', release: 'file.accepted', lines: { state_fees: { at: 'cost', from: 'file' } } },
})
```

```bash
npx services.dev check   # after services/annual-return.ts
```

```txt title="captured/tax-build.txt · magic values: none"
services/annual-return.ts · 45 lines · $1,800 per job, filed 5 days before its deadline, 20% off if late, or we pay the penalty and its interest, up to $5,000, after we ask the IRS to take it off, charged when file is accepted.
  clocks  file · a date your code decides, one for each jurisdiction · the member of record · moves on extend to a date your code decides · about 5% of the unpaid tax a month late, or $245 an owner · guarantee: penalties and their interest, up to $5,000, after notice-response, never for held time, from your line
          pay · a date your code decides · the buyer · hard: nothing moves it · about 0.5% of the unpaid tax a month late
          assessment · a date your code decides · the brand · on the taxpayer
          refund · a date your code decides · the member of record · on the taxpayer · hard: bars amended-return.file when claimsRefund
  ends    accepted · offers amended-return · emits k1.issued to each owner
  lines   state_fees · at cost, from file's receipts · leaves the take's base with its receipt
  plan    Team (services.dev) · subject.of_record.custodian at :31 · subject.steward at :32 · clocks.<c>.guarantee at :53
  steps   compute · code · cites prior-year safe harbor, cumulative by quarter → approve · owner · decide → review · ai · adversarial → pay · Treasury and states · one for each jurisdiction with an installment → record · code
  ✗ The rules table binds a name nothing reads: a key that names no Service file, or a row no step performs that is neither an advice class nor refused. Rename it to what it means, or take it out. rules.services has a key 'information-returns', and no Service file is services/information-returns.ts. rules.services has a key 'transcript', and no Service file is services/transcript.ts.
✗ 1 to fix
```

## services/information-returns.ts

```bash
cat services/information-returns.ts
```

```txt title="captured/tax-build.txt · magic values: none"
import { Service } from 'services.dev'
import { payees } from '../lib/tax.ts'
import rules from '../rules.ts'

// The calendar year's 1099s, by 31 January, whatever the tax year. The AI reads the ledger for every vendor paid $600 or
// more for services, your code lists them, and each payee with no W-9 on file is asked for one. Every name and TIN is
// matched under the brand's own TIN matching, the officer confirms the list, and the 1099-NECs are e-filed under the
// brand's transmitter code and sent to each payee. $15 a payee; the Tax Plan includes 25 a year. The date is the brand's.
export default Service({
  name: 'Information Returns',
  description: "The year's 1099s, filed by 31 January.",
  input: { owner: 'party: the officer who confirms', ein: 'text: such as 12-3456789', year: 'text: the calendar year, such as 2026', books: 'url: a read-only link to the ledger, such as https://books.example/acme' },
  output: { ledger: 'json: each payment to a vendor', payees: 'number', w9_asked: 'text[]', forms: 'file', receipt: 'text' },
  rules,
  subject: { kind: 'taxpayer', key: 'ein', period: 'year' },
  deliver: {
    read: { by: 'ai', cite: "the ledger's payments to each vendor" },
    list: { by: payees, cite: 'every payee paid $600 or more for services' },
    w9: { ask: 'each payee', to: 'upload', what: 'Form W-9', for: 'w9_asked', expires: '21d' },
    match: { submit: 'https://tin-matching.example.test/match', to: 'TIN matching', receipt: 'receipt' },
    confirm: { ask: 'owner', to: 'decide', choices: ['file them', 'hold one, in writing'] },
    file: { submit: 'https://iris.example.test/submit', to: 'IRS, through IRIS', receipt: 'receipt' },
    send: { by: 'ai', cite: "each payee's copy, by mail, or by email where they consented" },
  },
  clocks: { file: { due: job => `${Number(job.input.year) + 1}-01-31`, owner: 'brand', rungs: ['notice 14d', 'escalate 3d'] } },
  price: { usd: 15, per: 'payees', release: 'file' },
})
```

```bash
npx services.dev check   # after services/information-returns.ts
```

```txt title="captured/tax-build.txt · magic values: none"
services/information-returns.ts · 17 lines · $15 per payee, charged when file is accepted.
  bounds  open match, under the brand's tin_matching · open file, under the brand's iris
  ✗ The rules table binds a name nothing reads: a key that names no Service file, or a row no step performs that is neither an advice class nor refused. Rename it to what it means, or take it out. rules.services has a key 'transcript', and no Service file is services/transcript.ts.
✗ 1 to fix
```

## The plan, and the transcript

[The Tax Plan](https://services.dev/docs/examples/tax/tax-plan.md) and [the transcript](https://services.dev/docs/examples/tax/transcript.md) each show their file. The transcript reads the table's last name, and the brand is ready to sell:

```bash
npx services.dev check   # after services/tax-plan.ts
```

```txt title="captured/tax-build.txt · magic values: none"
services/tax-plan.ts · 18 lines · $2,400 per year, charged at the start of each year, 1 annual-return, 25 information-returns payees, 4 estimate, 12 question and 2 notice-response included each year.
  plan    Team (services.dev) · on.event at :29, past the Tax Plan's allowance: fact.restated mints an amended return
  ✗ The rules table binds a name nothing reads: a key that names no Service file, or a row no step performs that is neither an advice class nor refused. Rename it to what it means, or take it out. rules.services has a key 'transcript', and no Service file is services/transcript.ts.
✗ 1 to fix
```

```bash
npx services.dev check   # after services/transcript.ts
```

```txt title="captured/tax-build.txt · magic values: none"
services/transcript.ts · 18 lines · roadmap · $12 per job, within 48 hours, charged on order's receipt, or at disclose for a return the brand filed.
✓ ready to sell · 9 Services · AI delivers 2 alone · each keeps at least 50% of every job
```

## services/year-end-close.ts

```bash
cat services/year-end-close.ts
```

```txt title="captured/tax-build.txt · magic values: none"
import { Service } from 'services.dev'

// The year's books, closed before the return reads them. The AI reconciles each account to its statement and proposes
// the adjusting entries, an adversarial tie-out checks them, and the officer decides which to post. For $900 instead of
// $300, an accountant from gigs.accountants reviews the close and attests it in their own session: the brand imports that
// Pool, so it recruits no one and pays its operator a placement line on each filled review. The closed books are kept on
// the taxpayer for the year, and that year's return opens with its books item answered.
export default Service({
  name: 'Year-End Close',
  description: "A year's books, reconciled and closed.",
  input: {
    owner: 'party: the officer who decides', ein: 'text: such as 12-3456789', tax_year: 'text: such as 2026',
    books: 'url: a read-only link to the ledger, such as https://books.example/acme', statements: "file[]: each account's year-end statement",
  },
  output: { trial_balance: 'file', entries: 'file: each adjusting entry, with its source', closed_books: 'url: the books, closed for the year' },
  subject: { kind: 'taxpayer', key: 'ein', period: 'tax_year', keeps: { closed_books: 'closed_books' } },
  roles: { accountant: { pool: 'gigs.accountants/accountants', pay: 250 } },
  deliver: {
    reconcile: { by: 'ai', cite: 'each account against its statement' },
    check: { review: 'adversarial', lenses: ['numeric tie-out', 'cutoff'] },
    post: { ask: 'owner', to: 'decide', choices: ['post every entry', 'post some, in writing'] },
  },
  price: { usd: 300, approved: 900 },
})
```

```bash
npx services.dev check   # after services/year-end-close.ts
```

```txt title="captured/tax-build.txt · magic values: none"
  ledger  +$900.00 · AI $1.26 (example) · preparer $150.00 · signer $105.00 · you $643.74 · 4 jurisdictions
services/year-end-close.ts · 15 lines · $300 per job, or $900 with an accountant's approval, within 48 hours, charged on delivery.
  roles   accountant · from gigs.accountants/accountants, a Pool another operator runs · $250 an approval · placement $20.00 a filled review
  import  gigs.accountants · its operator runs the roster, verification, dispatch and payouts · terms pinned at 2026-09 · $250 is at or above its $200 minimum
          +$900.00 · AI $1.26 (example) · accountant $250.00 · placement $20.00 · you $628.74 · approved
  plan    Launch (services.dev) · subject at :16 · roles.accountant.pool at :17, gigs.accountants/accountants, a Pool another operator runs
✓ ready to sell · 10 Services · AI delivers 2 alone · each keeps at least 50% of every job
```

[Every branch](https://services.dev/docs/examples/tax/branches.md) runs what these files can do, and [its plans](https://services.dev/docs/examples/tax.md#its-plans) read the folder back as one.

---

- Up: [The tax example](https://services.dev/docs/examples/tax.md)
- Previous: [The tax example](https://services.dev/docs/examples/tax.md)
- Next: [One job, every side](https://services.dev/docs/examples/tax/walk.md)
