---
title: "x402 Signing Primitives (Go)"
canonical: "https://test.abhinandan.one/x402-payments-overview-04a296ff/go-x402-signing-primitives"
markdown_url: "https://test.abhinandan.one/x402-payments-overview-04a296ff/go-x402-signing-primitives.md"
publisher: "Primitive SDKs"
kind: "reference"
content_type: "reference"
category: "Go SDK"
parent: "x402-payments-overview-04a296ff"
description: "Reference for DeriveEIP3009Nonce, ComputePaymentValidityWindow, SignInteractionPayment, and BuildExactEvmPaymentPayload, the Go SDK's low-level x402 signing primitives."
keywords: ["DeriveEIP3009Nonce", "ComputePaymentValidityWindow", "SignInteractionPayment", "BuildExactEvmPaymentPayload", "TokenDomain", "X402Signer"]
last_modified: "2026-08-21T18:22:43.359885+00:00"
published_at: "2026-08-11T18:54:58.894074+00:00"
source_files:
  - "sdk-go/x402.go"
  - "sdk-go/x402_test.go"
sections:
  - {anchor: "deriveeip3009nonce", title: "DeriveEIP3009Nonce"}
  - {anchor: "noncebinding", title: "NonceBinding"}
  - {anchor: "computepaymentvaliditywindow", title: "ComputePaymentValidityWindow"}
  - {anchor: "validitywindowinput", title: "ValidityWindowInput"}
  - {anchor: "signinteractionpayment", title: "SignInteractionPayment"}
  - {anchor: "signinteractionpaymentinput", title: "SignInteractionPaymentInput"}
  - {anchor: "tokendomain", title: "TokenDomain"}
  - {anchor: "buildexactevmpaymentpayload", title: "BuildExactEvmPaymentPayload"}
  - {anchor: "transferauthorization", title: "TransferAuthorization"}
  - {anchor: "x402signer-and-privatekeysigner", title: "X402Signer and PrivateKeySigner"}
  - {anchor: "buildpayoutregistrationmessage", title: "BuildPayoutRegistrationMessage"}
  - {anchor: "next-steps", title: "Next steps"}
---

> Documentation index: https://test.abhinandan.one/llms.txt

# x402 Signing Primitives (Go)

Reference for the low-level Go building blocks behind Pay, DeriveEIP3009Nonce, ComputePaymentValidityWindow, SignInteractionPayment, and BuildExactEvmPaymentPayload, for callers who need to drive x402 signing themselves.

These are the building blocks `Client.Pay` uses internally to sign an [x402 payment challenge](https://test.abhinandan.one/x402-payments-overview.md). Reach for them directly only when `Pay` doesn't fit your signing flow, for example signing a challenge carried in an email reply and submitting the payment separately, or driving a hardware wallet or remote KMS instead of an in-process key. For the default flow, use [Creating a Payment Challenge](https://test.abhinandan.one/x402-payments-overview-04a296ff/go-x402-create-charge.md) and `Pay` as documented in [x402 Payments Overview](https://test.abhinandan.one/x402-payments-overview.md).

All four functions live in package `primitive` (module `github.com/primitivedotdev/sdks/sdk-go`). None of them perform I/O: they derive, assemble, and sign locally, and never send anything over the wire.

## DeriveEIP3009Nonce

`DeriveEIP3009Nonce` derives the EIP-3009 nonce bound to a specific interaction step. The platform recomputes this exact value and rejects a payment whose nonce doesn't match.

```go
func DeriveEIP3009Nonce(input NonceBinding) (string, error)
```

The byte layout is locked to a normative test vector the platform verifier also computes, and MUST NOT change:

```text
keccak256( utf8(lower(interaction_id)) || 0x00
         || utf8(lower(challenge_step_id)) || 0x00
         || hexdecode(challenge_nonce) )
```

The `0x00` separators pin the field boundaries; undelimited concatenation of variable-length strings is collision-ambiguous. The challenge nonce is decoded to its 32 raw bytes before hashing. Returns the 0x-prefixed 32-byte hash.

### NonceBinding

| Field | Type | Description |
|---|---|---|
| `InteractionID` | `string` | The interaction id, including its `@domain`. Lowercased before hashing. |
| `ChallengeStepID` | `string` | The challenge step id (a UUID). Lowercased before hashing. |
| `ChallengeNonce` | `string` | The challenger's per-challenge random nonce: exactly 64 lowercase hex chars, no `0x` prefix. |

```go
// main.go
package main

import (
	primitive "github.com/primitivedotdev/sdks/sdk-go"
)

nonce, err := primitive.DeriveEIP3009Nonce(primitive.NonceBinding{
	InteractionID:   "a1b2c3d4-0000-0000-0000-000000000001@payer.example",
	ChallengeStepID: "f00dface-0000-0000-0000-0000000000aa",
	ChallengeNonce:  "aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899",
})
// nonce == "0xc955a08812ab83f9e25c92e5162267b913957c3cc8678de1cf1449f77b516c6e"
```

Case is normalized before hashing: uppercasing `InteractionID` and `ChallengeStepID` produces the identical nonce. `ChallengeNonce` itself, however, must already be lowercase hex, or `DeriveEIP3009Nonce` returns an error.

**Errors**: returns an error if `ChallengeNonce` is not exactly 64 lowercase hex characters, or is not valid hex.

## ComputePaymentValidityWindow

`ComputePaymentValidityWindow` computes the EIP-3009 `(validAfter, validBefore)` window for a payment, landing it inside the band the platform accepts.

```go
func ComputePaymentValidityWindow(input ValidityWindowInput) (validAfter, validBefore *big.Int, err error)
```

`validBefore` governs on-chain validity: it must stay far enough in the future to settle, yet not so far that the total window exceeds the cap. `validAfter` is set generously in the past to absorb clock skew. By default the function clamps the computed window into the accepted band so a caller who doesn't override anything always gets a signable window.

### ValidityWindowInput

| Field | Type | Default | Description |
|---|---|---|---|
| `ChallengeExpiresAtSec` | `int64` | required | The challenge's `expires_at`, unix seconds. |
| `NowSec` | `int64` | required | Current time, unix seconds. |
| `SettlementMarginSec` | `int64` | 300 (5 min) | Headroom past expiry for verify+settle to complete. |
| `ClockSkewSec` | `int64` | 300 (5 min) | How far in the past to set `validAfter`. |
| `MaxWindowSec` | `int64` | `DefaultMaxWindowSec` (24h) | Hard ceiling on `validBefore - validAfter`. |
| `MinHeadroomSec` | `int64` | `DefaultMinSettlementHeadroomSec` (60s) | Minimum `validBefore - NowSec`. |
| `ValidBeforeSec` | `*int64` | derived | Pin `validBefore`. When nil, derived as `ChallengeExpiresAtSec + SettlementMarginSec`. |
| `ValidAfterSec` | `*int64` | derived | Pin `validAfter`. When nil, derived as `NowSec - ClockSkewSec`. |
| `Clamp` | `*bool` | true (nil = true) | When true, an out-of-band window is clamped into the accepted band instead of erroring. |

A signed EIP-3009 authorization stays settleable on-chain until `validBefore` regardless of interaction state, so an unbounded window is a standing "funds committed" risk; `DefaultMaxWindowSec` (24 hours) is the hard safety ceiling. `DefaultMinSettlementHeadroomSec` (60 seconds) is the floor below which the platform rejects a payment as "about to expire," because it needs SMTP + DKIM + verify + settle latency to clear.

**Clamping behavior**:

- Default (no pinned bounds, or `Clamp` left `nil`/`true`): a computed window outside `[floor, ceiling]` is silently clamped to fit.
- Pinned + `Clamp: false`: a `ValidBeforeSec` outside the band returns a specific error naming which bound was violated ("about to expire" vs. "authorization window too wide") instead of signing a doomed authorization.
- Pinned + `Clamp` unset or `true`: the pinned value is clamped like the computed one.

```go
// main.go
import (
	"log"
	"time"

	primitive "github.com/primitivedotdev/sdks/sdk-go"
)

validAfter, validBefore, err := primitive.ComputePaymentValidityWindow(primitive.ValidityWindowInput{
	ChallengeExpiresAtSec: challengeExpiresAtUnix,
	NowSec:                time.Now().Unix(),
})
if err != nil {
	log.Fatal(err)
}
```

Pinning with clamp disabled, to catch an out-of-band caller value instead of silently moving it:

```go
clamp := false
vbPin := time.Now().Unix() + 5 // only 5s headroom, below the 60s floor
_, _, err := primitive.ComputePaymentValidityWindow(primitive.ValidityWindowInput{
	ChallengeExpiresAtSec: challengeExpiresAtUnix,
	NowSec:                time.Now().Unix(),
	ValidBeforeSec:        &vbPin,
	Clamp:                 &clamp,
})
// err: "validBefore (...) is below the minimum settlement headroom ...
//       the authorization would be rejected as about to expire"
```

**Errors**: returns an error when `MaxWindowSec < MinHeadroomSec` (invalid config), when a pinned `ValidBeforeSec` falls outside the accepted band with `Clamp: false`, or when the resulting `validBefore` would not be after `validAfter` (typically an already-expired challenge or a pinned `ValidAfterSec` set too late).

## SignInteractionPayment

`SignInteractionPayment` derives the bound nonce, assembles the EIP-3009 authorization, and signs it with your `Sign` callback. It is the one step a stock x402 signer can't do on its own, since such signers generate the nonce internally with no injection point.

```go
func SignInteractionPayment(input SignInteractionPaymentInput) (TransferAuthorization, string, error)
```

### SignInteractionPaymentInput

| Field | Type | Description |
|---|---|---|
| `Sign` | `func(apitypes.TypedData) (string, error)` | Signs EIP-712 typed data with the caller's own key (e.g. `PrivateKeySigner.SignTypedData`). The key never leaves the caller. |
| `Payer` | `string` | The from address. |
| `Domain` | `TokenDomain` | The token's EIP-712 domain (see below). |
| `PayTo` | `string` | The recipient, the challenger's `payTo`. |
| `Amount` | `*big.Int` | Amount in token base units. |
| `NonceBinding` | `NonceBinding` | Same shape as for `DeriveEIP3009Nonce`. |
| `ValidAfter` | `*big.Int` | From `ComputePaymentValidityWindow`. |
| `ValidBefore` | `*big.Int` | From `ComputePaymentValidityWindow`. |

### TokenDomain

| Field | Type | Description |
|---|---|---|
| `Name` | `string` | The token's EIP-712 domain name. Take it from the challenge's `payment_requirements.extra`; a wrong value produces a signature the verifier rejects. |
| `Version` | `string` | The token's EIP-712 domain version, same source as `Name`. |
| `ChainID` | `int64` | The EVM chain id (`84532` for `base-sepolia`, `8453` for `base`). |
| `VerifyingContract` | `string` | The token contract address, from `payment_requirements.asset`. |

Returns the `TransferAuthorization` it built plus the hex signature string.

```go
pr := challenge.PaymentRequirements
auth, signature, err := primitive.SignInteractionPayment(primitive.SignInteractionPaymentInput{
	Sign:  payer.SignTypedData,
	Payer: payer.Address(),
	Domain: primitive.TokenDomain{
		Name:              pr.Extra.Name,
		Version:           pr.Extra.Version,
		ChainID:           84532, // base-sepolia
		VerifyingContract: pr.Asset,
	},
	PayTo:  pr.PayTo,
	Amount: amount,
	NonceBinding: primitive.NonceBinding{
		InteractionID:   challenge.NonceBinding.InteractionID,
		ChallengeStepID: challenge.NonceBinding.ChallengeStepID,
		ChallengeNonce:  challenge.NonceBinding.ChallengeNonce,
	},
	ValidAfter:  validAfter,
	ValidBefore: validBefore,
})
```

**Errors**: propagates a `DeriveEIP3009Nonce` error for a malformed `NonceBinding`, or any error your `Sign` callback returns.

## BuildExactEvmPaymentPayload

`BuildExactEvmPaymentPayload` assembles and validates the exact-EVM x402 wire payload from a signed authorization: the JSON body submitted to `POST /v1/x402/challenges/{id}/pay`.

```go
func BuildExactEvmPaymentPayload(network string, auth TransferAuthorization, signature string) (X402PaymentPayload, error)
```

| Parameter | Type | Description |
|---|---|---|
| `network` | `string` | Must be `"base"` or `"base-sepolia"`. |
| `auth` | `TransferAuthorization` | The authorization returned by `SignInteractionPayment` (or assembled by hand). |
| `signature` | `string` | The 0x-prefixed 65-byte (130 hex char) EIP signature. |

The numeric authorization fields (`Value`, `ValidAfter`, `ValidBefore`) are `*big.Int` in Go but decimal strings on the wire; this function stringifies them. The nonce passes through as hex. Validation rejects a malformed nonce or signature loudly rather than emitting a payload the platform will reject.

```go
payment, err := primitive.BuildExactEvmPaymentPayload(challenge.Network, auth, signature)
if err != nil {
	log.Fatal(err)
}
// submit `payment` to /v1/x402/challenges/{id}/pay
```

**Errors**: returns an error for an unsupported `network` (anything other than `"base"` / `"base-sepolia"`), a `signature` that isn't a 0x-prefixed 130-hex-char string, or an `auth.Nonce` that isn't a 0x-prefixed 64-hex-char string.

## TransferAuthorization

`TransferAuthorization` is the EIP-3009 `TransferWithAuthorization` message assembled by `SignInteractionPayment` and consumed by `BuildExactEvmPaymentPayload`.

| Field | Type | Description |
|---|---|---|
| `From` | `string` | The payer's address. |
| `To` | `string` | The recipient's address (`payTo`). |
| `Value` | `*big.Int` | Amount in token base units. |
| `ValidAfter` | `*big.Int` | Start of the validity window, unix seconds. |
| `ValidBefore` | `*big.Int` | End of the validity window, unix seconds. |
| `Nonce` | `string` | The 0x-prefixed 32-byte interaction-bound nonce. |

## X402Signer and PrivateKeySigner

`X402Signer` is the caller-held signer interface these primitives sign through; `PrivateKeySigner` is the built-in implementation backed by an in-memory secp256k1 key. The interface is:

```go
type X402Signer interface {
	Address() string
	SignTypedData(typedData apitypes.TypedData) (string, error)
	SignMessage(message string) (string, error)
}
```

`PrivateKeySigner`, built with `NewPrivateKeySigner(hexKey string)`, implements this directly from an in-memory secp256k1 key (with or without the `0x` prefix). The key stays in process memory and is never sent to Primitive. `SignMessage` (EIP-191 `personal_sign`) is only needed for `RegisterPayoutAddress`'s ownership proof; `SignTypedData` is the EIP-712 signer these primitives use.

```go
// main.go
import (
	"log"
	"os"

	primitive "github.com/primitivedotdev/sdks/sdk-go"
)

payer, err := primitive.NewPrivateKeySigner(os.Getenv("PAYER_KEY"))
if err != nil {
	log.Fatal(err)
}
```

Adapt a hardware wallet or remote KMS by implementing the same three-method interface.

## BuildPayoutRegistrationMessage

`BuildPayoutRegistrationMessage` builds the payout-address ownership message signed with `SignMessage` during [payout registration](https://test.abhinandan.one/x402-payments-overview-04a296ff/go-x402-register-payout.md). It is documented here because it's another no-I/O building block alongside the signing primitives above.

```go
func BuildPayoutRegistrationMessage(org, address, network, issuedAt string) string
```

This MUST be byte-identical to the platform's own message construction, or registration fails the ownership proof. The org id is embedded in the signed bytes, so a captured signature can never register the address under a different org. `address` is lowercased in the message regardless of input casing.
