AutomationOpen SourceFreeActiveMachine-verified· intermediate · ~10 min setup

Mastra: stop on incomplete private soccer moneyline outcome sets

Run a deterministic two-step Mastra workflow that checks supplied soccer moneyline response shapes for one home, away, and Draw outcome before any downstream private analysis, without treating runtime success as data completeness or a betting recommendation.

by Shilpa Mitra· verified today· v1.0.0

Run this workflow

CI-verified, 5/5 fixtures passing.

Build this with your agent

One copy-paste hands Claude Code, Codex, or Cursor the full recipe, steps included, nothing to fetch.

Intended Use

Developers handling private soccer odds responses who need a fail-closed structural gate before analysis. Start with the credential-free synthetic run, inspect aggregate diagnostics, and branch on shapeCheckPassed. Use live mode only in a private server-side runtime with your own account, lawful purpose, and compliance with the data license.

Not for

  • Bet selection, probability estimation, arbitrage execution, gambling advice, or any claim of profit, freshness, fair value, or completeness
  • Inferring that a requested sportsbook or market is globally unavailable: the gate describes only the supplied response and stops on empty, partial, malformed, or missing groups
  • Publicly displaying, redistributing, training on, or retaining API output beyond the provider's terms; the MIT repository license covers code, not sports-data rights
  • Client-side or logged API keys; live mode belongs in a private server-side runtime and must remain an explicit --live action
  • Treating ParlayAPI as an established or endorsed provider: its public terms identify a sole-proprietor operator, the examples repository is roughly two weeks old with no stars/forks, and service/coverage claims have not been independently benchmarked

The Stack

Tested Against

JacobiusMakes/parlayapi-notebooks@9d8bbdfa496b83e73420337280776e79cf8e9e01source archive@sha256:276de9f17315ca3827997a023565b7db3f366bb5bdada56d8a4b861c70bfe250node@22.22.0@mastra/core@1.66.0zod@4.3.6

Side effects & data flow

Network
codeload.github.com, nodejs.org, and registry.npmjs.org/security audit endpoints in CI, parlay-api.com exactly once only when a user explicitly runs live mode
Writes
./source.tar.gz, ./checked/, ./node-runtime/, ./npm-cache/, and node_modules/ in the disposable CI directory
Credentials
PARLAY_API_KEY only for explicit live mode; CI unsets it and makes no live request

Data privacy

  • GitHub, Node.js, and npm registries CI requests only public pinned source/runtime/dependency metadata; no API key, sportsbook response, participant name, event ID, or odds (retention: per the respective service policies)
  • ParlayAPI Optional live mode sends the API key plus fixed soccer_epl/book/market query; the response remains local and only aggregate counts cross the Mastra step boundary (retention: per ParlayAPI's terms and privacy policy)

Prerequisites

  • curl, tar, and a supported x64 Linux or Apple-silicon macOS host for the verification gate
  • Node.js 22.13+ for a real local run
  • A private server-side secret store and ParlayAPI account only for optional live mode
  • Legal and product review for any gambling-adjacent or public-data use

Steps

  1. 1

    Verify the source and execute the real workflow only in synthetic and mocked modes

    Download the immutable repository archive and reject digest, size, path, member-type, license, dependency, runtime, or privacy-contract drift before execution. Extract only the Mastra integration and root MIT license. Download an official Node.js 22.22.0 binary for the host and verify its release checksum. Install the exact package-lock graph with lifecycle scripts disabled, require a clean moderate-severity package audit, unset the live credential, run the real synthetic workflow, assert its fail-closed result, and run all 18 upstream tests including the mocked fixed-origin live transport.

    set -eu
    curl --proto '=https' --tlsv1.2 --retry 3 --max-filesize 1048576 -fsSL   https://codeload.github.com/JacobiusMakes/parlayapi-notebooks/tar.gz/9d8bbdfa496b83e73420337280776e79cf8e9e01   -o source.tar.gz
    node <<'NODE'
    const crypto = require("crypto");
    const fs = require("fs");
    const { execFileSync } = require("child_process");
    function bad(message) { console.error("BAD: " + message); process.exit(1); }
    const archive = fs.readFileSync("source.tar.gz");
    if (archive.length !== 193028) bad("source archive size changed");
    const digest = crypto.createHash("sha256").update(archive).digest("hex");
    if (digest !== "276de9f17315ca3827997a023565b7db3f366bb5bdada56d8a4b861c70bfe250") bad("source archive digest changed");
    const root = "parlayapi-notebooks-9d8bbdfa496b83e73420337280776e79cf8e9e01/";
    const names = execFileSync("tar", ["-tzf", "source.tar.gz"], { encoding: "utf8" }).trim().split("\n");
    if (names.length !== 64) bad("source archive member count changed");
    for (const name of names) {
      if (!name.startsWith(root) || name.startsWith("/") || name.split("/").includes("..")) bad("unsafe source archive path");
    }
    const verbose = execFileSync("tar", ["-tvzf", "source.tar.gz"], { encoding: "utf8" }).trim().split("\n");
    if (verbose.some((line) => !["-", "d"].includes(line[0]))) bad("source archive contains a non-file/directory member");
    console.log("ParlayAPI Mastra source pinned OK: 64 members at 9d8bbdf, sha256 " + digest);
    NODE
    mkdir -p checked
    ROOT=parlayapi-notebooks-9d8bbdfa496b83e73420337280776e79cf8e9e01
    tar -xOf source.tar.gz "$ROOT/LICENSE" > checked/LICENSE
    for name in README.md coverage.mjs coverage.test.mjs package-lock.json package.json run.mjs workflow.mjs; do
      tar -xOf source.tar.gz "$ROOT/integrations/mastra-coverage/$name" > "checked/$name"
    done
    node <<'NODE'
    const fs = require("fs");
    function bad(message) { console.error("BAD: " + message); process.exit(1); }
    function read(name) { return fs.readFileSync("checked/" + name, "utf8"); }
    function has(text, value, message) { if (!text.includes(value)) bad(message); }
    has(read("LICENSE"), "MIT License", "MIT license missing");
    const pkg = JSON.parse(read("package.json"));
    if (pkg.private !== true || pkg.type !== "module" || pkg.engines?.node !== ">=22.13.0") bad("package/runtime boundary changed");
    if (JSON.stringify(pkg.dependencies) !== JSON.stringify({ "@mastra/core": "1.66.0", zod: "4.3.6" })) bad("direct dependency pins changed");
    const lock = JSON.parse(read("package-lock.json"));
    if (lock.lockfileVersion !== 3 || Object.keys(lock.packages || {}).length !== 153) bad("lockfile graph changed");
    for (const [name, meta] of Object.entries(lock.packages)) {
      if (name && (!meta.integrity || !String(meta.resolved || "").startsWith("https://registry.npmjs.org/"))) bad("unlocked or non-npm dependency " + name);
      if (meta.hasInstallScript) bad("dependency lifecycle script declared by " + name);
    }
    const coverage = read("coverage.mjs");
    const workflow = read("workflow.mjs");
    const runner = read("run.mjs");
    has(workflow, "createWorkflow", "real Mastra workflow missing");
    has(workflow, "retries: 0", "zero-retry contract missing");
    has(workflow, "shouldPersistSnapshot: () => false", "snapshot persistence boundary changed");
    has(coverage, "const MAX_BYTES = 2 * 1024 * 1024", "response-size bound changed");
    has(coverage, "redirect: 'error'", "redirect rejection changed");
    has(coverage, "AbortSignal.timeout(15000)", "transport timeout changed");
    has(coverage, "https://parlay-api.com/v1/sports/", "fixed live origin changed");
    has(runner, "args[0] === '--live'", "explicit live-mode boundary changed");
    if (/console\.(?:log|error)\([^\n]*(?:key|price|event|team)/i.test(coverage + workflow)) bad("private fields may be logged");
    console.log("execution boundary OK: exact lockfile, no install scripts, explicit live only, zero retries, 15s/2MiB bounds, no snapshots");
    NODE
    case "$(uname -s)-$(uname -m)" in
      Linux-x86_64)
        NODE_ASSET=node-v22.22.0-linux-x64.tar.xz
        NODE_SHA=9aa8e9d2298ab68c600bd6fb86a6c13bce11a4eca1ba9b39d79fa021755d7c37
        ;;
      Darwin-arm64)
        NODE_ASSET=node-v22.22.0-darwin-arm64.tar.gz
        NODE_SHA=5ed4db0fcf1eaf84d91ad12462631d73bf4576c1377e192d222e48026a902640
        ;;
      *) echo "BAD: unsupported CI host for pinned Node.js binary" >&2; exit 1 ;;
    esac
    curl --proto '=https' --tlsv1.2 --retry 3 --max-filesize 67108864 -fsSL   "https://nodejs.org/dist/v22.22.0/$NODE_ASSET" -o node-runtime.tar
    ACTUAL_NODE_SHA=$(shasum -a 256 node-runtime.tar | cut -d ' ' -f 1)
    test "$ACTUAL_NODE_SHA" = "$NODE_SHA" || { echo "BAD: Node.js release digest changed" >&2; exit 1; }
    mkdir -p node-runtime npm-cache
    case "$NODE_ASSET" in
      *.tar.xz) tar -xJf node-runtime.tar -C node-runtime --strip-components=1 ;;
      *) tar -xzf node-runtime.tar -C node-runtime --strip-components=1 ;;
    esac
    test "$(node-runtime/bin/node --version)" = "v22.22.0" || { echo "BAD: wrong Node.js version" >&2; exit 1; }
    RUNTIME_BIN="$PWD/node-runtime/bin"
    NPM_CACHE="$PWD/npm-cache"
    cd checked
    env -u PARLAY_API_KEY PATH="$RUNTIME_BIN:/usr/bin:/bin" npm_config_cache="$NPM_CACHE" npm ci --ignore-scripts --no-audit
    env -u PARLAY_API_KEY PATH="$RUNTIME_BIN:/usr/bin:/bin" npm_config_cache="$NPM_CACHE" npm audit --package-lock-only --audit-level=moderate
    env -u PARLAY_API_KEY PATH="$RUNTIME_BIN:/usr/bin:/bin" node run.mjs > synthetic-result.json
    env -u PARLAY_API_KEY PATH="$RUNTIME_BIN:/usr/bin:/bin" node <<'NODE'
    const fs = require("fs");
    const result = JSON.parse(fs.readFileSync("synthetic-result.json", "utf8"));
    const expectedGroups = [
      { bookmaker: "synthetic_book_a", complete: 1, incomplete: 1 },
      { bookmaker: "synthetic_book_b", complete: 1, incomplete: 1 },
    ];
    if (result.status !== "observed" || result.synthetic !== true || result.scope !== "soccer_epl"
      || JSON.stringify(result.groups) !== JSON.stringify(expectedGroups)
      || result.complete !== 2 || result.incomplete !== 2
      || result.decision !== "review_incomplete_groups" || result.shapeCheckPassed !== false) {
      console.error("BAD: synthetic fail-closed result changed");
      process.exit(1);
    }
    console.log("synthetic Mastra gate OK: 2 complete, 2 incomplete, review_incomplete_groups, shapeCheckPassed false");
    NODE
    env -u PARLAY_API_KEY PATH="$RUNTIME_BIN:/usr/bin:/bin" node --test --test-concurrency=1
    echo "Mastra security suite OK: 18 offline/mocked tests; zero real API calls"
  2. 2

    Use live mode only as one private, explicit diagnostic

    After the synthetic gate passes, keep the integration in a private server-side workspace, store PARLAY_API_KEY in a secret manager, and run npm run live only when you intentionally want one account request. Treat any non-observed status, incomplete count, missing requested book, absent h2h group, or false shapeCheckPassed as a stop. Do not log or redistribute raw output, and review current pricing, coverage, terms, age/jurisdiction requirements, and retention rights first. Live service behavior and data accuracy are outside the CI attestation.

Eval, 5 fixtures

Last passed: verified today
  • source-pinnedcontainstimeout 300s · max $0

    Expected: ParlayAPI Mastra source pinned OK: 64 members at 9d8bbdf, sha256 276de9f17315ca3827997a023565b7db3f366bb5bdada56d8a4b861c70bfe250

  • execution-boundarycontainstimeout 300s · max $0

    Expected: execution boundary OK: exact lockfile, no install scripts, explicit live only, zero retries, 15s/2MiB bounds, no snapshots

  • synthetic-fail-closedcontainstimeout 300s · max $0

    Expected: synthetic Mastra gate OK: 2 complete, 2 incomplete, review_incomplete_groups, shapeCheckPassed false

  • security-suitecontainstimeout 300s · max $0

    Expected: Mastra security suite OK: 18 offline/mocked tests; zero real API calls

  • clean-exitexit_codetimeout 300s · max $0

    Expected: 0

Results

The default run uses fictional presence metadata and makes no API request. It reports two complete and two incomplete groups and returns review_incomplete_groups with shapeCheckPassed=false. FlowStacks verifies the whole source archive, extracts only the seven-file Mastra integration plus the root license, installs its exact lockfile with lifecycle scripts disabled, checks the dependency audit, executes the real two-step Mastra runtime, and passes all 18 tests. The optional live path is fenced: one private ParlayAPI request, fixed HTTPS origin, no retry, 15-second timeout, 2 MiB response cap, aggregate-only step output, and fail-closed partial/error handling.

Did this work for you?

Our CI checks the setup runs. You tell us if the whole thing worked. Tell us straight.

Related workflows

Liked this workflow?

Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).