sdk-4mica 0.5.2
Python SDK for interacting with the 4Mica payment network.
Requires Python: >=3.9

# 4Mica Python SDK

The official Python SDK for interacting with the 4Mica payment network.

## Overview

This SDK provides:

- **User Client**: deposit collateral, sign payments, and manage withdrawals in ETH or ERC20 tokens
- **Recipient Client**: create payment tabs, verify payment guarantees, and claim from user collateral
- **X402 Flow Helper**: generate X-PAYMENT headers for 402-protected HTTP resources via an X402-compatible service
- **Admin RPCs**: manage user suspension and admin API keys (when authorized)

## Installation

```bash
pip install sdk-4mica
```

Install BLS support for on-chain remuneration:

```bash
pip install 'sdk-4mica[bls]'
```

Python 3.9+ is required.

## Initialization and Configuration

The SDK requires a signing key and can use sensible defaults for the rest:

- `wallet_private_key` (**required**): private key used for on-chain gateway operations
- `evm_signer` (optional): custom signer implementing `EvmSigner` for payment/auth signing
- `rpc_url` (optional): URL of the 4Mica core RPC server. Defaults to `https://api.4mica.xyz/`.
- `ethereum_http_rpc_url` (optional): Ethereum JSON-RPC endpoint; fetched from core if omitted
- `contract_address` (optional): Core4Mica contract address; fetched from core if omitted
- `bearer_token` (optional): static bearer token for auth
- `auth_url` and `auth_refresh_margin_secs` (optional): SIWE auth config. Only used when auth is
  enabled via `enable_auth()` or when set via env (defaults to `rpc_url` and 60 seconds).

Note: `wallet_private_key` is currently required even when you supply `evm_signer`, because the
client always initializes the on-chain gateway.

### 1) Using ConfigBuilder

```python
import asyncio

from fourmica_sdk import Client, ConfigBuilder


async def main() -> None:
    cfg = (
        ConfigBuilder()
        .rpc_url("https://api.4mica.xyz/")
        .wallet_private_key("0x...")
        .build()
    )

    client = await Client.new(cfg)
    try:
        # use client.user, client.recipient, client.rpc
        pass
    finally:
        await client.aclose()


if __name__ == "__main__":
    asyncio.run(main())
```

### 2) Using Environment Variables

Set environment variables (example `.env`):

```bash
4MICA_WALLET_PRIVATE_KEY="0x..."
4MICA_RPC_URL="https://api.4mica.xyz/"
4MICA_ETHEREUM_HTTP_RPC_URL="http://localhost:8545"
4MICA_CONTRACT_ADDRESS="0x..."
4MICA_BEARER_TOKEN="Bearer <access_token>"
4MICA_AUTH_URL="https://api.4mica.xyz/"
4MICA_AUTH_REFRESH_MARGIN_SECS="60"
```

If you want to set them inline for a single command, use `env` since most shells do not allow
variable names that start with a digit:

```bash
env 4MICA_WALLET_PRIVATE_KEY="0x..." 4MICA_RPC_URL="https://api.4mica.xyz/" python app.py
```

Then in code:

```python
import asyncio

from fourmica_sdk import Client, ConfigBuilder


async def main() -> None:
    cfg = ConfigBuilder().from_env().build()
    client = await Client.new(cfg)
    try:
        # use client.user, client.recipient, client.rpc
        pass
    finally:
        await client.aclose()


if __name__ == "__main__":
    asyncio.run(main())
```

### 3) Using a Custom Signer

Provide a signer that implements `EvmSigner` (must expose `address`, `sign_typed_data`, and
`sign_message`). The private key is still required for on-chain operations.

```python
import asyncio

from fourmica_sdk import Client, ConfigBuilder, LocalAccountSigner


async def main() -> None:
    signer = LocalAccountSigner("0x...")
    cfg = ConfigBuilder().wallet_private_key("0x...").evm_signer(signer).build()
    client = await Client.new(cfg)
    try:
        pass
    finally:
        await client.aclose()


if __name__ == "__main__":
    asyncio.run(main())
```

### SIWE Auth (Optional)

Enable automatic SIWE auth refresh, or pass a static bearer token:

```python
import asyncio

from fourmica_sdk import Client, ConfigBuilder


async def main() -> None:
    cfg = (
        ConfigBuilder()
        .wallet_private_key("0x...")
        .rpc_url("https://api.4mica.xyz/")
        .enable_auth()
        .build()
    )

    client = await Client.new(cfg)
    try:
        await client.login()  # optional: first RPC call also triggers auth
    finally:
        await client.aclose()


if __name__ == "__main__":
    asyncio.run(main())
```

Or use a static token:

```python
import asyncio

from fourmica_sdk import Client, ConfigBuilder


async def main() -> None:
    cfg = (
        ConfigBuilder()
        .wallet_private_key("0x...")
        .bearer_token("Bearer <access_token>")
        .build()
    )
    client = await Client.new(cfg)
    try:
        pass
    finally:
        await client.aclose()


if __name__ == "__main__":
    asyncio.run(main())
```

Env vars: `4MICA_BEARER_TOKEN`, `4MICA_AUTH_URL`, `4MICA_AUTH_REFRESH_MARGIN_SECS`.

## Usage

The SDK exposes three main entry points:

- `client.user`: payer-side operations (collateral, signing, withdrawals)
- `client.recipient`: recipient-side operations (tabs, guarantees, remuneration)
- `X402Flow`: helper for 402-protected HTTP resources

Low-level admin RPCs are available under `client.rpc` (requires an admin API key):

```python
client.rpc.with_admin_api_key("ak_...")
await client.rpc.update_user_suspension("0xUser", True)
```

## Direct SDK Quick Start

This direct flow talks to 4mica core and shows the four core actions: deposit, open a tab, request
a guarantee, and settle (remunerate) it on-chain. The payer and recipient roles are separate in
production, so the SDK uses different keys. The snippets below are copied from the runnable scripts
in `examples/recipient_quickstart.py` and `examples/payer_quickstart.py`.

Key requirements:

- Payer flow uses the private key of the user `PAYER_KEY` and `RECIPIENT_ADDRESS`.

Four-step direct flow:

1. Deposit collateral (payer). For ETH, call `payer_client.user.deposit(amount)`. For ERC20,
   call `payer_client.user.approve_erc20(token, amount)` first, then
   `payer_client.user.deposit(amount, token)`. `token` is the contract address of the USDC/USDT.
2. Get `tab_id` and `req_id` (recipient). Call `recipient_client.recipient.create_tab(...)` which
   hits core `/core/payment-tabs`. The SDK returns `tab_id`; compute the next `req_id` by calling
   `latest = await recipient_client.recipient.get_latest_guarantee(tab_id)` and using
   `req_id = 0 if latest is None else latest.req_id + 1`.
3. Sign the guarantee (payer). Build `PaymentGuaranteeRequestClaims` with `tab_id`, `req_id`,
   amount, timestamp, and asset, then call
   `signature = await payer_client.user.sign_payment(claims, SigningScheme.EIP712)`.
4. Settle (recipient). Call `cert = await recipient_client.recipient.issue_payment_guarantee(...)`
   with the claims + payer signature, then `await recipient_client.recipient.remunerate(cert)` to
   settle on chain.

### Recipient (resource server) quick start

Recipient is the service provider that accepts a payer's credit. Each recipient can open a tab
with a user, a configurable line of credit that enables instant, trustless fair exchange between
payer and merchant. Run this script first to open a tab, compute the next `REQ_ID`, and print the
tab's asset address plus the amount the payer should sign.

```bash
export RECIPIENT_KEY=0x..
export USER_ADDRESS=0x.. # payer address
export AMOUNT_WEI=100000000000000000
```

```python
import asyncio
import os

from eth_account import Account
from fourmica_sdk import Client, ConfigBuilder

RECIPIENT_KEY = os.environ["RECIPIENT_KEY"]
USER_ADDRESS = os.environ["USER_ADDRESS"]
AMOUNT_WEI = int(os.getenv("AMOUNT_WEI", "100000000000000000"), 0)


async def main() -> None:
    recipient_cfg = ConfigBuilder().from_env().wallet_private_key(RECIPIENT_KEY).build()
    recipient_client = await Client.new(recipient_cfg)
    try:
        recipient_address = Account.from_key(RECIPIENT_KEY).address

        tab_id = await recipient_client.recipient.create_tab(
            user_address=USER_ADDRESS,
            recipient_address=recipient_address,
            erc20_token=None,
            ttl=None,
        )
        latest = await recipient_client.recipient.get_latest_guarantee(tab_id)
        req_id = latest.req_id + 1 if latest else 0
        tab = await recipient_client.recipient.get_tab(tab_id)
        asset_address = tab.asset_address if tab else None

        print("TAB_ID=", tab_id)
        print("REQ_ID=", req_id)
        print("AMOUNT_WEI=", AMOUNT_WEI)
        print("ASSET_ADDRESS=", asset_address)
    finally:
        await recipient_client.aclose()


if __name__ == "__main__":
    asyncio.run(main())
```

### Payer (client) quick start

Run this after you have a `TAB_ID` and `REQ_ID` from the recipient. It signs the guarantee and
optionally issues a certificate if you also pass `RECIPIENT_KEY`.

```bash
export PAYER_KEY=0x..
export RECIPIENT_ADDRESS=0x..
export TAB_ID=<tab_id>
export REQ_ID=<req_id>
export AMOUNT_WEI=100000000000000000
export ASSET_ADDRESS=0x0000000000000000000000000000000000000000
export RECIPIENT_KEY=0x.. # optional
```

```python
import asyncio
import json
import os
import time

from eth_account import Account
from fourmica_sdk import (
    AssetBalanceInfo,
    Client,
    ConfigBuilder,
    PaymentGuaranteeRequestClaims,
    SigningScheme,
)

PAYER_KEY = os.environ["PAYER_KEY"]
RECIPIENT_ADDRESS = os.environ["RECIPIENT_ADDRESS"]
TAB_ID = int(os.environ["TAB_ID"], 0)
REQ_ID = int(os.environ["REQ_ID"], 0)
DEFAULT_ASSET_ADDRESS = "0x0000000000000000000000000000000000000000"
REQUESTED_AMOUNT_WEI = int(os.getenv("AMOUNT_WEI", "100000000000000000"), 0)
ASSET_ADDRESS = os.getenv("ASSET_ADDRESS") or DEFAULT_ASSET_ADDRESS
RECIPIENT_KEY = os.getenv("RECIPIENT_KEY")


async def main() -> None:
    payer_cfg = ConfigBuilder().from_env().wallet_private_key(PAYER_KEY).build()
    payer_client = await Client.new(payer_cfg)
    recipient_client = None
    try:
        user_address = Account.from_key(PAYER_KEY).address
        amount_wei = REQUESTED_AMOUNT_WEI

        balance_raw = await payer_client.rpc.get_user_asset_balance(
            user_address, ASSET_ADDRESS
        )
        if balance_raw:
            balance = AssetBalanceInfo.from_rpc(balance_raw)
            available = max(balance.total - balance.locked, 0)
            print("COLLATERAL_TOTAL=", balance.total)
            print("COLLATERAL_LOCKED=", balance.locked)
            print("COLLATERAL_AVAILABLE=", available)
            if available <= 0:
                raise SystemExit("No available collateral for this asset.")
            if amount_wei > available:
                amount_wei = available
            if amount_wei <= 0:
                raise SystemExit("Requested amount exceeds available collateral.")
        else:
            print("COLLATERAL_TOTAL= <unknown>")
            print("COLLATERAL_LOCKED= <unknown>")
            print("COLLATERAL_AVAILABLE= <unknown>")
        print("AMOUNT_WEI=", amount_wei)

        claims = PaymentGuaranteeRequestClaims.new(
            user_address=user_address,
            recipient_address=RECIPIENT_ADDRESS,
            tab_id=TAB_ID,
            req_id=REQ_ID,
            amount=amount_wei,
            timestamp=int(time.time()),
            erc20_token=ASSET_ADDRESS,
        )
        signature = await payer_client.user.sign_payment(claims, SigningScheme.EIP712)

        print("PAYER_SIGNATURE=", signature.signature)
        print(
            "CLAIMS_JSON=",
            json.dumps(
                {
                    "user_address": claims.user_address,
                    "recipient_address": claims.recipient_address,
                    "tab_id": claims.tab_id,
                    "req_id": claims.req_id,
                    "amount": claims.amount,
                    "asset_address": claims.asset_address,
                    "timestamp": claims.timestamp,
                }
            ),
        )
        if RECIPIENT_KEY:
            recipient_cfg = (
                ConfigBuilder().from_env().wallet_private_key(RECIPIENT_KEY).build()
            )
            recipient_client = await Client.new(recipient_cfg)
            cert = await recipient_client.recipient.issue_payment_guarantee(
                claims, signature.signature, SigningScheme.EIP712
            )
            print("CERT_CLAIMS=", cert.claims)
            print("CERT_SIGNATURE=", cert.signature)
    finally:
        if recipient_client is not None:
            await recipient_client.aclose()
        await payer_client.aclose()


if __name__ == "__main__":
    asyncio.run(main())
```

## X402 Flow (HTTP 402)

The X402 helper turns `paymentRequirements` from a `402 Payment Required` response into an
X-PAYMENT header (and optional `/settle` call) that the facilitator will accept.

### What the SDK expects from `paymentRequirements`

At minimum you need:

- `scheme`, `network`, `payTo`, `asset`, and `maxAmountRequired` (v1) or `amount` (v2)
- `extra.tabEndpoint` for tab resolution (used to fetch tab metadata)

For X402 version 2, the `payment-required` payload must also include `x402Version`,
`resource`, and a non-empty `accepts` list.

### X402 Version 1

Version 1 returns payment requirements in the JSON response body:

```python
import asyncio

from fourmica_sdk import Client, ConfigBuilder, X402Flow
from fourmica_sdk import PaymentRequirementsV1


async def main() -> None:
    cfg = ConfigBuilder().wallet_private_key("0x...").build()
    client = await Client.new(cfg)
    flow = X402Flow.from_client(client)

    # 1) GET the protected endpoint and parse JSON body
    res = fetch_resource_somehow()
    requirements = PaymentRequirementsV1.from_raw(res["accepts"][0])

    # 2) Build the X-PAYMENT header with the SDK
    payment = await flow.sign_payment(requirements, "0xUser")

    # 3) Call the protected resource with the header
    headers = {"X-PAYMENT": payment.header}
    await call_resource_somehow(headers)

    await client.aclose()


if __name__ == "__main__":
    asyncio.run(main())
```

### X402 Version 2

Version 2 uses the `payment-required` header (base64-encoded) instead of a JSON response body:

```python
import asyncio
import base64
import json

from fourmica_sdk import Client, ConfigBuilder, X402Flow
from fourmica_sdk import X402PaymentRequired, PaymentRequirementsV2


async def main() -> None:
    cfg = ConfigBuilder().wallet_private_key("0x...").build()
    client = await Client.new(cfg)
    flow = X402Flow.from_client(client)

    # 1) GET the protected endpoint and extract payment-required header
    res = fetch_resource_somehow()
    header = res.headers.get("payment-required")
    if not header:
        raise RuntimeError("Missing payment-required header")

    # 2) Decode the header
    decoded = base64.b64decode(header).decode("utf8")
    payment_required = X402PaymentRequired.from_raw(json.loads(decoded))

    # 3) Select a payment option
    accepted = payment_required.accepts[0]

    # 4) Build the PAYMENT-SIGNATURE header with the SDK
    signed = await flow.sign_payment_v2(payment_required, accepted, "0xUser")

    # 5) Call the protected resource with the header
    headers = {"PAYMENT-SIGNATURE": signed.header}
    await call_resource_somehow(headers)

    await client.aclose()


if __name__ == "__main__":
    asyncio.run(main())
```

### Resource server / facilitator side

If your resource server proxies to the facilitator, you can reuse the SDK to settle after
verifying:

```python
from fourmica_sdk import Client, ConfigBuilder, X402Flow
from fourmica_sdk import PaymentRequirementsV1, X402SignedPayment


async def settle(
    facilitator_url: str,
    payment_requirements: PaymentRequirementsV1,
    payment: X402SignedPayment,
) -> None:
    core = await Client.new(
        ConfigBuilder().wallet_private_key("0x...").build()
    )
    flow = X402Flow.from_client(core)

    settled = await flow.settle_payment(payment, payment_requirements, facilitator_url)
    print("settlement result:", settled.settlement)

    await core.aclose()
```

Notes:

- `sign_payment` and `sign_payment_v2` always use EIP-712 signing and will error if the scheme is not 4mica.
- `UserClient.sign_payment` supports `SigningScheme.EIP712` (default) and `SigningScheme.EIP191`.
- `settle_payment` only hits `/settle`; resource servers should still call `/verify` first when enforcing access.

## Concepts

- Tabs are per `(user, recipient, asset)` credit ledgers. Core reuses an existing tab if it is still valid; otherwise it creates a new tab id using a SHA-256 hash of user, recipient, ttl, and a random UUID (treat `tab_id` as opaque).
- Tab lifecycle: tabs start `Pending`; the first valid guarantee opens the tab and sets `start_timestamp` to the claim timestamp. Guarantees must be within `[start_timestamp, start_timestamp + ttl]` and are rejected if the tab is closed or expired.
- Request ids: `req_id` is per-tab and strictly sequential. The first guarantee uses `req_id = 0`; each new guarantee must be `last_req_id + 1`. The facilitator returns `nextReqId` in `/tabs`.
- Guarantee request claims (v1) are the signed payload: `{ user_address, recipient_address, tab_id, req_id, amount, asset_address, timestamp }`. `asset_address` is the zero address for ETH if omitted. `timestamp` is seconds since epoch and is validated by core.
- Guarantee certificates are BLS signatures over `PaymentGuaranteeClaims` (core adds `domain`, `total_amount` which is the running sum for the tab, and `version`). The SDK models this as `BLSCert { claims, signature }`, where `claims` is ABI-encoded hex.

## X-PAYMENT header schema

`X-PAYMENT` is a base64-encoded JSON envelope:

```json
{
  "x402Version": 1,
  "scheme": "4mica-credit",
  "network": "polygon-amoy",
  "payload": {
    "claims": {
      "user_address": "<0x-prefixed checksum string>",
      "recipient_address": "<0x-prefixed checksum string>",
      "tab_id": "<decimal or 0x value>",
      "req_id": "<decimal or 0x value>",
      "amount": "<decimal or 0x value>",
      "asset_address": "<0x-prefixed checksum string>",
      "timestamp": 1716500000,
      "version": "v1"
    },
    "signature": "<0x-prefixed wallet signature>",
    "scheme": "eip712"
  }
}
```

Facilitators enforce that `scheme` / `network` match `/supported`, `payTo` matches
`recipient_address` in the claims, and `asset` / `maxAmountRequired` equal the signed `amount`.

## End-to-end credit flow (x402)

1. Resource sends `402 Payment Required` with `(scheme, network)` and a tab endpoint.
2. Client calls the tab endpoint with `{ userAddress, erc20Token?, ttlSeconds? }`; the recipient calls `/tabs` and returns tab metadata.
3. Client signs a guarantee with the SDK and wraps it into `X-PAYMENT`.
4. Client retries the protected call with `X-PAYMENT: <base64>`.
5. Recipient optionally calls `/verify` with `{ x402Version, paymentHeader, paymentRequirements }`.
6. Recipient calls `/settle` to obtain the certificate, then relays repayment details to the payer.

## API Methods Summary

### UserClient Methods

- `approve_erc20(token, amount)`
- `deposit(amount, erc20_token=None)`
- `get_user()`
- `get_tab_payment_status(tab_id)`
- `sign_payment(claims, scheme=SigningScheme.EIP712)`
- `pay_tab(tab_id, req_id, amount, recipient_address, erc20_token=None)`
- `request_withdrawal(amount, erc20_token=None)`
- `cancel_withdrawal(erc20_token=None)`
- `finalize_withdrawal(erc20_token=None)`

### RecipientClient Methods

- `create_tab(user_address, recipient_address, erc20_token, ttl)`
- `get_tab_payment_status(tab_id)`
- `issue_payment_guarantee(claims, signature, scheme)`
- `verify_payment_guarantee(cert)`
- `remunerate(cert)`
- `list_settled_tabs()`
- `list_pending_remunerations()`
- `get_tab(tab_id)`
- `list_recipient_tabs(settlement_statuses=None)`
- `get_tab_guarantees(tab_id)`
- `get_latest_guarantee(tab_id)`
- `get_guarantee(tab_id, req_id)`
- `list_recipient_payments()`
- `get_collateral_events_for_tab(tab_id)`
- `get_user_asset_balance(user_address, asset_address)`

### Admin / RPC Methods

Available under `client.rpc` (requires an admin API key):

- `update_user_suspension(user_address, suspended)`
- `create_admin_api_key({"name": ..., "scopes": [...]})`
- `list_admin_api_keys()`
- `revoke_admin_api_key(key_id)`

## Error Handling

All SDK errors extend `FourMicaError`. Common error types include `ConfigError`, `RpcError`,
`ClientInitializationError`, `ContractError`, `SigningError`, `VerificationError`, `X402Error`, and
`AuthError`.

## Notes

- Network methods are `async`; some helpers are synchronous (for example
  `RecipientClient.verify_payment_guarantee` and model constructors).
- BLS verification and remuneration require `py-ecc` (`pip install 'sdk-4mica[bls]'`).
- Numeric fields that pass through the SDK's `u256` helpers (amounts and ids) accept
  `int` or hex/decimal strings and are serialized to `0x`-prefixed hex when sent to the facilitator.
  Other fields (for example `ttl`) are forwarded as provided.

## License

MIT