ows-react-native
March 25, 2026 ยท View on GitHub
The first React Native library for the Open Wallet Standard (OWS) โ policy-gated autonomous payments on Solana using USDC, with Stripe MPP / x402 HTTP 402 support.
๐ฆ npmjs.com/package/ows-react-native
Why this exists
The Open Wallet Standard (OWS) defines how wallets and apps should communicate โ but there is no official React Native implementation. This library fills that gap.
It's designed specifically for AI agents that need to make autonomous micro-payments (e.g. paying for API access, model inference, data feeds) without human intervention, while keeping spending under strict policy controls.
Key problems this solves:
| Problem | This library's solution |
|---|---|
| RN apps can't safely hold private keys | Backend vault โ keys never leave the server |
| Agents need to pay HTTP 402 endpoints | parseMppChallenge() + payMppChallenge() |
| No spending guardrails for autonomous agents | OWS policy engine (per-tx limit, daily cap, allowlist) |
| No standard RN wallet interface | Hooks-first API matching the OWS spec |
| Large payments need human approval | Biometric gate via expo-local-authentication |
Features
OwsProviderโ React context with backend URL, active wallet, policies, and tx historyuseOwsWallet()โ Create wallets, load from vault, get addresses and balancesusePayWithOws()โ Parse + pay HTTP 402 / x402 / MPP challenges on SolanausePolicy()โ Set per-tx limits, daily caps, recipient allowlists, pause/resume<TransactionHistory />โ Drop-in component for payment historyparseMppChallenge()โ Parses x402 JSON, Stripe MPP headers, andX-Payment-Requiredheaders- Biometric approval โ Face ID / Touch ID gate for payments above your threshold
- Full TypeScript โ Complete type coverage, zero
anyin public API - New Architecture ready โ Compatible with React Native's Bridgeless mode
Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ React Native App (Oscar) โ
โ โ
โ useOwsWallet() usePayWithOws() โ
โ usePolicy() <TransactionHistory/> โ
โ โ
โ OwsProvider โโโโโโโโโโโโโโโโโโโโโโโบ โ
โโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ HTTP (localhost in dev, HTTPS in prod)
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ OWS Backend (Express) โ
โ โ
โ POST /create-wallet โ generates keypair
โ POST /pay-mpp โ enforces policy โ
โ POST /pay-solana-usdc โ
โ GET /get-balance/:id โ
โ POST /update-policy/:id โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ .ows/ vault (keys never leave) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ @solana/web3.js + @solana/spl-token
โผ
Solana devnet / mainnet
USDC SPL Token transfers
Security model: Private keys are generated and stored exclusively in the backend vault (.ows/). The React Native app only ever receives public addresses and transaction signatures. The backend re-validates all OWS policies before signing any transaction โ the client-side checks are a UX convenience only.
Installation
npm install ows-react-native
# If using Expo, also install native dependencies:
npx expo install expo-local-authentication expo-secure-store
Backend setup
The OWS backend is a standalone Express server that manages your wallet vault, enforces policies, and signs Solana transactions. Private keys never leave it.
# Clone the repo and start the backend
git clone https://github.com/rkmonarch/ows-react-native
cd ows-react-native/backend
npm install
npm run dev
# โ Listening on http://localhost:3001
Set
PORTandSOLANA_NETWORKenv vars to configure. Defaults to port3001ondevnet.
Quick Start
1. Wrap your app
import { OwsProvider } from 'ows-react-native';
export default function App() {
return (
// In production: use HTTPS and your deployed backend URL
<OwsProvider backendUrl="http://localhost:3001">
<YourApp />
</OwsProvider>
);
}
2. Create a wallet
import { useOwsWallet } from 'ows-react-native';
function SetupScreen() {
const { wallet, balance, createWallet } = useOwsWallet('solana');
return (
<>
<Button title="Create Wallet" onPress={() => createWallet('My Agent')} />
{wallet && <Text>Address: {wallet.address}</Text>}
{balance && <Text>USDC: ${balance.usdc.toFixed(2)}</Text>}
</>
);
}
3. Pay an x402 / MPP challenge (agent payment)
import { usePayWithOws, parseMppChallenge } from 'ows-react-native';
function AgentScreen() {
const { payMppChallenge, isLoading } = usePayWithOws();
const fetchProtectedResource = async () => {
const res = await fetch('https://api.example.com/research-results');
if (res.status === 402) {
// Parse the 402 challenge (x402 JSON or MPP header)
const body = await res.json();
const headers = Object.fromEntries(res.headers.entries());
const challenge = parseMppChallenge(body, headers);
// Pay it โ backend checks policy, signs, sends USDC on Solana
const result = await payMppChallenge(challenge);
console.log('Paid!', result.explorerUrl);
// Retry the request with payment proof
return fetch('https://api.example.com/research-results', {
headers: { 'X-Payment-Signature': result.signature },
});
}
return res;
};
}
4. Set spend policies
import { usePolicy } from 'ows-react-native';
function PolicyScreen() {
const { policy, setMaxPerTx, setDailyLimit, setAllowlist, pauseWallet } = usePolicy();
// Limit the agent to \$1.00 per payment, \$10.00 per day
await setMaxPerTx(1.00);
await setDailyLimit(10.00);
// Restrict to specific recipients (leave empty to allow all)
await setAllowlist(['RecipientAddress1...', 'RecipientAddress2...']);
// Emergency stop โ blocks all payments immediately
await pauseWallet();
}
5. Show transaction history
import { TransactionHistory } from 'ows-react-native';
function HistoryScreen() {
return (
<TransactionHistory
limit={20}
showExplorerLink
onTransactionPress={(tx) => console.log(tx)}
/>
);
}
x402 / MPP Challenge Format
This library parses all three common formats of HTTP 402 Payment Required challenges:
Format 1 โ x402 JSON body (recommended)
{
"x402Version": 1,
"accepts": [{
"scheme": "exact",
"network": "solana-devnet",
"maxAmountRequired": "0.10",
"payTo": "RecipientPubkey...",
"tokenMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"memo": "api-access-ref-123"
}]
}
Format 2 โ Stripe MPP JSON body
{
"scheme": "exact",
"network": "solana-devnet",
"amount": "0.10",
"recipient": "RecipientPubkey...",
"memo": "stripe-mpp-ref-456"
}
Format 3 โ WWW-Authenticate MPP header
WWW-Authenticate: MPP realm="API", amount="0.10", recipient="PubKey...", network="solana-devnet"
API Reference
OwsProvider
<OwsProvider
backendUrl="http://localhost:3001" // Required: backend URL
maxHistorySize={50} // Optional: tx history limit (default: 50)
>
useOwsWallet(chain)
const {
wallet, // OWSWallet | null โ currently active wallet
balance, // WalletBalance | null โ { sol, usdc, lamports, usdcRaw }
isLoading, // boolean
error, // string | null
createWallet, // (label?: string) => Promise<OWSWallet>
loadWallet, // (walletId: string) => Promise<OWSWallet>
listWallets, // () => Promise<OWSWallet[]>
getAddress, // () => string | null
getBalance, // () => Promise<WalletBalance>
refreshBalance, // () => Promise<void>
} = useOwsWallet('solana');
usePayWithOws()
const {
isLoading, // boolean
error, // string | null
lastPayment, // PaymentResult | null
payMppChallenge, // (challenge: MppChallenge) => Promise<PaymentResult>
payDirect, // (params: DirectPayParams) => Promise<PaymentResult>
} = usePayWithOws();
PaymentResult:
{
signature: string; // Solana transaction signature
explorerUrl: string; // Full Solana Explorer URL
amountUsdc: number; // Amount paid
recipient: string; // Recipient address
timestamp: string; // ISO-8601
memo?: string; // Memo attached to transaction
}
usePolicy()
const {
policy, // OWSPolicy | null
isLoading, // boolean
error, // string | null
setMaxPerTx, // (amount: number) => Promise<void>
setDailyLimit, // (amount: number) => Promise<void>
setAllowlist, // (addresses: string[]) => Promise<void>
pauseWallet, // () => Promise<void>
resumeWallet, // () => Promise<void>
refreshPolicy, // () => Promise<void>
} = usePolicy();
OWSPolicy:
{
maxPerTx: number; // Max USDC per transaction
dailyLimit: number; // Rolling 24h cap in USDC
allowlist: string[]; // Empty = allow all recipients
paused: boolean; // Kill-switch
merchantLimits?: Record<string, number>; // Per-address limits
}
parseMppChallenge(body, headers)
import { parseMppChallenge } from 'ows-react-native';
const challenge: MppChallenge = parseMppChallenge(
responseBody, // Record<string, unknown> | null
responseHeaders // Record<string, string>
);
// Throws if format is unrecognised
Backend API
The included Express backend exposes these endpoints:
| Method | Endpoint | Description |
|---|---|---|
GET | /health | Server status + network info |
POST | /create-wallet | Generate new Solana keypair, store in vault |
GET | /list-wallets | List all vault wallets (no secret keys) |
GET | /get-balance/:id | SOL + USDC balance on-chain |
GET | /get-policy/:id | Current OWS policy for wallet |
POST | /update-policy/:id | Update policy fields |
POST | /pay-solana-usdc | Direct USDC transfer (policy-gated) |
POST | /pay-mpp | Pay an x402/MPP challenge (policy-gated) |
POST | /mock-402 | Dev only โ returns a test 402 challenge |
Backend environment variables
# backend/.env
PORT=3001
SOLANA_NETWORK=devnet # or mainnet-beta
VAULT_DIR=./.ows # path to encrypted key vault
Solana Constants
| Network | USDC Mint Address |
|---|---|
| Devnet | 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU |
| Mainnet | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v |
Funding Devnet Wallets (Testing)
- SOL (for transaction fees): faucet.solana.com
- USDC (devnet): spl-token-faucet.com
- Verify: explorer.solana.com
End-to-End Test (curl)
# 1. Start the backend
cd backend && npm run dev
# 2. Create a wallet
curl -X POST http://localhost:3001/create-wallet \
-H "Content-Type: application/json" \
-d '{"label":"test-agent"}'
# โ { "wallet": { "id": "...", "address": "...", "chain": "solana" } }
# 3. Set policies
curl -X POST http://localhost:3001/update-policy/<WALLET_ID> \
-H "Content-Type: application/json" \
-d '{"maxPerTx": 0.50, "dailyLimit": 5.00}'
# 4. Get a mock 402 challenge
curl -X POST http://localhost:3001/mock-402 \
-H "Content-Type: application/json" \
-d '{"amount":"0.10"}'
# โ HTTP 402 with x402 JSON body
# 5. Pay the challenge
curl -X POST http://localhost:3001/pay-mpp \
-H "Content-Type: application/json" \
-d '{
"walletId": "<WALLET_ID>",
"challenge": { "recipient": "<ADDRESS>", "amountFloat": 0.10 }
}'
# โ { "result": { "signature": "...", "explorerUrl": "..." } }
# 6. View on Explorer
open "https://explorer.solana.com/tx/<SIGNATURE>?cluster=devnet"
Policy enforcement tests (red-team)
# Exceed maxPerTx โ should return 403
curl -X POST http://localhost:3001/pay-solana-usdc \
-d '{"walletId":"<ID>","recipient":"<ADDR>","amountUsdc":100}'
# โ 403 { "message": "Amount \$100.00 exceeds maxPerTx limit of \$0.50" }
# Blocked recipient (allowlist active)
curl -X POST http://localhost:3001/update-policy/<ID> \
-d '{"allowlist":["<ALLOWED_ADDR>"]}'
curl -X POST http://localhost:3001/pay-solana-usdc \
-d '{"walletId":"<ID>","recipient":"<OTHER_ADDR>","amountUsdc":0.01}'
# โ 403 { "message": "Recipient ... is not on the allowlist" }
# Paused wallet
curl -X POST http://localhost:3001/update-policy/<ID> -d '{"paused":true}'
curl -X POST http://localhost:3001/pay-solana-usdc \
-d '{"walletId":"<ID>","recipient":"<ADDR>","amountUsdc":0.01}'
# โ 403 { "message": "Wallet is paused โ no payments allowed" }
Security
โ ๏ธ Read before deploying to production
What is safe
- โ Public addresses are safe to share (they're on the blockchain)
- โ Transaction signatures are safe to expose
- โ Policy configs are safe to send over HTTP
What must be protected
- ๐
.ows/directory โ contains private keys. Never commit it. Add to.gitignore. - ๐ Backend API โ in production, add
Authorization: Bearer <token>header authentication - ๐ HTTPS โ run the backend behind TLS in production (NGINX, Caddy, etc.)
- ๐ Vault encryption โ for production, replace plain JSON files with AWS KMS, HashiCorp Vault, or a TEE/HSM
Policy enforcement
All policies are enforced server-side before any signing. The client-side pre-checks in hooks are UX convenience only โ a malicious client cannot bypass them by directly calling the backend without a valid walletId and passing the same policy checks.
Private key isolation
The /create-wallet endpoint returns only { id, address, chain, label, createdAt }. The secretKey field is stripped before the response is sent. No endpoint ever returns a private key.
Roadmap
- Native module path โ TurboModules + iOS Secure Enclave for on-device signing
- Streaming payments โ x402 streaming scheme support
- Multi-sig policy approval for spends above threshold
- Persistent transaction history via
expo-sqlite - Push notifications on payment confirmation
- EVM support (
chain: 'base'|'ethereum') - Mainnet launch checklist and audit
Project Structure
ows-react-native/
โโโ src/ # Library source (TypeScript)
โ โโโ index.ts # Public API exports
โ โโโ types.ts # All TypeScript types
โ โโโ components/
โ โ โโโ OwsProvider.tsx # React context + provider
โ โ โโโ TransactionHistory.tsx
โ โโโ hooks/
โ โ โโโ useOwsWallet.ts # Wallet lifecycle
โ โ โโโ usePayWithOws.ts # Payment hook
โ โ โโโ usePolicy.ts # Policy management
โ โโโ utils/
โ โโโ mppParser.ts # x402 / MPP challenge parser
โ โโโ apiClient.ts # Typed fetch wrapper
โโโ backend/
โ โโโ server.ts # Express server (wallet vault + Solana)
โ โโโ package.json
โโโ example/ # Oscar โ full Expo demo app
โ โโโ App.tsx
โ โโโ theme.ts # Design tokens
โ โโโ screens/
โ โโโ OnboardingScreen.tsx
โ โโโ DashboardScreen.tsx
โ โโโ PolicySetupScreen.tsx
โ โโโ AgentDemoScreen.tsx
โ โโโ HistoryScreen.tsx
โโโ lib/ # Built output (commonjs + module + types)
Contributing
Pull requests welcome. For major changes, open an issue first.
git clone https://github.com/rkmonarch/ows-react-native
cd ows-react-native
npm install
cd backend && npm install
npm run typecheck # type-check library
Related
- Wallet Standard โ the specification this implements
- Stripe MPP โ Machine-to-Machine Payment Protocol
- @solana/spl-token โ USDC transfers
- Solana devnet faucet
License
MIT ยฉ rkmonarch