---
title: "Low-Level Payment Signing"
canonical: "https://test.abhinandan.one/x402-payments-overview-04a296ff/python-x402-signing-primitives"
markdown_url: "https://test.abhinandan.one/x402-payments-overview-04a296ff/python-x402-signing-primitives.md"
publisher: "Primitive SDKs"
kind: "reference"
content_type: "reference"
category: "Python SDK"
parent: "x402-payments-overview-04a296ff"
description: "Reference for derive_eip3009_nonce, compute_payment_validity_window, sign_interaction_payment, and build_exact_evm_payment_payload in the Python x402 client."
keywords: ["derive_eip3009_nonce", "compute_payment_validity_window", "sign_interaction_payment", "build_exact_evm_payment_payload", "TokenDomain", "NonceBinding"]
last_modified: "2026-08-21T18:22:43.359885+00:00"
published_at: "2026-08-11T18:55:07.30744+00:00"
source_files:
  - "sdk-python/README.md"
sections:
  - {anchor: "when-to-use-these-instead-of-pay", title: "When to use these instead of `pay()`"}
  - {anchor: "derive_eip3009_nonce", title: "`derive_eip3009_nonce`"}
  - {anchor: "compute_payment_validity_window", title: "`compute_payment_validity_window`"}
  - {anchor: "sign_interaction_payment", title: "`sign_interaction_payment`"}
  - {anchor: "tokendomain-and-noncebinding", title: "`TokenDomain` and `NonceBinding`"}
  - {anchor: "build_exact_evm_payment_payload", title: "`build_exact_evm_payment_payload`"}
  - {anchor: "putting-it-together", title: "Putting it together"}
  - {anchor: "errors", title: "Errors"}
  - {anchor: "next-steps", title: "Next steps"}
---

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

# 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](https://test.abhinandan.one/x402-payments-overview-04a296ff/python-x402-email-payments.md)).

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.

```python
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:**

```text
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.

| Field | Type | Required | Description |
|---|---|---|---|
| `interaction_id` | `str` | Yes | The interaction id, including its `@domain`. Lowercased before hashing. |
| `challenge_step_id` | `str` | Yes | The challenge step id (a UUID). Lowercased before hashing. |
| `challenge_nonce` | `str` | Yes | The 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.

```python
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.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `challenge_expires_at_sec` | `int` | required | The challenge's `expires_at`, unix seconds. |
| `now_sec` | `int` | required | Current time, unix seconds. |
| `settlement_margin_sec` | `int` | 300 (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_sec` | `int` | 300 (5 min) | How far in the past to set `valid_after` for clock skew, when `valid_after_sec` is not pinned. |
| `max_window_sec` | `int` | 86400 (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_sec` | `int \| None` | derived | Pin `valid_before` explicitly (unix seconds) instead of deriving it from expiry + margin. |
| `valid_after_sec` | `int \| None` | derived | Pin `valid_after` explicitly (unix seconds) instead of deriving it from now − skew. |
| `min_headroom_sec` | `int` | 60 | Minimum `valid_before - now_sec`; below this the platform rejects the payment as about to expire, because it cannot clear SMTP + DKIM + verify + settle latency. |
| `clamp` | `bool` | `True` | When 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.

```python
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,
)
```

| Parameter | Type | Required | Description |
|---|---|---|---|
| `sign` | callable | Yes | Signs 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`. |
| `payer` | `str` | Yes | The payer's (from) address. |
| `domain` | `TokenDomain` | Yes | The 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_to` | `str` | Yes | The recipient: the challenge's `payment_requirements.payTo`. |
| `amount` | `int` | Yes | Amount in token base units. |
| `nonce_binding` | `NonceBinding` | Yes | Same shape as `derive_eip3009_nonce`'s input; derives the bound nonce internally. |
| `valid_after` | `int` | Yes | From `compute_payment_validity_window`. |
| `valid_before` | `int` | Yes | From `compute_payment_validity_window`. |

Returns `(authorization, signature)`.

### `TokenDomain` and `NonceBinding`

| `TokenDomain` field | Type | Description |
|---|---|---|
| `name` | `str` | The token's EIP-712 domain name (e.g. `"USDC"`), from `payment_requirements.extra`. |
| `version` | `str` | The token's EIP-712 domain version (e.g. `"2"`), from `payment_requirements.extra`. |
| `chain_id` | `int` | The EVM chain id for the challenge's network (`84532` for `base-sepolia`, `8453` for `base`). |
| `verifying_contract` | `str` | The token contract address, the challenge's `payment_requirements.asset`. |

| `NonceBinding` field | Type | Description |
|---|---|---|
| `interaction_id` | `str` | The interaction id, including `@domain`. |
| `challenge_step_id` | `str` | The challenge step id (a UUID). |
| `challenge_nonce` | `str` | 64 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`.

```python
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
```

| Parameter | Type | Required | Description |
|---|---|---|---|
| `network` | `str` | Yes | `"base"` or `"base-sepolia"`. |
| `authorization` | authorization object | Yes | The signed authorization returned by `sign_interaction_payment`. |
| `signature` | `str` | Yes | The 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:

```mermaid
flowchart LR
    A["derive_eip3009_nonce"] --> B["sign_interaction_payment"]
    C["compute_payment_validity_window"] --> B
    B --> D["build_exact_evm_payment_payload"]
    D --> E["POST /v1/x402/challenges/{id}/pay"]
```

`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](https://test.abhinandan.one/x402-payments-overview-04a296ff/python-x402-charge-and-pay.md). 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](https://test.abhinandan.one/x402-payments-overview-04a296ff/python-x402-email-payments.md).

## 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](https://test.abhinandan.one/python-errors-reference.md) for the full error catalog.
