vastlint

Embedding vastlint in an SSP, DSP, or ad server

vastlint is built for in-process validation inside ad tech infrastructure. Embed vastlint-core directly in your bid handler, SSAI stitcher, or ad server to validate every VAST response before committing the impression — no subprocess, no network round-trip, no external dependency to manage.

A typical OpenRTB bid cycle has 100–300 ms to work with. vastlint validates a 17 KB production VAST tag in 363 µs on a single core, adding less than 2.1% of the bid budget on the heaviest 44 KB tags. An SSAI platform doing 1,000 stitches/sec spends more time on DNS than on VAST validation.

Architectural diagram

VAST ad flow: wrapper chain to viewer

SSP / AD SERVERvastlint returns ValidationResult → ad server code decidesSERVING-SIDE DECISIONVIEWER / DELIVERYVAST wrappervalidate per hopad server: servead server: serve✗ invalid↺ fallbackDemand PartnerVAST wrapper tagChain Resolverfollows 1–4 hopsvastlint-core363 µs / tagAd Clientweb / mobileCTV / OTTvia SSAI stitcherRejectedFallback VASTvastlint validates before serving — reject or swap in <2.1% of the bid budgetvastlint validates before any impression fires — reject or swap in <2.1% of bid budgetvalid tagrejected (error)fallback served

Why validate at bid time?

VAST errors are silent. A missing <Impression>, a malformed duration, a VPAID tag on a CTV device — none of these produce a player error you can act on. The impression is lost, the tracking fires (or doesn't), and a discrepancy surfaces in a report three days later. By the time you see it, the revenue is gone.

Validating inline, before the bid is committed, lets you:

  • Reject invalid tags and serve a fallback in the same auction window
  • Return structured rule IDs to the demand partner so they can fix the creative at the source
  • Track per-partner error rates and escalate when they spike
  • Filter by revenue-impact rules to avoid false positives on advisory-only issues

Rust — vastlint-core

vastlint-core is a zero-dependency Rust library. Three compile-time dependencies (quick-xml, url, phf), no runtime dependencies. Rules are compiled Rust functions — no regex engine, no schema interpreter, no allocator pressure beyond the parse.

# Cargo.toml
[dependencies]
vastlint-core = "0.4"

Basic validation:

use vastlint_core::validate;

let result = validate(vast_xml);

if !result.summary.is_valid() {
    for issue in &result.issues {
        // issue.id     — stable rule ID (e.g. "VAST-2.0-inline-impression")
        // issue.severity — "error", "warning", or "info"
        // issue.message — human-readable description
        // issue.path   — XPath location in the document
        eprintln!("[{}] {} — {}", issue.severity, issue.id, issue.path);
    }
}

With per-deployment rule overrides (e.g. relax a CTV-only rule for web inventory):

use std::collections::HashMap;
use vastlint_core::{validate_with_context, RuleLevel, ValidationContext};

let mut overrides = HashMap::new();
overrides.insert("VAST-4.1-mezzanine-recommended", RuleLevel::Off);
overrides.insert("VAST-2.0-mediafile-https", RuleLevel::Error); // promote to error

let ctx = ValidationContext {
    rule_overrides: Some(overrides),
    ..Default::default()
};

let result = validate_with_context(vast_xml, ctx);

Full API documentation is on docs.rs/vastlint-core.

Go — vastlint-go (no Rust toolchain required)

vastlint-go wraps the same Rust core via CGo. Prebuilt static libraries are bundled in the module — you do not need a Rust toolchain on your build or production machines. Supported platforms: Linux (amd64, arm64), macOS (amd64, arm64).

go get github.com/aleksUIX/vastlint-go

Basic validation:

import vastlint "github.com/aleksUIX/vastlint-go"

result, err := vastlint.Validate(xmlBytes)
if err != nil {
    log.Fatal(err)
}
if !result.Valid {
    for _, issue := range result.Issues {
        log.Printf("[%s] %s: %s", issue.Severity, issue.ID, issue.Message)
    }
}

With wrapper depth cap and per-deployment overrides:

result, err := vastlint.ValidateWithOptions(xmlBytes, vastlint.Options{
    MaxWrapperDepth: 5, // IAB-recommended maximum
    RuleOverrides: map[string]string{
        "VAST-4.1-mezzanine-recommended": "off",   // web inventory, no SSAI
        "VAST-2.0-mediafile-https":       "error",  // enforce HTTPS strictly
    },
})
if err != nil || !result.Valid {
    // Quarantine the tag. Return result.Issues to the demand partner.
}

Elixir / Erlang — vastlint_erlang NIF

vastlint_erlang is a NIF (Native Implemented Function) for high-throughput BEAM pipelines. It uses the same compiled Rust core. Add it to your mix.exs:

# mix.exs
defp deps do
  [
    {:vastlint, "~> 0.4", github: "aleksUIX/vastlint-erlang"}
  ]
end
case Vastlint.validate(xml_string) do
  {:ok, %{summary: %{errors: 0}}} ->
    :ok

  {:ok, %{issues: issues}} ->
    errors = Enum.filter(issues, &(&1.severity == "error"))
    {:reject, errors}

  {:error, reason} ->
    {:error, reason}
end

Production patterns

Filter by revenue impact before rejecting

Not every error warrants a hard rejection. Use the rules reference to identify which rule IDs map to impression-killing defects (missing <Impression>, bad <Duration> format, no <MediaFile>) versus advisory issues. Run two passes: hard-reject on errors, quarantine-for-review on warnings.

Return rule IDs to the demand partner

Every issue in the result includes a stable id field (e.g. VAST-2.0-inline-impression) and an XPath path pointing to the exact element. Return these in your bid response or partner notification — they are actionable without manual triage.

Wrapper depth cap

The IAB recommends a maximum wrapper chain depth of 5. Set MaxWrapperDepth: 5 (Go) or the equivalent context field in Rust to enforce this. The rule VAST-2.0-wrapper-depth fires when the depth is exceeded.

Self-hosting (air-gapped deployments)

All bindings run entirely in-process. There is no network code in vastlint-core — no callbacks, no telemetry, no license checks. For teams that also need the web validator or REST API on-premise, the Docker image is FROM scratch, under 5 MB, with a cold-start under 10 ms.

Performance reference

Benchmarked on Apple M4 (10-core), production-realistic VAST tags:

Tag sizeSingle-core throughputSingle-core latency10-core throughput
17 KB2,747 tags/sec363 µs15,760 tags/sec
44 KB475 tags/sec2,104 µs2,635 tags/sec

A typical OpenRTB bid cycle takes 100–300 ms; validation adds less than 2.1% of that budget even on the heaviest tags.

Enterprise support & integration consulting

Evaluating vastlint for production use in a DSP bid pipeline, SSAI platform, ad server, or CTV device? The author is available for integration consulting, SLA-backed enterprise support agreements, custom rule development, and on-site architecture review.

Typical engagements include:

  • Architecture review for in-process bid-time validation
  • Custom rule development for proprietary spec extensions or internal trafficking requirements
  • Per-partner error rate dashboards and alerting integration
  • Priority issue resolution and private disclosure channel
  • Air-gapped or AWS Marketplace deployment support

Email: [email protected]

Or reach out through one of these channels:

Further reading