> For the complete documentation index, see [llms.txt](https://docs.antivamp.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.antivamp.io/for-launchpads/verify-signed-decisions.md).

# Verify Signed Decisions

Decisions are Ed25519-signed and expire in 5 minutes. Verify the signature, freshness, and identity fields before trusting any allow.

> **An unverified `decision` string is not a decision.**

Decisions from `POST /v1/launches/validate` are **Ed25519-signed** and short-lived so a launchpad can trust them without trusting the transport, and so a stale `allow` can never be replayed. Signing is **asymmetric**: AntiVamp signs with a private key, and you verify with the public key from `GET /v1/keys`.

{% hint style="danger" %}
🚨 **Never trust the `decision` field on its own.** Verify the Ed25519 **signature**, the **expiration**, the **nonce**, and that the **environment**, **launchpadId**, **chain**, and **launcher** all match what you requested — every time, before you act.
{% endhint %}

{% hint style="warning" %}
⚠️ If signing is unavailable the API returns an error rather than an unsigned `allow`. Treat any missing or invalid signature as `service_unavailable` and **fail closed**.
{% endhint %}

## Canonical payload

The signed bytes are these fields joined by `\n` in **exactly this order**, with `launcher` lower-cased:

```
version
identityKey
normalizedName
normalizedTicker
chain
launchpadId
launcher            (lowercased)
decision
reason
issuedAt
expiresAt
nonce
requestId
```

Decisions expire **5 minutes** after `issuedAt`. Verification allows a small clock-skew tolerance (±30s by default).

## Verify with the SDK (recommended)

The SDK fetches and caches public keys, matches by `keyId`, and checks the signature and freshness for you.

```ts
// enforceLaunch is the safest helper — it verifies and maps every decision:
const result = await antivamp.enforceLaunch(req);
if (!result.enforceable) return blockLaunch(result.userMessage);

// Or verify a decision object directly:
const v = await antivamp.verifyValidationDecision(decision);
// v = { valid: boolean,
//       reason: "ok" | "expired" | "not_yet_valid" | "unknown_key" | "bad_signature" | "no_signature" }
if (!v.valid) return reject(v.reason);
```

`verifyDecisions` defaults to `true` on the client, so verification is **fail-closed** unless you deliberately opt out (don't).

## Verify manually (Node)

For non-SDK stacks, reconstruct the canonical payload and verify against the matching public key.

```ts
import { createPublicKey, verify } from "node:crypto";

const { keys } = await (await fetch("https://api.antivamp.io/api/v1/keys")).json();

// Match the exact key that signed the decision — never fall back to keys[0].
const key = keys.find((k) => k.kid === d.keyId);
if (!key) throw new Error("unknown_key");

const spki = Buffer.concat([
  Buffer.from("302a300506032b6570032100", "hex"),
  Buffer.from(key.publicKeyBase64, "base64"),
]);
const pub = createPublicKey({ key: spki, format: "der", type: "spki" });

const canonical = [
  d.version, d.identityKey, d.normalizedName, d.normalizedTicker,
  d.chain, d.launchpadId, d.launcher.toLowerCase(), d.decision, d.reason,
  d.issuedAt, d.expiresAt, d.nonce ?? "", d.requestId,
].join("\n");

const okSig = verify(null, Buffer.from(canonical), pub, Buffer.from(d.signature, "base64"));
const fresh = Date.now() <= new Date(d.expiresAt).getTime() + 30_000;
const trust = okSig && fresh; // and: environment, launchpadId, chain, launcher all match your request
```

{% hint style="info" %}
🔐 The full trust and signature model — SPKI framing, key material, and rotation internals — lives in [Trust & Signature Model](/protocol/trust-and-signatures.md).
{% endhint %}

## Key rotation

`GET /v1/keys` returns every currently-accepted public key; exactly one is `active`. During rotation both the old and new keys appear — **match by `keyId`**, never pick an arbitrary key. Cache keys briefly (the SDK caches for \~5 minutes).

## Freshness & replay

Because each decision carries a unique `requestId` / `nonce` and a 5-minute `expiresAt`, an old captured `allow` cannot be replayed after expiry. Verify the identity fields (`chain`, `launchpadId`, `launcher`, environment) match your request so a decision issued for one context can never be re-used in another.

{% hint style="danger" %}
🚨 **Fail closed.** Reject `{ valid: false }`, expired decisions, unknown keys, and missing signatures. No fresh valid signed `allow` means *do not launch*.
{% endhint %}

{% hint style="info" %}
📡 View the machine-readable endpoint reference at [AntiVamp API](https://antivamp.io/api).
{% endhint %}

***

## Continue exploring

* ✅ [Validate Every Launch](/for-launchpads/validate-every-launch.md) — how you obtain the decision you verify here
* 🆘 [Partner Troubleshooting](/for-launchpads/troubleshooting.md) — what each verification `reason` means and how to recover
* 🔑 [Authentication](/developers/authentication.md) — API keys, prefixes, and environments
* ✍️ [Trust & Signature Model](/protocol/trust-and-signatures.md) — the cryptographic details in full
* 📦 [SDK Reference](/developers/sdk.md) — `verifyValidationDecision`, `getPublicKeys`


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.antivamp.io/for-launchpads/verify-signed-decisions.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
