{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"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","article":{"id":"e41a3199-8f2f-45bb-be6d-a50abe27a140","article_slug":"python-x402-signing-primitives","parent_article_slug":"x402-payments-overview-04a296ff","parent_article_title":"x402 Payments Overview","kind":"reference","published_at":"2026-08-11T18:55:07.30744+00:00","keywords":["derive_eip3009_nonce","compute_payment_validity_window","sign_interaction_payment","build_exact_evm_payment_payload","TokenDomain","NonceBinding"],"meta_description":"Reference for derive_eip3009_nonce, compute_payment_validity_window, sign_interaction_payment, and build_exact_evm_payment_payload in the Python x402 client.","og_image_url":null,"source_file_paths":["sdk-python/README.md"],"recording_id":null,"replayable":false,"task_name":"Low-Level Payment Signing","category":"Python SDK","summary":null,"description":"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.","content_kind":"repo_page","content_markdown":"## When to use these instead of `pay()`\n\nUse 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](python-x402-email-payments)).\n\nAll four functions are pure: no network I/O, no side effects. They are importable from the `primitive` package root.\n\n## `derive_eip3009_nonce`\n\nDerives 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.\n\n```python\nfrom primitive import NonceBinding, derive_eip3009_nonce\n\nnonce = derive_eip3009_nonce(\n    NonceBinding(\n        interaction_id=challenge.nonce_binding[\"interaction_id\"],\n        challenge_step_id=challenge.nonce_binding[\"challenge_step_id\"],\n        challenge_nonce=challenge.nonce_binding[\"challenge_nonce\"],\n    )\n)\n```\n\n**Byte layout:**\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, because undelimited concatenation of variable-length strings is collision-ambiguous. The challenge nonce is decoded to its 32 raw bytes before hashing.\n\n| Field | Type | Required | Description |\n|---|---|---|---|\n| `interaction_id` | `str` | Yes | The interaction id, including its `@domain`. Lowercased before hashing. |\n| `challenge_step_id` | `str` | Yes | The challenge step id (a UUID). Lowercased before hashing. |\n| `challenge_nonce` | `str` | Yes | The challenger's per-challenge random nonce: 64 lowercase hex chars, no `0x` prefix. |\n\n## `compute_payment_validity_window`\n\nComputes the EIP-3009 `(valid_after, valid_before)` window for a payment, landing it inside the band the platform accepts.\n\n```python\nimport math\nimport time\nfrom dateutil.parser import isoparse\nfrom primitive import compute_payment_validity_window\n\nvalid_after, valid_before = compute_payment_validity_window(\n    challenge_expires_at_sec=math.floor(isoparse(challenge.expires_at).timestamp()),\n    now_sec=math.floor(time.time()),\n)\n```\n\n`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.\n\n| Parameter | Type | Default | Description |\n|---|---|---|---|\n| `challenge_expires_at_sec` | `int` | required | The challenge's `expires_at`, unix seconds. |\n| `now_sec` | `int` | required | Current time, unix seconds. |\n| `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. |\n| `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. |\n| `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. |\n| `valid_before_sec` | `int \\| None` | derived | Pin `valid_before` explicitly (unix seconds) instead of deriving it from expiry + margin. |\n| `valid_after_sec` | `int \\| None` | derived | Pin `valid_after` explicitly (unix seconds) instead of deriving it from now − skew. |\n| `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. |\n| `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. |\n\n**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.\n\nReturns a `(valid_after, valid_before)` tuple of unix-second integers.\n\n## `sign_interaction_payment`\n\nDerives 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.\n\n```python\nfrom primitive import (\n    PrivateKeySigner,\n    TokenDomain,\n    NonceBinding,\n    sign_interaction_payment,\n)\n\npayer = PrivateKeySigner(os.environ[\"PAYER_KEY\"])\npr = challenge.payment_requirements\n\nauthorization, signature = sign_interaction_payment(\n    sign=payer.sign_typed_data,\n    payer=payer.address,\n    domain=TokenDomain(\n        name=pr.extra[\"name\"],\n        version=pr.extra[\"version\"],\n        chain_id=84532,  # base-sepolia\n        verifying_contract=pr.asset,\n    ),\n    pay_to=pr.pay_to,\n    amount=int(pr.max_amount_required),\n    nonce_binding=NonceBinding(\n        interaction_id=challenge.nonce_binding[\"interaction_id\"],\n        challenge_step_id=challenge.nonce_binding[\"challenge_step_id\"],\n        challenge_nonce=challenge.nonce_binding[\"challenge_nonce\"],\n    ),\n    valid_after=valid_after,\n    valid_before=valid_before,\n)\n```\n\n| Parameter | Type | Required | Description |\n|---|---|---|---|\n| `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`. |\n| `payer` | `str` | Yes | The payer's (from) address. |\n| `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. |\n| `pay_to` | `str` | Yes | The recipient: the challenge's `payment_requirements.payTo`. |\n| `amount` | `int` | Yes | Amount in token base units. |\n| `nonce_binding` | `NonceBinding` | Yes | Same shape as `derive_eip3009_nonce`'s input; derives the bound nonce internally. |\n| `valid_after` | `int` | Yes | From `compute_payment_validity_window`. |\n| `valid_before` | `int` | Yes | From `compute_payment_validity_window`. |\n\nReturns `(authorization, signature)`.\n\n### `TokenDomain` and `NonceBinding`\n\n| `TokenDomain` field | Type | Description |\n|---|---|---|\n| `name` | `str` | The token's EIP-712 domain name (e.g. `\"USDC\"`), from `payment_requirements.extra`. |\n| `version` | `str` | The token's EIP-712 domain version (e.g. `\"2\"`), from `payment_requirements.extra`. |\n| `chain_id` | `int` | The EVM chain id for the challenge's network (`84532` for `base-sepolia`, `8453` for `base`). |\n| `verifying_contract` | `str` | The token contract address, the challenge's `payment_requirements.asset`. |\n\n| `NonceBinding` field | Type | Description |\n|---|---|---|\n| `interaction_id` | `str` | The interaction id, including `@domain`. |\n| `challenge_step_id` | `str` | The challenge step id (a UUID). |\n| `challenge_nonce` | `str` | 64 lowercase hex chars, no `0x` prefix. |\n\n## `build_exact_evm_payment_payload`\n\nAssembles the exact-EVM x402 wire payload from a signed authorization, ready to submit to `/v1/x402/challenges/{id}/pay`.\n\n```python\nfrom primitive import build_exact_evm_payment_payload\n\npayment = build_exact_evm_payment_payload(\n    network=\"base-sepolia\",\n    authorization=authorization,\n    signature=signature,\n).to_dict()\n# submit `payment` to /v1/x402/challenges/{id}/pay\n```\n\n| Parameter | Type | Required | Description |\n|---|---|---|---|\n| `network` | `str` | Yes | `\"base\"` or `\"base-sepolia\"`. |\n| `authorization` | authorization object | Yes | The signed authorization returned by `sign_interaction_payment`. |\n| `signature` | `str` | Yes | The EIP-712 signature returned by `sign_interaction_payment`. |\n\nThe numeric authorization fields (`value`, `valid_after`, `valid_before`) are serialized as decimal strings on the wire, matching the schema the platform validates against.\n\n## Putting it together\n\nThe four primitives compose in this order for a manual signing flow:\n\n```mermaid\nflowchart LR\n    A[\"derive_eip3009_nonce\"] --> B[\"sign_interaction_payment\"]\n    C[\"compute_payment_validity_window\"] --> B\n    B --> D[\"build_exact_evm_payment_payload\"]\n    D --> E[\"POST /v1/x402/challenges/{id}/pay\"]\n```\n\n`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](python-x402-charge-and-pay). 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](python-x402-email-payments).\n\n## Errors\n\nEvery 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](python-errors-reference) for the full error catalog.\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Creating and Paying Challenges\" href=\"python-x402-charge-and-pay\">\n\nThe high-level charge() / pay() flow these primitives sit underneath.\n\n</Card>\n\n<Card title=\"Email-Native Payments\" href=\"python-x402-email-payments\">\n\nIssue and pay an x402 challenge that rides a real email thread.\n\n</Card>\n\n<Card title=\"x402 Payments Overview\" href=\"x402-payments-overview\">\n\nThe non-custodial payment model shared across every SDK.\n\n</Card>\n\n<Card title=\"Python SDK Error Reference\" href=\"python-errors-reference\">\n\nLook up X402Error conditions and suggested fixes.\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-python/README.md","raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Low-Level+Payment+Signing&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fpython-x402-signing-primitives","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}