Creator documentation

A reference endpoint you can clone

One file, standard library only, with the whole contract handled. Replace one function and deploy.

Sections

Run it#

Clone, configure, runbash
go build -o pingback .

NEXUS_CREATOR_KEY=nx_crt_...           \
NEXUS_REGISTERED_HOST=mcp.you.example  \
NEXUS_PUBLIC_KEY=base64...             \
PORT=8080 ./pingback
VariableRequiredWhat it does
NEXUS_CREATOR_KEYYesThe nx_crt_ value shown once when you published. Without it the process refuses to start
NEXUS_REGISTERED_HOSTWith a public keyThe host you registered — not the inbound Host header
NEXUS_PUBLIC_KEYYesEnables signature verification. The process refuses to start without it, unless you set NEXUS_ALLOW_UNVERIFIED=1 on purpose
NEXUS_ALLOW_UNVERIFIEDNoSet to 1 to run with no signature verification — a first local call, nothing else. It logs a warning on every start
NEXUS_DEBUG_SIGNINGNoSet to 1 to print the canonical string when verification fails. Off by default: that string embeds the request path and the correlation id, which are attacker-controlled
PORTNoDefaults to 8080

The only function you replace#

Everything else in the file is the contract. This is your tool. Until you change it, it echoes the envelope — which is the most useful thing a first call can tell you.

runToolgo
func runTool(r *http.Request, body []byte, verdict Verdict) string {
	sum := sha256.Sum256(body)
	return fmt.Sprintf(
		"pingback ok\n"+
			"  creator key:    matched\n"+
			"  signature:      %s\n"+
			"  correlation id: %s\n"+
			"  method + path:  %s %s\n"+
			"  host as sent:   %s\n"+
			"  body bytes:     %d\n"+
			"  body sha256:    %s\n"+
			"  Authorization:  %s\n"+
			"  X-Nexus-Api-Key present: %t  (must be false: the developer's key never reaches you)\n"+
			"  request body:   %s",
		verdict.SignatureStatus,
		r.Header.Get("X-Correlation-ID"),
		r.Method, RequestTarget(r),
		r.Host,
		len(body),
		hex.EncodeToString(sum[:]),
		describeHeader(r.Header.Get("Authorization")),
		r.Header.Get("X-Nexus-Api-Key") != "",
		truncate(string(body), 400),
	)
}

What to try once a call lands#

A ping-back is the right shape for learning the system, because these four cases teach more than a working call does:

Do thisExpect
Return a 500The developer is refunded automatically, and you are not paid — the failure happened before any byte reached them
Sleep 10 seconds before the first byteCut at 5 seconds idle. The developer gets 502 Upstream_Unavailable and is refunded
Return a 302 with a LocationClamped to 502 and the header stripped. We never follow a redirect
Send the wrong creator key deliberatelyYour own endpoint answers 401 — proving the check works before you rely on it

The whole file#

385 lines, no dependencies beyond the standard library.

examples/creator-pingback/main.gogo
// A complete Nexus402 creator endpoint you can host.
//
// Clone this directory, replace `runTool` with your own logic, deploy behind
// TLS, and publish the URL. Everything else here is the contract and should be
// left alone.
//
// IT IS A PING-BACK UNTIL YOU CHANGE IT: `runTool` echoes what it received,
// including which checks passed, so your first successful call shows you the
// whole envelope rather than a bare "ok".
//
// NO DEPENDENCIES. Standard library only, which is the point: Nexus402 ships
// no SDK, and a creator endpoint needs none. This file is a reference to copy,
// not a package to import — so it can never version-skew against the gateway.
//
//	go build -o pingback . && \
//	  NEXUS_CREATOR_KEY=nx_crt_...           \
//	  NEXUS_REGISTERED_HOST=mcp.you.example  \
//	  NEXUS_PUBLIC_KEY=base64...             \
//	  PORT=8080 ./pingback
//
// The two checks, in the order they matter:
//
//  1. `X-Nexus-Creator-Key` — a constant-time compare. Proves the request came
//     from Nexus402. It AUTHENTICATES on its own; it does not SECURE on its
//     own, which is what the second check is for.
//  2. The Ed25519 signature — proves the request was not altered, and binds it
//     to your host and to a 60-second window.
//
// BOTH ARE REQUIRED TO START. `NEXUS_PUBLIC_KEY` unset is a fatal error unless
// you also set `NEXUS_ALLOW_UNVERIFIED=1`, which is for a first local call and
// nothing else. This paragraph said the signature was "optional" and that the
// server "still serves" without it, which was true before the opt-in was added
// and false afterwards — a creator who believed it configured one variable and
// met a fatal the documentation had ruled out.
//
// Why bother with the second one, when the first authenticates? The creator key
// is a SHARED SECRET sitting on your server, sent on every request. If it leaks
// through a log, a proxy or a misconfigured CDN, anyone can impersonate
// Nexus402 to you and take free tool calls. The signature is asymmetric — you
// hold only a public key, so there is nothing on your side worth stealing.
//
// WHAT THE SIGNATURE DOES NOT DO: it narrows replay rather than closing it.
// There is no nonce and no seen-cache here, so a captured request can be
// replayed to this same host until its timestamp leaves the 60-second window.
// Closing that needs per-request state, which this example deliberately does
// not keep — if you need it, record the signed `X-Correlation-ID` and reject
// a repeat.
package main

import (
	"crypto/ed25519"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"
)

const (
	// maxRequestBody bounds what this endpoint will read. The gateway caps
	// your RESPONSE at 10 MiB; bounding the request too means a confused or
	// hostile caller cannot make this process hold an unbounded buffer.
	maxRequestBody = 1 << 20 // 1 MiB

	// signatureWindow must match the gateway's 60 seconds. Wider accepts older
	// replays; narrower rejects honest requests on ordinary clock skew.
	signatureWindow = 60 * time.Second
)

// Endpoint holds the configuration a creator endpoint needs.
type Endpoint struct {
	// CreatorKey is the `nx_crt_` value shown once when you published.
	CreatorKey string
	// RegisteredHost is the host you registered with Nexus402 — NOT the
	// inbound `Host` header.
	//
	// THIS IS THE DETAIL INTEGRATORS GET WRONG. The signature covers the host
	// the gateway SENT. Behind a proxy, a load balancer or a PaaS router the
	// header your process sees may have been rewritten, and a verifier reading
	// it would then reject every valid request. The registered value is a
	// constant you already know.
	RegisteredHost string
	// PublicKey is Nexus402's Ed25519 public key. Nil disables signature
	// verification; the creator-key check still runs.
	PublicKey ed25519.PublicKey
}

// Verdict is what the checks concluded, so a handler can report it honestly.
type Verdict struct {
	KeyMatched      bool
	SignatureStatus string
}

// refusesToStart is the no-public-key rule, in one place.
//
// A function rather than an inline condition so a test can assert the actual
// rule `main` uses. The opt-in must be EXACTLY "1": accepting "true", "yes"
// or any non-empty value would mean a stray or misspelled value silently
// turns verification off, which is the failure this rule exists to prevent.
func refusesToStart(publicKey, allowUnverified string) bool {
	return strings.TrimSpace(publicKey) == "" && allowUnverified != "1"
}

func main() {
	endpoint := Endpoint{
		CreatorKey:     os.Getenv("NEXUS_CREATOR_KEY"),
		RegisteredHost: os.Getenv("NEXUS_REGISTERED_HOST"),
	}
	if endpoint.CreatorKey == "" {
		log.Fatal("NEXUS_CREATOR_KEY is required: without it this endpoint cannot tell " +
			"Nexus402 from anyone else who found the URL")
	}
	// NO PUBLIC KEY IS A REFUSAL, not a default.
	//
	// This used to serve happily without one, reporting the signature as "not
	// checked". That is a FAIL-OPEN default in a file creators copy and host
	// publicly, and the forgotten-variable case is the common one. Without the
	// signature, the creator key is the only thing standing between a stranger
	// and this endpoint — and it is a bearer token sent on every request, so
	// one leak through a log, a proxy or a misconfigured CDN is enough. The
	// signature is also what binds the request to THIS host and to a 60-second
	// window, so losing it loses replay protection too.
	//
	// Unverified mode still exists, because it is genuinely useful for a first
	// local call. It now takes an explicit opt-in, so it cannot happen by
	// omission.
	raw := strings.TrimSpace(os.Getenv("NEXUS_PUBLIC_KEY"))
	if refusesToStart(raw, os.Getenv("NEXUS_ALLOW_UNVERIFIED")) {
		log.Fatal("NEXUS_PUBLIC_KEY is required. Without it this endpoint cannot tell a " +
			"request Nexus402 signed from one a stranger replayed, and the creator key " +
			"alone is a bearer token that leaks through logs and proxies. To run " +
			"WITHOUT signature verification on purpose — a first local call, nothing " +
			"else — set NEXUS_ALLOW_UNVERIFIED=1.")
	}
	if raw == "" {
		log.Print("WARNING: running with NO SIGNATURE VERIFICATION " +
			"(NEXUS_ALLOW_UNVERIFIED=1). Every request that carries the creator key is " +
			"accepted, replays included. Do not serve real traffic like this.")
	}
	if raw != "" {
		decoded, err := base64.StdEncoding.DecodeString(raw)
		if err != nil || len(decoded) != ed25519.PublicKeySize {
			log.Fatalf("NEXUS_PUBLIC_KEY is not a base64 Ed25519 public key")
		}
		endpoint.PublicKey = ed25519.PublicKey(decoded)
		if endpoint.RegisteredHost == "" {
			// REFUSED rather than guessed. Falling back to the inbound header
			// would work in testing and fail behind every real proxy, which is
			// the worst possible time to find out.
			log.Fatal("NEXUS_REGISTERED_HOST is required when NEXUS_PUBLIC_KEY is set: " +
				"the host is part of the signed message, and reading it from the " +
				"request header breaks behind any proxy that rewrites Host")
		}
	}

	http.HandleFunc("/mcp", endpoint.Handle)
	http.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("ok"))
	})

	// PARSED TO AN INTEGER, and the integer is what gets used.
	//
	// `PORT` is environment input, and it reaches both a log line and a listen
	// address. A value carrying a newline would forge log entries, so the raw
	// string is never printed or concatenated: `port` holds the parsed number
	// and every later use formats it with %d. Validating the string and then
	// using the string would be the same bug with a check in front of it.
	port := 8080
	if raw := os.Getenv("PORT"); raw != "" {
		n, err := strconv.Atoi(raw)
		if err != nil || n < 1 || n > 65535 {
			log.Fatal("PORT must be a number between 1 and 65535")
		}
		port = n
	}
	// #nosec G706 -- `port` is an int, formatted %d: it cannot carry a newline,
	// so there is nothing to inject. gosec tracks that os.Getenv reached this
	// call and not that strconv.Atoi stands between them. Narrowed to this one
	// line rather than excluding `examples/` in .golangci.yml, because this is
	// the file creators COPY and is the last place to turn a security linter off.
	log.Printf("creator endpoint on :%d/mcp (signature verification: %v)",
		port, endpoint.PublicKey != nil)
	// These bound the INBOUND direction. The gateway's own limits on you are
	// 5s to the first byte at any idle point and 120s for the whole response.
	server := &http.Server{
		Addr:              fmt.Sprintf(":%d", port),
		ReadHeaderTimeout: 5 * time.Second,
		ReadTimeout:       15 * time.Second,
		WriteTimeout:      30 * time.Second,
		IdleTimeout:       60 * time.Second,
	}
	log.Fatal(server.ListenAndServe())
}

// Handle authenticates the request, then runs the tool.
func (e Endpoint) Handle(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "POST only", http.StatusMethodNotAllowed)
		return
	}
	body, err := io.ReadAll(io.LimitReader(r.Body, maxRequestBody+1))
	if err != nil {
		http.Error(w, "unreadable body", http.StatusBadRequest)
		return
	}
	if len(body) > maxRequestBody {
		http.Error(w, "body too large", http.StatusRequestEntityTooLarge)
		return
	}

	verdict, err := e.Authenticate(r, body)
	if err != nil {
		// NO DETAIL IN THE BODY. A response that explains which check failed
		// tells a prober how close they are. The reason goes to your log.
		log.Printf("refused: %v", err)
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	// Content blocks are what the gateway relays to the developer's MCP
	// client, so this shape is what they actually see.
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	_ = json.NewEncoder(w).Encode(map[string]any{
		"content": []map[string]string{{
			"type": "text",
			"text": runTool(r, body, verdict),
		}},
	})
}

// Authenticate runs both checks and returns what it concluded.
func (e Endpoint) Authenticate(r *http.Request, body []byte) (Verdict, error) {
	// CONSTANT TIME. A byte-at-a-time comparison leaks the key one position at
	// a time to anyone who can measure the response.
	presented := r.Header.Get("X-Nexus-Creator-Key")
	if subtle.ConstantTimeCompare([]byte(presented), []byte(e.CreatorKey)) != 1 {
		return Verdict{}, fmt.Errorf("creator key mismatch")
	}
	verdict := Verdict{KeyMatched: true, SignatureStatus: "not checked (no public key configured)"}
	if e.PublicKey == nil {
		return verdict, nil
	}
	if err := e.VerifySignature(r, body); err != nil {
		return Verdict{}, err
	}
	verdict.SignatureStatus = "verified"
	return verdict, nil
}

// VerifySignature checks the six-field Ed25519 signature.
//
// SIX fields, and the order is fixed:
//
//	method \n host \n path-and-query \n timestamp \n correlation-id \n sha256(body)
//
// The host is one of them. It is what stops a request signed for one creator
// being replayed at another creator's endpoint, which matters because every
// creator verifies with the same platform key today.
func (e Endpoint) VerifySignature(r *http.Request, body []byte) error {
	raw := strings.TrimPrefix(r.Header.Get("X-Nexus-Signature"), "ed25519:")
	signature, err := base64.StdEncoding.DecodeString(raw)
	if err != nil || len(signature) != ed25519.SignatureSize {
		return fmt.Errorf("signature is not 64 base64-encoded bytes")
	}

	timestamp := r.Header.Get("X-Nexus-Timestamp")
	seconds, err := strconv.ParseInt(timestamp, 10, 64)
	if err != nil {
		return fmt.Errorf("timestamp %q is not unix seconds", timestamp)
	}
	// BOTH directions. Without an upper bound a future-dated request is valid
	// forever; without a lower one a captured request replays forever.
	if delta := time.Since(time.Unix(seconds, 0)); delta > signatureWindow || delta < -signatureWindow {
		return fmt.Errorf("timestamp is %s outside the %s window", delta, signatureWindow)
	}

	message := CanonicalString(r.Method, e.RegisteredHost, RequestTarget(r), timestamp,
		r.Header.Get("X-Correlation-ID"), body)
	if !ed25519.Verify(e.PublicKey, message, signature) {
		// THE CANONICAL STRING IS NOT LOGGED BY DEFAULT, and that is a change
		// from the obvious thing to do.
		//
		// It embeds the request path, the query and the correlation id -- all
		// attacker-controlled. Writing them to a creator’s log hands a stranger
		// a pen: text of their choosing in the log an operator reads, and one
		// line per failed probe, so a flood of garbage requests is a flood of
		// log volume. Neither is catastrophic; both are free to avoid.
		//
		// So the default says only THAT it failed, with a short hash of the
		// string, which is enough to tell "every request fails identically"
		// from "they differ". Set NEXUS_DEBUG_SIGNING=1 to see the bytes while
		// you are debugging your own deployment.
		if os.Getenv("NEXUS_DEBUG_SIGNING") == "1" {
			log.Printf("canonical string did not verify:\n%s", message)
		} else {
			sum := sha256.Sum256(message)
			log.Printf("signature did not verify (canonical sha256 %x, set "+
				"NEXUS_DEBUG_SIGNING=1 to print the string)", sum[:6])
		}
		return fmt.Errorf("ed25519 verification failed")
	}
	return nil
}

// CanonicalString builds the exact bytes Nexus402 signed.
func CanonicalString(method, host, target, timestamp, correlationID string, body []byte) []byte {
	// The RAW bytes are hashed. Hashing a re-serialised JSON value changes the
	// bytes and the hash will not match.
	sum := sha256.Sum256(body)
	return []byte(strings.Join([]string{
		method,
		host,
		target,
		timestamp,
		correlationID,
		hex.EncodeToString(sum[:]),
	}, "\n"))
}

// RequestTarget is the path WITH the query, matching what the gateway signs.
func RequestTarget(r *http.Request) string {
	if r.URL.RawQuery == "" {
		return r.URL.EscapedPath()
	}
	return r.URL.EscapedPath() + "?" + r.URL.RawQuery
}

// runTool is THE ONLY FUNCTION YOU REPLACE.
//
// It receives the authenticated request and returns the text your tool
// produces. Return quickly: the gateway allows 5 seconds to your first byte at
// any idle point, and 120 seconds for the whole response.
//
// Until you change it, it echoes the envelope — which is the most useful thing
// a first call can tell you.
func runTool(r *http.Request, body []byte, verdict Verdict) string {
	sum := sha256.Sum256(body)
	return fmt.Sprintf(
		"pingback ok\n"+
			"  creator key:    matched\n"+
			"  signature:      %s\n"+
			"  correlation id: %s\n"+
			"  method + path:  %s %s\n"+
			"  host as sent:   %s\n"+
			"  body bytes:     %d\n"+
			"  body sha256:    %s\n"+
			"  Authorization:  %s\n"+
			"  X-Nexus-Api-Key present: %t  (must be false: the developer's key never reaches you)\n"+
			"  request body:   %s",
		verdict.SignatureStatus,
		r.Header.Get("X-Correlation-ID"),
		r.Method, RequestTarget(r),
		r.Host,
		len(body),
		hex.EncodeToString(sum[:]),
		describeHeader(r.Header.Get("Authorization")),
		r.Header.Get("X-Nexus-Api-Key") != "",
		truncate(string(body), 400),
	)
}

func describeHeader(value string) string {
	if value == "" {
		return "(absent)"
	}
	return fmt.Sprintf("(present, %d bytes — the DEVELOPER's own, passed through)", len(value))
}

func truncate(s string, n int) string {
	if len(s) <= n {
		return s
	}
	return s[:n] + "..."
}