Important
This repository is archived and no longer the active home for the Abstract MPP plugin or its demo.
The packages and demo have moved to Abstract-Foundation/abstract-packages. For maintained source and ongoing updates, use that repository.
Machine Payments Protocol (MPP) implementation for Abstract chain.
Custom mppx payment method plugin that settles on Abstract using standard
ERC-20 / ERC-3009 — no TIP-20 tokens or Tempo-specific infrastructure required.
Implements two payment intents:
| Intent | Mechanism | Settlement |
|---|---|---|
charge |
ERC-3009 TransferWithAuthorization |
Client signs, server broadcasts |
session |
AbstractStreamChannel escrow + EIP-712 vouchers |
Cumulative off-chain, final on-chain |
sequenceDiagram
participant Client
participant Server
Client->>Server: GET resource
Server-->>Client: 402 payment required
Note over Client: Receive payment challenge
Note over Client: Sign payment authorization
Client->>Server: Retry request with payment proof
Note over Server: Verify proof and settle payment
Server-->>Client: 200 OK with receipt
flowchart TD
root["mpp-abstract/"]
root --> packages["packages/"]
root --> examples["examples/"]
packages --> contracts["contracts/<br/>Solidity: AbstractStreamChannel.sol"]
packages --> plugin["mppx-abstract/<br/>TypeScript plugin (client + server)"]
examples --> hono["hono-server/<br/>Hono server with paid routes"]
examples --> agent["agent-client/<br/>Autonomous agent that pays on-demand"]
sequenceDiagram
participant Client
participant Chain
participant Server
Server-->>Client: 402 challenge
Note over Client: Sign TransferWithAuthorization data
Client->>Server: Send payment authorization
Note over Server: Recover address and verify nonce
Server->>Chain: Call transferWithAuthorization
Note over Server: Pay gas directly or via paymaster
Server-->>Client: 200 with receipt
Key properties:
- No client transaction — client only signs typed data (gasless for payer)
- Server broadcasts the
transferWithAuthorizationcall - Replay protection — each authorization has a unique
nonce - Expiry —
validBeforetimestamp prevents stale authorizations
sequenceDiagram
participant Client
participant Channel
participant Server
Client->>Channel: Open channel and escrow deposit
Client->>Server: Send open authorization
Note over Server: Verify open transaction and voucher
Server-->>Client: 200 with receipt
loop For each subsequent request
Client->>Server: Send updated voucher
Note over Server: Verify EIP 712 signature and accept voucher
Server-->>Client: 200 with receipt
end
Client->>Server: Send close authorization
Server->>Channel: Close channel
Note over Server: Verify final voucher and refund remainder
Server-->>Client: 204
Key properties:
- Single on-chain open amortizes gas across many requests
- Off-chain vouchers — only signatures exchanged per request
- Cumulative accounting — server tracks highest accepted voucher
- Server-initiated close — server calls
close()with final voucher - Payer-initiated escape —
requestClose()+ grace period +withdraw()
| Tempo | Abstract | |
|---|---|---|
| Mechanism | Hosted fee-payer service (feePayerUrl) |
Native ZKsync paymaster (paymasterParams) |
| Infrastructure | Separate HTTP service to sign & broadcast | Just a contract address in the tx |
| Config | feePayerUrl: 'https://feepayer.example.com' |
paymasterAddress: '0x...' |
| Transaction type | Tempo TIP-20 tx with feePayer flag |
ZKsync EIP-712 tx with customData |
| Dependencies | Requires running & securing a service | Contract deployed on Abstract |
When paymasterAddress is set, the server includes:
customData: {
paymasterParams: {
paymaster: paymasterAddress,
paymasterInput: '0x',
},
}No additional infrastructure needed — Abstract's native AA handles the rest.
cd packages/contracts
forge installforge test -v
# 17 tests, all passNote: Abstract uses the ZKsync VM. For actual on-chain deployment you need
foundry-zksyncor thehardhat-zksynctoolchain. The standardforge buildcompiles contracts for testing purposes.
# Install foundry-zksync
curl -L https://raw.githubusercontent.com/matter-labs/foundry-zksync/main/install-foundry-zksync | bash
# Set env
export ABSTRACT_TESTNET_RPC=https://api.testnet.abs.xyz
export DEPLOYER_PRIVATE_KEY=0x...
# Deploy
forge script script/Deploy.s.sol \
--rpc-url abstract_testnet \
--broadcast \
--private-key $DEPLOYER_PRIVATE_KEY \
--zksync| Network | AbstractStreamChannel |
|---|---|
| Testnet (11124) | Deploy and set here |
| Mainnet (2741) | Deploy and set here |
npm install mppx mppx-abstract viemimport { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { Mppx, payment } from 'mppx/hono'
import { privateKeyToAccount } from 'viem/accounts'
import { abstract } from 'mppx-abstract/server'
import { USDC_E_TESTNET } from 'mppx-abstract'
const serverAccount = privateKeyToAccount(process.env.SERVER_PRIVATE_KEY as `0x${string}`)
const mppx = Mppx.create({
realm: 'api.myapp.xyz',
secretKey: process.env.MPP_SECRET_KEY!,
methods: [
abstract.charge({
account: serverAccount,
recipient: '0xYourRecipient',
currency: USDC_E_TESTNET,
amount: '0.01',
decimals: 6,
testnet: true,
// Optional: sponsor gas via Abstract paymaster
paymasterAddress: '0xYourPaymaster',
}),
abstract.session({
account: serverAccount,
recipient: '0xYourRecipient',
currency: USDC_E_TESTNET,
amount: '0.001',
suggestedDeposit: '1',
unitType: 'request',
testnet: true,
}),
],
})
const app = new Hono()
// One-time payment per request
app.get('/api/data',
payment(mppx.charge, { amount: '0.01', currency: USDC_E_TESTNET, decimals: 6, recipient: '0x...' }),
(c) => c.json({ data: 'premium content' }),
)
// Payment channel — lower cost per request
app.get('/api/stream',
payment(mppx.session, {
amount: '0.001',
currency: USDC_E_TESTNET,
decimals: 6,
recipient: '0x...',
unitType: 'request',
suggestedDeposit: '1',
}),
(c) => c.json({ stream: 'streaming content' }),
)
serve({ fetch: app.fetch, port: 3000 })import { Mppx } from 'mppx/client'
import { privateKeyToAccount } from 'viem/accounts'
import { abstractCharge, abstractSession } from 'mppx-abstract/client'
const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY as `0x${string}`)
const mppx = Mppx.create({
methods: [
// Sign ERC-3009 authorizations for charge requests
abstractCharge({ account }),
// Open payment channel, send vouchers for session requests
abstractSession({
account,
deposit: '5', // pre-fund 5 USDC.e
}),
],
})
// Automatically handles 402 → sign → retry
const response = await mppx.fetch('https://api.myapp.xyz/api/data')
const data = await response.json()
console.log('Receipt:', response.headers.get('Payment-Receipt'))USDC.e on Abstract Testnet supports minting via the Open Minter contract:
import { createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { writeContract } from 'viem/actions'
const OPEN_MINTER_TESTNET = '0x86C3FA1c8d7dcDebAC1194531d080e6e6fF9afF5'
const account = privateKeyToAccount('0x...')
const client = createWalletClient({
account,
chain: { id: 11124, name: 'Abstract Testnet', nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 }, rpcUrls: { default: { http: ['https://api.testnet.abs.xyz'] } } },
transport: http('https://api.testnet.abs.xyz'),
})
// Mint 100 USDC.e to your address
await writeContract(client, {
account,
address: OPEN_MINTER_TESTNET,
abi: [{ name: 'mint', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }], outputs: [] }],
functionName: 'mint',
args: [account.address, 100_000_000n], // 100 USDC.e (6 decimals)
chain: null,
})The commands below are preserved for historical reference. The current source lives in
Abstract-Foundation/abstract-packages— clone that repository instead.
# 1. Clone and install
git clone https://github.com/Abstract-Foundation/abstract-packages
cd abstract-packages
pnpm install
# 2. Build the plugin
cd packages/mppx-abstract && npx tsc
# 3. Run contract tests
cd packages/contracts && forge test -v
# 4. Run the example server
cd examples/hono-server
MPP_SECRET_KEY=dev-secret \
SERVER_PRIVATE_KEY=0x... \
PAY_TO=0x... \
tsx src/index.ts
# 5. Run the agent client (in another terminal)
cd examples/agent-client
AGENT_PRIVATE_KEY=0x... \
SERVER_URL=http://localhost:3000 \
tsx src/agent.ts| Abstract Testnet | Abstract Mainnet | |
|---|---|---|
| Chain ID | 11124 | 2741 |
| RPC | https://api.testnet.abs.xyz |
https://api.mainnet.abs.xyz |
| USDC.e | 0xbd28Bd5A3Ef540d1582828CE2A1a657353008C61 |
0x84A71ccD554Cc1b02749b35d22F684CC8ec987e1 |
| AbstractStreamChannel | 0x29635C384f451a72ED2e2a312BCeb8b0bDC0923c |
0x29635C384f451a72ED2e2a312BCeb8b0bDC0923c |
| Explorer | https://explorer.testnet.abs.xyz | https://explorer.abs.xyz |
| VM | ZKsync (native AA, FCFS sequencer) | ZKsync |
| Feature | Tempo | Abstract |
|---|---|---|
| Token standard | TIP-20 (Tempo-specific) | ERC-20 / ERC-3009 (standard) |
| Charge mechanism | Tempo-signed tx bundle | ERC-3009 transferWithAuthorization |
| Escrow contract | TempoStreamChannel |
AbstractStreamChannel (port) |
| Gas sponsorship | Hosted fee-payer service | Native ZKsync paymaster |
| Fee-payer infra | Separate HTTP service required | Just a contract address |
| Chain | Tempo mainnet | Abstract (ZKsync-based) |
| Method name in MPP | "tempo" |
"abstract" |
| EIP-712 domain | "Tempo Stream Channel" | "Abstract Stream Channel" |
AbstractStreamChannel.sol is a direct port of TempoStreamChannel.sol with three changes:
ITIP20→IERC20(OpenZeppelin)TempoUtilities.isTIP20()→require(token != address(0))- EIP-712 domain name →
"Abstract Stream Channel"
All function signatures, events, errors, and business logic are identical.
This repository is archived and is not accepting new issues or pull requests.
If you need to contribute to the actively maintained packages or demo, start in Abstract-Foundation/abstract-packages.