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.
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.6Side 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
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
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 todaysource-pinnedcontainstimeout 300s · max $0Expected:
ParlayAPI Mastra source pinned OK: 64 members at 9d8bbdf, sha256 276de9f17315ca3827997a023565b7db3f366bb5bdada56d8a4b861c70bfe250execution-boundarycontainstimeout 300s · max $0Expected:
execution boundary OK: exact lockfile, no install scripts, explicit live only, zero retries, 15s/2MiB bounds, no snapshotssynthetic-fail-closedcontainstimeout 300s · max $0Expected:
synthetic Mastra gate OK: 2 complete, 2 incomplete, review_incomplete_groups, shapeCheckPassed falsesecurity-suitecontainstimeout 300s · max $0Expected:
Mastra security suite OK: 18 offline/mocked tests; zero real API callsclean-exitexit_codetimeout 300s · max $0Expected:
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
- Self-hosting the open-source stack? Prove your backup actually restores before you need it
- Chat with a CSV, but pin a known-answer guardrail so a wrong query cannot pass
- Advisor pattern: cap how often the expensive model gets called, and catch drift
- Route through a gateway with a tested open-weights fallback
- ReMe pattern: define prospective memory as a schedule your agent can tick off
- Grind a huge one-time job overnight on a free tier's tiny rate limit
Liked this workflow?
Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).