<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Purvi</title>
    <description>The latest articles on DEV Community by Purvi (@purvi09).</description>
    <link>https://dev.to/purvi09</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4046918%2F5d27d9df-5774-4d2b-a465-4b0801f3ff94.png</url>
      <title>DEV Community: Purvi</title>
      <link>https://dev.to/purvi09</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/purvi09"/>
    <language>en</language>
    <item>
      <title>SigNoz TraceBill: usage-based billing where every charge opens into the request behind it</title>
      <dc:creator>Purvi</dc:creator>
      <pubDate>Sat, 25 Jul 2026 21:33:25 +0000</pubDate>
      <link>https://dev.to/purvi09/signoz-tracebill-usage-based-billing-where-every-charge-opens-into-the-request-behind-it-35lb</link>
      <guid>https://dev.to/purvi09/signoz-tracebill-usage-based-billing-where-every-charge-opens-into-the-request-behind-it-35lb</guid>
      <description>&lt;p&gt;&lt;strong&gt;Team NULL POINTERS&lt;/strong&gt; · &lt;strong&gt;Agents of SigNoz Hackathon&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqvdjwt517inqmc6z5t9j.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqvdjwt517inqmc6z5t9j.jpeg" alt="TraceBill dashboard" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We built a billing product for APIs. You add three lines to your service, and TraceBill meters every request, prices it, and produces an invoice per customer. The part we care about is that each line on that invoice can be opened up to show the individual API calls it's made of, and each of those calls can be opened to show its trace.&lt;/p&gt;

&lt;p&gt;SigNoz is what makes that possible. It's the store and query layer underneath the whole thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem we started from
&lt;/h2&gt;

&lt;p&gt;If you bill per API call, you normally build a second pipeline just to count. The app emits usage events, a service aggregates them, a number lands in a table, and that number becomes the bill.&lt;/p&gt;

&lt;p&gt;It works right up until a customer asks why their invoice is $4,000. At that point all you have is the counter. You can't show them the 4,000 requests, because you never kept them. You threw away the detail at aggregation time and kept the total.&lt;/p&gt;

&lt;p&gt;Meanwhile, if that service is instrumented with OpenTelemetry, every one of those 4,000 requests already exists as a span, with a route, a duration and a response size on it. The billing data and the telemetry data are the same data. So we tried building the billing product directly on top of the traces, and keeping the evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the integration looks like
&lt;/h2&gt;

&lt;p&gt;For a company using TraceBill, setup is installing the SDK and calling init() with two things: their key, and a function that says who to bill for a given request.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftdmbd4gceuhw9w1m8b1r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftdmbd4gceuhw9w1m8b1r.png" alt="Integration Code" width="799" height="143"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the whole integration. There are no usage events to send and no counters to maintain. Under the hood init() starts an OpenTelemetry Node SDK with HTTP and Express auto-instrumentation, and attaches three attributes to the request spans the app already produces: billing.customer_id, billing.sku (derived as get./api/products from the method and route) and billing.response_bytes. Those spans get exported over OTLP to us.&lt;/p&gt;

&lt;p&gt;For the demo we wrote a fake e-commerce API called Astro Store and a traffic generator that hits it as three different customers at different volumes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get out
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6s6jv3t2dw6rjurzpjrc.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6s6jv3t2dw6rjurzpjrc.jpeg" alt="Tracebill Dashboard" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Usage shows up on the dashboard within seconds of the traffic arriving: revenue for the current period, billable call counts, and a running total per customer.&lt;/p&gt;

&lt;p&gt;Dashboard showing revenue, billable calls and per-customer running totals&lt;/p&gt;

&lt;p&gt;Each customer gets an invoice for the period. You can price each endpoint separately, give any of them a free tier, and charge for compute time and data egress alongside raw call volume. On top of that, a customer can be put on their own negotiated contract that overrides only the parts you name. A volume rate on a single endpoint, a larger free allowance, a discount on checkout: whatever the contract doesn't mention, it inherits from your standard rates. Two customers sending identical traffic can come out with completely different invoices.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6iqunzzi64f7ddibo9hz.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6iqunzzi64f7ddibo9hz.jpeg" alt="Invoices" width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An invoice with per-SKU line items, compute and egress&lt;/p&gt;

&lt;p&gt;The receipts column is the point of the project. Click a line item and it expands into the actual requests that make it up, with timestamps, routes, durations, status codes and response sizes.&lt;/p&gt;

&lt;p&gt;Click one of those and you get its trace waterfall.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl45cthrqtqoqbqomaxkn.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl45cthrqtqoqbqomaxkn.jpeg" alt="Trace Waterfall" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Request detail showing the trace waterfall for a single billed call&lt;/p&gt;

&lt;p&gt;That request took 31.4 ms, of which 30.3 ms was a database query. This is a billing product showing you a flame graph, which felt strange to build and makes sense once you use it. "Why is this month higher" stops being a conversation about trust.&lt;/p&gt;

&lt;p&gt;Each invoice also has a read-only share link you can send the end customer, scoped to that one invoice. They can audit every charge themselves rather than taking your word for it.&lt;/p&gt;

&lt;p&gt;The customer-facing read-only invoice view&lt;/p&gt;

&lt;p&gt;Two more things TraceBill does with the same data. It verifies invoices before they go out, by comparing the metered call count against the number of per-request records actually on file for each line, and flags a mismatch with the delta. And it enforces spend limits: when a customer crosses their quota, the SDK's optional guard() middleware starts returning 429 at the API's edge. Refused calls are recorded but never billed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we used SigNoz
&lt;/h2&gt;

&lt;p&gt;SigNoz is what TraceBill is built on. It plays the role Postgres or ClickHouse would play in another product: the store and query layer that everything else sits on top of. We didn't write an ingest pipeline, a time-series store or an aggregation service, because SigNoz already is those things. It's doing the work in four places.&lt;/p&gt;

&lt;p&gt;OTLP ingest. Traces from customer services come in over OTLP/HTTP on port 4318. They don't go straight there, though. They go through a small gateway of ours first, which authenticates the tenant's ingest key and stamps tracebill.tenant_id into every resource before forwarding. Any value the client supplied gets overwritten, so one tenant can't read or bill as another even at the OTLP layer.&lt;/p&gt;

&lt;p&gt;Query API v5 for metering. This is the core of it. Metering a tenant for a billing period is a single POST /api/v5/query_range&lt;br&gt;
, a scalar builder query over the traces signal, with count(), sum(duration_nano) and sum(billing.response_bytes) aggregated and grouped by billing.customer_id and billing.sku. One round trip gives us calls, compute time and egress bytes broken down per customer per endpoint. There is no aggregation service in TraceBill and no rollup tables, because that query is the meter.&lt;/p&gt;

&lt;p&gt;Query API v5 again, two more ways. The same builder query with requestType: 'raw' and a list of selectFields returns paged span lists, which become the per-request receipts under each line item. With requestType: 'time_series' and a stepInterval it returns chart data for the dashboard. Three shapes of one endpoint cover the entire product surface.&lt;/p&gt;

&lt;p&gt;Trace fetches for the waterfall. GET /api/v1/traces/:id returns the full span list for a trace, which we render in our own white-labeled waterfall inside the invoice. Every span is checked to carry the right tenant id before anything is served, and a mismatch returns 404.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that cost us the most time
&lt;/h2&gt;

&lt;p&gt;The first invoice TraceBill generated was wrong by exactly six times. Not double, not off by a rounding error. Six times over, consistently, for each of the three fake customers our traffic generator was pretending to be.&lt;/p&gt;

&lt;p&gt;That was easy to spot because we knew the right answer. The generator sends a fixed number of requests, so when it reports sending 30 and the invoice says 180, something is badly off.&lt;/p&gt;

&lt;p&gt;A single request to our demo API doesn't produce one span. It produces six: the server span, two Express middleware spans, two handler spans, and the database query we'd added by hand to give the waterfalls some depth. Our query was counting all of them.&lt;/p&gt;

&lt;p&gt;The fix is one clause in the filter expression. Adding kind_string = 'Server' restricts the count to inbound requests, which is what a billable API call actually is. We found it by pulling the raw span list for one of the demo customers and reading it, at which point six near-identical rows sharing a single trace id are hard to miss.&lt;/p&gt;

&lt;p&gt;If you take one thing from this post, take that. Before you write any pricing logic, look at the actual spans you're about to count. We spent the first hour assuming our arithmetic was wrong, when the arithmetic was fine and the input was wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;GitHub Link: &lt;a href="https://github.com/yashagw/tracebill" rel="noopener noreferrer"&gt;https://github.com/yashagw/tracebill&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;The whole stack comes up with docker compose up -d, telemetry backend included. No accounts and no API keys. Open the portal on port 4500, run the traffic generator, and watch an invoice build itself. Then click a line item, click a request, and see where its time went.&lt;/p&gt;

&lt;p&gt;The thing we'd tell anyone considering this approach: billing off traces doesn't save you from building a metering pipeline. It just means the raw evidence is still sitting there when someone asks you to justify a number, and that turns out to be worth quite a lot.&lt;/p&gt;

&lt;p&gt;TraceBill was built by &lt;strong&gt;Team NULL POINTERS&lt;/strong&gt; and &lt;strong&gt;AI helper&lt;/strong&gt;: Claude Code for the SigNoz hackathon.The SigNoz docs cover the query API, and the OpenTelemetry Node instrumentation options are at opentelemetry.io.&lt;/p&gt;

&lt;p&gt;YouTube Video Link: &lt;a href="https://www.youtube.com/watch?v=iR02ApaliGk" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=iR02ApaliGk&lt;/a&gt;&lt;/p&gt;

</description>
      <category>signoz</category>
      <category>hackathon</category>
    </item>
  </channel>
</rss>
