{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"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","article":{"id":"faa78bb4-81e1-4909-9728-766def8b0b5e","article_slug":"go-x402-signing-primitives","parent_article_slug":"x402-payments-overview-04a296ff","parent_article_title":"x402 Payments Overview","kind":"reference","published_at":"2026-08-11T18:54:58.894074+00:00","keywords":["DeriveEIP3009Nonce","ComputePaymentValidityWindow","SignInteractionPayment","BuildExactEvmPaymentPayload","TokenDomain","X402Signer"],"meta_description":"Reference for DeriveEIP3009Nonce, ComputePaymentValidityWindow, SignInteractionPayment, and BuildExactEvmPaymentPayload, the Go SDK's low-level x402 signing primitives.","og_image_url":null,"source_file_paths":["sdk-go/x402.go","sdk-go/x402_test.go"],"recording_id":null,"replayable":false,"task_name":"x402 Signing Primitives (Go)","category":"Go SDK","summary":null,"description":"Reference for the low-level Go building blocks behind Pay, DeriveEIP3009Nonce, ComputePaymentValidityWindow, SignInteractionPayment, and BuildExactEvmPaymentPayload, for callers who need to drive x402 signing themselves.","content_kind":"repo_page","content_markdown":"These are the building blocks `Client.Pay` uses internally to sign an [x402 payment challenge](x402-payments-overview). 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](go-x402-create-charge) and `Pay` as documented in [x402 Payments Overview](x402-payments-overview).\n\nAll 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.\n\n## DeriveEIP3009Nonce\n\n`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.\n\n```go\nfunc DeriveEIP3009Nonce(input NonceBinding) (string, error)\n```\n\nThe byte layout is locked to a normative test vector the platform verifier also computes, and MUST NOT change:\n\n```text\nkeccak256( utf8(lower(interaction_id)) || 0x00\n         || utf8(lower(challenge_step_id)) || 0x00\n         || hexdecode(challenge_nonce) )\n```\n\nThe `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.\n\n### NonceBinding\n\n| Field | Type | Description |\n|---|---|---|\n| `InteractionID` | `string` | The interaction id, including its `@domain`. Lowercased before hashing. |\n| `ChallengeStepID` | `string` | The challenge step id (a UUID). Lowercased before hashing. |\n| `ChallengeNonce` | `string` | The challenger's per-challenge random nonce: exactly 64 lowercase hex chars, no `0x` prefix. |\n\n```go\n// main.go\npackage main\n\nimport (\n\tprimitive \"github.com/primitivedotdev/sdks/sdk-go\"\n)\n\nnonce, err := primitive.DeriveEIP3009Nonce(primitive.NonceBinding{\n\tInteractionID:   \"a1b2c3d4-0000-0000-0000-000000000001@payer.example\",\n\tChallengeStepID: \"f00dface-0000-0000-0000-0000000000aa\",\n\tChallengeNonce:  \"aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899\",\n})\n// nonce == \"0xc955a08812ab83f9e25c92e5162267b913957c3cc8678de1cf1449f77b516c6e\"\n```\n\nCase 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.\n\n**Errors**: returns an error if `ChallengeNonce` is not exactly 64 lowercase hex characters, or is not valid hex.\n\n## ComputePaymentValidityWindow\n\n`ComputePaymentValidityWindow` computes the EIP-3009 `(validAfter, validBefore)` window for a payment, landing it inside the band the platform accepts.\n\n```go\nfunc ComputePaymentValidityWindow(input ValidityWindowInput) (validAfter, validBefore *big.Int, err error)\n```\n\n`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.\n\n### ValidityWindowInput\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `ChallengeExpiresAtSec` | `int64` | required | The challenge's `expires_at`, unix seconds. |\n| `NowSec` | `int64` | required | Current time, unix seconds. |\n| `SettlementMarginSec` | `int64` | 300 (5 min) | Headroom past expiry for verify+settle to complete. |\n| `ClockSkewSec` | `int64` | 300 (5 min) | How far in the past to set `validAfter`. |\n| `MaxWindowSec` | `int64` | `DefaultMaxWindowSec` (24h) | Hard ceiling on `validBefore - validAfter`. |\n| `MinHeadroomSec` | `int64` | `DefaultMinSettlementHeadroomSec` (60s) | Minimum `validBefore - NowSec`. |\n| `ValidBeforeSec` | `*int64` | derived | Pin `validBefore`. When nil, derived as `ChallengeExpiresAtSec + SettlementMarginSec`. |\n| `ValidAfterSec` | `*int64` | derived | Pin `validAfter`. When nil, derived as `NowSec - ClockSkewSec`. |\n| `Clamp` | `*bool` | true (nil = true) | When true, an out-of-band window is clamped into the accepted band instead of erroring. |\n\nA 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.\n\n**Clamping behavior**:\n\n- Default (no pinned bounds, or `Clamp` left `nil`/`true`): a computed window outside `[floor, ceiling]` is silently clamped to fit.\n- 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.\n- Pinned + `Clamp` unset or `true`: the pinned value is clamped like the computed one.\n\n```go\n// main.go\nimport (\n\t\"log\"\n\t\"time\"\n\n\tprimitive \"github.com/primitivedotdev/sdks/sdk-go\"\n)\n\nvalidAfter, validBefore, err := primitive.ComputePaymentValidityWindow(primitive.ValidityWindowInput{\n\tChallengeExpiresAtSec: challengeExpiresAtUnix,\n\tNowSec:                time.Now().Unix(),\n})\nif err != nil {\n\tlog.Fatal(err)\n}\n```\n\nPinning with clamp disabled, to catch an out-of-band caller value instead of silently moving it:\n\n```go\nclamp := false\nvbPin := time.Now().Unix() + 5 // only 5s headroom, below the 60s floor\n_, _, err := primitive.ComputePaymentValidityWindow(primitive.ValidityWindowInput{\n\tChallengeExpiresAtSec: challengeExpiresAtUnix,\n\tNowSec:                time.Now().Unix(),\n\tValidBeforeSec:        &vbPin,\n\tClamp:                 &clamp,\n})\n// err: \"validBefore (...) is below the minimum settlement headroom ...\n//       the authorization would be rejected as about to expire\"\n```\n\n**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).\n\n## SignInteractionPayment\n\n`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.\n\n```go\nfunc SignInteractionPayment(input SignInteractionPaymentInput) (TransferAuthorization, string, error)\n```\n\n### SignInteractionPaymentInput\n\n| Field | Type | Description |\n|---|---|---|\n| `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. |\n| `Payer` | `string` | The from address. |\n| `Domain` | `TokenDomain` | The token's EIP-712 domain (see below). |\n| `PayTo` | `string` | The recipient, the challenger's `payTo`. |\n| `Amount` | `*big.Int` | Amount in token base units. |\n| `NonceBinding` | `NonceBinding` | Same shape as for `DeriveEIP3009Nonce`. |\n| `ValidAfter` | `*big.Int` | From `ComputePaymentValidityWindow`. |\n| `ValidBefore` | `*big.Int` | From `ComputePaymentValidityWindow`. |\n\n### TokenDomain\n\n| Field | Type | Description |\n|---|---|---|\n| `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. |\n| `Version` | `string` | The token's EIP-712 domain version, same source as `Name`. |\n| `ChainID` | `int64` | The EVM chain id (`84532` for `base-sepolia`, `8453` for `base`). |\n| `VerifyingContract` | `string` | The token contract address, from `payment_requirements.asset`. |\n\nReturns the `TransferAuthorization` it built plus the hex signature string.\n\n```go\npr := challenge.PaymentRequirements\nauth, signature, err := primitive.SignInteractionPayment(primitive.SignInteractionPaymentInput{\n\tSign:  payer.SignTypedData,\n\tPayer: payer.Address(),\n\tDomain: primitive.TokenDomain{\n\t\tName:              pr.Extra.Name,\n\t\tVersion:           pr.Extra.Version,\n\t\tChainID:           84532, // base-sepolia\n\t\tVerifyingContract: pr.Asset,\n\t},\n\tPayTo:  pr.PayTo,\n\tAmount: amount,\n\tNonceBinding: primitive.NonceBinding{\n\t\tInteractionID:   challenge.NonceBinding.InteractionID,\n\t\tChallengeStepID: challenge.NonceBinding.ChallengeStepID,\n\t\tChallengeNonce:  challenge.NonceBinding.ChallengeNonce,\n\t},\n\tValidAfter:  validAfter,\n\tValidBefore: validBefore,\n})\n```\n\n**Errors**: propagates a `DeriveEIP3009Nonce` error for a malformed `NonceBinding`, or any error your `Sign` callback returns.\n\n## BuildExactEvmPaymentPayload\n\n`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`.\n\n```go\nfunc BuildExactEvmPaymentPayload(network string, auth TransferAuthorization, signature string) (X402PaymentPayload, error)\n```\n\n| Parameter | Type | Description |\n|---|---|---|\n| `network` | `string` | Must be `\"base\"` or `\"base-sepolia\"`. |\n| `auth` | `TransferAuthorization` | The authorization returned by `SignInteractionPayment` (or assembled by hand). |\n| `signature` | `string` | The 0x-prefixed 65-byte (130 hex char) EIP signature. |\n\nThe 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.\n\n```go\npayment, err := primitive.BuildExactEvmPaymentPayload(challenge.Network, auth, signature)\nif err != nil {\n\tlog.Fatal(err)\n}\n// submit `payment` to /v1/x402/challenges/{id}/pay\n```\n\n**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.\n\n## TransferAuthorization\n\n`TransferAuthorization` is the EIP-3009 `TransferWithAuthorization` message assembled by `SignInteractionPayment` and consumed by `BuildExactEvmPaymentPayload`.\n\n| Field | Type | Description |\n|---|---|---|\n| `From` | `string` | The payer's address. |\n| `To` | `string` | The recipient's address (`payTo`). |\n| `Value` | `*big.Int` | Amount in token base units. |\n| `ValidAfter` | `*big.Int` | Start of the validity window, unix seconds. |\n| `ValidBefore` | `*big.Int` | End of the validity window, unix seconds. |\n| `Nonce` | `string` | The 0x-prefixed 32-byte interaction-bound nonce. |\n\n## X402Signer and PrivateKeySigner\n\n`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:\n\n```go\ntype X402Signer interface {\n\tAddress() string\n\tSignTypedData(typedData apitypes.TypedData) (string, error)\n\tSignMessage(message string) (string, error)\n}\n```\n\n`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.\n\n```go\n// main.go\nimport (\n\t\"log\"\n\t\"os\"\n\n\tprimitive \"github.com/primitivedotdev/sdks/sdk-go\"\n)\n\npayer, err := primitive.NewPrivateKeySigner(os.Getenv(\"PAYER_KEY\"))\nif err != nil {\n\tlog.Fatal(err)\n}\n```\n\nAdapt a hardware wallet or remote KMS by implementing the same three-method interface.\n\n## BuildPayoutRegistrationMessage\n\n`BuildPayoutRegistrationMessage` builds the payout-address ownership message signed with `SignMessage` during [payout registration](go-x402-register-payout). It is documented here because it's another no-I/O building block alongside the signing primitives above.\n\n```go\nfunc BuildPayoutRegistrationMessage(org, address, network, issuedAt string) string\n```\n\nThis 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.\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"x402 Payments Overview\" href=\"x402-payments-overview\">\n\nThe full non-custodial payment model: registering payouts, charging, paying, and spend policy.\n\n</Card>\n\n<Card title=\"Creating a Payment Challenge\" href=\"go-x402-create-charge\">\n\nCreate a payment challenge with Client.Charge, the high-level entry point these primitives sit underneath.\n\n</Card>\n\n<Card title=\"x402 Errors\" href=\"go-x402-errors\">\n\nInterpret X402Error status codes and retry-after headers when a payment request fails.\n\n</Card>\n\n<Card title=\"Registering a Payout Address\" href=\"go-x402-register-payout\">\n\nProve control of a wallet and register it as your default x402 payout destination.\n\n</Card>\n\n</CardGroup>","canonical_base_url":"https://test.abhinandan.one","seo_indexing_enabled":true,"last_modified":"2026-08-21T18:22:43.359885+00:00","video_url":null,"voiceover_url":null,"tools_used":[],"demonstrated_by":[],"steps":[],"related_links":[],"intro":null,"prerequisites":[],"verification":[],"troubleshooting":[],"suggest_edit_url":"https://github.com/abhi-browzer/primitive-sdks/edit/main/sdk-go/x402.go","raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+x402+Signing+Primitives+%28Go%29&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fgo-x402-signing-primitives","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}