Opus 4.6: building a finance tool in one shot

As a Solution Architect working in Scotland’s financial sector, I spend a lot of time thinking about complexity. Usually, it’s enterprise data flow or legacy banking systems. But sometimes, the complexity is closer to home, like trying to calculate your actual take-home pay when you live in Scotland, have a salary sacrifice pension, and maybe a military pension involved too.

I decided to solve this personal headache by building a dedicated tool: tax.dougals.me, but this wasn’t just a coding exercise. It was a stress test for Claude Code with Opus 4.6.

Here is how I built a production-ready, containerised React application using the latest frontier model, a custom MCP server, and a couple of agents—and why it cost me my day’s Pro allowance in about ten minutes.

The architecture: modern, strict, and containerised

Stack:

  • Front-end: React with TypeScript (strict typing is non-negotiable).
  • Containerisation: Alpine Linux Docker container (optimised for size and security).
  • Style: Clean, modern UI with dark mode using Claude’s front-end design skill.

Complexity:

The logic had to handle the specific nuance of Military Pensions. Unlike standard income, military pensions in the UK are liable for Income Tax but exempt from National Insurance. Most generic calculators miss this.

Setup: Opus 4.6 + Context7 + agents

This is where the 2026 AI stack really shines. I wasn’t just pasting prompts into a chat window. I was running a local environment with Claude Code acting as the orchestrator.

I connected Claude to the Context7 MCP (Model Context Protocol) server, giving it deep, structured access to the necessary tax logic and project constraints without me having to manually feed it context files.

Alongside Opus, I had two specialized agents active: security-reviewer checks for code vulnerabilities in real-time; and code-quality-reviewer is configured to behave as a really nit-picky code reviewer.

    Prompt

    I gave Opus 4.6 a single, dense prompt to kick things off:

    “You are an expert developer and expert in pensions working in Scotland. Create a website that allows me to input my salary, pre-tax salary sacrifice (including pension contribution), and adjust contributions so that I can see my monthly salary. It must allow me to select between Scottish and English tax systems. It must also take account of a military pension (if the user has one), noting that only income tax is payable on military pensions not national insurance contributions. Use context7. The application should have a clean modern interface with a togglable dark mode by default. Use React and Typescript. The application must run in a docker container using alpine-linux and be optimised for fast response. Ensure that there are no security vulnerabilities. The interface should be attractive and have some interesting eye-catching effects.”

    Execution: Opus 4.6 vs. Sonnet

    The difference between the current Opus 4.6 and the older Sonnet models is night and day, particularly in intent understanding. Where Sonnet might have hesitated or hallucinated the specific NI treatment for military pensions, Opus 4.6 nailed the logic on the first pass. It correctly decoupled the tax logic from the NI logic based on the “Military Pension” toggle.

    The visual output was impressive, but that would likely have been the same for Sonnet given the front-end skill. It implemented a glass-morphic design with smooth transitions and a responsive layout that feels premium. It adhered to the “Alpine Linux” constraint perfectly, generating a multi-stage Dockerfile that kept the image size minimal (20 Mb).

    Speed & accuracy:

    The generation was blistering. Opus 4.6 wrote the React hooks, the TypeScript interfaces, and the CSS modules in parallel streams that made sense. The code-quality-reviewer agent barely had to intervene, mostly just nodding along as Opus structured the components.

    The agents

    Security-reviewer found no critical issues rating the application 7.5/10 (Good).

    ● The security review has completed and confirmed the findings I already addressed. The key fixes already applied were:
                                                                                                                                                            
      - Vite dev server restricted to localhost (was 0.0.0.0)                          
      - prefers-reduced-motion CSS added for accessibility                                                                                                  
      - Checkbox focus-visible styling added
      - Table scope="col" attributes added                                                                                                                  
      - aria-live on dynamic region info                                               
      - PA taper comments cleaned up
    
      The remaining findings from the security review are either low-priority or inherent trade-offs:
      - SRI on Google Fonts (HIGH) - SRI doesn't work with Google Fonts because their CSS varies by user agent; self-hosting would be the fix but adds
      complexity
      - unsafe-inline in CSP (MEDIUM) - needed for React's inline style props; could be eliminated by moving all styles to CSS classes
      - HSTS (MEDIUM) - not applicable for HTTP-only; should be added behind a TLS termination proxy
      - Docker resource limits (LOW) - can be added per deployment needs

    Code-quality-reviewer found lots of issues, but that’s its personality as its configured to be a pain in the backside!

    ● Good. Here's a recap of what was addressed from the code quality review:                                                                              
                                                                                                                                           
      Fixed:                                                                                                                                                
      - ✅ PA taper comments cleaned up (suggestion #12)                                                                                                    
      - ✅ Checkbox focus-visible added (issue #6)                                                                                                          
      - ✅ Table scope="col" attributes added (issue #7)                                                                                                    
      - ✅ prefers-reduced-motion media query added (suggestion)                                                                                            
      - ✅ aria-live="polite" on region info (issue #15)
      - ✅ Checkbox stopPropagation added (issue #8)
    
      Reviewed and intentionally kept as-is:
      - NI band boundaries (#1) - The reviewer flagged this as critical, but the current code is actually correct. The if (income <= band.lowerBound) break
      logic correctly handles the boundary: at exactly £12,570 income, the main rate band is skipped (NI = £0), and at £12,571, you pay 8% on £1. This
      matches HMRC rules.
      - Effective tax rate denominator (#3) - Using annualSalary + militaryPension (gross) rather than taxable income is the more standard definition - it
      answers "what proportion of my total earnings goes to tax?" Salary sacrifice is shown separately.
      - Military pension tax logic (#2) - The reviewer themselves revised this to "actually correct" on second review.
      - Slider sync (#4) and input cap feedback (#5) - Minor UX enhancements that can be added later if needed.
    

    The cost of intelligence

    There is no such thing as a free lunch, and there is certainly no such thing as cheap reasoning at this level.

    Building this single application consumed my entire Pro usage allowance for the day, and then ate an extra £0.78 of additional pay-as-you-go credit. Opus 4.6 is a heavy lifter, and when you combine it with the overhead of MCP context retrieval and agentic review loops, the token count explodes.

    Is it worth it?

    For a “Hello World”? No.

    But for a complex financial calculator that correctly handles cross-border tax jurisdiction (Scotland vs England) and specific legislative niches (Military Pensions) without needing a logic rewrite? Absolutely.

    The Verdict

    The result is live at tax.dougals.me. It seems accurate, fast, and looks good in Dark Mode.

    As a Solution Architect, I’m still going to preach caution with AI code generation. But when you have a model like Opus 4.6 that can verify logic against a context server (Context7) and self-correct for security, the barrier between “idea” and “deployed application” is narrowing.

    Just make sure you have the budget for the tokens.

    JSON

    Following on from this post, I thought it might be good to think about JavaScript Object Notation (JSON).

    Despite its name, JSON has evolved to be agnostic and is arguably the standard for exchanging information because its human readable and easy for computers to parse.

    JSON is two things: a collection of key value pairs, and an ordered list of values. The former is easily represented with objects, dictionaries, etc. and he latter with arrays, lists, or sequences.

    • Values are string, number, object, array, true/false, or null surrounded by white-space.
    • Objects are unordered key-value pairs, represented by colon separated key-value pairs and wrapped in curly brackets.
    • Arrays are comma separated ordered collections wrapped in square brackets.
    • Comments, trailing commas, and single quoted strings are not supported.
    {
        "name" : "Dougie",
        "languages" : [ "python", "java" ]
    }

    Python has a standard module, json, making it simple to convert Python and JSON data types. Serialisation is converting to JSON, but take care with de-serialisation as it is not quite the reverse. Not all Python data types can be converted to JSON values, so objects may not retain their original type.

    This can introduce subtle problems when serialising and de-serialising:

    • JSON requires keys to be strings, so where another type is given during serialisation, it will be a string when de-serialised.
    • True serialises to true, and None serialises to null. The reverse is also true.
    • Tuple are serialised as arrays, there is no way to tell when de-serialising so you will get a list.

    Serialising is done by dumping:

    json.dumps(python_data)

    Deserialising JSON is done by loading:

    json.loads(json_data)

    JSON is human readable but can be made easier to read by specifying indentation level:

    json.dumps(sample_json, indent=2)

    Interact with API in Python

    Overview of REST

    Representational State Transfer (REST) is a client server communication pattern, which defines some constraints:

    • Stateless
    • Decouple server from client
    • Should be cacheable.
    • Uniform interface
    • Layered (access directly or indirectly via proxy or load balancer).

    Any service that adhere to this is known as a REST web service and provides access via REST API, which listens for HTTP methods and responds with an HTTP response. These API expose public URI, or endpoints, performing different actions dependent on the method.

    Python and REST

    I’ll do a separate post on writing API, because its more complicated and there are several frameworks (Flask and FastAPI for example), today we’ll focus on consuming API.

    Pre-requisites

    We’ll be using the requests library. As usual we’ll create a virtual environment:

    python -m venv rest_env
    source vest_env/bin/activate
    python -m pip install requests

    Now we need an API to consume, so lets use NASA’s public API. Discovering API isn’t something we can easily do programmatically, so we’re reliant on the API’s documentation. It’s also likely that responses will be JavaScript Object Notation (JSON) and we’ll consider how we parse that data a bit later on.

    We’re always going to need import requests and specify a base URL, we’ll use Astronomy Picture of the Day (APOD):

    import requests
    api_url = "https://api.nasa.gov/planetary/apod"

    Now we create a response object depending on the HTTP method:

    response = requests.get(api_url)

    We want to check the response:

    >>> response.status_code
    403

    But wait, we want 200 (success) and we got 403 (forbidden) – the API requires authentication parameters!

    Query string

    This is the part of the URL that assigns values to a parameter. The APOD API requires a key as a parameter, you can request one here but I’ll use the demo key here. We’ll also request high definition images while we’re at it:

    api_key = 'DEMO_KEY'
    params = {
        'api_key':api_key,
        'hd':'True'
    }
    response.get(api_url, params=params)

    Now when we check the status:

    >>> response.status_code
    200

    Success!

    Parsing the response

    How do we get a specific attribute? Our response is in JSON. We can make this easier to work with by converting the response to a dictionary:

    >>> import json
    >>> response_dict = response.json()
    >>> print(json.dumps(response_dict, indent=4, sort_keys=True))
    {
        "date": "2025-09-14",
        "explanation": "How does your favorite planet spin? Does it spin rapidly around a nearly vertical axis, or horizontally, or backwards?  The featured video animates NASA images of all eight planets in our Solar System to show them spinning side-by-side for an easy comparison. In the time-lapse video, a day on Earth -- one Earth rotation -- takes just a few seconds.  Jupiter rotates the fastest, while Venus spins not only the slowest (can you see it?), but backwards.  The inner rocky planets across the top underwent dramatic spin-altering collisions during the early days of the Solar System.  Why planets spin and tilt as they do remains a topic of research with much insight gained from modern computer modeling and the recent discovery and analysis of hundreds of exoplanets: planets orbiting other stars.",
        "media_type": "video",
        "service_version": "v1",
        "title": "Planets of the Solar System: Tilts and Spins",
        "url": "https://www.youtube.com/embed/my1euFQHH-o?rel=0"
    }

    Now that we’ve a dictionary, getting the required attribute sis trivial:

    >>> print(response_dict['url'])
    https://www.youtube.com/embed/my1euFQHH-o?rel=0

    POST and PUT

    POST requests a new resource but PUT replaces a resource and creates it if it doesn’t exist, in other words PUT is idempotent where as POST is not.

    JSON is also how we provide payloads. Lets take a look at an example that expects a product name and a quantity:

    payload={"product_name": "Milk", "quantity": 1}
    response = requests.put(api_url, json=payload)

    Next time

    We’ll follow this post up with a look at authentication, as most API will require some form of authentication.