Metadata-Version: 2.4
Name: 402signal
Version: 0.2.0
Summary: Check a paid API's current offer before an agent pays, and assess the returned response against requirements set before paying
Author-email: 402Signal <ross@402signal.com>
License: Apache-2.0
Project-URL: Homepage, https://402signal.com/
Project-URL: Documentation, https://402signal.com/developers
Project-URL: Source, https://github.com/402signalhq/402signal/tree/main/sdk/python
Project-URL: Changelog, https://github.com/402signalhq/402signal/blob/main/CHANGELOG.md
Keywords: x402,mpp,agent payments,402,usdc,transparency log
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Security :: Cryptography
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: cryptography>=42
Provides-Extra: agentcore-langgraph
Requires-Dist: bedrock-agentcore[langgraph]>=1.24.0; extra == "agentcore-langgraph"
Provides-Extra: agentcore-strands
Requires-Dist: bedrock-agentcore[strands-agents]>=1.24.0; extra == "agentcore-strands"
Dynamic: license-file

# 402signal (Python)

Client helpers and an offline receipt verifier for buyers that use
[402Signal](https://402signal.com/), the pre-flight check for agent payments
over x402 and MPP. Python 3.10 or newer; the only dependency is `cryptography`
(Ed25519).

```sh
pip install 402signal
```

## What it does

- `signal402.challenge(request)` sends the unpaid request and returns the HTTP
  402 fee requirements, so your x402 client can authorize the $0.003 checking
  fee with your own wallet.
- `signal402.check(request, payment_signature)` resends the identical JSON with
  your `PAYMENT-SIGNATURE` header and classifies the answer (`live`, `miss`,
  `binding_unavailable`, `settled_evidence_failed`, `unknown_settlement`,
  `refused`). `recover(...)` re-reads a lost answer with the same
  authorization and your private `Replay-Key` instead of paying again.
- `signal402.verify_route_receipt(response, trusted_log_vkey)` verifies a
  retained paid answer offline: the reveal recomputes the public commitment,
  the public leaf hashes to the receipt's leaf, the RFC 6962 inclusion path
  reaches the checkpoint root, and the checkpoint carries an Ed25519 signature
  from the log key you pinned. It never trusts a key found in the response.

- `signal402.verify_route(...)` compares a retained receipt with the seller's
  current HTTP 402 and the exact request before you sign: the same checks as the
  Node guard `@402signal/route-guard` (`quote_changed`, `resource_changed`,
  `quote_expired`, ...), pinned by the same published vectors.
- `signal402.acceptance` assesses a paid response against requirements you set
  before paying, and `signal402.gate.PurchaseGate` puts the check, one payment of
  the verified offer and the assessment around a runtime's own payment layer, with
  adapters for AWS Bedrock AgentCore Payments (below).

This package does not hold keys, sign, pay, or retry.

## Verify a retained answer

```python
import json
import os
import signal402

response = json.load(open("retained-route-response.json"))
verified = signal402.verify_route_receipt(response, trusted_log_vkey=os.environ["SIGNAL_LOG_VKEY"])
print(verified["origin"], verified["tree_size"], verified["index"])
```

`ReceiptError` is raised on any mismatch. Keep the complete paid response,
including `pq_trust.transparency.receipt` and `reveal`, together with the
original request; the reveal contains private request and decision evidence,
so do not put it in public logs.

## Run a check

```python
import signal402

request = {"url": "https://seller.example/x402", "require_route_binding": True}
fee = signal402.challenge(request)          # fee.outcome == "challenge"; fee.body["accepts"] lists the rails
signature = my_x402_client.authorize(fee.body)   # your wallet, your code
answer = signal402.check(request, signature, replay_key=my_private_replay_key)
if answer.outcome == "live":
    signal402.verify_route_receipt(answer.body, trusted_log_vkey=VKEY)
```

Read `live`, `payable`, `selected_payment` and `billing` together; the
outcome name is a summary. A settled fee is not reversed if the seller's offer
later changes, and a qualifying check is not a guarantee of delivery or output
quality.

The client refuses redirects (`signal402.RedirectRefused`) to keep payment
headers on the configured endpoint. The router URL must be https (plain http only to a loopback address,
for fixtures) with no credentials, query or fragment, and every answer is read
to at most 256 KiB (`signal402.MAX_RESPONSE_BYTES`).

## Compare a receipt with the seller's 402 before paying

```python
verified = signal402.verify_route(
    answer.text, json.dumps(request), VKEY,
    url=seller_url, method="GET",
    challenge={"status": 402, "payment_required": header_value, "body_text": body_text},
)
# verified["accepted"] is the only requirement to hand to your payment client.
```

`RouteGuardError.code` names the refusal. The challenge must be the seller's
current, unredirected 402; its header and body channels must agree.

## AWS Bedrock AgentCore Payments

With `pip install "402signal[agentcore-langgraph]"` (or `agentcore-strands`), 402Signal
decides what AgentCore pays, using only documented interfaces. With AgentCore's
automatic payment turned off, a seller's 402 is checked first; AgentCore then pays
only the offer 402Signal verified, once, and the paid result is assessed.

```python
from signal402.integrations.langgraph import Signal402PaymentsMiddleware

payments = AgentCorePaymentsMiddleware(AgentCorePaymentsConfig(..., auto_payment=False))
signal = Signal402PaymentsMiddleware(payments, trusted_log_vkey=VKEY, network="eip155:8453",
                                     acceptance_for=lambda url, method, body: CONTRACT)
agent = create_agent(model=..., tools=[...], middleware=[payments, signal])
```

For Strands, pass `Signal402PaymentsHooks(plugin, ...)` from
`signal402.integrations.strands` in the agent's `hooks`. See
[the integration notes](https://github.com/402signalhq/402signal/blob/main/docs/agentcore-payments.md).

## Check the response you paid for

`signal402.acceptance` evaluates a seller's response against requirements you
declared before paying. It uses the standard library only, runs locally and
matches the JavaScript evaluator on the shared vectors.

```python
from signal402 import acceptance

contract = acceptance.prepare_contract({
    "type": "402signal.acceptance_contract", "version": 1, "profile": "json-acceptance-v1",
    "rules": [{"id": "price", "path": "/price", "check": "number", "min": 0}],
})
# contract.sha256 is the value to keep with the purchase, before paying.
record = acceptance.assess_output(contract, {
    "status": 200, "content_type": "application/json", "body": body_bytes, "complete": True,
})
print(record["verdict"], record["reason"])
```

Pass `complete=True` only when you read the whole body; anything else is
`INDETERMINATE`. A verdict checks shape, not truth, and never authorizes another
payment, a retry or a refund. See the
[acceptance contract](https://github.com/402signalhq/402signal/blob/main/docs/acceptance-contract-v1.md).

## Development

```sh
PYTHONPATH=sdk/python/src python3 -m unittest discover -s sdk/python/tests
```

Publishing runs from `.github/workflows/publish-pypi.yml` on a `python-v*` tag
with PyPI Trusted Publishing; no API token is stored.
