Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,17 @@ completion:
--prompt "Hello, how are you?" \
--max-tokens 50

chat:
chat-og-evm:
python -m opengradient.cli chat \
--model $(MODEL) --mode TEE \
--model $(MODEL) \
--messages '[{"role":"user","content":"Tell me a fun fact"}]' \
--max-tokens 150 --network og-evm

chat-base-testnet:
python -m opengradient.cli chat \
--model $(MODEL) \
--messages '[{"role":"user","content":"Tell me a fun fact"}]' \
--max-tokens 150
--max-tokens 150 --network base-testnet

chat-stream:
python -m opengradient.cli chat \
Expand Down
13 changes: 12 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ python examples/upload_model.py

## x402 LLM Examples

#### `x402_permit2.py`
Grants Permit2 approval so x402 payments can spend OPG on Base Sepolia.

```bash
python examples/x402_permit2.py
```

**What it does:**
- Sends an `approve(PERMIT2, amount)` transaction for OPG
- Uses `OG_PRIVATE_KEY` to sign and submit the transaction
- Prints allowance before and after approval

#### `run_x402_llm.py`
Runs LLM inference with x402 transaction processing.

Expand Down Expand Up @@ -187,4 +199,3 @@ Browse available models on the [OpenGradient Model Hub](https://hub.opengradient
- Run `opengradient --help` for CLI command reference
- Visit our [documentation](https://docs.opengradient.ai/) for detailed guides
- Check the main [README](../README.md) for SDK overview

9 changes: 7 additions & 2 deletions examples/run_x402_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@
import os

import opengradient as og
from x402_permit2 import check_permit2_approval

network = "base-testnet"

client = og.Client(
private_key=os.environ.get("OG_PRIVATE_KEY"),
)

check_permit2_approval(client.wallet_address, network)

messages = [
{"role": "user", "content": "What is Python?"},
{"role": "assistant", "content": "Python is a high-level programming language."},
Expand All @@ -27,6 +32,6 @@
model=og.TEE_LLM.GPT_4_1_2025_04_14,
messages=messages,
x402_settlement_mode=og.x402SettlementMode.SETTLE_METADATA,
network=network
)
print(f"Response: {result.chat_output['content']}")
print(f"Payment hash: {result.payment_hash}")
print(f"Response: {result.chat_output['content']}")
12 changes: 10 additions & 2 deletions examples/run_x402_llm_stream.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
import os

import opengradient as og
from x402_permit2 import check_permit2_approval

network = "base-testnet"

client = og.Client(
private_key=os.environ.get("OG_PRIVATE_KEY"),
)

check_permit2_approval(client.wallet_address, network)

messages = [
{"role": "user", "content": "Describe to me the 7 network layers?"},
{"role": "user", "content": "What is Python?"},
{"role": "assistant", "content": "Python is a high-level programming language."},
{"role": "user", "content": "What makes it good for beginners?"},
]

stream = client.llm.chat(
model=og.TEE_LLM.GPT_4_1_2025_04_14,
messages=messages,
x402_settlement_mode=og.x402SettlementMode.SETTLE_METADATA,
stream=True,
max_tokens=1000,
max_tokens=300,
network=network,
)

for chunk in stream:
Expand Down
141 changes: 141 additions & 0 deletions examples/x402_permit2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import argparse
import os
from typing import Optional

import opengradient as og
from x402v2.mechanisms.evm.constants import PERMIT2_ADDRESS
from web3 import Web3

BASE_OPG_ADDRESS = "0x240b09731D96979f50B2C649C9CE10FcF9C7987F"
BASE_SEPOLIA_RPC = "https://sepolia.base.org"
BASE_TESTNET = "base-testnet"
MAX_UINT256 = (1 << 256) - 1

ERC20_ABI = [
{
"inputs": [
{"name": "owner", "type": "address"},
{"name": "spender", "type": "address"},
],
"name": "allowance",
"outputs": [{"name": "", "type": "uint256"}],
"stateMutability": "view",
"type": "function",
},
{
"inputs": [
{"name": "spender", "type": "address"},
{"name": "amount", "type": "uint256"},
],
"name": "approve",
"outputs": [{"name": "", "type": "bool"}],
"stateMutability": "nonpayable",
"type": "function",
},
]


def _get_base_sepolia_web3() -> Web3:
return Web3(Web3.HTTPProvider(BASE_SEPOLIA_RPC))


def get_permit2_allowance(client_address: str) -> int:
w3 = _get_base_sepolia_web3()
token = w3.eth.contract(address=Web3.to_checksum_address(BASE_OPG_ADDRESS), abi=ERC20_ABI)
return token.functions.allowance(
Web3.to_checksum_address(client_address),
Web3.to_checksum_address(PERMIT2_ADDRESS),
).call()


def check_permit2_approval(client_address: str, network: str) -> None:
"""Raise an error if Permit2 approval is missing for base-testnet."""
if network != BASE_TESTNET:
return

allowance = get_permit2_allowance(client_address)
print(f"Current OPG Permit2 allowance: {allowance}")

if allowance == 0:
raise RuntimeError(
f"ERROR: No Permit2 approval found for address {client_address}. "
f"Approve Permit2 ({PERMIT2_ADDRESS}) to spend OPG ({BASE_OPG_ADDRESS}) "
"on Base Sepolia before using x402 payments."
)


def grant_permit2_approval(
private_key: str,
amount: int = MAX_UINT256,
gas_multiplier: float = 1.2,
nonce: Optional[int] = None,
) -> str:
"""Send ERC-20 approve(spender=Permit2, amount=amount) for OPG on Base Sepolia."""
w3 = _get_base_sepolia_web3()

account = w3.eth.account.from_key(private_key)
token = w3.eth.contract(address=Web3.to_checksum_address(BASE_OPG_ADDRESS), abi=ERC20_ABI)

tx_nonce = nonce
if tx_nonce is None:
tx_nonce = w3.eth.get_transaction_count(account.address, "pending")

approve_fn = token.functions.approve(Web3.to_checksum_address(PERMIT2_ADDRESS), amount)
estimated_gas = approve_fn.estimate_gas({"from": account.address})

tx = approve_fn.build_transaction(
{
"from": account.address,
"nonce": tx_nonce,
"gas": int(estimated_gas * gas_multiplier),
"gasPrice": w3.eth.gas_price,
"chainId": w3.eth.chain_id,
}
)

signed = account.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)

if receipt.status != 1:
raise RuntimeError(f"Permit2 approval transaction failed: {tx_hash.hex()}")

return tx_hash.hex()


def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Grant Permit2 approval to spend OPG on Base Sepolia.")
parser.add_argument(
"--amount",
type=int,
default=MAX_UINT256,
help="Approval amount in base units (default: max uint256).",
)
return parser.parse_args()


def main() -> None:
args = _parse_args()

private_key = os.environ.get("OG_PRIVATE_KEY")
if not private_key:
raise RuntimeError("OG_PRIVATE_KEY is not set.")

client = og.Client(private_key=private_key)
wallet_address = client.wallet_address

before = get_permit2_allowance(wallet_address)
print(f"Wallet: {wallet_address}")
print(f"Permit2: {PERMIT2_ADDRESS}")
print(f"OPG Token: {BASE_OPG_ADDRESS}")
print(f"Allowance before: {before}")

tx_hash = grant_permit2_approval(private_key=private_key, amount=args.amount)
after = get_permit2_allowance(wallet_address)

print(f"Approval transaction hash: {tx_hash}")
print(f"Allowance after: {after}")


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies = [
"openai>=1.58.1",
"pydantic>=2.9.2",
"og-test-x402==0.0.9",
"og-test-v2-x402==0.0.6"
]

[project.scripts]
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ requests>=2.32.3
langchain>=0.3.7
openai>=1.58.1
pydantic>=2.9.2
og-test-x402==0.0.9
og-test-x402==0.0.9
og-test-v2-x402==0.0.6
15 changes: 14 additions & 1 deletion src/opengradient/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
DEFAULT_OG_FAUCET_URL,
DEFAULT_RPC_URL,
)
from .types import InferenceMode, x402SettlementMode
from .types import InferenceMode, x402Network, x402SettlementMode

OG_CONFIG_FILE = Path.home() / ".opengradient_config.json"

Expand Down Expand Up @@ -74,6 +74,11 @@ def convert(self, value, param, ctx):
"settle-metadata": x402SettlementMode.SETTLE_METADATA,
}

x402Networks = {
"og-evm": x402Network.OG_EVM,
"base-testnet": x402Network.BASE_TESTNET,
}


def initialize_config(ctx):
"""Interactively initialize OpenGradient config"""
Expand Down Expand Up @@ -461,6 +466,12 @@ def print_llm_completion_result(model_cid, tx_hash, llm_output, is_vanilla=True)
default="settle-batch",
help="Settlement mode for x402 payments: settle (hashes only), settle-batch (batched, default), settle-metadata (full data)",
)
@click.option(
"--network",
type=click.Choice(x402Networks.keys()),
default="og-evm",
help="x402 network to use for TEE chat payments: og-evm (default) or base-testnet",
)
@click.option("--stream", is_flag=True, default=False, help="Stream the output from the LLM")
@click.pass_context
def chat(
Expand All @@ -475,6 +486,7 @@ def chat(
tools_file: Optional[Path],
tool_choice: Optional[str],
x402_settlement_mode: Optional[str],
network: str,
stream: bool,
):
"""
Expand Down Expand Up @@ -561,6 +573,7 @@ def chat(
tools=parsed_tools,
tool_choice=tool_choice,
x402_settlement_mode=x402SettlementModes[x402_settlement_mode],
network=x402Networks[network],
stream=stream,
)

Expand Down
2 changes: 2 additions & 0 deletions src/opengradient/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def __init__(
email: Optional[str] = None,
password: Optional[str] = None,
twins_api_key: Optional[str] = None,
wallet_address: str = None,
rpc_url: str = DEFAULT_RPC_URL,
api_url: str = DEFAULT_API_URL,
contract_address: str = DEFAULT_INFERENCE_CONTRACT_ADDRESS,
Expand Down Expand Up @@ -79,6 +80,7 @@ def __init__(

# Create namespaces
self.model_hub = ModelHub(hub_user=hub_user)
self.wallet_address = wallet_account.address

self.llm = LLM(
wallet_account=wallet_account,
Expand Down
Loading