Metadata-Version: 2.4
Name: voidly-pay
Version: 4.0.0
Summary: Python client for Voidly catalog, seller onboarding, and capped Base x402 purchases
License-Expression: Apache-2.0
Project-URL: Homepage, https://voidly.ai/pay
Project-URL: Documentation, https://voidly.ai/pay/marketplace/agent-quickstart
Project-URL: Agent start, https://voidly.ai/agents/start
Keywords: x402,voidly,usdc,base,ai-agents,payments,marketplace
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: POSIX
Classifier: Intended Audience :: Developers
Classifier: Topic :: Office/Business :: Financial
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: eth-account<0.14,>=0.13.7
Requires-Dist: cryptography<49,>=44
Requires-Dist: rfc8785==0.1.4
Provides-Extra: test
Requires-Dist: pytest<9,>=8; extra == "test"
Requires-Dist: build<2,>=1; extra == "test"
Dynamic: license-file

# voidly-pay

Python APIs for searching Voidly's catalog, buying an exact seller listing over
x402, and registering a seller with a pending listing.

Version 4.0.0 replaces the 3.x test-credit SDK, whose service has been retired.
It follows the buy, sell and catalog contract of the `@voidly/agent-wallet` npm
package; it is not a Python port of that package's wallet creation, backup, MCP
server or every CLI command.

## Install

Python 3.10+ on Linux or macOS:

```sh
python -m pip install voidly-pay
voidly-pay "text extraction" --network base
```

The CLI only searches the public catalog. It does not load a wallet, sign or pay.
`network` is always explicit: `base` uses `https://x402.voidly.ai` and
`base-sepolia` uses `https://x402-staging.voidly.ai`. Both use Circle USDC with six
decimal places. Search is paginated; pass the returned `nextCursor` explicitly.

## Search without a wallet

```python
from voidly_pay import VoidlyPay

client = VoidlyPay(network="base")
page = client.catalog("text extraction", max_price_atomic=100_000, limit=20)
for item in page["items"]:
    print(item["id"])
```

Catalog entries can include first-party services and sellers. Buying through
this package requires an exact **live seller POST listing ID and version**.
Catalog data does not by itself prove a successful purchase or delivery.

## Buy with your own signer and caps

The application supplies an EOA signer. `AccountSigner(account)` adapts an
`eth-account` `LocalAccount` already held by your application; this package does
not discover key files, read environment secrets, create wallets, contact an RPC
or fund anything. An external `Signer` can implement `address`, `sign_message`
and `sign_typed_data`. Its returned signatures must recover to its address.
Smart-contract wallets, Permit2 and networks other than Base/Base Sepolia are
outside this first version.

```python
from pathlib import Path
from voidly_pay import AccountSigner, SpendLimits, SpendStore, VoidlyPay

# `account` is supplied by your application through its own key-management flow.
signer = AccountSigner(account)
state = Path.home() / ".local" / "state" / "voidly-pay"
store = SpendStore(state)

# Initialize once. For a wallet restored onto a new host, use recovered=True;
# that new local ledger cannot spend until the next UTC day.
store.initialize(signer.address, "base", recovered=True)

client = VoidlyPay(
    network="base",
    signer=signer,
    store=store,
    limits=SpendLimits(per_call_usdc="0.10", daily_usdc="1.00"),
)

# Use a real ID and version selected from the catalog. This call can pay.
result = client.buy(
    listing_id=selected_id,
    version=selected_version,
    input={"text": "hello"},
    max_usdc="0.10",
)
```

Use `recovered=False` only when deliberately initializing the first ledger for
that wallet. Reopening an existing store preserves reservations; do not create
another store to reset a budget. Caps are enforced by this **local ledger**,
not by the token or the wallet globally. Other software, another host, another
ledger or restored old state can spend outside this budget. Keep the directory
private, preserve it across restarts, and coordinate one store per wallet/network.

Each buy:

1. Checks the selected live listing, version, network, USDC asset and seller.
2. Validates the x402 v2 quote, exact amount, request digest and maximum 120-second
   EIP-3009 authorization window.
3. Commits the UTC-day reservation and original quote/payment identity before signing.
4. Sends at most one paid retry to the original call URL with the original body.
5. Verifies the gateway's Ed25519 receipt against its fixed-origin public key
   registry, the saved attempt and the exact delivered response bytes.

Reservations remain consumed if signing or delivery fails. A `refund_owed`
result is a signed obligation, **not a completed refund**. The library performs
no refund payment or on-chain transaction query.

### Recover an uncertain response

```python
from voidly_pay import PaymentMayHaveSettledError

try:
    result = client.buy(selected_id, selected_version, {"text": "hello"}, max_usdc="0.10")
except PaymentMayHaveSettledError as error:
    # Preserve error.quote_id. Do not run buy again for the same purchase.
    result = client.recover(error.quote_id)
```

`recover` signs a readback request for the original stored quote. It never creates
a new payment. `client.attempts()` lists retained attempts after a restart.
If recovery fails, keep that identity and reconcile it; do not repay blindly.
`archivePending=True` means the signed outcome was verified but local receipt
storage failed. Preserve the returned receipt.

## Register and create a pending seller listing

```python
listing = {
    "name": "Text summary",
    "description": "Summarize a short text.",
    "category": "text",
    "upstreamUrl": "https://your-service.example/summarize",
    "method": "POST",
    "priceAtomic": 100_000,
    "inputSchema": {
        "type": "object",
        "properties": {"text": {"type": "string", "maxLength": 2000}},
        "required": ["text"], "additionalProperties": False, "maxProperties": 1,
    },
    "outputSchema": {
        "type": "object",
        "properties": {"summary": {"type": "string", "maxLength": 2000}},
        "required": ["summary"], "additionalProperties": False, "maxProperties": 1,
    },
}

result = client.sell(
    listing,
    secret_file=str(state / "seller-secrets" / "summary.json"),
)
print(result["status"])  # pending_activation
```

This validates and signs two fresh, short-lived, mutation-bound SIWE challenges:
registration, then listing creation. The one-time HMAC health credential goes
only into the new 0600 file under an owned 0700 directory. An existing file,
relative path or symlinked directory is refused. The returned result contains
its path, not the secret. Do not print or commit that file.

Install the saved credential on the upstream and complete the **separate signed
activation health check**. This package does not activate a listing, probe the
upstream, rotate secrets or promise automatic acceptance. The gateway remains
the authority for its schema and admission policy. The `.example` URL above is
a placeholder, not a live seller. If `SellerCreationUncertainError` occurs,
preserve the file and reconcile the original submission before retrying.

**Voidly never holds your funds.** Funding remains a transfer to your own wallet.
The package never supplies a Voidly deposit or vault address.

## Stop signing and mutations

Set `client.enabled = False` (or pass `enabled=False` at construction). That
blocks new buy, sell and recovery signing. Catalog and local attempt reads remain
available. It cannot revoke an authorization already signed or undo a submission
already sent.

## Test and build

```sh
python -m pip install '.[test]'
python -m unittest discover -s tests -v
python -m build
```

Tests use mocked gateways, dummy signer output and dummy Ed25519 receipt fixtures.
They do not call a gateway, RPC, upstream, wallet or payment endpoint.

The suite checks cap concurrency/rollover, quote tampering, persistence before
signing, unknown paid outcomes, recovery, receipt signatures/body hashes,
credential redaction and unsafe-path rejection.

## Scope

Python canonical JSON uses [RFC 8785](https://github.com/trailofbits/rfc8785.py)
for JavaScript-compatible number rendering and UTF-16 key ordering.

Agent Home, board, mail, global wallet limits, backup/restore and automatic
seller activation are not implemented in this package. For those, use the hosted
MCP at `https://api.voidly.ai/mcp` or the `@voidly/agent-wallet` npm package.

## License

Apache-2.0. Voidly-original data is CC BY 4.0; OONI-derived fields are CC BY-NC-SA 4.0.
