Skip to main content
Contact us
A tablet showing the ambassador list and the Excel workbook it exports for one partner

Project Story

An ambassador management system that replaced spreadsheet reporting

Octagon manages the athletes and other talent on contract with Mastercard. We built the system they run that program in, including a report engine that writes the same Excel workbooks Octagon used to fill in by hand, and expiring share links for public ambassador profiles.

Client
Mastercard, for Octagon
Industry
Sponsorship and talent management
When
2025 to 2026
Status
Live. Reporting phases 1 to 3 shipped in 2026
  • Next.js
  • TypeScript
  • tRPC
  • Prisma
  • PostgreSQL
  • ExcelJS
  • Amazon S3
  • Radix UI

What it keeps track of

Each ambassador has a contract, and each contract promises a set number of four things: appearances, appearance hours, social posts and content shoot hours. Octagon’s team has to know how much of that has been used and when each contract runs out.

We started building the system in February 2025. It holds ambassador profiles and contracts, appearance requests and inquiries from people outside the system. Admins, owners and managers each get their own level of access. It’s a Next.js app with tRPC over PostgreSQL through Prisma, and proof files from past appearances live in Amazon S3. A few weeks into the first build, Octagon’s project manager told us they were “in an exceptionally good place and scarily ahead of schedule.” That was the platform. The reporting still ended in Excel.

One place for the math

In August 2026 Octagon sent us the spreadsheets their team was filling in by hand from the system’s exports. Before changing any of the math, we wrote characterization tests that pinned exactly what the old export produced, so we’d know if the rebuild moved a number.

Then we pulled all the utilization math into one pure module with no database access. Every report reads from it. The rules it follows came straight from Octagon’s workbooks:

  • Utilization is stored as a ratio (0.68 for 68%) and only rounded when it’s displayed.
  • Remaining is never clamped. A negative number means the ambassador delivered more than the contract asked for, and each report decides how to show that.
  • “Total Assets” is the straight sum of the four asset counts, the same as the Total Assets row in Octagon’s files.
  • Dollar utilization is value used over total value, computed from the dollars instead of from the counts.

The rebuild did move one number on purpose. One of the older reports built its all-assets total by counting appearances and adding one for each other asset type present, which didn’t match Octagon’s sheets. The characterization tests name that change, and every other output matched the baseline.

Reports that come out as Octagon's own workbooks

Each report is a short definition: a key, a filter over the shared data layer, a summary tab, the tabs to build and a filename. One runner sits behind all of them. It fetches the filtered records, aggregates them, hands them to the tab builders and returns an .xlsx buffer. The registry is a plain list of entries, and adding a report means writing a definition. Four run through it today.

  • End of Contract, quarterly. It picks every ambassador whose expired contract ended in that quarter, then shows their whole contract history, because that’s how Octagon’s file stacked it. The first tab is named for the quarter (“Q1 2026”) and the remaining column is labeled Assets Lost. Ambassadors are grouped by passion (golf, for example).
  • By-Passion, monthly. Golf was the first one, and we generalized it to every category. An ambassador with more than one passion shows up in full in each one. The numbers are a snapshot as of the last instant of the month, matching how Octagon’s July file counted.
  • Master Doc, the year-long workbook Octagon used to maintain by hand, now generated. For a past year it freezes at December 31, so regenerating it later gives the same numbers.
  • Ambassador Report, the original export, moved onto the same runner with its output pinned by the tests.

Exports can be filtered four ways (ambassador, passion, region, contract end date), and the summary tab recalculates for whatever’s left. Dollar values per asset are admin-only. Whether to include them is a required option on every run with no default, so a manager’s export omits the value columns and rate tables entirely, headers included.

Ambassador records are filtered and exported into the workbook tabs Octagon's team already used.

Matching the formatting cell by cell

The last pass was fiddly. We generate the workbooks with ExcelJS and put our output beside Octagon’s reference files until they matched.

The body font is Arial 11, as in every one of their workbooks. Header rows are bold, bordered and filled in Mastercard yellow. The label column is 30 characters wide and data columns are 18. Total Assets rows are bold, and the list tabs people scroll have the header row frozen. Quantities can be halves, so they use a 0.## format. Money uses an accounting format with a $- zero, the way Octagon’s example workbook showed it. Totals rows carry cents.

Empty prices took the most back and forth. A rate of zero in the database means nobody entered one, so rate cells stay blank instead of reading $0. Utilization stays blank too when nothing was priced, since 0 out of 0 isn’t 0%. Octagon’s own sheet showed #DIV/0! there, which was a formula artifact we chose not to copy.

Sheet names have their own rules in Excel. We strip the characters Excel rejects and trim the edges, including the trailing spaces some of Octagon’s own tab names had. Names are capped at Excel’s 31 characters, and two ambassadors whose names collide get a (2) suffix, compared case-insensitively the way Excel does.

Profiles you can share with a link

Octagon also needed a way to show people outside the system which ambassadors are available to work with. So an admin can now create a share link and revoke it whenever they like. The expiry choices are 7, 30 or 90 days, or none at all. A link created without an expiry gets 30 days. One that never expires has to be asked for explicitly.

The link opens a roster people can filter by category, region or gender. Each ambassador has a profile page with a bio, social accounts with follower counts, their Mastercard partnership details and what’s still left on their contract. That’s counted in appearances, hours and posts. Dollar values stay internal. Only ambassadors under an active contract appear. An Inquire button sends the request into the same inquiry queue Octagon already works from, and admins can choose to add proof from past appearances to a profile.

Under the hood, each link is a random UUID stored with a nullable expiry and a revoked flag. Every public read checks the token again, so a revoked link stops working on the next request. Unknown, expired and revoked tokens all get the same not-found answer, so someone probing links can’t tell which is which. The admin list shows when each link was last opened, stamped at most once every five minutes. Filter inputs are capped at 100 values of 120 characters each, so a link holder can’t hand the database an unbounded query. Proof files go out as presigned S3 links that last an hour, and an open profile page fetches fresh ones every 30 minutes.

An admin creates a share link, and it opens a polished ambassador profile showing what's still available on the contract.

What a link holder can see

These pages are open to anyone holding the link, so they read from a fixed list of fields through Prisma select projections. Anything not on the list is never queried, including internal notes, contract dates and quotas, the four dollar rates, request history and anything about the people who made requests.

The tests go a step further than checking the payloads. They read every column on the database models behind the public pages from Prisma’s schema metadata, and each one has to be classified as public or internal. A new column fails the suite until someone decides which side it’s on, and a new public endpoint fails it the same way.

What we can't show you yet

We don’t have usage numbers for the new reports to share, so we’re not going to guess at time saved. We’ve also proposed letting Octagon’s team ask questions of the data in plain English. That part isn’t built.

Questions about this project

HQ, a software design and development agency in Ogden, Utah, built the ambassador management system Octagon uses to run Mastercard's ambassador program. It tracks ambassador contracts against four assets (appearances, appearance hours, social posts and content shoot hours) and generates Octagon's quarterly End of Contract, monthly By-Passion and yearly Master Doc reports as Excel workbooks in the same layout Octagon's team used to fill in by hand.

HQ first wrote characterization tests that pinned the old export's numbers. It then moved all utilization math into one pure module and defined each report as data run by a single report runner, with the workbooks generated by ExcelJS. Finally HQ compared the output with Octagon's reference files and matched fonts, column widths, header fills, bold totals, frozen headers, number formats, tab names and how unpriced values appear.

In the system HQ built for Octagon, utilization is used divided by contracted, stored as a ratio and rounded only for display. Remaining is never clamped, so over-delivery shows as a negative number. Total Assets is the straight sum of appearances, appearance hours, social posts and content shoot hours, matching the client's workbooks. Dollar utilization is computed from dollar values. Where no rate was entered, the rate cell stays blank and value cells show an accounting dash. Utilization is left empty there instead of showing 0%.

Yes. Admins create share links that expire after 7, 30 or 90 days or never (30 days is the default) and can revoke them at any time. A link opens a filterable roster and individual ambassador profiles showing a bio, social accounts, partnership details and remaining contract availability as counts, with an Inquire button that feeds Octagon's inquiry queue. The public pages read only from a fixed list of fields, and tests classify every database column as public or internal so a new one can't reach the public pages by accident.

Each share link HQ built is a random UUID token stored with an optional expiry and a revoked flag, and it is checked on every public request, so revoking a link takes effect immediately. Unknown, expired and revoked tokens return the same not-found response. Filter inputs are length-capped, proof files are served as presigned Amazon S3 links that expire after an hour, and dollar values and internal notes are never selected for the public pages.

HQ started building the system for Octagon in February 2025. The reporting rebuild and the shareable ambassador profiles were delivered in three phases in August and September 2026.

Let's build together

Whether you're looking to design, build, update, or refresh an existing app or website, we can help. Even if we're not a good fit, we will help point you to somebody who might be.

Contact us