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
25 changes: 25 additions & 0 deletions docs/product-hub/content/ap2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Agent Payments Protocol (AP2)

The **Agent Payments Protocol (AP2)** is the modern standard for autonomous commerce within the Open Commerce Protocol (OCP). It defines a cryptographically secure framework for delegated authority, enabling AI agents to act as fiduciary representatives of their users.

## Mandates: The Chain of Evidence

At the heart of AP2 are **Mandates**—stateless, portable digital contracts (signed JWS) that authorize specific commerce intents.

### Intent Mandates
An **Intent Mandate** is issued by a user to an agent, defining the scope of its authority.
- `max_budget`: The absolute spending limit for the mandate.
- `expiration`: When the agent's authority expires.
- `allowed_merchants`: A whitelist of DIDs authorized for interaction.
- `purpose_code`: The specific intent (e.g., `PROCUREMENT`, `SUBSCRIPTION`).

### Cart Mandates
A **Cart Mandate** is generated by the agent during a specific transaction. It links back to an Intent Mandate and includes a cryptographic hash of the current cart contents, preventing price-switching or tampering after commitment.

## Verifiable Credentials (VCs)
OCP agents use **Verifiable Credentials** to prove their authority without exposing the user's PII.
- **DIDs**: Leveraging `did:key` for agent portability and `did:web` for protocol services.
- **Zero-Knowledge Proofs**: (Coming Soon) Prove budget availability without revealing the exact balance.

## Zero Trust Verification
The OCP Secure Enclave (Vault) enforces Mandates at the point of signing. Even if the application layer is compromised, the enclave will refuse to sign any transaction that violates the budget or policy constraints of the provided Mandate.
34 changes: 34 additions & 0 deletions docs/product-hub/content/mpp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Machine Payment Protocol (MPP)

The **Machine Payment Protocol (MPP)** is the native OCP standard for autonomous tool-call and data-packet payments. It enables machines to handle **Payment Required (HTTP 402)** flows autonomously and without manual intervention.

## 402 Flow: Autonomous Retries

The MPP middleware enables OCP agents to detect when a request requires a payment:
1. **Initial Request**: The agent makes an API call or requests data.
2. **402 Required**: The service responds with a `402 Payment Required` status, including headers for `X-MPP-Amount` and `X-MPP-Merchant-DID`.
3. **Mandate Validation**: The agent's middleware checks its available Intent Mandates for budget and authorized merchants.
4. **Autonomous Cart Mandate**: The agent generates and signs a new Cart Mandate for the exact amount.
5. **Retry**: The agent retries the request with the `X-OCP-Cart-Mandate` header.

## x402 Extension: Stablecoin Settlements

MPP supports the **x402 Extension** for 24/7, low-latency settlements using stablecoins like **USDC** and **PYUSD**.
- **Instant Finality**: Machines can settle micro-payments and tool-calls in real-time.
- **Programmable Settlement**: Rules-based settlement logic within the Web3Service.
- **Micro-transaction Optimization**: Zero-fee internal ledger transfers before settlement.

## Getting Started with MPP

Implement the MPP handler in your OCP project to enable autonomous 402 responses:

```javascript
const { MPP402Handler } = require('@open-commerce-protocol/core');

const mpp = new MPP402Handler(agentService, mandateService);

// Execute an autonomous request that handles 402 retries
const result = await mpp.executeAutonomousRequest(agent, async (config) => {
return await axios.post('https://api.agent-services.com/data', { data: '...' }, config);
}, intentMandate);
```
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
"url": "https://github.com/dcplatforms/Open-Commerce-Protocol/issues"
},
"homepage": "https://github.com/dcplatforms/Open-Commerce-Protocol#readme",
"bin": {
"ocp": "scripts/ocp-cli.js"
},
"dependencies": {
"axios": "^1.6.0",
"bcryptjs": "^2.4.3",
Expand All @@ -52,6 +55,7 @@
"express-rate-limit": "^7.1.0",
"graphql": "^15.8.0",
"helmet": "^7.1.0",
"commander": "^11.1.0",
"joi": "^17.13.3",
"jsonwebtoken": "^9.0.3",
"mongoose": "^8.0.0",
Expand All @@ -61,6 +65,7 @@
},
"devDependencies": {
"@types/node": "^20.10.0",
"commander": "^11.1.0",
"eslint": "^8.54.0",
"jest": "^29.7.0",
"mongodb-memory-server": "^9.0.0",
Expand Down
131 changes: 131 additions & 0 deletions scripts/ocp-cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/usr/bin/env node

/**
* OCP CLI - Open Commerce Protocol Command Line Interface
*
* Scaffolds OCP projects, manages agent identities, issues mandates, and checks balances.
*/

const { Command } = require('commander');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const MandateService = require('../src/services/mandate');

const program = new Command();

program
.name('ocp')
.description('CLI for Open Commerce Protocol (OCP) SDK')
.version('1.0.0');

// ocp init
program.command('init')
.description('Scaffolds a new OCP project with local vault simulation')
.action(() => {
console.log('Scaffolding new OCP project...');
const projectStructure = [
'src',
'src/agents',
'src/mandates',
'config'
];

projectStructure.forEach(dir => {
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
});

const envContent = `
TOKENIZATION_API_KEY=test-key
TOKENIZATION_BASE_URL=http://localhost:8080
MANDATE_SIGNING_KEY=${crypto.randomBytes(32).toString('hex')}
STRICT_MANDATE_MODE=true
`.trim();

fs.writeFileSync('.env', envContent);
console.log('Project initialized successfully. Local vault simulation configured in .env');
});

// ocp agent:create
program.command('agent:create')
.description('Generates an agent identity (did:key) and linked wallet')
.argument('<name>', 'Name of the agent')
.action((name) => {
const agentId = `agent_${crypto.randomBytes(4).toString('hex')}`;
const agentDid = `did:key:${crypto.randomBytes(16).toString('hex')}`;

const agentData = {
id: agentId,
name: name,
did: agentDid,
wallet_address: `0x${crypto.randomBytes(20).toString('hex')}`,
created_at: new Date().toISOString()
};

if (!fs.existsSync('src/agents')) fs.mkdirSync('src/agents', { recursive: true });
fs.writeFileSync(`src/agents/${agentId}.json`, JSON.stringify(agentData, null, 2));

console.log(`Agent created: ${name}`);
console.log(`ID: ${agentId}`);
console.log(`DID: ${agentDid}`);
});

// ocp mandate:issue
program.command('mandate:issue')
.description('Interactively creates a signed Intent Mandate for an agent')
.option('--agent <id>', 'Agent ID')
.option('--budget <amount>', 'Maximum budget', '100')
.option('--currency <code >', 'Currency', 'USD')
.action(async (options) => {
if (!options.agent) {
console.error('Error: Agent ID required. Use --agent <id>');
return;
}

const signingKey = process.env.MANDATE_SIGNING_KEY || 'default-secret-key';
const mandateService = new MandateService({ signingKey });

const mandateToken = await mandateService.issueIntentMandate({
userDid: 'did:key:user-local',
agentDid: `did:key:${options.agent}`,
maxBudget: parseFloat(options.budget),
currency: options.currency,
purposeCode: 'CLI_ISSUED'
});

const decoded = await mandateService.verifyMandate(mandateToken);
const mandateId = decoded.mandate_id;

if (!fs.existsSync('src/mandates')) fs.mkdirSync('src/mandates', { recursive: true });
fs.writeFileSync(`src/mandates/${mandateId}.jwt`, mandateToken);

console.log(`Intent Mandate issued for agent ${options.agent}`);
console.log(`Mandate ID: ${mandateId}`);
console.log(`Budget: ${options.budget} ${options.currency}`);
console.log(`Saved to: src/mandates/${mandateId}.jwt`);
});

// ocp wallet:balance
program.command('wallet:balance')
.description('Checks real-time balances across ledger and Web3 rails')
.argument('<address>', 'Wallet address or Agent ID')
.action((address) => {
console.log(`Checking balances for ${address}...`);
// Mock balance retrieval
const balances = {
ledger: '500.00 USD',
web3: {
eth: '1.25 ETH',
usdc: '250.00 USDC',
pyusd: '100.00 PYUSD'
}
};

console.log(`Ledger Balance: ${balances.ledger}`);
console.log(`Web3 Balances:`);
console.log(` - ETH: ${balances.web3.eth}`);
console.log(` - USDC: ${balances.web3.usdc}`);
console.log(` - PYUSD: ${balances.web3.pyusd}`);
});

program.parse();
84 changes: 84 additions & 0 deletions src/middleware/mpp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* MPP (Machine Payment Protocol) 402 Handler Middleware
*
* Enables agents to autonomously handle 'Payment Required' (HTTP 402) flows.
* If an agent hits a 402, it checks for a valid mandate, generates a cart mandate,
* and retries the request with the payment header.
*/

class MPP402Handler {
constructor(agentService, mandateService) {
this.agentService = agentService;
this.mandateService = mandateService;
}

/**
* Handle an autonomous request that might result in a 402
* @param {Object} agent - The acting agent
* @param {Function} requestFn - The function that executes the request
* @param {string} intentMandateToken - The agent's pre-authorized intent mandate
*/
async executeAutonomousRequest(agent, requestFn, intentMandateToken) {
try {
let response = await requestFn();

if (response.status === 402) {
return await this._handle402Response(agent, response, intentMandateToken, requestFn);
}

return response;
} catch (error) {
if (error.response?.status === 402) {
return await this._handle402Response(agent, error.response, intentMandateToken, requestFn);
}
throw error;
}
}

/**
* Handle 402 response and retry
* @private
*/
async _handle402Response(agent, response, intentMandateToken, requestFn) {
console.log(`MPP: Handling 402 Payment Required for agent ${agent.name}`);

// 1. Extract payment requirement details from headers or body
// MPP standard uses headers like 'X-MPP-Amount' and 'X-MPP-Merchant-DID'
const amount = parseFloat(response.headers?.['x-mpp-amount'] || response.data?.amount);
const currency = response.headers?.['x-mpp-currency'] || response.data?.currency || 'USD';
const merchantDid = response.headers?.['x-mpp-merchant-did'] || response.data?.merchant_did;
const cartItems = response.data?.cart_items || [{ item: 'API_CALL', quantity: 1 }];

if (!amount || !merchantDid) {
throw new Error('Incomplete payment requirements in 402 response');
}

// 2. Validate Intent Mandate
const decodedIntent = await this.mandateService.verifyMandate(intentMandateToken);

// Check if the 402 request is within the intent's budget
if (amount > decodedIntent.max_budget.value) {
throw new Error(`MPP: Payment amount ${amount} exceeds intent mandate budget of ${decodedIntent.max_budget.value}`);
}

// 3. Generate a Cart Mandate for the specific 402 request
console.log(`MPP: Issuing autonomous cart mandate for amount ${amount}`);
const cartMandateToken = await this.mandateService.issueCartMandate({
intentMandate: intentMandateToken,
cartItems,
totalPrice: amount,
merchantDid
});

// 4. Retry the request with the Payment Mandate header
console.log(`MPP: Retrying request with Cart Mandate...`);
return await requestFn({
headers: {
'X-OCP-Cart-Mandate': cartMandateToken,
'Authorization': `Bearer ${agent.id}`
}
});
}
}

module.exports = MPP402Handler;
43 changes: 43 additions & 0 deletions src/services/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

const crypto = require('crypto');
const MandateService = require('./mandate');

class AgentService {
constructor(database, config = {}) {
Expand All @@ -14,6 +15,7 @@ class AgentService {
defaultSpendingLimit: config.defaultSpendingLimit || 1000,
defaultAuthorizedCounterparties: config.defaultAuthorizedCounterparties || []
};
this.mandateService = new MandateService(config.mandateConfig);
}

/**
Expand Down Expand Up @@ -100,6 +102,47 @@ class AgentService {
}
}

/**
* Issue an Intent Mandate for an agent
* @param {Object} params - Intent parameters
*/
async issueIntentMandate({ userDid, agentId, maxBudget, currency, expiration, purposeCode, allowedMerchants }) {
try {
const agent = await this.getAgent(agentId);
const agentDid = agent.metadata?.get('did') || `did:key:${agentId}`;

return await this.mandateService.issueIntentMandate({
userDid,
agentDid,
maxBudget,
currency,
expiration,
purposeCode,
allowedMerchants
});
} catch (error) {
throw this._handleError('issueIntentMandate', error);
}
}

/**
* Issue a Verifiable Credential for an agent
*/
async issueAgentVC({ userDid, agentId, capabilities }) {
try {
const agent = await this.getAgent(agentId);
const agentDid = agent.metadata?.get('did') || `did:key:${agentId}`;

return await this.mandateService.issueAgentVC({
userDid,
agentDid,
capabilities
});
} catch (error) {
throw this._handleError('issueAgentVC', error);
}
}

/**
* Perform an Agent-to-Agent (A2A) transfer (conceptual)
* @param {Object} params - Transfer parameters
Expand Down
Loading
Loading