> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shadowfeed.app/llms.txt
> Use this file to discover all available pages before exploring further.

# HMAC Integration Guide

> Add HMAC verifier middleware to your API. Copy-pasteable code for TypeScript, Python, and Go.

# HMAC Integration Guide

This guide adds **HMAC request authentication** to your API so it accepts signed requests from ShadowFeed and skips x402 payment for those calls.

<Note>
  If you're using **Hosted Mirror** mode (we host your data, no API server on your side), skip this entire guide. HMAC is only needed for **Partner Bridge** mode.
</Note>

<Note>
  **Already selling this data via x402 on Solana, Base, or another EVM chain?** That's the most common case. You do **not** stand up a new endpoint — you point ShadowFeed at the *same* API you already gate with x402. ShadowFeed adds a second payment rail (STX on Stacks) in front of it: the agent pays ShadowFeed in STX, and ShadowFeed proves itself to your server with an HMAC signature so your existing x402 paywall **lets the call through for free**. Your direct Solana/EVM buyers are unaffected. Read [Coexisting with your existing x402 paywall](#coexisting-with-your-existing-x402-paywall) before wiring anything.
</Note>

<Tip>
  **Use the SDK if you can.** `@shadowfeed/provider-sdk` ships this verifier as a drop-in Express/Hono adapter plus a `shadowfeed verify` command that handshake-tests your endpoint before you go live — and it gets the canonical path right automatically (the #1 thing hand-rolled integrations get wrong). Hand-rolling is fully supported and documented below, but **whichever path you choose, run the connection test before activating** — see [Verify your wiring before going live](#verify-your-wiring-before-going-live).
</Tip>

## TL;DR — 3 things to do

<Steps>
  <Step title="Grab your HMAC secret">
    From the provider dashboard or the success screen after onboarding.
  </Step>

  <Step title="Set it on your server">
    `SHADOWFEED_PARTNER_SECRET=...` as env var on **your API server** — not on ShadowFeed.
  </Step>

  <Step title="Paste the middleware">
    Copy the snippet for your stack below. Mount BEFORE your x402 middleware.
  </Step>
</Steps>

## How HMAC fits into the request flow

```
Agent buys data
    │ pays STX
    ▼
ShadowFeed
    │ signs canonical string with shared secret
    │ adds 4 headers:
    │   X-Sf-Partner:   shadowfeed
    │   X-Sf-Timestamp: 1715616000
    │   X-Sf-Nonce:     <uuid>
    │   X-Sf-Signature: <hmac-hex>
    │
    ▼
Your API
    │ middleware checks:
    │   ✓ timestamp within ±5 min
    │   ✓ nonce not seen before
    │   ✓ HMAC signature matches recomputed value
    │
    ▼
Skip x402 payment requirement, serve data
```

The signature is computed from a **canonical string**:

```text theme={null}
{METHOD}\n{PATH}\n{TIMESTAMP}\n{NONCE}\n{SHA256(BODY)}
```

Both sides must derive the **same** canonical string for the signature to verify. The body hash is empty string `""` for GET requests.

## Coexisting with your existing x402 paywall

If your endpoint is already gated by x402 (Solana, Base, other EVM), nothing about that setup changes. ShadowFeed becomes an additional, HMAC-authenticated caller in front of the *same* route:

```
Direct buyer ──pay USDC (Solana/EVM x402)──▶ your x402 middleware ──▶ data
                                                   ▲
ShadowFeed agent ──pay STX (Stacks)──▶ ShadowFeed │ no payment — just an HMAC signature
                                          │        │
                                          └─ signs ─┘ HMAC valid ⇒ bypass x402 ⇒ data
```

Two rules make this work:

<Steps>
  <Step title="Run the HMAC check BEFORE your x402 middleware">
    A ShadowFeed call carries no x402 payment — it carries an `X-Sf-*` signature instead. If your x402 middleware runs first, it returns `402 Payment Required` to ShadowFeed before the HMAC verifier ever sees the request.
  </Step>

  <Step title="On a valid signature, bypass the paywall and serve data">
    Set a flag (e.g. `skipX402`) your payment middleware honors. On a missing or invalid signature, fall through to your normal x402 flow so your direct Solana/EVM buyers keep paying as usual.
  </Step>
</Steps>

<Warning>
  **A broken HMAC bypass is silent revenue loss, not a loud error.** If the signature fails to verify (wrong secret, or — far more often — a path mismatch), the request falls through to your x402 paywall, which answers `402`. ShadowFeed treats any non-2xx upstream response as a failed delivery: **the agent's STX is not credited to you, and they receive an error even though they paid.** You won't see a 401 in your own logs — you'll see a 402 you served to ShadowFeed and wonder why revenue is zero. This is why the [path rules](#verifier-middleware) and the [pre-launch test](#verify-your-wiring-before-going-live) below are not optional.
</Warning>

## Setup checklist

* [ ] HMAC secret saved (from onboarding success screen or **Rotate secret** button)
* [ ] Decided which server hosts your `partner_endpoint`
* [ ] Set `SHADOWFEED_PARTNER_SECRET=your-secret` as env var on that server
* [ ] Confirmed your API can read the env var (`process.env`, `c.env`, `os.environ`, etc.)

## Verifier middleware

Pick the language tab matching your stack. All three implementations use the same canonical string format and are mutually compatible.

<Warning>
  **Get the PATH right or every single request fails — this is the most common integration killer.**

  ShadowFeed signs the canonical string using the **exact `source_path` you registered for the feed**, then calls `partner_endpoint + source_path`. Your verifier must rebuild the canonical string with that **same path** — not necessarily what `new URL(req.url).pathname` returns after a mount point, reverse proxy, or x402 router has rewritten it.

  Two registration rules keep both sides aligned:

  1. **`partner_endpoint` is an origin only** — `https://api.you.com`, no path, no trailing slash. Register `https://api.you.com/v1` with feed path `/whales` and ShadowFeed signs `/whales` but calls `…/v1/whales`; your server sees `/v1/whales` ≠ `/whales` → **401 on every call.**
  2. **`source_path` is the literal route your server already serves** — if your real route is `/whale-alerts`, register exactly that. A typo or a stale `/v1` prefix makes ShadowFeed hit a path that doesn't exist → **404 on every call.** (A real early integration did exactly this: it 404'd on every request and served zero data for weeks before anyone noticed — because the operator was watching their own logs for 401s that never came.)

  The simple examples below assume an origin-only `partner_endpoint` and a router mounted at the root. If your API sits behind a path prefix, see [Mounted behind a prefix or an x402 router](#mounted-behind-a-prefix-or-an-x402-router).
</Warning>

<Tabs>
  <Tab title="TypeScript / Workers">
    ```typescript theme={null}
    import { Hono } from 'hono';

    // Replace with your existing route file.
    const app = new Hono<{ Bindings: { SHADOWFEED_PARTNER_SECRET: string } }>();

    // Add this middleware BEFORE any x402 payment middleware so signed
    // requests bypass the payment requirement.
    app.use('*', async (c, next) => {
      if (c.req.header('X-Sf-Partner') !== 'shadowfeed') {
        return next();  // not a ShadowFeed call → continue normal x402 flow
      }
      const ok = await verifyShadowfeedHmac(c.req.raw, c.env.SHADOWFEED_PARTNER_SECRET);
      if (!ok) return c.json({ error: 'invalid HMAC signature' }, 401);
      c.set('skipX402', true);   // your x402 middleware reads this flag
      return next();
    });

    async function verifyShadowfeedHmac(req: Request, secret: string): Promise<boolean> {
      const ts    = parseInt(req.headers.get('X-Sf-Timestamp') ?? '', 10);
      const nonce = req.headers.get('X-Sf-Nonce') ?? '';
      const sig   = req.headers.get('X-Sf-Signature') ?? '';
      if (!ts || !nonce || !sig) return false;

      // 5-minute window — rejects replays of leaked signatures.
      if (Math.abs(Date.now() / 1000 - ts) > 300) return false;

      // Replay protection — store seen nonces in KV for 5 minutes.
      // (Omit if you can't tolerate the extra KV write; the timestamp window
      // already bounds the attack to 5 min after capture.)
      //
      //   const seen = await env.NONCES.get(nonce);
      //   if (seen) return false;
      //   await env.NONCES.put(nonce, '1', { expirationTtl: 300 });

      const enc = new TextEncoder();
      const body = req.method === 'GET' ? '' : await req.clone().text();
      const bodyHash = body
        ? [...new Uint8Array(await crypto.subtle.digest('SHA-256', enc.encode(body)))]
            .map(b => b.toString(16).padStart(2, '0')).join('')
        : '';
      const canonical = [
        req.method.toUpperCase(),
        new URL(req.url).pathname,
        String(ts), nonce, bodyHash,
      ].join('\n');

      const key = await crypto.subtle.importKey(
        'raw', enc.encode(secret),
        { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
      );
      const expected = [...new Uint8Array(
        await crypto.subtle.sign('HMAC', key, enc.encode(canonical)),
      )].map(b => b.toString(16).padStart(2, '0')).join('');

      return sig.toLowerCase() === expected.toLowerCase();
    }
    ```

    **Where to set the secret (Cloudflare Workers):**

    ```bash theme={null}
    wrangler secret put SHADOWFEED_PARTNER_SECRET
    # Paste the secret when prompted
    ```

    This stores it as a Worker secret — accessible at runtime via `c.env.SHADOWFEED_PARTNER_SECRET`, never visible in your code or logs.
  </Tab>

  <Tab title="Python / FastAPI">
    ```python theme={null}
    import os, hmac, hashlib, time
    from fastapi import FastAPI, Request, HTTPException

    PARTNER_SECRET = os.environ["SHADOWFEED_PARTNER_SECRET"].encode()
    _seen_nonces: dict[str, float] = {}    # swap for Redis in production

    app = FastAPI()

    async def verify_shadowfeed_hmac(request: Request) -> bool:
        ts    = request.headers.get("X-Sf-Timestamp", "")
        nonce = request.headers.get("X-Sf-Nonce", "")
        sig   = request.headers.get("X-Sf-Signature", "")
        if not (ts and nonce and sig):
            return False
        if abs(time.time() - int(ts)) > 300:
            return False

        # Replay protection — sweep + check
        now = time.time()
        expired = [n for n, t in _seen_nonces.items() if now - t > 300]
        for n in expired:
            _seen_nonces.pop(n, None)
        if nonce in _seen_nonces:
            return False
        _seen_nonces[nonce] = now

        body = await request.body() if request.method != "GET" else b""
        body_hash = hashlib.sha256(body).hexdigest() if body else ""
        canonical = "\n".join([
            request.method.upper(),
            request.url.path,
            ts, nonce, body_hash,
        ]).encode()

        expected = hmac.new(PARTNER_SECRET, canonical, hashlib.sha256).hexdigest()
        return hmac.compare_digest(sig.lower(), expected.lower())

    @app.middleware("http")
    async def shadowfeed_partner_middleware(request: Request, call_next):
        if request.headers.get("X-Sf-Partner") == "shadowfeed":
            if not await verify_shadowfeed_hmac(request):
                raise HTTPException(401, "invalid HMAC signature")
            request.state.skip_x402 = True
        return await call_next(request)
    ```

    **Where to set the secret (typical Python deployments):**

    | Platform  | Command                                           |
    | --------- | ------------------------------------------------- |
    | Heroku    | `heroku config:set SHADOWFEED_PARTNER_SECRET=...` |
    | Render    | Service → Environment → Add                       |
    | Railway   | Project → Variables → Add                         |
    | Docker    | `docker run -e SHADOWFEED_PARTNER_SECRET=... ...` |
    | Local dev | `.env` file + `python-dotenv`                     |
  </Tab>

  <Tab title="Go / net/http">
    ```go theme={null}
    package main

    import (
        "crypto/hmac"
        "crypto/sha256"
        "encoding/hex"
        "io"
        "net/http"
        "os"
        "strconv"
        "strings"
        "sync"
        "time"
    )

    var (
        partnerSecret = []byte(os.Getenv("SHADOWFEED_PARTNER_SECRET"))
        nonceCache    = sync.Map{} // map[string]int64 — nonce → expiresUnix
    )

    func abs(n int64) int64 { if n < 0 { return -n }; return n }

    func verifyShadowfeedHMAC(r *http.Request) bool {
        tsStr := r.Header.Get("X-Sf-Timestamp")
        nonce := r.Header.Get("X-Sf-Nonce")
        sig   := r.Header.Get("X-Sf-Signature")
        if tsStr == "" || nonce == "" || sig == "" { return false }

        ts, err := strconv.ParseInt(tsStr, 10, 64)
        if err != nil { return false }
        if abs(time.Now().Unix() - ts) > 300 { return false }

        // Replay protection
        if _, seen := nonceCache.Load(nonce); seen { return false }
        nonceCache.Store(nonce, time.Now().Unix() + 300)

        var bodyBytes []byte
        if r.Method != "GET" {
            bodyBytes, _ = io.ReadAll(r.Body)
            r.Body = io.NopCloser(strings.NewReader(string(bodyBytes)))
        }
        bodyHash := ""
        if len(bodyBytes) > 0 {
            h := sha256.Sum256(bodyBytes)
            bodyHash = hex.EncodeToString(h[:])
        }

        canonical := strings.Join([]string{
            strings.ToUpper(r.Method), r.URL.Path,
            tsStr, nonce, bodyHash,
        }, "\n")

        mac := hmac.New(sha256.New, partnerSecret)
        mac.Write([]byte(canonical))
        expected := hex.EncodeToString(mac.Sum(nil))
        return hmac.Equal([]byte(strings.ToLower(sig)), []byte(expected))
    }

    func shadowfeedMiddleware(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if r.Header.Get("X-Sf-Partner") == "shadowfeed" {
                if !verifyShadowfeedHMAC(r) {
                    http.Error(w, "invalid HMAC signature", 401)
                    return
                }
                // Mark context so downstream skips x402 payment requirement.
            }
            next.ServeHTTP(w, r)
        })
    }
    ```
  </Tab>
</Tabs>

## Mounted behind a prefix or an x402 router

x402 frameworks often mount everything under a prefix (e.g. `/api` or `/x402`). When that happens, `new URL(req.url).pathname` includes the prefix — but ShadowFeed signed the path **without** it. Strip the known prefix before building the canonical string:

```typescript theme={null}
// ShadowFeed signed the source_path you registered, e.g. "/whales".
// If your app is mounted at "/api", the inbound pathname is "/api/whales".
const MOUNT_PREFIX = '/api';                         // your mount, or '' if root
const fullPath     = new URL(req.url).pathname;      // "/api/whales"
const canonicalPath = fullPath.startsWith(MOUNT_PREFIX)
  ? (fullPath.slice(MOUNT_PREFIX.length) || '/')     // "/whales"
  : fullPath;
// → use canonicalPath (NOT fullPath) in the canonical string.
```

The **`@shadowfeed/provider-sdk`** adapters do this for you — they reconstruct the signed path from the route parameter, so the canonical path is identical no matter where the router is mounted. If you can adopt the SDK, you skip this class of bug entirely.

## Verify your wiring before going live

Do **not** make a real STX purchase your first test — it costs money and gives a vague pass/fail. Use a handshake test that signs and probes your endpoint exactly the way a buyer would, for free.

<Tabs>
  <Tab title="Dashboard (any stack)">
    In your provider dashboard, click **Test connection** (calls `POST /providers/id/:id/hmac/test`). ShadowFeed signs a request with your stored secret against your first active feed's `source_path`, fires it at `partner_endpoint + source_path`, and reports back:

    | Result                     | Meaning                                                                                                    |
    | -------------------------- | ---------------------------------------------------------------------------------------------------------- |
    | `ok`                       | Signature verified, your route returned data. You're ready.                                                |
    | `hmac_rejected` (401/403)  | Secret mismatch **or** path mismatch — re-check both sides.                                                |
    | `upstream_error` (404/5xx) | ShadowFeed reached your host but the route 404'd or errored — check `source_path` and middleware ordering. |
    | `network`                  | Couldn't reach `partner_endpoint` at all — DNS, TLS, or firewall.                                          |
    | `platform_secret_missing`  | Rotate the secret from the dashboard so we store an encrypted copy to sign with.                           |

    The response echoes `probed_path` so you can confirm ShadowFeed is hitting the exact route you expect.
  </Tab>

  <Tab title="SDK CLI">
    If you publish a `.well-known/shadowfeed-feeds.json` manifest (the SDK does this automatically), run the self-handshake from your own machine:

    ```bash theme={null}
    npx shadowfeed verify \
      --endpoint https://api.you.com \
      --secret "$SHADOWFEED_PARTNER_SECRET"
    ```

    It checks the manifest is reachable and valid, signs a GET against your first feed, and confirms your verifier accepts it and returns valid JSON — printing a green check per step.
  </Tab>
</Tabs>

Once the handshake passes, confirm the full payment path end-to-end with a single real purchase via the [ShadowFeed Agent SDK](/sdk/installation):

```typescript theme={null}
import { ShadowFeed } from 'shadowfeed-agent';

const sf = new ShadowFeed({ privateKey: process.env.AGENT_PRIVATE_KEY, network: 'mainnet' });
const result = await sf.buy('p/your-handle/your-feed-slug');
console.log(result.data);  // your data, served through the HMAC-authenticated path
```

When it works end-to-end: the agent pays STX → ShadowFeed signs + forwards → your middleware verifies and bypasses x402 → your route returns data → 97% of the price is credited to your `pending_revenue` counter. Confirm via the dashboard or `GET /providers/id/your-id/analytics`.

## Common pitfalls

| Symptom                                                                          | Cause                                                                                             | Fix                                                                                                                                                          |
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Always 401 from your endpoint                                                    | Wrong secret **or** path mismatch (your verifier signs a different PATH than ShadowFeed did)      | Re-paste secret on your server; confirm `partner_endpoint` is origin-only and your canonical PATH equals the registered `source_path` (strip mount prefixes) |
| ShadowFeed reports 404 / "route exists?"                                         | `source_path` doesn't match a real route, or `partner_endpoint` already contains part of the path | Register `partner_endpoint` as origin-only and `source_path` as the exact route your server serves                                                           |
| Buyers pay STX but you earn nothing; your logs show a `402` served to ShadowFeed | HMAC bypass didn't fire, so the call fell through to your x402 paywall                            | Run the HMAC check **before** x402; fix the secret/path so the bypass triggers (use **Test connection**)                                                     |
| Some calls 401, others succeed                                                   | Clock drift > 5 min                                                                               | Sync your server clock (NTP)                                                                                                                                 |
| 401 with body hash mismatch                                                      | Body modified by proxy/middleware before verifier sees it                                         | Verify BEFORE any body parsing; use `req.clone().text()` (TS)                                                                                                |
| Pre-paid calls work, post-paid agent calls fail                                  | Middleware ordering                                                                               | Place HMAC check BEFORE your x402 middleware                                                                                                                 |
| Worked, then suddenly all 401s                                                   | Secret rotated but not deployed                                                                   | Re-set the env var with the new secret                                                                                                                       |

## Building with Claude Code

If you use [Claude Code](https://claude.com/claude-code) to maintain your codebase, paste this prompt to bootstrap the integration:

> I want to integrate my API as a ShadowFeed external data provider. My stack is **\[Cloudflare Workers / FastAPI / Go / etc.]**. I have the HMAC secret saved as `SHADOWFEED_PARTNER_SECRET`. The full integration guide is at [docs.shadowfeed.app/providers/hmac-integration](https://docs.shadowfeed.app/providers/hmac-integration).
>
> Walk me through:
>
> 1. Adding the HMAC verifier middleware to my existing routes (preserve current x402 / auth)
> 2. Setting the env var on my deployment platform
> 3. Testing the integration end-to-end
>
> My API base URL is `https://api.mycompany.com` and the feed I want to expose is at `/v1/whales`.

Claude can read this doc, your codebase, and walk you through the wiring step-by-step.

## Reference

### Canonical string

```text theme={null}
{METHOD}\n{PATH}\n{TIMESTAMP}\n{NONCE}\n{SHA256(BODY)}
```

Replace each placeholder with the actual values per request.

| Component      | Value                                                                                                                                                                                                                              |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `METHOD`       | Uppercase HTTP verb: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`                                                                                                                                                                       |
| `PATH`         | The registered `source_path` — no host, no query string. Must match byte-for-byte on both sides; trailing slash matters. Don't put a query string in `source_path` (your verifier's `pathname` would drop it and break the match). |
| `TIMESTAMP`    | Unix seconds (10 digits in 2026). Must be within ±300s of server clock.                                                                                                                                                            |
| `NONCE`        | Random unique string per request. Recommended: UUID v4.                                                                                                                                                                            |
| `SHA256(BODY)` | Lowercase hex SHA-256 of raw request body. Empty string `""` if no body.                                                                                                                                                           |

### Headers sent by ShadowFeed

| Header           | Value                                          |
| ---------------- | ---------------------------------------------- |
| `X-Sf-Partner`   | `shadowfeed` (constant marker)                 |
| `X-Sf-Timestamp` | Unix seconds at signing time                   |
| `X-Sf-Nonce`     | Random nonce                                   |
| `X-Sf-Signature` | HMAC-SHA256(secret, canonical) — lowercase hex |

### Signature algorithm

```text theme={null}
signature = hex(HMAC_SHA256(secret_bytes, canonical_string_bytes))
```

<Warning>
  Use **constant-time comparison** when verifying — never use plain string equality. Python: `hmac.compare_digest`. Go: `hmac.Equal`. TypeScript: compare byte-by-byte after equal-length check.
</Warning>

## Next steps

<CardGroup cols={2}>
  <Card title="Withdrawals & Revenue" icon="wallet" href="/providers/withdrawals">
    How to cash out accumulated STX.
  </Card>

  <Card title="Troubleshooting" icon="circle-question" href="/providers/troubleshooting">
    Debug HMAC failures, request issues.
  </Card>
</CardGroup>
