Documentation Index: Fetch llms.txt first to discover every published page. This page is also available as Markdown at /x402-payments-overview-04a296ff/python-x402-signing-primitives.md.
Verified · 8/11/2026

Low-Level Payment Signing

Reference for the individual signing primitives, nonce derivation, validity window computation, and EIP-712 signing, that `pay()` normally composes for you, for callers who need to drive the x402 signing flow themselves.

When to use these instead of pay()#

Use the low-level signing primitives only when X402Client.pay(), which derives the nonce, builds the authorization, signs it, and submits it in one call, doesn't fit your signing flow. That means driving a hardware wallet, submitting the payment through a different channel than the SDK's HTTP call, or signing a challenge carried in an email reply before sending it separately (see Email-Native Payments).

All four functions are pure: no network I/O, no side effects. They are importable from the primitive package root.

derive_eip3009_nonce#

Derives the EIP-3009 nonce bound to a specific interaction step. The platform recomputes this exact value server-side and rejects a payment whose nonce doesn't match, so the byte layout below is load-bearing and must not be reimplemented differently.

from primitive import NonceBinding, derive_eip3009_nonce

nonce = derive_eip3009_nonce(
    NonceBinding(
        interaction_id=challenge.nonce_binding["interaction_id"],
        challenge_step_id=challenge.nonce_binding["challenge_step_id"],
        challenge_nonce=challenge.nonce_binding["challenge_nonce"],
    )
)

Byte layout:

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

The 0x00 separators pin the field boundaries, because undelimited concatenation of variable-length strings is collision-ambiguous. The challenge nonce is decoded to its 32 raw bytes before hashing.

FieldTypeRequiredDescription
interaction_idstrYesThe interaction id, including its @domain. Lowercased before hashing.
challenge_step_idstrYesThe challenge step id (a UUID). Lowercased before hashing.
challenge_noncestrYesThe challenger's per-challenge random nonce: 64 lowercase hex chars, no 0x prefix.

compute_payment_validity_window#

Computes the EIP-3009 (valid_after, valid_before) window for a payment, landing it inside the band the platform accepts.

import math
import time
from dateutil.parser import isoparse
from primitive import compute_payment_validity_window

valid_after, valid_before = compute_payment_validity_window(
    challenge_expires_at_sec=math.floor(isoparse(challenge.expires_at).timestamp()),
    now_sec=math.floor(time.time()),
)

valid_before governs on-chain validity: it must stay at least 60 seconds in the future so the payment can settle, yet not so far that the total window exceeds the 24-hour cap. valid_after is set generously in the past for clock skew. Both ends are payer landmines: too tight and the platform rejects the authorization as about to expire; too wide and it rejects the window as too wide. By default the function clamps the computed window into the accepted band, so a caller who doesn't override always gets a signable window.

ParameterTypeDefaultDescription
challenge_expires_at_secintrequiredThe challenge's expires_at, unix seconds.
now_secintrequiredCurrent time, unix seconds.
settlement_margin_secint300 (5 min)Headroom past expiry for verify + settle to complete; added to challenge_expires_at_sec when valid_before_sec is not pinned.
clock_skew_secint300 (5 min)How far in the past to set valid_after for clock skew, when valid_after_sec is not pinned.
max_window_secint86400 (24h)Hard ceiling on valid_before - valid_after. A signed EIP-3009 authorization stays settleable on-chain until valid_before regardless of interaction state, so this is the safety ceiling against a standing "funds committed" risk.
valid_before_secint | NonederivedPin valid_before explicitly (unix seconds) instead of deriving it from expiry + margin.
valid_after_secint | NonederivedPin valid_after explicitly (unix seconds) instead of deriving it from now − skew.
min_headroom_secint60Minimum valid_before - now_sec; below this the platform rejects the payment as about to expire, because it cannot clear SMTP + DKIM + verify + settle latency.
clampboolTrueWhen true, an out-of-band window (computed or pinned) is silently clamped into the accepted band. Set False to instead raise a specific error naming which bound was violated.

Pinning behavior: if you pass valid_before_sec or valid_after_sec explicitly, that's treated as intent to pin the bound. With clamp=False, an out-of-band pinned value raises a specific error naming which bound was violated, instead of silently signing a doomed authorization. With clamp=True (the default), the pinned value is clamped into the band like the computed one.

Returns a (valid_after, valid_before) tuple of unix-second integers.

sign_interaction_payment#

Derives the bound nonce, assembles the EIP-3009 authorization, and signs it with your callback. This is the one piece a stock x402 signer can't do: it generates the nonce internally with no injection point, so this helper exists specifically to let you supply the interaction-bound nonce.

from primitive import (
    PrivateKeySigner,
    TokenDomain,
    NonceBinding,
    sign_interaction_payment,
)

payer = PrivateKeySigner(os.environ["PAYER_KEY"])
pr = challenge.payment_requirements

authorization, signature = sign_interaction_payment(
    sign=payer.sign_typed_data,
    payer=payer.address,
    domain=TokenDomain(
        name=pr.extra["name"],
        version=pr.extra["version"],
        chain_id=84532,  # base-sepolia
        verifying_contract=pr.asset,
    ),
    pay_to=pr.pay_to,
    amount=int(pr.max_amount_required),
    nonce_binding=NonceBinding(
        interaction_id=challenge.nonce_binding["interaction_id"],
        challenge_step_id=challenge.nonce_binding["challenge_step_id"],
        challenge_nonce=challenge.nonce_binding["challenge_nonce"],
    ),
    valid_after=valid_after,
    valid_before=valid_before,
)
ParameterTypeRequiredDescription
signcallableYesSigns EIP-712 typed data with the caller's own key and returns the signature. The key never leaves the caller; pass e.g. PrivateKeySigner.sign_typed_data.
payerstrYesThe payer's (from) address.
domainTokenDomainYesThe token's EIP-712 domain: name, version, chain_id, verifying_contract. name/version must be the actual token's domain params; take them from challenge.payment_requirements.extra rather than hardcoding them, or the verifier rejects the signature.
pay_tostrYesThe recipient: the challenge's payment_requirements.payTo.
amountintYesAmount in token base units.
nonce_bindingNonceBindingYesSame shape as derive_eip3009_nonce's input; derives the bound nonce internally.
valid_afterintYesFrom compute_payment_validity_window.
valid_beforeintYesFrom compute_payment_validity_window.

Returns (authorization, signature).

TokenDomain and NonceBinding#

TokenDomain fieldTypeDescription
namestrThe token's EIP-712 domain name (e.g. "USDC"), from payment_requirements.extra.
versionstrThe token's EIP-712 domain version (e.g. "2"), from payment_requirements.extra.
chain_idintThe EVM chain id for the challenge's network (84532 for base-sepolia, 8453 for base).
verifying_contractstrThe token contract address, the challenge's payment_requirements.asset.
NonceBinding fieldTypeDescription
interaction_idstrThe interaction id, including @domain.
challenge_step_idstrThe challenge step id (a UUID).
challenge_noncestr64 lowercase hex chars, no 0x prefix.

build_exact_evm_payment_payload#

Assembles the exact-EVM x402 wire payload from a signed authorization, ready to submit to /v1/x402/challenges/{id}/pay.

from primitive import build_exact_evm_payment_payload

payment = build_exact_evm_payment_payload(
    network="base-sepolia",
    authorization=authorization,
    signature=signature,
).to_dict()
# submit `payment` to /v1/x402/challenges/{id}/pay
ParameterTypeRequiredDescription
networkstrYes"base" or "base-sepolia".
authorizationauthorization objectYesThe signed authorization returned by sign_interaction_payment.
signaturestrYesThe EIP-712 signature returned by sign_interaction_payment.

The numeric authorization fields (value, valid_after, valid_before) are serialized as decimal strings on the wire, matching the schema the platform validates against.

Putting it together#

The four primitives compose in this order for a manual signing flow:

sign_interaction_payment calls derive_eip3009_nonce for you, so you only call it directly when you need the nonce on its own. For the standard synthetic-challenge flow this whole sequence is what X402Client.pay does internally; see Creating and Paying Challenges. For the email-carried challenge flow, pay_email_challenge runs the same sequence and returns the signed envelope instead of submitting it; see Email-Native Payments.

Errors#

Every function raises primitive.X402Error (status 0, since no network call is made) on a malformed input, a bad nonce format, an out-of-band pinned validity window with clamp=False, or a malformed signature/network passed to build_exact_evm_payment_payload. See Python SDK Error Reference for the full error catalog.

Next steps#

Was this page helpful?

© Primitive SDKs

Powered by Browzer