Skip to content

Commit 22354b4

Browse files
x402 payments on base sepolia (#158)
* v2 updates * v2 updates * updates * updates * updates * updates * Delete uv.lock --------- Co-authored-by: Aniket Dixit <dixitaniket199@gmail.com>
1 parent c5d8f0e commit 22354b4

13 files changed

Lines changed: 372 additions & 84 deletions

File tree

Makefile

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,17 @@ completion:
6868
--prompt "Hello, how are you?" \
6969
--max-tokens 50
7070

71-
chat:
71+
chat-og-evm:
7272
python -m opengradient.cli chat \
73-
--model $(MODEL) --mode TEE \
73+
--model $(MODEL) \
74+
--messages '[{"role":"user","content":"Tell me a fun fact"}]' \
75+
--max-tokens 150 --network og-evm
76+
77+
chat-base-testnet:
78+
python -m opengradient.cli chat \
79+
--model $(MODEL) \
7480
--messages '[{"role":"user","content":"Tell me a fun fact"}]' \
75-
--max-tokens 150
81+
--max-tokens 150 --network base-testnet
7682

7783
chat-stream:
7884
python -m opengradient.cli chat \

examples/README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ python examples/upload_model.py
4848

4949
## x402 LLM Examples
5050

51+
#### `x402_permit2.py`
52+
Grants Permit2 approval so x402 payments can spend OPG on Base Sepolia.
53+
54+
```bash
55+
python examples/x402_permit2.py
56+
```
57+
58+
**What it does:**
59+
- Sends an `approve(PERMIT2, amount)` transaction for OPG
60+
- Uses `OG_PRIVATE_KEY` to sign and submit the transaction
61+
- Prints allowance before and after approval
62+
5163
#### `run_x402_llm.py`
5264
Runs LLM inference with x402 transaction processing.
5365

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

examples/run_x402_llm.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,16 @@
1212
import os
1313

1414
import opengradient as og
15+
from x402_permit2 import check_permit2_approval
16+
17+
network = "base-testnet"
1518

1619
client = og.Client(
1720
private_key=os.environ.get("OG_PRIVATE_KEY"),
1821
)
1922

23+
check_permit2_approval(client.wallet_address, network)
24+
2025
messages = [
2126
{"role": "user", "content": "What is Python?"},
2227
{"role": "assistant", "content": "Python is a high-level programming language."},
@@ -27,6 +32,6 @@
2732
model=og.TEE_LLM.GPT_4_1_2025_04_14,
2833
messages=messages,
2934
x402_settlement_mode=og.x402SettlementMode.SETTLE_METADATA,
35+
network=network
3036
)
31-
print(f"Response: {result.chat_output['content']}")
32-
print(f"Payment hash: {result.payment_hash}")
37+
print(f"Response: {result.chat_output['content']}")

examples/run_x402_llm_stream.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
import os
22

33
import opengradient as og
4+
from x402_permit2 import check_permit2_approval
5+
6+
network = "base-testnet"
47

58
client = og.Client(
69
private_key=os.environ.get("OG_PRIVATE_KEY"),
710
)
811

12+
check_permit2_approval(client.wallet_address, network)
13+
914
messages = [
10-
{"role": "user", "content": "Describe to me the 7 network layers?"},
15+
{"role": "user", "content": "What is Python?"},
16+
{"role": "assistant", "content": "Python is a high-level programming language."},
17+
{"role": "user", "content": "What makes it good for beginners?"},
1118
]
1219

1320
stream = client.llm.chat(
1421
model=og.TEE_LLM.GPT_4_1_2025_04_14,
1522
messages=messages,
1623
x402_settlement_mode=og.x402SettlementMode.SETTLE_METADATA,
1724
stream=True,
18-
max_tokens=1000,
25+
max_tokens=300,
26+
network=network,
1927
)
2028

2129
for chunk in stream:

examples/x402_permit2.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import argparse
2+
import os
3+
from typing import Optional
4+
5+
import opengradient as og
6+
from x402v2.mechanisms.evm.constants import PERMIT2_ADDRESS
7+
from web3 import Web3
8+
9+
BASE_OPG_ADDRESS = "0x240b09731D96979f50B2C649C9CE10FcF9C7987F"
10+
BASE_SEPOLIA_RPC = "https://sepolia.base.org"
11+
BASE_TESTNET = "base-testnet"
12+
MAX_UINT256 = (1 << 256) - 1
13+
14+
ERC20_ABI = [
15+
{
16+
"inputs": [
17+
{"name": "owner", "type": "address"},
18+
{"name": "spender", "type": "address"},
19+
],
20+
"name": "allowance",
21+
"outputs": [{"name": "", "type": "uint256"}],
22+
"stateMutability": "view",
23+
"type": "function",
24+
},
25+
{
26+
"inputs": [
27+
{"name": "spender", "type": "address"},
28+
{"name": "amount", "type": "uint256"},
29+
],
30+
"name": "approve",
31+
"outputs": [{"name": "", "type": "bool"}],
32+
"stateMutability": "nonpayable",
33+
"type": "function",
34+
},
35+
]
36+
37+
38+
def _get_base_sepolia_web3() -> Web3:
39+
return Web3(Web3.HTTPProvider(BASE_SEPOLIA_RPC))
40+
41+
42+
def get_permit2_allowance(client_address: str) -> int:
43+
w3 = _get_base_sepolia_web3()
44+
token = w3.eth.contract(address=Web3.to_checksum_address(BASE_OPG_ADDRESS), abi=ERC20_ABI)
45+
return token.functions.allowance(
46+
Web3.to_checksum_address(client_address),
47+
Web3.to_checksum_address(PERMIT2_ADDRESS),
48+
).call()
49+
50+
51+
def check_permit2_approval(client_address: str, network: str) -> None:
52+
"""Raise an error if Permit2 approval is missing for base-testnet."""
53+
if network != BASE_TESTNET:
54+
return
55+
56+
allowance = get_permit2_allowance(client_address)
57+
print(f"Current OPG Permit2 allowance: {allowance}")
58+
59+
if allowance == 0:
60+
raise RuntimeError(
61+
f"ERROR: No Permit2 approval found for address {client_address}. "
62+
f"Approve Permit2 ({PERMIT2_ADDRESS}) to spend OPG ({BASE_OPG_ADDRESS}) "
63+
"on Base Sepolia before using x402 payments."
64+
)
65+
66+
67+
def grant_permit2_approval(
68+
private_key: str,
69+
amount: int = MAX_UINT256,
70+
gas_multiplier: float = 1.2,
71+
nonce: Optional[int] = None,
72+
) -> str:
73+
"""Send ERC-20 approve(spender=Permit2, amount=amount) for OPG on Base Sepolia."""
74+
w3 = _get_base_sepolia_web3()
75+
76+
account = w3.eth.account.from_key(private_key)
77+
token = w3.eth.contract(address=Web3.to_checksum_address(BASE_OPG_ADDRESS), abi=ERC20_ABI)
78+
79+
tx_nonce = nonce
80+
if tx_nonce is None:
81+
tx_nonce = w3.eth.get_transaction_count(account.address, "pending")
82+
83+
approve_fn = token.functions.approve(Web3.to_checksum_address(PERMIT2_ADDRESS), amount)
84+
estimated_gas = approve_fn.estimate_gas({"from": account.address})
85+
86+
tx = approve_fn.build_transaction(
87+
{
88+
"from": account.address,
89+
"nonce": tx_nonce,
90+
"gas": int(estimated_gas * gas_multiplier),
91+
"gasPrice": w3.eth.gas_price,
92+
"chainId": w3.eth.chain_id,
93+
}
94+
)
95+
96+
signed = account.sign_transaction(tx)
97+
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
98+
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
99+
100+
if receipt.status != 1:
101+
raise RuntimeError(f"Permit2 approval transaction failed: {tx_hash.hex()}")
102+
103+
return tx_hash.hex()
104+
105+
106+
def _parse_args() -> argparse.Namespace:
107+
parser = argparse.ArgumentParser(description="Grant Permit2 approval to spend OPG on Base Sepolia.")
108+
parser.add_argument(
109+
"--amount",
110+
type=int,
111+
default=MAX_UINT256,
112+
help="Approval amount in base units (default: max uint256).",
113+
)
114+
return parser.parse_args()
115+
116+
117+
def main() -> None:
118+
args = _parse_args()
119+
120+
private_key = os.environ.get("OG_PRIVATE_KEY")
121+
if not private_key:
122+
raise RuntimeError("OG_PRIVATE_KEY is not set.")
123+
124+
client = og.Client(private_key=private_key)
125+
wallet_address = client.wallet_address
126+
127+
before = get_permit2_allowance(wallet_address)
128+
print(f"Wallet: {wallet_address}")
129+
print(f"Permit2: {PERMIT2_ADDRESS}")
130+
print(f"OPG Token: {BASE_OPG_ADDRESS}")
131+
print(f"Allowance before: {before}")
132+
133+
tx_hash = grant_permit2_approval(private_key=private_key, amount=args.amount)
134+
after = get_permit2_allowance(wallet_address)
135+
136+
print(f"Approval transaction hash: {tx_hash}")
137+
print(f"Allowance after: {after}")
138+
139+
140+
if __name__ == "__main__":
141+
main()

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ dependencies = [
2828
"openai>=1.58.1",
2929
"pydantic>=2.9.2",
3030
"og-test-x402==0.0.9",
31+
"og-test-v2-x402==0.0.6"
3132
]
3233

3334
[project.scripts]

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ requests>=2.32.3
77
langchain>=0.3.7
88
openai>=1.58.1
99
pydantic>=2.9.2
10-
og-test-x402==0.0.9
10+
og-test-x402==0.0.9
11+
og-test-v2-x402==0.0.6

src/opengradient/cli.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
DEFAULT_OG_FAUCET_URL,
2121
DEFAULT_RPC_URL,
2222
)
23-
from .types import InferenceMode, x402SettlementMode
23+
from .types import InferenceMode, x402Network, x402SettlementMode
2424

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

@@ -74,6 +74,11 @@ def convert(self, value, param, ctx):
7474
"settle-metadata": x402SettlementMode.SETTLE_METADATA,
7575
}
7676

77+
x402Networks = {
78+
"og-evm": x402Network.OG_EVM,
79+
"base-testnet": x402Network.BASE_TESTNET,
80+
}
81+
7782

7883
def initialize_config(ctx):
7984
"""Interactively initialize OpenGradient config"""
@@ -461,6 +466,12 @@ def print_llm_completion_result(model_cid, tx_hash, llm_output, is_vanilla=True)
461466
default="settle-batch",
462467
help="Settlement mode for x402 payments: settle (hashes only), settle-batch (batched, default), settle-metadata (full data)",
463468
)
469+
@click.option(
470+
"--network",
471+
type=click.Choice(x402Networks.keys()),
472+
default="og-evm",
473+
help="x402 network to use for TEE chat payments: og-evm (default) or base-testnet",
474+
)
464475
@click.option("--stream", is_flag=True, default=False, help="Stream the output from the LLM")
465476
@click.pass_context
466477
def chat(
@@ -475,6 +486,7 @@ def chat(
475486
tools_file: Optional[Path],
476487
tool_choice: Optional[str],
477488
x402_settlement_mode: Optional[str],
489+
network: str,
478490
stream: bool,
479491
):
480492
"""
@@ -561,6 +573,7 @@ def chat(
561573
tools=parsed_tools,
562574
tool_choice=tool_choice,
563575
x402_settlement_mode=x402SettlementModes[x402_settlement_mode],
576+
network=x402Networks[network],
564577
stream=stream,
565578
)
566579

src/opengradient/client/client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def __init__(
5050
email: Optional[str] = None,
5151
password: Optional[str] = None,
5252
twins_api_key: Optional[str] = None,
53+
wallet_address: str = None,
5354
rpc_url: str = DEFAULT_RPC_URL,
5455
api_url: str = DEFAULT_API_URL,
5556
contract_address: str = DEFAULT_INFERENCE_CONTRACT_ADDRESS,
@@ -79,6 +80,7 @@ def __init__(
7980

8081
# Create namespaces
8182
self.model_hub = ModelHub(hub_user=hub_user)
83+
self.wallet_address = wallet_account.address
8284

8385
self.llm = LLM(
8486
wallet_account=wallet_account,

0 commit comments

Comments
 (0)