Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.tryhoard.com/llms.txt

Use this file to discover all available pages before exploring further.

Security

Hoard’s sync agent runs on your machine and drives a real browser session against your TCGplayer seller account. That’s a lot of trust, so we treat the integrity of what runs on your machine as a first-class engineering problem, not a marketing line. This page explains exactly what we do, why, and — for the security-minded — how to verify our claims yourself without taking our word for any of it.

TL;DR for sellers

  • Every binary you download is cryptographically signed by us. Your agent refuses to install or run a tampered binary.
  • The browser-automation script the agent runs (selectors, URLs, click steps) is also signed. The same protection applies to the instructions, not just the program.
  • Older versions can’t be replayed. If an attacker somehow got their hands on a copy of our distribution server, they can’t trick your agent into rolling back to a known-bad version.
  • You can verify every claim above using only public toolscurl, openssl, and our open verification code. We show you how below.
If you’re a customer, that’s the relevant summary. If you’re a security professional or curious operator, keep reading.

What’s signed and how

Hoard ships two kinds of cryptographically signed artifacts:
ArtifactPath / URLSigning method
Agent binaryreleases.tryhoard.com/hoard-agent_<os>_<arch>_v<version>ed25519 over file bytes
Workflow YAML bundlereleases.tryhoard.com/workflows/tcgplayer/current.jsoned25519 over a domain-separated payload (see below)
Both use the same ed25519 keypair. The private key is held in GitHub Actions secrets and only signs at release time; it never leaves CI. The public key is compiled into every agent binary as a Go constant:
// agent/internal/update/signing_key.go
var PublicKeyHex = "8bd9a1f834e031b028e81e8a1ccc96fbda0c4669aafdfb7fd2308b5e1913ee07"
That’s the value you’d verify against. It hasn’t changed since signed releases shipped, and rotation would itself be a signed event published in this changelog.

Why two artifacts when they share a key

The agent binary is the program. The workflow YAML is the script that program runs (which buttons to click, what URL parameters mean, how to recognize “logged in”). TCGplayer ships UI changes more often than we ship agent releases, so we want a path to push selector fixes faster than a binary release cycle. Same trust boundary, different update cadence. To prevent a binary signature from ever being mistakenly accepted as a workflow signature (or vice versa), the workflow signed payload is prefixed with a fixed domain separator string:
hoard-workflow-v1
version=<unix-timestamp>
sha256=<hex>
signed_at=<rfc3339>
<yaml-bytes>
A binary signature is computed over raw file bytes with no prefix, so the same private key produces signatures that are simultaneously valid in their own context and cryptographically invalid in the other. Our test suite pins this property as a regression check (see TestVerify_RejectsCrossProtocolSignature in agent/internal/workflowfetch/fetcher_test.go).

Anti-rollback

Both signed artifacts include a monotonic version. For the agent binary it’s the release tag (v0.8.18 etc.). For the workflow YAML it’s the unix timestamp of the last commit to the canonical YAML on main, computed by CI as git log -1 --format=%ct -- agent/workflows/tcgplayer.yaml. Monotonic by construction, no human bumping, no off-by-one. The agent persists the highest version it has ever successfully verified to its config directory and refuses to load anything older, even if the signature is valid. So an attacker who compromised our CDN couldn’t replay an older bundle that contained a known-bad selector to redirect your purchases somewhere else. The version pin moves forward only; it never resets.

How to verify our claims yourself

This is the part most companies skip. Everything below works with publicly available tools — no Hoard account needed.

1. Pull the live signed bundle

curl -s https://releases.tryhoard.com/workflows/tcgplayer/current.json > bundle.json
It’s a small JSON object with these fields:
{
  "version": 1715226000,
  "sha256": "<hex of the yaml content>",
  "signed_at": "2026-05-09T05:30:00Z",
  "signature": "<128 hex chars of ed25519 signature>",
  "yaml_b64": "<base64 of the workflow YAML>"
}

2. Reconstruct the signed payload

The signed bytes are header || yaml_bytes, where header is the literal text:
hoard-workflow-v1
version=<version>
sha256=<sha256>
signed_at=<signed_at>
(four lines, each terminated with \n).
VERSION=$(jq -r .version bundle.json)
SHA=$(jq -r .sha256 bundle.json)
SIGNED_AT=$(jq -r .signed_at bundle.json)
SIG=$(jq -r .signature bundle.json)
YAML=$(jq -r .yaml_b64 bundle.json | base64 -d)

printf 'hoard-workflow-v1\nversion=%s\nsha256=%s\nsigned_at=%s\n' \
  "$VERSION" "$SHA" "$SIGNED_AT" > /tmp/signed.bin
printf '%s' "$YAML" >> /tmp/signed.bin

3. Verify the signature

The verification logic the agent runs is open in our repo at agent/internal/workflowfetch/fetcher.go. It’s about 200 lines of Go, no external crypto libraries, just crypto/ed25519 from the standard library. If you have Go installed, the simplest end-to-end check is to clone our repo and run our test suite:
git clone https://github.com/tryhoard/hoard
cd hoard/agent
go test ./internal/workflowfetch/... -v
You’ll see the 14 security properties exercised: tampered YAML rejection, bad signature rejection, cross-protocol-confusion rejection, rollback rejection, far-future timestamp rejection, malformed input rejection, oversized bundle rejection, and the byte-layout pin. Each test name and assertion is human-readable. If you want to verify a single live bundle against our public key without cloning, this 20-line Go script does it:
package main

import (
    "crypto/ed25519"
    "crypto/sha256"
    "encoding/base64"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
)

const PublicKeyHex = "8bd9a1f834e031b028e81e8a1ccc96fbda0c4669aafdfb7fd2308b5e1913ee07"

func main() {
    resp, _ := http.Get("https://releases.tryhoard.com/workflows/tcgplayer/current.json")
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)

    var b struct {
        Version   int64
        SHA256    string
        SignedAt  string `json:"signed_at"`
        Signature string
        YAMLB64   string `json:"yaml_b64"`
    }
    json.Unmarshal(body, &b)

    yaml, _ := base64.StdEncoding.DecodeString(b.YAMLB64)
    h := sha256.Sum256(yaml)
    if hex.EncodeToString(h[:]) != b.SHA256 {
        fmt.Fprintln(os.Stderr, "sha256 mismatch")
        os.Exit(1)
    }
    pub, _ := hex.DecodeString(PublicKeyHex)
    sig, _ := hex.DecodeString(b.Signature)
    payload := fmt.Sprintf("hoard-workflow-v1\nversion=%d\nsha256=%s\nsigned_at=%s\n",
        b.Version, b.SHA256, b.SignedAt)
    payload += string(yaml)
    if !ed25519.Verify(ed25519.PublicKey(pub), []byte(payload), sig) {
        fmt.Fprintln(os.Stderr, "signature INVALID")
        os.Exit(1)
    }
    fmt.Printf("VERIFIED v%d\n", b.Version)
}
If our distribution server, our DNS, our CDN, or our TLS termination ever served a bundle that didn’t pass that script, every Hoard agent in the field would also reject it. There is no mode where we can push selector code that fails this check; our own release CI does this exact verification before uploading and refuses to publish a bundle that doesn’t round-trip.

Threat model

What an attacker cannot do without the private signing key (which lives only in GitHub Actions secrets):
  • Push a malicious agent binary that your agent will install and run.
  • Push a malicious workflow YAML that redirects clicks to a different domain or scrapes additional fields.
  • Roll your agent back to a previously-known-vulnerable selector to re-enable an exploit we’ve already fixed.
  • Replay a binary signature as a workflow signature (the hoard-workflow-v1 domain separator blocks this even though both artifacts share a key).
  • Tamper with a bundle in transit. TLS already protects this, but signature verification provides a second independent layer; the agent rejects the bundle even if TLS is somehow bypassed.
What an attacker could do:
  • Compromise of the GitHub Actions signing secret would let them push either malicious binaries or malicious workflows. This is the same risk surface as any signed-release ecosystem (Linux distros, Apple notarization, etc.). Mitigations: secret access is restricted to release workflows by GitHub branch protection, and the signing private key never touches developer workstations or local CI runners. Signed-release detection is on the binary update path: the agent verifies the signature of every binary it downloads and restores from backup if verification fails.
  • Refuse to serve updates entirely (DDoS our CDN). Your agent keeps running the last-good cached workflow indefinitely; an attacker who can take our distribution offline cannot use that to push you a bad version, only delay good ones.
  • Show you a stale-but-still-valid bundle (replay attack on the network layer). The version pin blocks this — your agent has already accepted version N, and a replay of version N or an older signed version is rejected on receipt.

Source you can inspect

These are the files that implement everything described above. You’re welcome to read them and tell us what’s wrong.

Reporting a security issue

If you find a vulnerability, please email security@tryhoard.com. We commit to:
  • Acknowledging receipt within one business day.
  • Assigning a tracking ID and a single point of contact.
  • Treating coordinated disclosure as the default — we’ll work with you on a public-release timeline that gives users time to update.
Pentester reports that include reproducible failure modes and a proposed fix or test case will get priority triage. We’re a small team that takes this seriously; we’d rather learn from you than from a postmortem.