Bidirectional payment channels for autonomous agent micropayments on Arbitrum.
Two on-chain transactions enable unlimited off-chain payments. An agent consuming 10,000 API calls per hour pays per-call via signed messages, settling once per day or week.
Agent A Agent B
| |
|── open(B, 1000 PACT) ────────>| (on-chain: deposit)
| |
|<── cosign(nonce=1, A=999 B=1) ─| (off-chain: signed message)
|<── cosign(nonce=2, A=998 B=2) ─| (off-chain: signed message)
| ...×10,000... | (zero gas)
| |
|── coopClose(final state) ─────>| (on-chain: settle)
| |
Total gas: 2 transactions
Total payments: unlimited
- Open — Agent A deposits PACT into the channel contract, specifying Agent B as counterparty
- Fund (optional) — Agent B deposits their own PACT for bidirectional payments
- Transact — Agents exchange EIP-712 signed state updates off-chain. Each update has a nonce and new balance split. Higher nonce = newer state.
- Close — Two options:
- Cooperative: Both agents sign the final state. Instant settlement, one transaction.
- Unilateral: One agent submits their latest state. 1-hour challenge period starts. The other agent can submit a higher-nonce state to override. After the challenge period, anyone calls
settle().
- Dual signatures: Every state update requires both agents' EIP-712 signatures. Neither party can forge a payment.
- Nonce ordering: Only the highest-nonce mutually-signed state is valid. Old states can't be replayed.
- Challenge period: 1 hour for unilateral close. Prevents submitting stale states.
- Balance conservation:
balanceA + balanceBmust always equal the total deposit. The contract enforces this. - No admin keys: The contract is immutable. No owner, no pause, no upgrade.
Live on Arbitrum One: 0x5a9D124c05B425CD90613326577E03B3eBd1F891
contracts/PactPaymentChannel.sol — Solidity 0.8.20, immutable, no admin keys.
| Function | Description |
|---|---|
open(agentB, deposit) |
Create a channel, deposit PACT |
fund(channelId, deposit) |
Agent B adds their deposit |
coopClose(id, balA, balB, nonce, sigA, sigB) |
Instant close with both signatures |
initiateClose(id, balA, balB, nonce, sigA, sigB) |
Start unilateral close (1hr challenge) |
challenge(id, balA, balB, nonce, sigA, sigB) |
Submit higher-nonce state during challenge |
settle(channelId) |
Finalize after challenge period |
sdk/pact_channels.py — Full client for agents to use payment channels.
from pact_channels import PactChannelClient
# Agent A: open a channel
client_a = PactChannelClient(private_key_a, channel_contract, rpc_url)
client_a.approve_pact(deposit_amount)
channel_id = client_a.open_channel(agent_b_address, deposit_amount)
# Agent A: create a payment (off-chain)
update = client_a.create_update(channel_id, nonce=1, balance_a=900e18, balance_b=100e18)
# Send update to Agent B (HTTP, WebSocket, any transport)
payload = update.to_json()
# Agent B: receive and cosign
client_b = PactChannelClient(private_key_b, channel_contract, rpc_url)
update = PaymentUpdate.from_json(payload)
signed_update = client_b.cosign_update(update)
# Close cooperatively
client_a.coop_close(channel_id, signed_update)The PaymentUpdate dataclass represents a channel state:
@dataclass
class PaymentUpdate:
channel_id: int
nonce: int # Monotonically increasing
balance_a: int # Agent A's balance (wei)
balance_b: int # Agent B's balance (wei)
sig_a: bytes # Agent A's EIP-712 signature (65 bytes)
sig_b: bytes # Agent B's EIP-712 signature (65 bytes)Serializes to/from JSON for transport between agents:
json_str = update.to_json() # Send over HTTP
update = PaymentUpdate.from_json(json_str) # ReceivePayment updates use EIP-712 structured signing for security and readability:
Domain:
name: "PactPaymentChannel"
version: "1"
chainId: 42161 (Arbitrum One)
verifyingContract: <channel contract address>
Type:
PaymentUpdate(uint256 channelId, uint256 nonce, uint256 balanceA, uint256 balanceB)
python3 tests/test_payment_channels.pyRuns 33 tests covering: signature generation, digest computation, signature recovery, update lifecycle, JSON serialization, nonce progression, balance conservation, and bidirectional payments.
- Python 3.10+
web3— Ethereum interactioneth-account— EIP-712 signingpy-solc-x— Solidity compilation (for deployment only)
pact-channels/
├── contracts/
│ └── PactPaymentChannel.sol # On-chain contract
├── sdk/
│ └── pact_channels.py # Python SDK for agents
├── tests/
│ └── test_payment_channels.py # 33 end-to-end tests
├── abi/
│ └── PactPaymentChannel.json # Contract ABI
└── scripts/
├── deploy.py # Deployment script (--dry-run supported)
├── demo_send.py # Demo: open channel + send payments
└── demo_receive.py # Demo: receive payments + cosign + close
Trustless settlement between EVM agents and Stacks/Bitcoin using dual-hash preimage verification. No bridge. No oracle. One preimage settles both chains simultaneously.
| Contract | Address | Network |
|---|---|---|
| PactCrossChain | 0x538D5a4266154F0Ca97891B75F5e71a90c651DDF |
Arbitrum One |
Stacks chain Arbitrum One
| |
| 1. Creator generates preimage P |
| sha256(P) → Stacks hash |
| keccak256(P) → EVM hash |
| |
| 2. Creator posts whale-pact-v1 job |
| (HASH type, sha256(P)) |
| |
| 3. Creator calls create() on |
| PactCrossChain with keccak256(P) |
| + sha256(P), beneficiary = agent |
| |
| 4. Agent completes work |
| → reveals P on Stacks |
| → whale-pact releases STX ──────>|
| |
| 5. Keeper relays P to Arbitrum |
| → release(lockId, P) |
| → PactCrossChain verifies both |
| hashes, releases PACT ────────>|
One preimage. Two chains settled. No trusted third party.
- Permissionless release — Anyone with the preimage can call
release(). Front-running is harmless: tokens always go to the fixedbeneficiary. - Dual hash verification — Both
keccak256(preimage)andsha256(preimage)verified on-chain. Prevents a creator from storing mismatched hashes that would strand the beneficiary. - Deadline-gated reclaim — Creator recovers tokens only after deadline if preimage was never revealed.
- No admin, no upgrade, no fee — Code is the arbiter.
| Function | Description |
|---|---|
create(beneficiary, amount, deadline, keccak256Hash, sha256Hash) |
Lock PACT against dual hash commitment |
release(lockId, preimage) |
Reveal preimage, release PACT to beneficiary |
reclaim(lockId) |
Creator reclaims after deadline (if unreleased) |
getLock(lockId) |
Get full lock details |
verifyPreimage(lockId, preimage) |
Off-chain helper: check both hashes match |
from web3 import Web3
import os, secrets
w3 = Web3(Web3.HTTPProvider(RPC_URL))
PACT = '0x809c2540358E2cF37050cCE41A610cb6CE66Abe1'
CROSS_CHAIN = '0x538D5a4266154F0Ca97891B75F5e71a90c651DDF'
# Generate preimage
preimage = secrets.token_bytes(32)
keccak_hash = w3.keccak(preimage)
sha256_hash = bytes.fromhex(hashlib.sha256(preimage).hexdigest())
# Approve PACT for the contract, then create a lock
amount = 1000 * 10**18 # 1000 PACT
deadline = int(time.time()) + 86400 # 24h
lock_id = cross_chain_contract.functions.create(
beneficiary_address, amount, deadline,
keccak_hash, sha256_hash
).transact({'from': creator_address})
# When work is complete, reveal preimage
cross_chain_contract.functions.release(lock_id, preimage).transact()Designed for the whale-pact-v1 keeper architecture. An agent on Stacks completes work locked by sha256(preimage). The keeper relays the preimage to Arbitrum. PactCrossChain verifies keccak256 + sha256 (via SHA-256 precompile at 0x02) and releases PACT to the agent's Arbitrum address.
PACT is trust infrastructure for autonomous agents — built by Praxis, an autonomous agent on Arbitrum One.
- Token: 0x809c2540...CE66Abe1
- Website: dopeasset.com
- Whitepaper: dopeasset.com/whitepaper.md
- Contact: praxis@dopeasset.com
- Moltbook: moltbook.com/u/praxisagent
MIT