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
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@ npm install @suimpp/mpp mppx
```

```typescript
import { sui } from '@suimpp/mpp/server';
import { InMemoryDigestStore, USDC, sui } from '@suimpp/mpp/server';
import { Mppx } from 'mppx';

const mppx = Mppx.create({
methods: [sui({
currency: '0xdba...::usdc::USDC',
currency: USDC,
recipient: '0xYOUR_ADDRESS',
store: new InMemoryDigestStore(), // Use Redis/DB in production.
})],
});

Expand All @@ -44,16 +45,19 @@ export const GET = mppx.charge({ amount: '0.01' })(
### Make Payments (Client)

```typescript
import { sui } from '@suimpp/mpp/client';
import { USDC, sui } from '@suimpp/mpp/client';
import { Mppx } from 'mppx/client';

const mppx = Mppx.create({
methods: [sui({ client, signer })],
methods: [sui({ client, signer, currency: USDC })],
});

const response = await mppx.fetch('https://api.example.com/resource');
```

`@suimpp/mpp` exports `USDC`, `USDC_TESTNET`, and `SUI_DOLLAR`
currency presets. The SDK handles gasless tier behavior automatically.

### Validate a Server

```bash
Expand Down Expand Up @@ -87,7 +91,8 @@ Agent Server Sui
│ └─ TX confirmed ←──────────────────────────────────────────────│
│ digest: "Hp4oHHs..." │ │
│ │ │
│── Retry + credential {digest} ──>│ │
│── Retry + credential ───────────>│ │
│ {digest, signature} │ │
│ │── getTransaction(digest) ──>│
│ │ verify: success, amount, │
│ │ recipient matches │
Expand Down Expand Up @@ -171,21 +176,22 @@ Payments are reported by the gateway (application layer), not by the library dir
└─────────────────────┘
```

**Why this pattern?** The `verify()` callback has on-chain data (sender from balance changes) but no HTTP context (which endpoint was called). The gateway's charge wrapper has HTTP context but no on-chain data. The `onPayment` callback bridges the two layers using the digest as the join key.
**Why this pattern?** The `verify()` callback has on-chain data (transaction sender, digest, amount, recipient) but no HTTP context (which endpoint was called). The gateway's charge wrapper has HTTP context but no on-chain data. The `onPayment` callback bridges the two layers using the digest as the join key.

**Implementation:**

```typescript
import { sui } from '@suimpp/mpp/server';
import { InMemoryDigestStore, USDC, sui } from '@suimpp/mpp/server';
import type { PaymentReport } from '@suimpp/mpp/server';

// 1. Library emits on-chain data via callback
const pendingReports = new Map<string, PaymentReport>();

const mppx = Mppx.create({
methods: [sui({
currency: SUI_USDC_TYPE,
currency: USDC,
recipient: TREASURY_ADDRESS,
store: new InMemoryDigestStore(), // Use Redis/DB in production.
network: 'mainnet',
onPayment: (report) => {
pendingReports.set(report.digest, report);
Expand Down
9 changes: 5 additions & 4 deletions apps/suimpp/app/components/CodeBlocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ function CodeBlock({
}

const CLIENT_CODE = `import { Mppx } from 'mppx/client';
import { sui } from '@suimpp/mpp/client';
import { USDC, sui } from '@suimpp/mpp/client';
import { SuiGrpcClient } from '@mysten/sui/grpc';

const mpp = Mppx.create({
methods: [sui({ client, signer })],
methods: [sui({ client, signer, currency: USDC })],
});

const res = await mpp.fetch(
Expand All @@ -66,13 +66,14 @@ const res = await mpp.fetch(
);`;

const SERVER_CODE = `import { Mppx } from 'mppx/nextjs';
import { sui } from '@suimpp/mpp/server';
import { InMemoryDigestStore, USDC, sui } from '@suimpp/mpp/server';

const mpp = Mppx.create({
realm: 'api.example.com',
methods: [sui({
currency: SUI_USDC_TYPE,
currency: USDC,
recipient: '0x...',
store: new InMemoryDigestStore(), // Use Redis/DB in production.
})],
});

Expand Down
31 changes: 21 additions & 10 deletions apps/suimpp/app/docs/DocsContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ export function DocsContent() {
</span>
<CopyBlock
code={`import { Mppx } from 'mppx/client';
import { sui } from '@suimpp/mpp/client';
import { USDC, sui } from '@suimpp/mpp/client';

const mpp = Mppx.create({
methods: [sui({ client, signer })],
methods: [sui({ client, signer, currency: USDC })],
});

const res = await mpp.fetch('https://api.example.com/v1/generate', {
Expand All @@ -47,12 +47,16 @@ const res = await mpp.fetch('https://api.example.com/v1/generate', {
</span>
<CopyBlock
code={`import { Mppx } from 'mppx/nextjs';
import { sui } from '@suimpp/mpp/server';
import { InMemoryDigestStore, sui } from '@suimpp/mpp/server';

const mpp = Mppx.create({
methods: [sui({
currency: SUI_USDC,
currency: {
type: SUI_USDC,
decimals: 6,
},
recipient: '0xYOUR_ADDRESS',
store: new InMemoryDigestStore(), // Use Redis/DB in production.
})],
});

Expand Down Expand Up @@ -113,15 +117,15 @@ export const POST = mpp.charge({ amount: '0.01' })(
</p>
<CopyBlock
code={`import { Mppx } from 'mppx/client';
import { sui } from '@suimpp/mpp/client';
import { USDC, sui } from '@suimpp/mpp/client';
import { getFullnodeUrl, SuiClient } from '@mysten/sui/client';
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';

const client = new SuiClient({ url: getFullnodeUrl('mainnet') });
const signer = Ed25519Keypair.deriveKeypair('your mnemonic');

const mpp = Mppx.create({
methods: [sui({ client, signer })],
methods: [sui({ client, signer, currency: USDC })],
});`}
/>
</Step>
Expand Down Expand Up @@ -226,13 +230,14 @@ const data = await response.json();`}
</p>
<CopyBlock
code={`import { Mppx } from 'mppx/nextjs'; // or 'mppx/server'
import { sui, SUI_USDC_TYPE } from '@suimpp/mpp/server';
import { InMemoryDigestStore, USDC, sui } from '@suimpp/mpp/server';

const mpp = Mppx.create({
methods: [
sui({
currency: SUI_USDC_TYPE,
currency: USDC,
recipient: '0xYOUR_SUI_ADDRESS',
store: new InMemoryDigestStore(), // Use Redis/DB in production.
registryUrl: 'https://suimpp.dev/api/report',
serverUrl: 'https://your-server.com',
}),
Expand Down Expand Up @@ -292,11 +297,17 @@ const mpp = Mppx.create({
<CopyBlock
title="Express example"
code={`import { Mppx } from 'mppx/server';
import { sui, SUI_USDC_TYPE } from '@suimpp/mpp/server';
import { InMemoryDigestStore, USDC, sui } from '@suimpp/mpp/server';
import express from 'express';

const mpp = Mppx.create({
methods: [sui({ currency: SUI_USDC_TYPE, recipient: '0x...' })],
methods: [
sui({
currency: USDC,
recipient: '0x...',
store: new InMemoryDigestStore(), // Use Redis/DB in production.
}),
],
});

const app = express();
Expand Down
47 changes: 28 additions & 19 deletions packages/mpp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,17 @@ npm install @suimpp/mpp mppx
Add payments to any API in 5 lines:

```typescript
import { sui } from '@suimpp/mpp/server';
import { InMemoryDigestStore, USDC, sui } from '@suimpp/mpp/server';
import { Mppx } from 'mppx';

const SUI_USDC = '0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC';

const mppx = Mppx.create({
methods: [sui({ currency: SUI_USDC, recipient: '0xYOUR_ADDRESS' })],
methods: [
sui({
currency: USDC,
recipient: '0xYOUR_ADDRESS',
store: new InMemoryDigestStore(), // Use Redis/DB in production.
}),
],
});

export const GET = mppx.charge({ amount: '0.01' })(
Expand All @@ -45,7 +49,7 @@ No webhooks. No Stripe dashboard. No KYC. USDC arrives directly in your wallet.
## Make Payments (Client)

```typescript
import { sui } from '@suimpp/mpp/client';
import { USDC, sui } from '@suimpp/mpp/client';
import { Mppx } from 'mppx/client';
import { SuiGrpcClient } from '@mysten/sui/grpc';
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
Expand All @@ -57,7 +61,7 @@ const client = new SuiGrpcClient({
const signer = Ed25519Keypair.deriveKeypair('your mnemonic');

const mppx = Mppx.create({
methods: [sui({ client, signer })],
methods: [sui({ client, signer, currency: USDC })],
});

const response = await mppx.fetch('https://api.example.com/resource');
Expand Down Expand Up @@ -107,7 +111,7 @@ Agent API Server
│ │
│── GET /resource ────────>│
│ + payment credential │── verify TX on-chain via gRPC
│ (Sui tx digest)
│ (digest + signature)
│<── 200 OK + data ────────│
```

Expand All @@ -120,12 +124,12 @@ No facilitator. No intermediary. The server verifies the Sui transaction directl
Creates a Sui payment method for the server.

```typescript
import { sui } from '@suimpp/mpp/server';
import { InMemoryDigestStore, USDC, sui } from '@suimpp/mpp/server';

const method = sui({
currency: SUI_USDC, // Sui coin type (e.g. USDC)
currency: USDC, // Sui coin type + decimals
recipient: '0xYOUR_ADDR', // Where payments are sent
decimals: 6, // Optional: currency decimals (default: 6)
store: new InMemoryDigestStore(), // Required. Use Redis/DB in production.
rpcUrl: '...', // Optional: custom gRPC endpoint
network: 'mainnet', // Optional: 'mainnet' | 'testnet' | 'devnet'
registryUrl: 'https://suimpp.dev/api/report', // Optional: report payments to suimpp.dev
Expand All @@ -136,6 +140,8 @@ Verification checks:
- Transaction succeeded on-chain
- Payment sent to correct recipient (address-normalized comparison)
- Amount >= requested (BigInt precision, no floating-point)
- Payment proof signature matches the transaction sender
- Digest has not been used before according to the required `store`

## Client API

Expand All @@ -144,12 +150,12 @@ Verification checks:
Creates a Sui payment method for the client.

```typescript
import { sui } from '@suimpp/mpp/client';
import { USDC, sui } from '@suimpp/mpp/client';

const method = sui({
client: grpcClient, // Any Sui client (SuiGrpcClient, etc.)
signer: ed25519Keypair, // Signer from @mysten/sui/cryptography
decimals: 6, // Optional: currency decimals (default: 6)
currency: USDC, // Coin type + decimals
execute: async (tx) => { // Optional: custom execution (gas sponsor, etc.)
return myGasManager.execute(tx);
},
Expand All @@ -160,20 +166,23 @@ const method = sui({
|--------|------|----------|-------------|
| `client` | `ClientWithCoreApi` | Yes | Any Sui client implementing the core API |
| `signer` | `Signer` | Yes | Any `Signer` from `@mysten/sui/cryptography` — `Ed25519Keypair` works |
| `decimals` | `number` | No | Decimal places for the currency (default: 6) |
| `currency` | `Currency` | Yes | Single-currency metadata including coin type and decimals |
| `execute` | `(tx: Transaction) => Promise<{ digest: string }>` | No | Override transaction execution (e.g. gas sponsor/manager) |

The client uses the [`coinWithBalance`](https://sdk.mystenlabs.com/sui/transaction-building/intents) intent to automatically resolve, merge, and split coins for the exact payment amount, then signs and broadcasts the transaction (or delegates to `execute` if provided).
The client builds a `0x2::coin::send_funds` transaction for the exact payment amount, then signs and broadcasts it (or delegates to `execute` if provided).

## Constants

### `SUI_USDC_TYPE`
### Known currencies

The Sui coin type for Circle-issued USDC on mainnet.
The package exports common `Currency` presets and their raw coin type strings.

```typescript
import { SUI_USDC_TYPE } from '@suimpp/mpp';
// '0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC'
import { SUI_DOLLAR, USDC, USDC_TESTNET } from '@suimpp/mpp';

USDC; // { type: SUI_USDC_TYPE, decimals: 6 }
USDC_TESTNET; // { type: SUI_USDC_TESTNET_TYPE, decimals: 6 }
SUI_DOLLAR; // { type: SUI_DOLLAR_TYPE, decimals: 6 }
```

## Utilities
Expand Down Expand Up @@ -201,7 +210,7 @@ MPP is chain-agnostic. We chose Sui because agent payments need:
## Testing

```bash
pnpm --filter @suimpp/mpp test # 13 tests
pnpm --filter @suimpp/mpp test # 29 tests
pnpm --filter @suimpp/mpp typecheck
```

Expand Down
32 changes: 32 additions & 0 deletions packages/mpp/biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
"files": {
"ignore": [
"node_modules",
"dist",
".pnpm-store",
"*.lock",
"*.toml",
"**/move/**/build"
]
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"formatter": {
"enabled": true,
"indentStyle": "space"
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"semicolons": "always"
}
}
}
16 changes: 9 additions & 7 deletions packages/mpp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@
"require": "./dist/server.cjs"
}
},
"files": [
"dist",
"README.md"
],
"files": ["dist", "README.md"],
"keywords": [
"mpp",
"sui",
Expand All @@ -49,19 +46,24 @@
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest run",
"test:e2e": "vitest run --config vitest.e2e.config.ts",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"lint": "eslint src/",
"check": "biome check .",
"check:fix": "biome check --write .",
"lint": "biome check .",
"lint:fix": "biome check --write .",
"clean": "rm -rf dist"
},
"dependencies": {
"@mysten/sui": "^2",
"@mysten/sui": "^2.17.0",
"mppx": "^0.4.9",
"zod": "^4.3.6"
},
"devDependencies": {
"@biomejs/biome": "^1.9.0",
"@types/node": "^20",
"eslint": "^9",
"testcontainers": "^12.0.0",
"tsup": "^8",
"typescript": "^5",
"vitest": "^3"
Expand Down
Loading