@mixrpay/agent-sdk 0.8.9 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +49 -113
- package/dist/agent-wallet-PG5YUSUR.js +2 -0
- package/dist/chunk-IBXQMJ7G.js +1 -0
- package/dist/chunk-IUGFHZWI.js +32 -0
- package/dist/chunk-SWK4Q2A3.js +2 -0
- package/dist/cli.js +147 -0
- package/dist/credentials-QUSJGKLZ.js +2 -0
- package/dist/credentials-XJV2ESBQ.js +2 -0
- package/dist/credentials-Z4HQDXFU.js +1 -0
- package/dist/index.cjs +28 -4642
- package/dist/index.d.cts +1175 -308
- package/dist/index.d.ts +1175 -308
- package/dist/index.js +28 -4604
- package/dist/mcp-server.js +35 -0
- package/package.json +24 -5
package/dist/index.cjs
CHANGED
|
@@ -1,4645 +1,31 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
for (
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
1
|
+
'use strict';var w=require('fs'),$=require('path'),j=require('os'),url=require('url'),accounts=require('viem/accounts'),viem=require('viem'),chains=require('viem/chains');var _documentCurrentScript=typeof document!=='undefined'?document.currentScript:null;function _interopNamespace(e){if(e&&e.__esModule)return e;var n=Object.create(null);if(e){Object.keys(e).forEach(function(k){if(k!=='default'){var d=Object.getOwnPropertyDescriptor(e,k);Object.defineProperty(n,k,d.get?d:{enumerable:true,get:function(){return e[k]}});}})}n.default=e;return Object.freeze(n)}var w__namespace=/*#__PURE__*/_interopNamespace(w);var $__namespace=/*#__PURE__*/_interopNamespace($);var j__namespace=/*#__PURE__*/_interopNamespace(j);var be=Object.defineProperty;var xe=(l,e)=>()=>(l&&(e=l(l=0)),e);var _e=(l,e)=>{for(var t in e)be(l,t,{get:e[t],enumerable:true});};var W={};_e(W,{deleteCredentials:()=>ge,getCredentialsFilePath:()=>me,hasCredentials:()=>pe,loadCredentials:()=>H,saveCredentials:()=>ue});function ce(){if(j__namespace.platform()==="win32"){let t=process.env.APPDATA||$__namespace.join(j__namespace.homedir(),"AppData","Roaming");return $__namespace.join(t,"mixrpay")}let e=process.env.XDG_CONFIG_HOME||$__namespace.join(j__namespace.homedir(),".config");return $__namespace.join(e,"mixrpay")}function F(){return $__namespace.join(ce(),"credentials.json")}function Le(){let l=ce();w__namespace.existsSync(l)||w__namespace.mkdirSync(l,{recursive:true,mode:448});}function de(l){try{w__namespace.chmodSync(l,384);}catch{}}function ue(l){try{Le();let e=F(),t={};if(w__namespace.existsSync(e))try{let r=w__namespace.readFileSync(e,"utf-8");t=JSON.parse(r);}catch{t={};}let s={...t,...l,updatedAt:new Date().toISOString()},n=Object.fromEntries(Object.entries(s).filter(([r,a])=>a!==void 0));return w__namespace.writeFileSync(e,JSON.stringify(n,null,2),{mode:384}),de(e),!0}catch(e){return console.error("[MixrPay] Failed to save credentials:",e),false}}function H(){try{let l=F();if(!w__namespace.existsSync(l))return {success:!0,credentials:{}};let e=w__namespace.readFileSync(l,"utf-8");return {success:!0,credentials:JSON.parse(e)}}catch(l){return {success:false,error:l instanceof Error?l.message:"Failed to load credentials"}}}function ge(l){try{let e=F();if(!w__namespace.existsSync(e))return !0;if(!l)return w__namespace.unlinkSync(e),!0;let t=H();if(!t.success)return !1;let s={...t.credentials};for(let n of l)delete s[n];return Object.keys(s).length===0?w__namespace.unlinkSync(e):(w__namespace.writeFileSync(e,JSON.stringify(s,null,2),{mode:384}),de(e)),!0}catch(e){return console.error("[MixrPay] Failed to delete credentials:",e),false}}function pe(){let l=H();if(!l.success)return false;let{credentials:e}=l;return !!(e.sessionKey||e.apiToken||e.masterKey)}function me(){return F()}var M=xe(()=>{});var c=class extends Error{code;retryAfterMs;constructor(e,t="MIXRPAY_ERROR",s){super(e),this.name="MixrPayError",this.code=t,this.retryAfterMs=s,Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor);}isRetryable(){return false}},v=class extends c{required;available;topUpUrl;constructor(e,t){let s=e-t;super(`Insufficient balance: need $${e.toFixed(2)}, have $${t.toFixed(2)} (short $${s.toFixed(2)}). Top up your wallet to continue.`,"INSUFFICIENT_BALANCE"),this.name="InsufficientBalanceError",this.required=e,this.available=t,this.topUpUrl="https://mixrpay.com/manage/wallet";}},D=class extends c{expiredAt;constructor(e){super(`Session key expired at ${e}. Request a new session key from the wallet owner at https://mixrpay.com/manage/invites`,"SESSION_KEY_EXPIRED"),this.name="SessionKeyExpiredError",this.expiredAt=e;}},k=class extends c{limitType;limit;attempted;constructor(e,t,s){let r={per_tx:"Per-transaction",daily:"Daily",total:"Total",client_max:"Client-side"}[e]||e,a=e==="daily"?"Wait until tomorrow or request a higher limit.":e==="client_max"?"Increase maxPaymentUsd in your AgentWallet configuration.":"Request a new session key with a higher limit.";super(`${r} spending limit exceeded: limit is $${t.toFixed(2)}, attempted $${s.toFixed(2)}. ${a}`,"SPENDING_LIMIT_EXCEEDED"),this.name="SpendingLimitExceededError",this.limitType=e,this.limit=t,this.attempted=s;}isRetryable(){return this.limitType==="daily"}},S=class extends c{reason;txHash;constructor(e,t){let s=`Payment failed: ${e}`;t&&(s+=` (tx: ${t} - check on basescan.org)`),super(s,"PAYMENT_FAILED"),this.name="PaymentFailedError",this.reason=e,this.txHash=t;}isRetryable(){return true}},_=class extends c{reason;constructor(e="Invalid session key format"){super(`${e}. Session keys should be in format: sk_live_<64 hex chars> or sk_test_<64 hex chars>. Get one from https://mixrpay.com/manage/invites`,"INVALID_SESSION_KEY"),this.name="InvalidSessionKeyError",this.reason=e;}},T=class extends c{reason;constructor(e){super(`x402 protocol error: ${e}. This may indicate a server configuration issue. If the problem persists, contact the API provider.`,"X402_PROTOCOL_ERROR"),this.name="X402ProtocolError",this.reason=e;}isRetryable(){return true}},E=class extends c{sessionId;expiredAt;constructor(e,t){super(`Session ${e} has expired${t?` at ${t}`:""}. A new session will be created automatically on your next request.`,"SESSION_EXPIRED"),this.name="SessionExpiredError",this.sessionId=e,this.expiredAt=t;}},U=class extends c{sessionId;limit;requested;remaining;constructor(e,t,s,n){super(`Session spending limit exceeded: limit is $${e.toFixed(2)}, requested $${t.toFixed(2)}, remaining $${s.toFixed(2)}. Create a new session with a higher limit to continue.`,"SESSION_LIMIT_EXCEEDED"),this.name="SessionLimitExceededError",this.sessionId=n,this.limit=e,this.requested=t,this.remaining=s;}},I=class extends c{sessionId;constructor(e){super(`Session ${e} not found. It may have been deleted or never existed. Create a new session with getOrCreateSession().`,"SESSION_NOT_FOUND"),this.name="SessionNotFoundError",this.sessionId=e;}},R=class extends c{sessionId;reason;constructor(e,t){super(`Session ${e} has been revoked${t?`: ${t}`:""}. Create a new session with getOrCreateSession().`,"SESSION_REVOKED"),this.name="SessionRevokedError",this.sessionId=e,this.reason=t;}},O=class extends c{attempted;allowedPatterns;constructor(e,t){let s=t.slice(0,3).join(", "),n=t.length>3?"...":"";super(`Payment to "${e}" not allowed. Session allowlist: ${s}${n}. Update the session allowlist or create a new session.`,"MERCHANT_NOT_ALLOWED"),this.name="MerchantNotAllowedError",this.attempted=e,this.allowedPatterns=t;}},J=class extends c{constructor(e=6e4,t){super(t||`Rate limit exceeded. Please wait ${Math.ceil(e/1e3)} seconds before retrying.`,"RATE_LIMIT_EXCEEDED",e),this.name="RateLimitError";}isRetryable(){return true}},B=class extends c{reason;constructor(e,t){super(`Network error: ${e}. Check your connection and try again.`,"NETWORK_ERROR",5e3),this.name="NetworkError",this.reason=e,t?.stack&&(this.stack=t.stack);}isRetryable(){return true}},G=class extends c{reason;statusCode;constructor(e,t=401){super(`Authentication failed: ${e}`,t===401?"UNAUTHORIZED":"FORBIDDEN"),this.name="AuthenticationError",this.reason=e,this.statusCode=t;}isRetryable(){return false}};function Ae(l){return l instanceof c}function Se(l){return l instanceof c||l instanceof Error?l.message:String(l)}var Pe={8453:"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",84532:"0x036CbD53842c5426634e7929541eC2318f3dCF7e"},Ue={name:"USD Coin",version:"2"},Ie={TransferWithAuthorization:[{name:"from",type:"address"},{name:"to",type:"address"},{name:"value",type:"uint256"},{name:"validAfter",type:"uint256"},{name:"validBefore",type:"uint256"},{name:"nonce",type:"bytes32"}]},N=class l{privateKey;account;address;isTest;constructor(e,t){this.privateKey=e,this.account=accounts.privateKeyToAccount(e),this.address=this.account.address,this.isTest=t;}toString(){return `${this.isTest?"sk_test_":"sk_live_"}${this.privateKey.slice(2)}`}get privateKeyHex(){return this.privateKey.slice(2)}get rawPrivateKey(){return this.privateKey}static fromString(e){let t=/^sk_(live|test)_([a-fA-F0-9]{64})$/,s=e.match(t);if(!s)throw new _("Session key must be in format sk_live_{64_hex} or sk_test_{64_hex}");let n=s[1],r=s[2];try{let a=`0x${r}`;return new l(a,n==="test")}catch(a){throw new _(`Failed to decode session key: ${a}`)}}async signTransferAuthorization(e,t){return accounts.signTypedData({privateKey:this.privateKey,domain:e,types:Ie,primaryType:"TransferWithAuthorization",message:t})}async signMessage(e){return accounts.signMessage({privateKey:this.privateKey,message:e})}getDefaultChainId(){return this.isTest?84532:8453}};function te(l){let e=Pe[l.chainId];if(!e)throw new Error(`USDC not supported on chain ${l.chainId}`);let t={...Ue,chainId:l.chainId,verifyingContract:e},s={from:l.fromAddress,to:l.toAddress,value:l.value,validAfter:l.validAfter,validBefore:l.validBefore,nonce:l.nonce};return {domain:t,message:s}}function X(){let l=new Uint8Array(32);return crypto.getRandomValues(l),`0x${Array.from(l).map(e=>e.toString(16).padStart(2,"0")).join("")}`}function Re(l,e){return `MixrPay:${l}:${e.toLowerCase()}`}async function se(l){let e=Date.now(),t=Re(e,l.address),s=await l.signMessage(t);return {address:l.address,timestamp:e,signature:s}}var Oe="0.9.2",ne=".config/mixrpay",Ke="wallet.json";function ae(){let l=process.env.MIXRPAY_WORKSPACE_DIR||process.env.OPENCLAW_WORKSPACE_DIR;return l?$.join(l,ne):$.join(j.homedir(),ne)}function C(){return $.join(ae(),Ke)}async function z(l,e){let t=ae(),s=C();w.existsSync(t)||w.mkdirSync(t,{recursive:true,mode:448});let n={address:e.toLowerCase(),privateKey:l,createdAt:new Date().toISOString(),sdkVersion:Oe};w.writeFileSync(s,JSON.stringify(n,null,2),{encoding:"utf-8",mode:384});try{w.chmodSync(s,384);}catch{}}async function L(){let l=process.env.MIXRPAY_WALLET_KEY;if(l){if(l.startsWith("0x")&&l.length===66)return l;if(l.length===64)return `0x${l}`;console.warn("[MixrPay] MIXRPAY_WALLET_KEY has invalid format, ignoring");}let e=C();if(!w.existsSync(e))return null;try{let t=w.readFileSync(e,"utf-8"),s=JSON.parse(t);return !s.privateKey||!s.privateKey.startsWith("0x")?(console.warn("[MixrPay] Wallet file has invalid format"),null):s.privateKey}catch(t){return console.warn("[MixrPay] Failed to read wallet file:",t),null}}async function je(){let l=C();if(!w.existsSync(l)){let e=process.env.MIXRPAY_WALLET_KEY;return e&&(e.startsWith("0x")?e:`0x${e}`).length===66,null}try{let e=w.readFileSync(l,"utf-8");return JSON.parse(e)}catch{return null}}async function V(){let l=process.env.MIXRPAY_WALLET_KEY;return l&&(l.length===64||l.startsWith("0x")&&l.length===66)?true:w.existsSync(C())}function Me(){return C()}async function Ne(){let l=C();if(!w.existsSync(l))return false;try{let{unlinkSync:e}=await import('fs');return e(l),!0}catch{return false}}async function ie(l){let e=null,t=l.headers.get("X-Payment-Required");if(t)try{e=JSON.parse(t);}catch{}if(!e){let n=l.headers.get("WWW-Authenticate");if(n?.startsWith("X-402 "))try{let r=n.slice(6);e=JSON.parse(atob(r));}catch{}}if(!e)try{e=await l.json();}catch{}if(!e)throw new T("Could not parse payment requirements from 402 response");if(!e.recipient)throw new T("Missing recipient in payment requirements");if(!e.amount)throw new T("Missing amount in payment requirements");let s=Math.floor(Date.now()/1e3);return {recipient:e.recipient,amount:BigInt(e.amount),currency:e.currency||"USDC",chainId:e.chainId||e.chain_id||8453,facilitatorUrl:e.facilitatorUrl||e.facilitator_url||"https://x402.org/facilitator",nonce:e.nonce||X().slice(2),expiresAt:e.expiresAt||e.expires_at||s+300,description:e.description}}async function oe(l,e,t){let s=l.nonce.length===64?`0x${l.nonce}`:X(),r=BigInt(Math.floor(Date.now()/1e3))-60n,a=BigInt(l.expiresAt),{domain:i,message:o}=te({fromAddress:t,toAddress:l.recipient,value:l.amount,validAfter:r,validBefore:a,nonce:s,chainId:l.chainId}),d=await e.signTransferAuthorization(i,o),p={x402Version:1,scheme:"exact",network:l.chainId===8453?"base":"base-sepolia",payload:{signature:d,authorization:{from:t,to:l.recipient,value:l.amount.toString(),validAfter:r.toString(),validBefore:a.toString(),nonce:s}}};return btoa(JSON.stringify(p))}function le(l){return Date.now()/1e3>l.expiresAt}function Z(l){return Number(l.amount)/1e6}var we="0.9.4",x=process.env.MIXRPAY_BASE_URL||"https://www.mixrpay.com";var Ye=3e4,ye={BASE_MAINNET:{chainId:8453,name:"Base",isTestnet:false},BASE_SEPOLIA:{chainId:84532,name:"Base Sepolia",isTestnet:true}},fe={debug:0,info:1,warn:2,error:3,none:4},Q=class{level;prefix;constructor(e="none",t="[MixrPay]"){this.level=e,this.prefix=t;}setLevel(e){this.level=e;}shouldLog(e){return fe[e]>=fe[this.level]}debug(...e){this.shouldLog("debug")&&console.log(`${this.prefix} \u{1F50D}`,...e);}info(...e){this.shouldLog("info")&&console.log(`${this.prefix} \u2139\uFE0F`,...e);}warn(...e){this.shouldLog("warn")&&console.warn(`${this.prefix} \u26A0\uFE0F`,...e);}error(...e){this.shouldLog("error")&&console.error(`${this.prefix} \u274C`,...e);}payment(e,t,s){if(this.shouldLog("info")){let n=s?` for "${s}"`:"";console.log(`${this.prefix} \u{1F4B8} Paid $${e.toFixed(4)} to ${t.slice(0,10)}...${n}`);}}},ee=class l{sessionKey;walletAddress;maxPaymentUsd;onPayment;baseUrl;timeout;logger;payments=[];totalSpentUsd=0;sessionKeyInfo;sessionKeyInfoFetchedAt;allowlist;allowlistFetchedAt;selfCustodyAddress;selfCustodyKey;agentInstanceId;apiKey;constructor(e){this.validateConfig(e),this.sessionKey=N.fromString(e.sessionKey),this.walletAddress=e.walletAddress||this.sessionKey.address,this.maxPaymentUsd=e.maxPaymentUsd,this.onPayment=e.onPayment,this.baseUrl=(e.baseUrl||x).replace(/\/$/,""),this.timeout=e.timeout||Ye,e.apiKey&&(this.apiKey=e.apiKey),this.logger=new Q(e.logLevel||"none"),this.logger.debug("AgentWallet initialized",{walletAddress:this.walletAddress,isTestnet:this.isTestnet(),maxPaymentUsd:this.maxPaymentUsd,hasApiKey:!!this.apiKey});}validateConfig(e){if(!e.sessionKey)throw new _("Session key is required. Get one from the wallet owner at https://mixrpay.com/manage/invites");let t=e.sessionKey.trim();if(!t.startsWith("sk_live_")&&!t.startsWith("sk_test_")){let r=t.slice(0,8);throw new _(`Invalid session key prefix. Expected 'sk_live_' (mainnet) or 'sk_test_' (testnet), got '${r}'`)}let s=72;if(t.length!==s)throw new _(`Invalid session key length. Expected ${s} characters, got ${t.length}. Make sure you copied the complete key.`);let n=t.slice(8);if(!/^[0-9a-fA-F]+$/.test(n))throw new _("Invalid session key format. The key should contain only hexadecimal characters after the prefix.");if(e.maxPaymentUsd!==void 0){if(e.maxPaymentUsd<=0)throw new c("maxPaymentUsd must be a positive number");e.maxPaymentUsd>1e4&&this.logger?.warn("maxPaymentUsd is very high ($"+e.maxPaymentUsd+"). Consider using a lower limit for safety.");}}async fetchAllowlist(){if(this.allowlist!==void 0&&this.allowlistFetchedAt&&Date.now()-this.allowlistFetchedAt<3e5)return this.allowlist;try{let t=await this.getSessionKeyInfo();return this.allowlist=t.allowedMerchants||[],this.allowlistFetchedAt=Date.now(),this.logger.debug("Fetched allowlist",{patterns:this.allowlist.length,allowAll:this.allowlist.length===0}),this.allowlist}catch(t){return this.logger.warn("Failed to fetch allowlist, allowing all merchants",{error:t}),this.allowlist=[],this.allowlistFetchedAt=Date.now(),this.allowlist}}async validateMerchantAllowed(e,t){let s=await this.fetchAllowlist();if(s.length===0)return;if(!this.matchesAllowlist(e,s,t))throw new O(e,s)}matchesAllowlist(e,t,s){for(let n of t)if(this.matchPattern(e,n,s))return true;return false}matchPattern(e,t,s){return s==="url"?this.matchUrlPattern(e,t):this.matchToolPattern(e,t)}matchUrlPattern(e,t){let s;try{e.includes("://")?s=new URL(e).hostname.toLowerCase():s=e.toLowerCase();}catch{return false}let r=t.toLowerCase().trim();if(r.includes("://"))try{r=new URL(r).hostname;}catch{}if(s===r)return true;if(r.startsWith("*.")){let a=r.substring(2);if(s.endsWith(`.${a}`)&&s!==a)return true}return false}matchToolPattern(e,t){let s=e.toLowerCase().trim(),n=t.toLowerCase().trim();if(s===n)return true;if(n.endsWith("/*")){let r=n.substring(0,n.length-2);if(s.startsWith(`${r}/`))return true}return false}static async register(e){let{privateKey:t,name:s}=e,n=(e.baseUrl||x).replace(/\/$/,""),a=accounts.privateKeyToAccount(t).address,i=await fetch(`${n}/api/v1/agent/challenge?wallet=${a}&action=register`);if(!i.ok){let h=await i.json().catch(()=>({}));throw new c(h.error||`Failed to get challenge: ${i.status}`)}let{challenge:o,message:d}=await i.json(),p=await accounts.signMessage({message:d,privateKey:t}),u=await fetch(`${n}/api/v1/agent/register`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({challenge:o,external_wallet:a,signature:p,name:s})});if(!u.ok){let h=await u.json().catch(()=>({})),y=h.error||`Registration failed with status ${u.status}`,m=h.request_id,f=(h.code||"").toLowerCase(),b="";u.status===503?b=" The service may be temporarily unavailable. Please try again later.":u.status===500?b=" This is a server error. Please contact support with the request ID.":(f==="missing_challenge"||f==="missing_signature")&&(b=" This may indicate an SDK bug. Please update to the latest version.");let q=m?`${y} (request_id: ${m})${b}`:`${y}${b}`;throw new c(q)}let g=await u.json();return {userId:g.user_id,depositAddress:g.deposit_address}}static async checkServerHealth(e){let t=(e||x).replace(/\/$/,"");try{let s=await fetch(`${t}/api/health/ready?details=true`);if(!s.ok)return {healthy:!1,database:"unknown",agentRegistrationAvailable:!1,walletServiceConfigured:!1,error:`Health check failed with status ${s.status}`};let n=await s.json();return {healthy:n.status==="ready",database:n.database||"unknown",agentRegistrationAvailable:n.services?.agentRegistration?.available??!1,walletServiceConfigured:n.services?.wallet?.configured??!1}}catch(s){return {healthy:false,database:"unreachable",agentRegistrationAvailable:false,walletServiceConfigured:false,error:s instanceof Error?s.message:"Failed to reach server"}}}static async getSessionKey(e){let{privateKey:t,spendingLimitUsd:s,maxPerTxUsd:n,maxDailyUsd:r,durationDays:a}=e,i=(e.baseUrl||x).replace(/\/$/,""),d=accounts.privateKeyToAccount(t).address,p=await fetch(`${i}/api/v1/agent/challenge?wallet=${d}&action=session-key`);if(!p.ok){let f=await p.json().catch(()=>({}));throw new c(f.error||`Failed to get challenge: ${p.status}`)}let{challenge:u,message:g}=await p.json(),h=await accounts.signMessage({message:g,privateKey:t}),y=await fetch(`${i}/api/v1/agent/session-key`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({challenge:u,external_wallet:d,signature:h,spending_limit_usd:s,max_per_tx_usd:n,max_daily_usd:r,duration_days:a})});if(!y.ok){let f=await y.json().catch(()=>({}));throw new c(f.error||`Session key creation failed: ${y.status}`)}let m=await y.json();return {sessionKey:m.session_key,address:m.address,sessionKeyId:m.session_key_id,expiresAt:new Date(m.expires_at),limits:{maxTotalUsd:m.limits.max_total_usd,maxPerTxUsd:m.limits.max_per_tx_usd,maxDailyUsd:m.limits.max_daily_usd}}}static async getStatus(e){let{privateKey:t}=e,s=(e.baseUrl||x).replace(/\/$/,""),r=accounts.privateKeyToAccount(t).address,a=await fetch(`${s}/api/v1/agent/challenge?wallet=${r}&action=status`);if(!a.ok){let g=await a.json().catch(()=>({}));throw new c(g.error||`Failed to get challenge: ${a.status}`)}let{challenge:i,message:o}=await a.json(),d=await accounts.signMessage({message:o,privateKey:t}),p=await fetch(`${s}/api/v1/agent/status?challenge=${i}&external_wallet=${r}&signature=${encodeURIComponent(d)}`);if(!p.ok){let g=await p.json().catch(()=>({}));throw new c(g.error||`Failed to get status: ${p.status}`)}let u=await p.json();return {depositAddress:u.deposit_address,balanceUsd:u.balance_usd,activeSessions:u.active_sessions.map(g=>({id:g.id,expiresAt:new Date(g.expires_at),totalSpentUsd:g.total_spent_usd,remainingUsd:g.remaining_usd,maxTotalUsd:g.max_total_usd})),totalSpentUsd:u.total_spent_usd}}static async revokeSessionKey(e){let{privateKey:t,sessionKeyId:s}=e,n=(e.baseUrl||x).replace(/\/$/,""),a=accounts.privateKeyToAccount(t).address,i=await fetch(`${n}/api/v1/agent/challenge?wallet=${a}&action=revoke`);if(!i.ok){let g=await i.json().catch(()=>({}));throw new c(g.error||`Failed to get challenge: ${i.status}`)}let{challenge:o,message:d}=await i.json(),p=await accounts.signMessage({message:d,privateKey:t}),u=await fetch(`${n}/api/v1/agent/session-key/revoke`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({challenge:o,external_wallet:a,signature:p,session_key_id:s})});if(!u.ok){let g=await u.json().catch(()=>({}));throw new c(g.error||`Revocation failed: ${u.status}`)}return true}static async withdraw(e){let{privateKey:t,amountUsd:s}=e,n=(e.baseUrl||x).replace(/\/$/,""),a=accounts.privateKeyToAccount(t).address,i=await fetch(`${n}/api/v1/agent/challenge?wallet=${a}&action=withdraw`);if(!i.ok){let h=await i.json().catch(()=>({}));throw new c(h.error||`Failed to get challenge: ${i.status}`)}let{challenge:o,message:d}=await i.json(),p=await accounts.signMessage({message:d,privateKey:t}),u=await fetch(`${n}/api/v1/agent/withdraw`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({challenge:o,external_wallet:a,signature:p,to_address:a,amount_usd:s})});if(!u.ok){let h=await u.json().catch(()=>({}));throw new c(h.error||`Withdrawal failed: ${u.status}`)}let g=await u.json();return {txHash:g.tx_hash,amountUsd:g.amount_usd,remainingBalanceUsd:g.remaining_balance_usd}}static async claimInvite(e){let{inviteCode:t,privateKey:s}=e,n=(e.baseUrl||x).replace(/\/$/,""),a=accounts.privateKeyToAccount(s).address,i=await fetch(`${n}/api/v1/agent/challenge?wallet=${a}&action=claim-invite`);if(!i.ok){let h=await i.json().catch(()=>({}));throw new c(h.error||`Failed to get challenge: ${i.status}`)}let{challenge:o,message:d}=await i.json(),p=await accounts.signMessage({message:d,privateKey:s}),u=await fetch(`${n}/api/v1/agent/claim-invite`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({invite_code:t,challenge:o,agent_wallet_address:a,signature:p})});if(!u.ok){let y=(await u.json().catch(()=>({}))).error||`Failed to claim invite: ${u.status}`,m="";throw u.status===404?m=" The invite code may be invalid or misspelled.":u.status===400&&(y.includes("already claimed")?m=" This invite has already been used by another agent.":y.includes("expired")?m=" Ask the wallet owner to create a new invite.":y.includes("revoked")&&(m=" The wallet owner has revoked this invite.")),new c(`${y}${m}`)}let g=await u.json();return {sessionKey:g.session_key,address:g.address,sessionKeyId:g.session_key_id,expiresAt:new Date(g.expires_at),limits:{budgetUsd:g.limits.budget_usd,budgetPeriod:g.limits.budget_period,maxPerTxUsd:g.limits.max_per_tx_usd},allowedMerchants:g.allowed_merchants||[],inviterName:g.inviter_name||"Anonymous"}}static async activate(e,t){let s=(t?.baseUrl||x).replace(/\/$/,""),n=new l({sessionKey:e,baseUrl:s}),r=await n.getSessionAuthHeaders(),a=await fetch(`${s}/api/v2/agent/activate`,{method:"POST",headers:{"Content-Type":"application/json",...r}});if(!a.ok){let o=await a.json().catch(()=>({}));throw new c(o.error||`Activation failed: ${a.status}`)}let i=await a.json();return {wallet:n,budget:{maxTotalUsd:i.budget.max_total_usd,maxDailyUsd:i.budget.max_daily_usd,maxPerTxUsd:i.budget.max_per_tx_usd,spentUsd:i.budget.spent_usd,remainingUsd:i.budget.remaining_usd},capabilities:{executeTransactions:i.capabilities.execute_transactions,gasSponsored:i.capabilities.gas_sponsored,batchedTx:i.capabilities.batched_tx},skills:i.skills||[],tools:i.tools||[],gatewayProviders:i.gateway_providers||[]}}static async connect(e){let t=(e?.baseUrl||process.env.MIXRPAY_BASE_URL||x).replace(/\/$/,""),s=e?.logLevel||"none",n=process.env.MIXRPAY_INSTANCE_ID;if(e?.sessionKey){let p=new l({sessionKey:e.sessionKey,baseUrl:t,logLevel:s,maxPaymentUsd:e?.maxPaymentUsd,onPayment:e?.onPayment});return n&&p.setAgentInstanceId(n),p}let r=process.env.MIXRPAY_SESSION_KEY;if(r){let p=new l({sessionKey:r,baseUrl:t,logLevel:s,maxPaymentUsd:e?.maxPaymentUsd,onPayment:e?.onPayment});return n&&p.setAgentInstanceId(n),p}let a=process.env.MIXRPAY_API_KEY||process.env.MIXRPAY_AGENT_TOKEN;if(a&&(a.startsWith("agt_live_")||a.startsWith("agt_test_")))return l.fromApiKey(a,{baseUrl:t,logLevel:s});let i=e?.accessCode||process.env.MIXRPAY_ACCESS_CODE;if(i)return l.fromAccessCode(i,{baseUrl:t,logLevel:s});let o=e?.masterKey||process.env.MIXRPAY_MASTER_KEY,d=e?.agentName||process.env.MIXRPAY_AGENT_NAME;if(o&&d)return l.fromMasterKey(o,d,{baseUrl:t,logLevel:s});try{let{loadCredentials:p}=await Promise.resolve().then(()=>(M(),W)),u=p();if(u.success&&u.credentials.sessionKey){let g=new l({sessionKey:u.credentials.sessionKey,baseUrl:u.credentials.baseUrl||t,logLevel:s,maxPaymentUsd:e?.maxPaymentUsd,onPayment:e?.onPayment,apiKey:u.credentials.apiToken});return n&&g.setAgentInstanceId(n),g}if(u.success&&u.credentials.apiToken)return l.fromApiKey(u.credentials.apiToken,{baseUrl:t,logLevel:s});if(u.success&&u.credentials.masterKey&&u.credentials.defaultAgentName)return l.fromMasterKey(u.credentials.masterKey,u.credentials.defaultAgentName,{baseUrl:t,logLevel:s})}catch{}if(e?.interactive)return l.deviceFlowLogin({baseUrl:t,logLevel:s});throw new c(`No MixrPay credentials found. Options:
|
|
2
|
+
1. Set MIXRPAY_SESSION_KEY environment variable
|
|
3
|
+
2. Set MIXRPAY_API_KEY or MIXRPAY_AGENT_TOKEN environment variable (agt_live_xxx)
|
|
4
|
+
3. Set MIXRPAY_ACCESS_CODE environment variable (mixr-xxx)
|
|
5
|
+
4. Pass sessionKey or accessCode to connect()
|
|
6
|
+
5. Run \`npx mixrpay login\` to cache credentials
|
|
7
|
+
6. Use connect({ interactive: true }) for browser login`)}static async fromApiKey(e,t){let s=(t?.baseUrl||x).replace(/\/$/,"");if(!e.startsWith("agt_live_")&&!e.startsWith("agt_test_"))throw new c(`Invalid API key format. Expected 'agt_live_' or 'agt_test_' prefix, got '${e.slice(0,9)}...'`);let n=await fetch(`${s}/api/v2/agent/session-from-token`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`}});if(!n.ok){let o=await n.json().catch(()=>({}));throw new c(o.error||`Failed to exchange API key for session: ${n.status}`)}let r=await n.json(),a=r.session_key;if(!a)throw new c("No session_key in API response");let i=new l({sessionKey:a,baseUrl:s,logLevel:t?.logLevel});return i.setApiKey(e),r.agent_instance_id&&i.setAgentInstanceId(r.agent_instance_id),i}static async fromAccessCode(e,t){let s=(t?.baseUrl||x).replace(/\/$/,"");if(!e.startsWith("mixr-"))throw new c(`Invalid access code format. Expected 'mixr-' prefix, got '${e.slice(0,5)}...'`);let n=await fetch(`${s}/api/v2/agent/connect`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:e})});if(!n.ok){let p=await n.json().catch(()=>({})),u=(p.code||"").toLowerCase(),g="";throw n.status===404||u==="invalid_code"?g=" Check the code for typos or get a new one from the MixrPay dashboard.":n.status===400&&(u==="code_claimed"?g=" This code has already been used. Get a new code from the dashboard.":u==="code_expired"?g=" This code has expired. Get a new code from the dashboard.":u==="code_revoked"&&(g=" This code has been revoked by the owner.")),new c((p.error||`Failed to activate access code: ${n.status}`)+g)}let r=await n.json(),a=r.session_key,i=r.token,o=r.agent_id;if(!a)throw new c("No session_key in access code response");try{let{saveCredentials:p}=await Promise.resolve().then(()=>(M(),W));p({sessionKey:a,apiToken:i,baseUrl:s});}catch{}let d=new l({sessionKey:a,baseUrl:s,logLevel:t?.logLevel});return i&&(d.apiKey=i),o&&(d.agentInstanceId=o),d}static async fromMasterKey(e,t,s){let n=(s?.baseUrl||x).replace(/\/$/,"");if(!e.startsWith("mk_live_")&&!e.startsWith("mk_test_"))throw new c("Invalid master key format. Expected 'mk_live_' or 'mk_test_' prefix.");let r=await fetch(`${n}/api/v2/master-keys/session`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify({agent_name:t})});if(!r.ok){let d=await r.json().catch(()=>({}));throw r.status===404?new c(`Agent '${t}' not found. Use the MixrPay dashboard to create it.`):new c(d.error||`Failed to get session from master key: ${r.status}`)}let a=await r.json(),i=a.session_key;if(!i)throw new c("No session_key in master key response");let o=new l({sessionKey:i,baseUrl:n,logLevel:s?.logLevel});return a.agent?.id&&(o.agentInstanceId=a.agent.id),o}static async deviceFlowLogin(e){let t=(e?.baseUrl||x).replace(/\/$/,""),s=await fetch(`${t}/api/v2/auth/device/authorize`,{method:"POST",headers:{"Content-Type":"application/json"}});if(!s.ok){let h=await s.json().catch(()=>({}));throw new c(h.error||`Device flow initiation failed: ${s.status}`)}let n=await s.json(),{device_code:r,user_code:a,verification_uri:i,verification_uri_complete:o,expires_in:d,interval:p}=n;console.log(`
|
|
8
|
+
\u{1F510} MixrPay Login
|
|
9
|
+
`),console.log(`Visit: ${o||i}`),console.log(`Enter code: ${a}
|
|
10
|
+
`),console.log(`Waiting for authentication...
|
|
11
|
+
`);let u=(p||5)*1e3,g=Date.now()+(d||300)*1e3;for(;Date.now()<g;){await new Promise(f=>setTimeout(f,u));let h=await fetch(`${t}/api/v2/auth/device/token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({device_code:r})});if(h.ok){let b=(await h.json()).session_key;if(b){console.log(`\u2705 Authenticated successfully!
|
|
12
|
+
`);try{let{saveCredentials:q}=await Promise.resolve().then(()=>(M(),W));q({sessionKey:b,baseUrl:t});}catch{}return new l({sessionKey:b,baseUrl:t,logLevel:e?.logLevel})}}let m=(await h.json().catch(()=>({}))).error;if(m!=="authorization_pending")if(m==="slow_down"){await new Promise(f=>setTimeout(f,u));continue}else {if(m==="expired_token")throw new c("Device code expired. Please try again.");if(m==="access_denied")throw new c("Authentication was denied by the user.");if(m)throw new c(`Device flow error: ${m}`)}}throw new c("Device flow timed out. Please try again.")}async spawnChildInvite(e){let{budgetUsd:t,name:s,allowedMerchants:n,expiresInDays:r}=e,a=await this.getSessionAuthHeaders(),i=await fetch(`${this.baseUrl}/api/v1/agent/spawn`,{method:"POST",headers:{"Content-Type":"application/json",...a},body:JSON.stringify({budget_usd:t,name:s,allowed_merchants:n,expires_in_days:r})});if(!i.ok){let d=await i.json().catch(()=>({}));throw i.status===409?new c("Concurrent modification - please retry"):i.status===429?new c("Rate limited - too many spawn attempts"):new c(d.error||`Failed to spawn child: ${i.status}`)}let o=await i.json();return {inviteCode:o.invite_code,inviteId:o.invite_id,budgetUsd:o.budget_usd,expiresAt:new Date(o.expires_at),depth:o.depth,maxSpawnBudget:o.max_spawn_budget,allowedMerchants:o.allowed_merchants||[]}}async getAvailableBudget(){let e=await this.getSessionAuthHeaders(),t=await fetch(`${this.baseUrl}/api/v1/agent/descendants`,{method:"GET",headers:e});if(!t.ok){let n=await t.json().catch(()=>({}));throw new c(n.error||`Failed to get budget: ${t.status}`)}let s=await t.json();return {totalBudget:s.budget.total_usd,spent:s.budget.spent_usd,allocatedToChildren:s.budget.allocated_to_children_usd,available:s.budget.available_usd,maxSpawnBudget:s.max_spawn_budget,canSpawn:s.can_spawn}}async getChildSessions(){let e=await this.getSessionAuthHeaders(),t=await fetch(`${this.baseUrl}/api/v1/agent/descendants`,{method:"GET",headers:e});if(!t.ok){let n=await t.json().catch(()=>({}));throw new c(n.error||`Failed to get children: ${t.status}`)}return (await t.json()).children||[]}async listChildSessions(){return this.getChildSessions()}async fetch(e,t){this.logger.debug(`Fetching ${t?.method||"GET"} ${e}`),await this.validateMerchantAllowed(e,"url");let s=crypto.randomUUID(),n=this.extractCorrelationId(t?.headers),r=new AbortController,a=setTimeout(()=>r.abort(),this.timeout);try{let i=await fetch(e,{...t,signal:r.signal});return this.logger.debug(`Initial response: ${i.status}`),i.status===402&&(this.logger.info(`Payment required for ${e}`),i=await this.handlePaymentRequired(e,t,i,s,n)),i}catch(i){throw i instanceof Error&&i.name==="AbortError"?new c(`Request timeout after ${this.timeout}ms`):i}finally{clearTimeout(a);}}extractCorrelationId(e){if(!e)return;if(e instanceof Headers)return e.get("X-Correlation-Id")||e.get("x-correlation-id")||void 0;if(Array.isArray(e)){let s=e.find(([n])=>n.toLowerCase()==="x-correlation-id");return s?s[1]:void 0}let t=e;return t["X-Correlation-Id"]||t["x-correlation-id"]||void 0}async handlePaymentRequired(e,t,s,n,r){let a;try{a=await ie(s),this.logger.debug("Payment requirements:",{amount:`$${Z(a).toFixed(4)}`,recipient:a.recipient,description:a.description});}catch(u){throw this.logger.error("Failed to parse payment requirements:",u),new S(`Failed to parse payment requirements: ${u}. The server may not be properly configured for x402 payments.`)}if(le(a))throw new S("Payment requirements have expired. This usually means the request took too long. Try again.");let i=Z(a);if(this.maxPaymentUsd!==void 0&&i>this.maxPaymentUsd)throw new k("client_max",this.maxPaymentUsd,i);let o;try{this.logger.debug("Signing payment authorization..."),o=await oe(a,this.sessionKey,this.walletAddress);}catch(u){throw this.logger.error("Failed to sign payment:",u),new S(`Failed to sign payment: ${u}. This may indicate an issue with the session key.`)}this.logger.debug("Retrying request with payment...");let d=new Headers(t?.headers);d.set("X-PAYMENT",o);let p=await fetch(e,{...t,headers:d});if(p.status!==402){let u={amountUsd:i,recipient:a.recipient,txHash:p.headers.get("X-Payment-TxHash"),timestamp:new Date,description:a.description,url:e,requestId:n,correlationId:r};this.payments.push(u),this.totalSpentUsd+=i,this.logger.payment(i,a.recipient,a.description),this.onPayment&&this.onPayment(u);}else await this.handlePaymentError(p);return p}async handlePaymentError(e){let t={};try{t=await e.json();}catch{}let s=(t.error_code||"").toLowerCase(),n=t.error||t.message||"Payment required";if(this.logger.error("Payment failed:",{errorCode:s,errorMessage:n,errorData:t}),s==="insufficient_balance"){let r=t.required||0,a=t.available||0;throw new v(r,a)}else throw s==="session_key_expired"?new D(t.expired_at||"unknown"):s==="spending_limit_exceeded"?new k(t.limit_type||"unknown",t.limit||0,t.attempted||0):new S(n)}getWalletAddress(){return this.walletAddress}isTestnet(){return this.sessionKey.isTest}getNetwork(){return this.isTestnet()?ye.BASE_SEPOLIA:ye.BASE_MAINNET}async getBalance(){this.logger.debug("Fetching wallet balance...");try{let e=await fetch(`${this.baseUrl}/api/v1/session-key/info`,{headers:{"X-Session-Key":this.sessionKey.address.toLowerCase()}});if(!e.ok)throw new Error("Failed to get session key info");let t=await e.json(),s=t.wallet_address||t.walletAddress;if(!s)throw new Error("No wallet address in session key info");let n=await fetch(`${this.baseUrl}/api/wallet/balance?address=${s}&chain=base`);if(n.ok){let r=await n.json(),a=r.balance_usdc||r.balance_usd||0;return this.logger.debug(`Balance: $${a}`),a}}catch(e){this.logger.warn("Failed to fetch balance:",e);}return this.logger.debug("Using estimated balance based on tracking"),Math.max(0,100-this.totalSpentUsd)}async canAfford(e){let t=await this.getBalance(),s=t>=e;return {canAfford:s,balance:t,shortfall:s?0:e-t,remainingAfter:s?t-e:0}}async getSessionKeyInfo(e=false){let t=this.sessionKeyInfoFetchedAt?Date.now()-this.sessionKeyInfoFetchedAt:1/0;if(!e&&this.sessionKeyInfo&&t<6e4)return this.sessionKeyInfo;this.logger.debug("Fetching session key info...");try{let s=await fetch(`${this.baseUrl}/api/v1/session-key/info`,{headers:{"X-Session-Key":this.sessionKey.address.toLowerCase()}});if(s.ok){let n=await s.json();return this.sessionKeyInfo={address:this.sessionKey.address,walletAddress:n.wallet_address??n.walletAddress??null,isValid:n.is_valid??n.isValid??!0,limits:{perTxUsd:n.per_tx_limit_usd??n.perTxLimitUsd??null,dailyUsd:n.daily_limit_usd??n.dailyLimitUsd??null,totalUsd:n.total_limit_usd??n.totalLimitUsd??null},usage:{todayUsd:n.today_spent_usd??n.todaySpentUsd??0,totalUsd:n.total_spent_usd??n.totalSpentUsd??0,txCount:n.tx_count??n.txCount??0},expiresAt:n.expires_at?new Date(n.expires_at):null,createdAt:n.created_at?new Date(n.created_at):null,name:n.name,allowedMerchants:n.allowed_merchants??n.allowedMerchants??[]},this.sessionKeyInfoFetchedAt=Date.now(),this.sessionKeyInfo}}catch(s){this.logger.warn("Failed to fetch session key info:",s);}return {address:this.sessionKey.address,walletAddress:null,isValid:true,limits:{perTxUsd:null,dailyUsd:null,totalUsd:null},usage:{todayUsd:this.totalSpentUsd,totalUsd:this.totalSpentUsd,txCount:this.payments.length},expiresAt:null,createdAt:null}}async getSpendingStats(){this.logger.debug("Fetching spending stats...");try{let e=await fetch(`${this.baseUrl}/api/v1/session-key/stats`,{headers:{"X-Session-Key":this.sessionKey.address.toLowerCase()}});if(e.ok){let t=await e.json();return {totalSpentUsd:t.total_spent_usd||t.totalSpentUsd||this.totalSpentUsd,txCount:t.tx_count||t.txCount||this.payments.length,remainingDailyUsd:t.remaining_daily_usd||t.remainingDailyUsd||null,remainingTotalUsd:t.remaining_total_usd||t.remainingTotalUsd||null,expiresAt:t.expires_at?new Date(t.expires_at):null}}}catch(e){this.logger.warn("Failed to fetch spending stats:",e);}return {totalSpentUsd:this.totalSpentUsd,txCount:this.payments.length,remainingDailyUsd:null,remainingTotalUsd:null,expiresAt:null}}getPaymentHistory(){return [...this.payments]}getTotalSpent(){return this.totalSpentUsd}async createSelfCustodyWallet(){if(!this.agentInstanceId)throw new c("Agent instance ID is required for self-custody wallet. Use AgentWallet.fromApiKey() or set MIXRPAY_INSTANCE_ID environment variable.","NO_AGENT_INSTANCE_ID");let e=await L();if(e){let i=accounts.privateKeyToAccount(e);return this.selfCustodyAddress=i.address,this.selfCustodyKey=e,this.logger.info("Loaded existing self-custody wallet",{address:i.address}),{address:i.address,privateKey:e}}let t=accounts.generatePrivateKey(),s=accounts.privateKeyToAccount(t),n=Date.now(),r=`MixrPay Wallet Registration
|
|
13
|
+
Address: ${s.address}
|
|
14
|
+
Timestamp: ${n}`,a=await s.signMessage({message:r});return await this.registerWalletAddress(s.address,a,n),await z(t,s.address),this.selfCustodyAddress=s.address,this.selfCustodyKey=t,this.logger.info("Created self-custody wallet",{address:s.address}),{address:s.address,privateKey:t}}async registerWalletAddress(e,t,s){if(!this.apiKey)throw new c("API key is required for wallet registration. Use AgentWallet.fromApiKey() to initialize.","NO_API_KEY");let n=await fetch(`${this.baseUrl}/api/v2/agent/wallet/register`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.apiKey}`},body:JSON.stringify({address:e,signature:t,timestamp:s})});if(!n.ok){let r=await n.json().catch(()=>({}));throw new c(r.error||`Failed to register wallet: ${n.status}`,"WALLET_REGISTRATION_FAILED")}}async loadSelfCustodyWallet(){let e=await L();if(!e)return false;let t=accounts.privateKeyToAccount(e);return this.selfCustodyAddress=t.address,this.selfCustodyKey=e,this.logger.debug("Loaded self-custody wallet",{address:t.address}),true}async hasSelfCustodyWallet(){return await V()}getSelfCustodyAddress(){return this.selfCustodyAddress||null}async executeFromOwnWallet(e){if(!this.selfCustodyKey||!this.selfCustodyAddress)throw new c("Self-custody wallet not loaded. Call loadSelfCustodyWallet() or createSelfCustodyWallet() first.","WALLET_NOT_LOADED");let{to:t,value:s,data:n,gasLimit:r}=e,a=accounts.privateKeyToAccount(this.selfCustodyKey),i=viem.createWalletClient({account:a,chain:chains.base,transport:viem.http()});this.logger.debug("Executing transaction from self-custody wallet",{from:this.selfCustodyAddress,to:t,value:s?.toString()});try{let o=await i.sendTransaction({to:t,value:s??BigInt(0),data:n,gas:r});this.logger.info("Transaction submitted",{txHash:o});let d=!1;try{await this.reportTransaction(o,t,s??BigInt(0)),d=!0;}catch(p){this.logger.warn("Failed to report transaction to MixrPay",{txHash:o,error:p instanceof Error?p.message:p});}return {txHash:o,reportedToMixrPay:d}}catch(o){throw this.logger.error("Transaction failed",{error:o instanceof Error?o.message:o}),new c(`Transaction failed: ${o instanceof Error?o.message:"Unknown error"}`,"TRANSACTION_FAILED")}}async transferUSDC(e){if(!this.selfCustodyKey||!this.selfCustodyAddress)throw new c("Self-custody wallet not loaded. Call loadSelfCustodyWallet() or createSelfCustodyWallet() first.","WALLET_NOT_LOADED");let{to:t,amountUsdc:s}=e,n="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",r=6,a=viem.parseUnits(s.toFixed(r),r),i=viem.encodeFunctionData({abi:[{name:"transfer",type:"function",inputs:[{name:"to",type:"address"},{name:"amount",type:"uint256"}],outputs:[{type:"bool"}]}],functionName:"transfer",args:[t,a]});return this.logger.debug("Transferring USDC",{from:this.selfCustodyAddress,to:t,amountUsdc:s,amountMinorUnits:a.toString()}),(await this.executeFromOwnWallet({to:n,data:i})).txHash}async reportTransaction(e,t,s){if(!this.apiKey){this.logger.warn("Cannot report transaction - no API key set");return}let n=await fetch(`${this.baseUrl}/api/v2/agent/tx/report`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.apiKey}`},body:JSON.stringify({tx_hash:e,to_address:t,value:s.toString(),chain_id:chains.base.id})});if(!n.ok){let r=await n.json().catch(()=>({}));throw new c(r.error||`Failed to report transaction: ${n.status}`,"TX_REPORT_FAILED")}}getAgentInstanceId(){return this.agentInstanceId}setAgentInstanceId(e){this.agentInstanceId=e;}setApiKey(e){this.apiKey=e;}mcp(){let e=$.join($.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))),"mcp-server.js"),t={MIXRPAY_SESSION_KEY:this.sessionKey.toString(),MIXRPAY_BASE_URL:this.baseUrl};return this.apiKey&&(t.MIXRPAY_API_KEY=this.apiKey),this.agentInstanceId&&(t.MIXRPAY_INSTANCE_ID=this.agentInstanceId),this.selfCustodyKey&&(t.AGENT_WALLET_KEY=this.selfCustodyKey),this.maxPaymentUsd&&(t.MIXRPAY_MAX_PAYMENT_USD=String(this.maxPaymentUsd)),{command:"node",args:[e],env:t}}async runDiagnostics(){this.logger.info("Running diagnostics...");let e=[],t=[],s={},n,r;s.sessionKeyFormat=true;try{let o=Date.now(),d=await fetch(`${this.baseUrl}/health`,{method:"GET",signal:AbortSignal.timeout(5e3)});n=Date.now()-o,s.apiConnectivity=d.ok,d.ok||(e.push(`API server returned ${d.status}. Check baseUrl configuration.`),t.push("Verify the baseUrl configuration points to a valid MixrPay server.")),n>2e3&&(e.push(`High API latency: ${n}ms. This may cause timeouts.`),t.push("Consider using a server closer to your region or check network connectivity."));}catch{s.apiConnectivity=false,e.push("Cannot connect to MixrPay API. Check your network connection and baseUrl."),t.push("Verify network connectivity and that the MixrPay server is running.");}try{let o=await this.getSessionKeyInfo(!0);s.sessionKeyValid=o.isValid,o.isValid||(e.push("Session key is invalid or has been revoked."),t.push("Request a new session key from the wallet owner at https://mixrpay.com/manage/invites"));let d=new Date,p=null;o.expiresAt&&(p=(o.expiresAt.getTime()-d.getTime())/(1e3*60*60),o.expiresAt<d?(s.sessionKeyValid=!1,e.push(`Session key expired on ${o.expiresAt.toISOString()}`),t.push("Create a new session key to continue making payments.")):p<24&&(e.push(`Session key expires in ${p.toFixed(1)} hours.`),t.push("Consider creating a new session key before the current one expires.")));let u=await this.getSpendingStats();r={remainingDailyUsd:u.remainingDailyUsd,remainingTotalUsd:u.remainingTotalUsd,expiresAt:o.expiresAt,expiresInHours:p},u.remainingDailyUsd!==null&&u.remainingDailyUsd<1&&(e.push(`Daily limit nearly exhausted: $${u.remainingDailyUsd.toFixed(2)} remaining.`),t.push("Wait until tomorrow for daily limit to reset, or request a higher daily limit.")),u.remainingTotalUsd!==null&&u.remainingTotalUsd<1&&(e.push(`Total limit nearly exhausted: $${u.remainingTotalUsd.toFixed(2)} remaining.`),t.push("Request a new session key with a higher total limit."));}catch{s.sessionKeyValid=false,e.push("Could not verify session key validity."),t.push("Check network connectivity and try again.");}let a=0;try{a=await this.getBalance(),s.hasBalance=a>0,a<=0?(e.push("Wallet has no USDC balance. Top up at https://mixrpay.com/manage/wallet"),t.push("Deposit USDC to your wallet address to enable payments.")):a<1&&(e.push(`Low balance: $${a.toFixed(2)}. Consider topping up.`),t.push("Top up your wallet balance to avoid payment failures."));}catch{s.hasBalance=false,e.push("Could not fetch wallet balance."),t.push("Check network connectivity and try again.");}let i=e.length===0;return this.logger.info("Diagnostics complete:",{healthy:i,issues:e,latencyMs:n}),{healthy:i,issues:e,checks:s,sdkVersion:we,network:this.getNetwork().name,walletAddress:this.walletAddress,sessionLimits:r,latencyMs:n,recommendations:t}}async generateSiwe(e){let{domain:t,uri:s,statement:n,nonce:r=crypto.randomUUID(),issuedAt:a=new Date,expirationTime:i,notBefore:o,requestId:d,resources:p}=e,u=this.sessionKey.address,g=this.getNetwork().chainId,h="1",y=`${t} wants you to sign in with your Ethereum account:
|
|
15
|
+
`;if(y+=`${u}
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
var index_exports = {};
|
|
22
|
-
__export(index_exports, {
|
|
23
|
-
AgentWallet: () => AgentWallet,
|
|
24
|
-
InsufficientBalanceError: () => InsufficientBalanceError,
|
|
25
|
-
InvalidSessionKeyError: () => InvalidSessionKeyError,
|
|
26
|
-
MerchantNotAllowedError: () => MerchantNotAllowedError,
|
|
27
|
-
MixrPayError: () => MixrPayError,
|
|
28
|
-
PaymentFailedError: () => PaymentFailedError,
|
|
29
|
-
SDK_VERSION: () => SDK_VERSION,
|
|
30
|
-
SessionExpiredError: () => SessionExpiredError,
|
|
31
|
-
SessionKeyExpiredError: () => SessionKeyExpiredError,
|
|
32
|
-
SessionLimitExceededError: () => SessionLimitExceededError,
|
|
33
|
-
SessionNotFoundError: () => SessionNotFoundError,
|
|
34
|
-
SessionRevokedError: () => SessionRevokedError,
|
|
35
|
-
SpendingLimitExceededError: () => SpendingLimitExceededError,
|
|
36
|
-
X402ProtocolError: () => X402ProtocolError,
|
|
37
|
-
getErrorMessage: () => getErrorMessage,
|
|
38
|
-
isMixrPayError: () => isMixrPayError
|
|
39
|
-
});
|
|
40
|
-
module.exports = __toCommonJS(index_exports);
|
|
17
|
+
`,n&&(y+=`${n}
|
|
41
18
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
super(message);
|
|
53
|
-
this.name = "MixrPayError";
|
|
54
|
-
this.code = code;
|
|
55
|
-
this.retryAfterMs = retryAfterMs;
|
|
56
|
-
if (Error.captureStackTrace) {
|
|
57
|
-
Error.captureStackTrace(this, this.constructor);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Check if this error is retryable.
|
|
62
|
-
*
|
|
63
|
-
* Returns true if the operation might succeed on retry (e.g., transient network issues).
|
|
64
|
-
* Returns false if the error requires user action to resolve (e.g., insufficient balance).
|
|
65
|
-
*
|
|
66
|
-
* @returns true if the operation should be retried, false otherwise
|
|
67
|
-
*
|
|
68
|
-
* @example
|
|
69
|
-
* ```typescript
|
|
70
|
-
* try {
|
|
71
|
-
* await wallet.fetch(...);
|
|
72
|
-
* } catch (error) {
|
|
73
|
-
* if (error instanceof MixrPayError && error.isRetryable()) {
|
|
74
|
-
* // Retry the operation
|
|
75
|
-
* await retry();
|
|
76
|
-
* } else {
|
|
77
|
-
* // Handle permanent failure
|
|
78
|
-
* throw error;
|
|
79
|
-
* }
|
|
80
|
-
* }
|
|
81
|
-
* ```
|
|
82
|
-
*/
|
|
83
|
-
isRetryable() {
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
var InsufficientBalanceError = class extends MixrPayError {
|
|
88
|
-
/** Amount required for the payment in USD */
|
|
89
|
-
required;
|
|
90
|
-
/** Current available balance in USD */
|
|
91
|
-
available;
|
|
92
|
-
/** URL where the user can top up their wallet */
|
|
93
|
-
topUpUrl;
|
|
94
|
-
constructor(required, available) {
|
|
95
|
-
const shortage = required - available;
|
|
96
|
-
super(
|
|
97
|
-
`Insufficient balance: need $${required.toFixed(2)}, have $${available.toFixed(2)} (short $${shortage.toFixed(2)}). Top up your wallet to continue.`,
|
|
98
|
-
"INSUFFICIENT_BALANCE"
|
|
99
|
-
);
|
|
100
|
-
this.name = "InsufficientBalanceError";
|
|
101
|
-
this.required = required;
|
|
102
|
-
this.available = available;
|
|
103
|
-
this.topUpUrl = "https://mixrpay.com/manage/wallet";
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
var SessionKeyExpiredError = class extends MixrPayError {
|
|
107
|
-
/** When the session key expired */
|
|
108
|
-
expiredAt;
|
|
109
|
-
constructor(expiredAt) {
|
|
110
|
-
super(
|
|
111
|
-
`Session key expired at ${expiredAt}. Request a new session key from the wallet owner at https://mixrpay.com/manage/invites`,
|
|
112
|
-
"SESSION_KEY_EXPIRED"
|
|
113
|
-
);
|
|
114
|
-
this.name = "SessionKeyExpiredError";
|
|
115
|
-
this.expiredAt = expiredAt;
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
var SpendingLimitExceededError = class extends MixrPayError {
|
|
119
|
-
/** Type of limit that was exceeded */
|
|
120
|
-
limitType;
|
|
121
|
-
/** The limit amount in USD */
|
|
122
|
-
limit;
|
|
123
|
-
/** The amount that was attempted in USD */
|
|
124
|
-
attempted;
|
|
125
|
-
constructor(limitType, limit, attempted) {
|
|
126
|
-
const limitNames = {
|
|
127
|
-
per_tx: "Per-transaction",
|
|
128
|
-
daily: "Daily",
|
|
129
|
-
total: "Total",
|
|
130
|
-
client_max: "Client-side"
|
|
131
|
-
};
|
|
132
|
-
const limitName = limitNames[limitType] || limitType;
|
|
133
|
-
const suggestion = limitType === "daily" ? "Wait until tomorrow or request a higher limit." : limitType === "client_max" ? "Increase maxPaymentUsd in your AgentWallet configuration." : "Request a new session key with a higher limit.";
|
|
134
|
-
super(
|
|
135
|
-
`${limitName} spending limit exceeded: limit is $${limit.toFixed(2)}, attempted $${attempted.toFixed(2)}. ${suggestion}`,
|
|
136
|
-
"SPENDING_LIMIT_EXCEEDED"
|
|
137
|
-
);
|
|
138
|
-
this.name = "SpendingLimitExceededError";
|
|
139
|
-
this.limitType = limitType;
|
|
140
|
-
this.limit = limit;
|
|
141
|
-
this.attempted = attempted;
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Daily limits reset at midnight, so those are retryable (after waiting).
|
|
145
|
-
* Other limit types require user action (new session key, config change).
|
|
146
|
-
* @returns true only for daily limits
|
|
147
|
-
*/
|
|
148
|
-
isRetryable() {
|
|
149
|
-
return this.limitType === "daily";
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
var PaymentFailedError = class extends MixrPayError {
|
|
153
|
-
/** Detailed reason for the failure */
|
|
154
|
-
reason;
|
|
155
|
-
/** Transaction hash if the tx was submitted (for debugging) */
|
|
156
|
-
txHash;
|
|
157
|
-
constructor(reason, txHash) {
|
|
158
|
-
let message = `Payment failed: ${reason}`;
|
|
159
|
-
if (txHash) {
|
|
160
|
-
message += ` (tx: ${txHash} - check on basescan.org)`;
|
|
161
|
-
}
|
|
162
|
-
super(message, "PAYMENT_FAILED");
|
|
163
|
-
this.name = "PaymentFailedError";
|
|
164
|
-
this.reason = reason;
|
|
165
|
-
this.txHash = txHash;
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Payment failures are often transient (network issues, temporary server errors).
|
|
169
|
-
* @returns true - payment failures should generally be retried
|
|
170
|
-
*/
|
|
171
|
-
isRetryable() {
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
var InvalidSessionKeyError = class extends MixrPayError {
|
|
176
|
-
/** Detailed reason why the key is invalid */
|
|
177
|
-
reason;
|
|
178
|
-
constructor(reason = "Invalid session key format") {
|
|
179
|
-
super(
|
|
180
|
-
`${reason}. Session keys should be in format: sk_live_<64 hex chars> or sk_test_<64 hex chars>. Get one from https://mixrpay.com/manage/invites`,
|
|
181
|
-
"INVALID_SESSION_KEY"
|
|
182
|
-
);
|
|
183
|
-
this.name = "InvalidSessionKeyError";
|
|
184
|
-
this.reason = reason;
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
var X402ProtocolError = class extends MixrPayError {
|
|
188
|
-
/** Detailed reason for the protocol error */
|
|
189
|
-
reason;
|
|
190
|
-
constructor(reason) {
|
|
191
|
-
super(
|
|
192
|
-
`x402 protocol error: ${reason}. This may indicate a server configuration issue. If the problem persists, contact the API provider.`,
|
|
193
|
-
"X402_PROTOCOL_ERROR"
|
|
194
|
-
);
|
|
195
|
-
this.name = "X402ProtocolError";
|
|
196
|
-
this.reason = reason;
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Protocol errors may be caused by temporary server issues.
|
|
200
|
-
* @returns true - worth retrying in case server recovers
|
|
201
|
-
*/
|
|
202
|
-
isRetryable() {
|
|
203
|
-
return true;
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
var SessionExpiredError = class extends MixrPayError {
|
|
207
|
-
/** ID of the expired session */
|
|
208
|
-
sessionId;
|
|
209
|
-
/** When the session expired (ISO string or Date) */
|
|
210
|
-
expiredAt;
|
|
211
|
-
constructor(sessionId, expiredAt) {
|
|
212
|
-
super(
|
|
213
|
-
`Session ${sessionId} has expired${expiredAt ? ` at ${expiredAt}` : ""}. A new session will be created automatically on your next request.`,
|
|
214
|
-
"SESSION_EXPIRED"
|
|
215
|
-
);
|
|
216
|
-
this.name = "SessionExpiredError";
|
|
217
|
-
this.sessionId = sessionId;
|
|
218
|
-
this.expiredAt = expiredAt;
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
var SessionLimitExceededError = class extends MixrPayError {
|
|
222
|
-
/** ID of the session */
|
|
223
|
-
sessionId;
|
|
224
|
-
/** The session's spending limit in USD */
|
|
225
|
-
limit;
|
|
226
|
-
/** The amount requested in USD */
|
|
227
|
-
requested;
|
|
228
|
-
/** Remaining balance in the session (limit - used) */
|
|
229
|
-
remaining;
|
|
230
|
-
constructor(limit, requested, remaining, sessionId) {
|
|
231
|
-
super(
|
|
232
|
-
`Session spending limit exceeded: limit is $${limit.toFixed(2)}, requested $${requested.toFixed(2)}, remaining $${remaining.toFixed(2)}. Create a new session with a higher limit to continue.`,
|
|
233
|
-
"SESSION_LIMIT_EXCEEDED"
|
|
234
|
-
);
|
|
235
|
-
this.name = "SessionLimitExceededError";
|
|
236
|
-
this.sessionId = sessionId;
|
|
237
|
-
this.limit = limit;
|
|
238
|
-
this.requested = requested;
|
|
239
|
-
this.remaining = remaining;
|
|
240
|
-
}
|
|
241
|
-
};
|
|
242
|
-
var SessionNotFoundError = class extends MixrPayError {
|
|
243
|
-
/** The session ID that was not found */
|
|
244
|
-
sessionId;
|
|
245
|
-
constructor(sessionId) {
|
|
246
|
-
super(
|
|
247
|
-
`Session ${sessionId} not found. It may have been deleted or never existed. Create a new session with getOrCreateSession().`,
|
|
248
|
-
"SESSION_NOT_FOUND"
|
|
249
|
-
);
|
|
250
|
-
this.name = "SessionNotFoundError";
|
|
251
|
-
this.sessionId = sessionId;
|
|
252
|
-
}
|
|
253
|
-
};
|
|
254
|
-
var SessionRevokedError = class extends MixrPayError {
|
|
255
|
-
/** The session ID that was revoked */
|
|
256
|
-
sessionId;
|
|
257
|
-
/** Optional reason for revocation */
|
|
258
|
-
reason;
|
|
259
|
-
constructor(sessionId, reason) {
|
|
260
|
-
super(
|
|
261
|
-
`Session ${sessionId} has been revoked${reason ? `: ${reason}` : ""}. Create a new session with getOrCreateSession().`,
|
|
262
|
-
"SESSION_REVOKED"
|
|
263
|
-
);
|
|
264
|
-
this.name = "SessionRevokedError";
|
|
265
|
-
this.sessionId = sessionId;
|
|
266
|
-
this.reason = reason;
|
|
267
|
-
}
|
|
268
|
-
};
|
|
269
|
-
var MerchantNotAllowedError = class extends MixrPayError {
|
|
270
|
-
/** The merchant/tool that was attempted */
|
|
271
|
-
attempted;
|
|
272
|
-
/** The patterns that are allowed by the session */
|
|
273
|
-
allowedPatterns;
|
|
274
|
-
constructor(attempted, allowedPatterns) {
|
|
275
|
-
const patternsPreview = allowedPatterns.slice(0, 3).join(", ");
|
|
276
|
-
const suffix = allowedPatterns.length > 3 ? "..." : "";
|
|
277
|
-
super(
|
|
278
|
-
`Payment to "${attempted}" not allowed. Session allowlist: ${patternsPreview}${suffix}. Update the session allowlist or create a new session.`,
|
|
279
|
-
"MERCHANT_NOT_ALLOWED"
|
|
280
|
-
);
|
|
281
|
-
this.name = "MerchantNotAllowedError";
|
|
282
|
-
this.attempted = attempted;
|
|
283
|
-
this.allowedPatterns = allowedPatterns;
|
|
284
|
-
}
|
|
285
|
-
};
|
|
286
|
-
function isMixrPayError(error) {
|
|
287
|
-
return error instanceof MixrPayError;
|
|
288
|
-
}
|
|
289
|
-
function getErrorMessage(error) {
|
|
290
|
-
if (error instanceof MixrPayError) {
|
|
291
|
-
return error.message;
|
|
292
|
-
}
|
|
293
|
-
if (error instanceof Error) {
|
|
294
|
-
return error.message;
|
|
295
|
-
}
|
|
296
|
-
return String(error);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// src/session-key.ts
|
|
300
|
-
var USDC_ADDRESSES = {
|
|
301
|
-
8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
302
|
-
// Base Mainnet
|
|
303
|
-
84532: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
|
|
304
|
-
// Base Sepolia
|
|
305
|
-
};
|
|
306
|
-
var USDC_EIP712_DOMAIN_BASE = {
|
|
307
|
-
name: "USD Coin",
|
|
308
|
-
version: "2"
|
|
309
|
-
};
|
|
310
|
-
var TRANSFER_WITH_AUTHORIZATION_TYPES = {
|
|
311
|
-
TransferWithAuthorization: [
|
|
312
|
-
{ name: "from", type: "address" },
|
|
313
|
-
{ name: "to", type: "address" },
|
|
314
|
-
{ name: "value", type: "uint256" },
|
|
315
|
-
{ name: "validAfter", type: "uint256" },
|
|
316
|
-
{ name: "validBefore", type: "uint256" },
|
|
317
|
-
{ name: "nonce", type: "bytes32" }
|
|
318
|
-
]
|
|
319
|
-
};
|
|
320
|
-
var SessionKey = class _SessionKey {
|
|
321
|
-
privateKey;
|
|
322
|
-
account;
|
|
323
|
-
address;
|
|
324
|
-
isTest;
|
|
325
|
-
constructor(privateKey, isTest) {
|
|
326
|
-
this.privateKey = privateKey;
|
|
327
|
-
this.account = (0, import_accounts.privateKeyToAccount)(privateKey);
|
|
328
|
-
this.address = this.account.address;
|
|
329
|
-
this.isTest = isTest;
|
|
330
|
-
}
|
|
331
|
-
/**
|
|
332
|
-
* Get the private key as hex string (without 0x prefix).
|
|
333
|
-
* Used for API authentication headers.
|
|
334
|
-
*/
|
|
335
|
-
get privateKeyHex() {
|
|
336
|
-
return this.privateKey.slice(2);
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* Get the raw private key with 0x prefix.
|
|
340
|
-
* Used internally for signing operations.
|
|
341
|
-
*/
|
|
342
|
-
get rawPrivateKey() {
|
|
343
|
-
return this.privateKey;
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Parse a session key string into a SessionKey object.
|
|
347
|
-
*
|
|
348
|
-
* @param sessionKey - Session key in format sk_live_... or sk_test_...
|
|
349
|
-
* @returns SessionKey object
|
|
350
|
-
* @throws InvalidSessionKeyError if the format is invalid
|
|
351
|
-
*/
|
|
352
|
-
static fromString(sessionKey) {
|
|
353
|
-
const pattern = /^sk_(live|test)_([a-fA-F0-9]{64})$/;
|
|
354
|
-
const match = sessionKey.match(pattern);
|
|
355
|
-
if (!match) {
|
|
356
|
-
throw new InvalidSessionKeyError(
|
|
357
|
-
"Session key must be in format sk_live_{64_hex} or sk_test_{64_hex}"
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
const env = match[1];
|
|
361
|
-
const hexKey = match[2];
|
|
362
|
-
try {
|
|
363
|
-
const privateKey = `0x${hexKey}`;
|
|
364
|
-
return new _SessionKey(privateKey, env === "test");
|
|
365
|
-
} catch (e) {
|
|
366
|
-
throw new InvalidSessionKeyError(`Failed to decode session key: ${e}`);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
/**
|
|
370
|
-
* Sign EIP-712 typed data for TransferWithAuthorization.
|
|
371
|
-
*
|
|
372
|
-
* @param domain - EIP-712 domain
|
|
373
|
-
* @param message - Transfer authorization message
|
|
374
|
-
* @returns Hex-encoded signature
|
|
375
|
-
*/
|
|
376
|
-
async signTransferAuthorization(domain, message) {
|
|
377
|
-
return (0, import_accounts.signTypedData)({
|
|
378
|
-
privateKey: this.privateKey,
|
|
379
|
-
domain,
|
|
380
|
-
types: TRANSFER_WITH_AUTHORIZATION_TYPES,
|
|
381
|
-
primaryType: "TransferWithAuthorization",
|
|
382
|
-
message
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
/**
|
|
386
|
-
* Sign a plain message (EIP-191 personal sign).
|
|
387
|
-
*
|
|
388
|
-
* @param message - Message to sign
|
|
389
|
-
* @returns Hex-encoded signature
|
|
390
|
-
*/
|
|
391
|
-
async signMessage(message) {
|
|
392
|
-
return (0, import_accounts.signMessage)({
|
|
393
|
-
privateKey: this.privateKey,
|
|
394
|
-
message
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
/**
|
|
398
|
-
* Get the chain ID based on whether this is a test key.
|
|
399
|
-
*/
|
|
400
|
-
getDefaultChainId() {
|
|
401
|
-
return this.isTest ? 84532 : 8453;
|
|
402
|
-
}
|
|
403
|
-
};
|
|
404
|
-
function buildTransferAuthorizationData(params) {
|
|
405
|
-
const usdcAddress = USDC_ADDRESSES[params.chainId];
|
|
406
|
-
if (!usdcAddress) {
|
|
407
|
-
throw new Error(`USDC not supported on chain ${params.chainId}`);
|
|
408
|
-
}
|
|
409
|
-
const domain = {
|
|
410
|
-
...USDC_EIP712_DOMAIN_BASE,
|
|
411
|
-
chainId: params.chainId,
|
|
412
|
-
verifyingContract: usdcAddress
|
|
413
|
-
};
|
|
414
|
-
const message = {
|
|
415
|
-
from: params.fromAddress,
|
|
416
|
-
to: params.toAddress,
|
|
417
|
-
value: params.value,
|
|
418
|
-
validAfter: params.validAfter,
|
|
419
|
-
validBefore: params.validBefore,
|
|
420
|
-
nonce: params.nonce
|
|
421
|
-
};
|
|
422
|
-
return { domain, message };
|
|
423
|
-
}
|
|
424
|
-
function generateNonce() {
|
|
425
|
-
const bytes = new Uint8Array(32);
|
|
426
|
-
crypto.getRandomValues(bytes);
|
|
427
|
-
return `0x${Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
428
|
-
}
|
|
429
|
-
function buildSessionAuthMessage(timestamp, address) {
|
|
430
|
-
return `MixrPay:${timestamp}:${address.toLowerCase()}`;
|
|
431
|
-
}
|
|
432
|
-
async function createSessionAuthPayload(sessionKey) {
|
|
433
|
-
const timestamp = Date.now();
|
|
434
|
-
const message = buildSessionAuthMessage(timestamp, sessionKey.address);
|
|
435
|
-
const signature = await sessionKey.signMessage(message);
|
|
436
|
-
return {
|
|
437
|
-
address: sessionKey.address,
|
|
438
|
-
timestamp,
|
|
439
|
-
signature
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// src/agent-wallet.ts
|
|
444
|
-
var import_accounts2 = require("viem/accounts");
|
|
445
|
-
|
|
446
|
-
// src/x402.ts
|
|
447
|
-
async function parse402Response(response) {
|
|
448
|
-
let paymentData = null;
|
|
449
|
-
const paymentHeader = response.headers.get("X-Payment-Required");
|
|
450
|
-
if (paymentHeader) {
|
|
451
|
-
try {
|
|
452
|
-
paymentData = JSON.parse(paymentHeader);
|
|
453
|
-
} catch {
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
if (!paymentData) {
|
|
457
|
-
const authHeader = response.headers.get("WWW-Authenticate");
|
|
458
|
-
if (authHeader?.startsWith("X-402 ")) {
|
|
459
|
-
try {
|
|
460
|
-
const encoded = authHeader.slice(6);
|
|
461
|
-
paymentData = JSON.parse(atob(encoded));
|
|
462
|
-
} catch {
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
if (!paymentData) {
|
|
467
|
-
try {
|
|
468
|
-
paymentData = await response.json();
|
|
469
|
-
} catch {
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
if (!paymentData) {
|
|
473
|
-
throw new X402ProtocolError("Could not parse payment requirements from 402 response");
|
|
474
|
-
}
|
|
475
|
-
if (!paymentData.recipient) {
|
|
476
|
-
throw new X402ProtocolError("Missing recipient in payment requirements");
|
|
477
|
-
}
|
|
478
|
-
if (!paymentData.amount) {
|
|
479
|
-
throw new X402ProtocolError("Missing amount in payment requirements");
|
|
480
|
-
}
|
|
481
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
482
|
-
return {
|
|
483
|
-
recipient: paymentData.recipient,
|
|
484
|
-
amount: BigInt(paymentData.amount),
|
|
485
|
-
currency: paymentData.currency || "USDC",
|
|
486
|
-
chainId: paymentData.chainId || paymentData.chain_id || 8453,
|
|
487
|
-
facilitatorUrl: paymentData.facilitatorUrl || paymentData.facilitator_url || "https://x402.org/facilitator",
|
|
488
|
-
nonce: paymentData.nonce || generateNonce().slice(2),
|
|
489
|
-
// Remove 0x prefix for storage
|
|
490
|
-
expiresAt: paymentData.expiresAt || paymentData.expires_at || now + 300,
|
|
491
|
-
description: paymentData.description
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
async function buildXPaymentHeader(requirements, sessionKey, walletAddress) {
|
|
495
|
-
const nonceHex = requirements.nonce.length === 64 ? `0x${requirements.nonce}` : generateNonce();
|
|
496
|
-
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
497
|
-
const validAfter = now - 60n;
|
|
498
|
-
const validBefore = BigInt(requirements.expiresAt);
|
|
499
|
-
const { domain, message } = buildTransferAuthorizationData({
|
|
500
|
-
fromAddress: walletAddress,
|
|
501
|
-
toAddress: requirements.recipient,
|
|
502
|
-
value: requirements.amount,
|
|
503
|
-
validAfter,
|
|
504
|
-
validBefore,
|
|
505
|
-
nonce: nonceHex,
|
|
506
|
-
chainId: requirements.chainId
|
|
507
|
-
});
|
|
508
|
-
const signature = await sessionKey.signTransferAuthorization(domain, message);
|
|
509
|
-
const paymentPayload = {
|
|
510
|
-
x402Version: 1,
|
|
511
|
-
scheme: "exact",
|
|
512
|
-
network: requirements.chainId === 8453 ? "base" : "base-sepolia",
|
|
513
|
-
payload: {
|
|
514
|
-
signature,
|
|
515
|
-
authorization: {
|
|
516
|
-
from: walletAddress,
|
|
517
|
-
to: requirements.recipient,
|
|
518
|
-
value: requirements.amount.toString(),
|
|
519
|
-
validAfter: validAfter.toString(),
|
|
520
|
-
validBefore: validBefore.toString(),
|
|
521
|
-
nonce: nonceHex
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
};
|
|
525
|
-
return btoa(JSON.stringify(paymentPayload));
|
|
526
|
-
}
|
|
527
|
-
function isPaymentExpired(requirements) {
|
|
528
|
-
return Date.now() / 1e3 > requirements.expiresAt;
|
|
529
|
-
}
|
|
530
|
-
function getAmountUsd(requirements) {
|
|
531
|
-
return Number(requirements.amount) / 1e6;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// src/agent-wallet.ts
|
|
535
|
-
var SDK_VERSION = "0.8.7";
|
|
536
|
-
var DEFAULT_BASE_URL = process.env.MIXRPAY_BASE_URL || "https://www.mixrpay.com";
|
|
537
|
-
var DEFAULT_TIMEOUT = 3e4;
|
|
538
|
-
var NETWORKS = {
|
|
539
|
-
BASE_MAINNET: { chainId: 8453, name: "Base", isTestnet: false },
|
|
540
|
-
BASE_SEPOLIA: { chainId: 84532, name: "Base Sepolia", isTestnet: true }
|
|
541
|
-
};
|
|
542
|
-
var LOG_LEVELS = {
|
|
543
|
-
debug: 0,
|
|
544
|
-
info: 1,
|
|
545
|
-
warn: 2,
|
|
546
|
-
error: 3,
|
|
547
|
-
none: 4
|
|
548
|
-
};
|
|
549
|
-
var Logger = class {
|
|
550
|
-
level;
|
|
551
|
-
prefix;
|
|
552
|
-
constructor(level = "none", prefix = "[MixrPay]") {
|
|
553
|
-
this.level = level;
|
|
554
|
-
this.prefix = prefix;
|
|
555
|
-
}
|
|
556
|
-
setLevel(level) {
|
|
557
|
-
this.level = level;
|
|
558
|
-
}
|
|
559
|
-
shouldLog(level) {
|
|
560
|
-
return LOG_LEVELS[level] >= LOG_LEVELS[this.level];
|
|
561
|
-
}
|
|
562
|
-
debug(...args) {
|
|
563
|
-
if (this.shouldLog("debug")) {
|
|
564
|
-
console.log(`${this.prefix} \u{1F50D}`, ...args);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
info(...args) {
|
|
568
|
-
if (this.shouldLog("info")) {
|
|
569
|
-
console.log(`${this.prefix} \u2139\uFE0F`, ...args);
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
warn(...args) {
|
|
573
|
-
if (this.shouldLog("warn")) {
|
|
574
|
-
console.warn(`${this.prefix} \u26A0\uFE0F`, ...args);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
error(...args) {
|
|
578
|
-
if (this.shouldLog("error")) {
|
|
579
|
-
console.error(`${this.prefix} \u274C`, ...args);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
payment(amount, recipient, description) {
|
|
583
|
-
if (this.shouldLog("info")) {
|
|
584
|
-
const desc = description ? ` for "${description}"` : "";
|
|
585
|
-
console.log(`${this.prefix} \u{1F4B8} Paid $${amount.toFixed(4)} to ${recipient.slice(0, 10)}...${desc}`);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
};
|
|
589
|
-
var AgentWallet = class {
|
|
590
|
-
sessionKey;
|
|
591
|
-
walletAddress;
|
|
592
|
-
maxPaymentUsd;
|
|
593
|
-
onPayment;
|
|
594
|
-
baseUrl;
|
|
595
|
-
timeout;
|
|
596
|
-
logger;
|
|
597
|
-
// Payment tracking
|
|
598
|
-
payments = [];
|
|
599
|
-
totalSpentUsd = 0;
|
|
600
|
-
// Session key info cache
|
|
601
|
-
sessionKeyInfo;
|
|
602
|
-
sessionKeyInfoFetchedAt;
|
|
603
|
-
// Merchant allowlist (fetched from server)
|
|
604
|
-
allowlist;
|
|
605
|
-
allowlistFetchedAt;
|
|
606
|
-
/**
|
|
607
|
-
* Create a new AgentWallet instance.
|
|
608
|
-
*
|
|
609
|
-
* @param config - Configuration options
|
|
610
|
-
* @throws {InvalidSessionKeyError} If the session key format is invalid
|
|
611
|
-
*
|
|
612
|
-
* @example Basic initialization
|
|
613
|
-
* ```typescript
|
|
614
|
-
* const wallet = new AgentWallet({
|
|
615
|
-
* sessionKey: 'sk_live_abc123...'
|
|
616
|
-
* });
|
|
617
|
-
* ```
|
|
618
|
-
*
|
|
619
|
-
* @example With all options
|
|
620
|
-
* ```typescript
|
|
621
|
-
* const wallet = new AgentWallet({
|
|
622
|
-
* sessionKey: 'sk_live_abc123...',
|
|
623
|
-
* maxPaymentUsd: 5.0, // Client-side safety limit
|
|
624
|
-
* onPayment: (p) => log(p), // Track payments
|
|
625
|
-
* logLevel: 'info', // Enable logging
|
|
626
|
-
* timeout: 60000, // 60s timeout
|
|
627
|
-
* });
|
|
628
|
-
* ```
|
|
629
|
-
*/
|
|
630
|
-
constructor(config) {
|
|
631
|
-
this.validateConfig(config);
|
|
632
|
-
this.sessionKey = SessionKey.fromString(config.sessionKey);
|
|
633
|
-
this.walletAddress = config.walletAddress || this.sessionKey.address;
|
|
634
|
-
this.maxPaymentUsd = config.maxPaymentUsd;
|
|
635
|
-
this.onPayment = config.onPayment;
|
|
636
|
-
this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
637
|
-
this.timeout = config.timeout || DEFAULT_TIMEOUT;
|
|
638
|
-
this.logger = new Logger(config.logLevel || "none");
|
|
639
|
-
this.logger.debug("AgentWallet initialized", {
|
|
640
|
-
walletAddress: this.walletAddress,
|
|
641
|
-
isTestnet: this.isTestnet(),
|
|
642
|
-
maxPaymentUsd: this.maxPaymentUsd
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
/**
|
|
646
|
-
* Validate the configuration before initialization.
|
|
647
|
-
*/
|
|
648
|
-
validateConfig(config) {
|
|
649
|
-
if (!config.sessionKey) {
|
|
650
|
-
throw new InvalidSessionKeyError(
|
|
651
|
-
"Session key is required. Get one from the wallet owner at https://mixrpay.com/manage/invites"
|
|
652
|
-
);
|
|
653
|
-
}
|
|
654
|
-
const key = config.sessionKey.trim();
|
|
655
|
-
if (!key.startsWith("sk_live_") && !key.startsWith("sk_test_")) {
|
|
656
|
-
throw new InvalidSessionKeyError(
|
|
657
|
-
`Invalid session key prefix. Expected 'sk_live_' (mainnet) or 'sk_test_' (testnet), got '${key.slice(0, 10)}...'`
|
|
658
|
-
);
|
|
659
|
-
}
|
|
660
|
-
const expectedLength = 8 + 64;
|
|
661
|
-
if (key.length !== expectedLength) {
|
|
662
|
-
throw new InvalidSessionKeyError(
|
|
663
|
-
`Invalid session key length. Expected ${expectedLength} characters, got ${key.length}. Make sure you copied the complete key.`
|
|
664
|
-
);
|
|
665
|
-
}
|
|
666
|
-
const hexPortion = key.slice(8);
|
|
667
|
-
if (!/^[0-9a-fA-F]+$/.test(hexPortion)) {
|
|
668
|
-
throw new InvalidSessionKeyError(
|
|
669
|
-
"Invalid session key format. The key should contain only hexadecimal characters after the prefix."
|
|
670
|
-
);
|
|
671
|
-
}
|
|
672
|
-
if (config.maxPaymentUsd !== void 0) {
|
|
673
|
-
if (config.maxPaymentUsd <= 0) {
|
|
674
|
-
throw new MixrPayError("maxPaymentUsd must be a positive number");
|
|
675
|
-
}
|
|
676
|
-
if (config.maxPaymentUsd > 1e4) {
|
|
677
|
-
this.logger?.warn("maxPaymentUsd is very high ($" + config.maxPaymentUsd + "). Consider using a lower limit for safety.");
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
// ===========================================================================
|
|
682
|
-
// Merchant Allowlist Validation
|
|
683
|
-
// ===========================================================================
|
|
684
|
-
/**
|
|
685
|
-
* Fetch the merchant allowlist from the server.
|
|
686
|
-
* Caches the result for 5 minutes to avoid excessive API calls.
|
|
687
|
-
*/
|
|
688
|
-
async fetchAllowlist() {
|
|
689
|
-
const CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
690
|
-
if (this.allowlist !== void 0 && this.allowlistFetchedAt) {
|
|
691
|
-
const age = Date.now() - this.allowlistFetchedAt;
|
|
692
|
-
if (age < CACHE_TTL_MS) {
|
|
693
|
-
return this.allowlist;
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
try {
|
|
697
|
-
const info = await this.getSessionKeyInfo();
|
|
698
|
-
this.allowlist = info.allowedMerchants || [];
|
|
699
|
-
this.allowlistFetchedAt = Date.now();
|
|
700
|
-
this.logger.debug("Fetched allowlist", {
|
|
701
|
-
patterns: this.allowlist.length,
|
|
702
|
-
allowAll: this.allowlist.length === 0
|
|
703
|
-
});
|
|
704
|
-
return this.allowlist;
|
|
705
|
-
} catch (error) {
|
|
706
|
-
this.logger.warn("Failed to fetch allowlist, allowing all merchants", { error });
|
|
707
|
-
this.allowlist = [];
|
|
708
|
-
this.allowlistFetchedAt = Date.now();
|
|
709
|
-
return this.allowlist;
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
/**
|
|
713
|
-
* Validate that a target is allowed by the session's allowlist.
|
|
714
|
-
*
|
|
715
|
-
* @param target - URL, domain, or tool name to check
|
|
716
|
-
* @param type - Type of target: 'url' for HTTP requests, 'tool' for MCP tools
|
|
717
|
-
* @throws {MerchantNotAllowedError} If target is not in allowlist
|
|
718
|
-
*/
|
|
719
|
-
async validateMerchantAllowed(target, type) {
|
|
720
|
-
const allowlist = await this.fetchAllowlist();
|
|
721
|
-
if (allowlist.length === 0) {
|
|
722
|
-
return;
|
|
723
|
-
}
|
|
724
|
-
const isAllowed = this.matchesAllowlist(target, allowlist, type);
|
|
725
|
-
if (!isAllowed) {
|
|
726
|
-
throw new MerchantNotAllowedError(target, allowlist);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
/**
|
|
730
|
-
* Check if a target matches any pattern in the allowlist.
|
|
731
|
-
*/
|
|
732
|
-
matchesAllowlist(target, allowlist, type) {
|
|
733
|
-
for (const pattern of allowlist) {
|
|
734
|
-
if (this.matchPattern(target, pattern, type)) {
|
|
735
|
-
return true;
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
return false;
|
|
739
|
-
}
|
|
740
|
-
/**
|
|
741
|
-
* Check if a target matches a single pattern.
|
|
742
|
-
*/
|
|
743
|
-
matchPattern(target, pattern, type) {
|
|
744
|
-
if (type === "url") {
|
|
745
|
-
return this.matchUrlPattern(target, pattern);
|
|
746
|
-
} else {
|
|
747
|
-
return this.matchToolPattern(target, pattern);
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
/**
|
|
751
|
-
* Match a URL or domain against a pattern.
|
|
752
|
-
*/
|
|
753
|
-
matchUrlPattern(target, pattern) {
|
|
754
|
-
let hostname;
|
|
755
|
-
try {
|
|
756
|
-
if (target.includes("://")) {
|
|
757
|
-
const url = new URL(target);
|
|
758
|
-
hostname = url.hostname.toLowerCase();
|
|
759
|
-
} else {
|
|
760
|
-
hostname = target.toLowerCase();
|
|
761
|
-
}
|
|
762
|
-
} catch {
|
|
763
|
-
return false;
|
|
764
|
-
}
|
|
765
|
-
const normalizedPattern = pattern.toLowerCase().trim();
|
|
766
|
-
let patternDomain = normalizedPattern;
|
|
767
|
-
if (patternDomain.includes("://")) {
|
|
768
|
-
try {
|
|
769
|
-
patternDomain = new URL(patternDomain).hostname;
|
|
770
|
-
} catch {
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
if (hostname === patternDomain) {
|
|
774
|
-
return true;
|
|
775
|
-
}
|
|
776
|
-
if (patternDomain.startsWith("*.")) {
|
|
777
|
-
const baseDomain = patternDomain.substring(2);
|
|
778
|
-
if (hostname.endsWith(`.${baseDomain}`) && hostname !== baseDomain) {
|
|
779
|
-
return true;
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
return false;
|
|
783
|
-
}
|
|
784
|
-
/**
|
|
785
|
-
* Match an MCP tool name against a pattern.
|
|
786
|
-
*/
|
|
787
|
-
matchToolPattern(toolName, pattern) {
|
|
788
|
-
const normalizedTool = toolName.toLowerCase().trim();
|
|
789
|
-
const normalizedPattern = pattern.toLowerCase().trim();
|
|
790
|
-
if (normalizedTool === normalizedPattern) {
|
|
791
|
-
return true;
|
|
792
|
-
}
|
|
793
|
-
if (normalizedPattern.endsWith("/*")) {
|
|
794
|
-
const baseProvider = normalizedPattern.substring(0, normalizedPattern.length - 2);
|
|
795
|
-
if (normalizedTool.startsWith(`${baseProvider}/`)) {
|
|
796
|
-
return true;
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
return false;
|
|
800
|
-
}
|
|
801
|
-
// ===========================================================================
|
|
802
|
-
// Static Agent Registration Methods
|
|
803
|
-
// ===========================================================================
|
|
804
|
-
/**
|
|
805
|
-
* Register a new agent with MixrPay.
|
|
806
|
-
*
|
|
807
|
-
* This creates a Privy-managed embedded wallet for the agent's payments.
|
|
808
|
-
* The agent proves ownership of their external wallet by signing a challenge.
|
|
809
|
-
*
|
|
810
|
-
* @param options - Registration options including the private key
|
|
811
|
-
* @returns Registration result with deposit address
|
|
812
|
-
* @throws {MixrPayError} If registration fails
|
|
813
|
-
*
|
|
814
|
-
* @example
|
|
815
|
-
* ```typescript
|
|
816
|
-
* const { depositAddress, userId } = await AgentWallet.register({
|
|
817
|
-
* privateKey: process.env.AGENT_WALLET_KEY as `0x${string}`,
|
|
818
|
-
* name: 'My Trading Agent',
|
|
819
|
-
* });
|
|
820
|
-
*
|
|
821
|
-
* console.log(`Fund your agent at: ${depositAddress}`);
|
|
822
|
-
* ```
|
|
823
|
-
*/
|
|
824
|
-
static async register(options) {
|
|
825
|
-
const { privateKey, name } = options;
|
|
826
|
-
const baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
827
|
-
const account = (0, import_accounts2.privateKeyToAccount)(privateKey);
|
|
828
|
-
const walletAddress = account.address;
|
|
829
|
-
const challengeResponse = await fetch(
|
|
830
|
-
`${baseUrl}/api/v1/agent/challenge?wallet=${walletAddress}&action=register`
|
|
831
|
-
);
|
|
832
|
-
if (!challengeResponse.ok) {
|
|
833
|
-
const error = await challengeResponse.json().catch(() => ({}));
|
|
834
|
-
throw new MixrPayError(error.error || `Failed to get challenge: ${challengeResponse.status}`);
|
|
835
|
-
}
|
|
836
|
-
const { challenge, message } = await challengeResponse.json();
|
|
837
|
-
const signature = await (0, import_accounts2.signMessage)({ message, privateKey });
|
|
838
|
-
const registerResponse = await fetch(`${baseUrl}/api/v1/agent/register`, {
|
|
839
|
-
method: "POST",
|
|
840
|
-
headers: { "Content-Type": "application/json" },
|
|
841
|
-
body: JSON.stringify({
|
|
842
|
-
challenge,
|
|
843
|
-
external_wallet: walletAddress,
|
|
844
|
-
signature,
|
|
845
|
-
name
|
|
846
|
-
})
|
|
847
|
-
});
|
|
848
|
-
if (!registerResponse.ok) {
|
|
849
|
-
const error = await registerResponse.json().catch(() => ({}));
|
|
850
|
-
const errorMessage = error.error || `Registration failed with status ${registerResponse.status}`;
|
|
851
|
-
const requestId = error.request_id;
|
|
852
|
-
const errorCode = error.code;
|
|
853
|
-
let helpText = "";
|
|
854
|
-
if (registerResponse.status === 503) {
|
|
855
|
-
helpText = " The service may be temporarily unavailable. Please try again later.";
|
|
856
|
-
} else if (registerResponse.status === 500) {
|
|
857
|
-
helpText = " This is a server error. Please contact support with the request ID.";
|
|
858
|
-
} else if (errorCode === "MISSING_CHALLENGE" || errorCode === "MISSING_SIGNATURE") {
|
|
859
|
-
helpText = " This may indicate an SDK bug. Please update to the latest version.";
|
|
860
|
-
}
|
|
861
|
-
const fullMessage = requestId ? `${errorMessage} (request_id: ${requestId})${helpText}` : `${errorMessage}${helpText}`;
|
|
862
|
-
throw new MixrPayError(fullMessage);
|
|
863
|
-
}
|
|
864
|
-
const data = await registerResponse.json();
|
|
865
|
-
return {
|
|
866
|
-
userId: data.user_id,
|
|
867
|
-
depositAddress: data.deposit_address
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
/**
|
|
871
|
-
* Check if the MixrPay server is properly configured for agent registration.
|
|
872
|
-
*
|
|
873
|
-
* Use this to diagnose registration issues before attempting to register.
|
|
874
|
-
*
|
|
875
|
-
* @param baseUrl - MixrPay API base URL (default: https://www.mixrpay.com)
|
|
876
|
-
* @returns Server health status including agent registration availability
|
|
877
|
-
*
|
|
878
|
-
* @example
|
|
879
|
-
* ```typescript
|
|
880
|
-
* const status = await AgentWallet.checkServerHealth();
|
|
881
|
-
* if (!status.agentRegistrationAvailable) {
|
|
882
|
-
* console.error('Agent registration is not available:', status);
|
|
883
|
-
* }
|
|
884
|
-
* ```
|
|
885
|
-
*/
|
|
886
|
-
static async checkServerHealth(baseUrl) {
|
|
887
|
-
const url = (baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
888
|
-
try {
|
|
889
|
-
const response = await fetch(`${url}/api/health/ready?details=true`);
|
|
890
|
-
if (!response.ok) {
|
|
891
|
-
return {
|
|
892
|
-
healthy: false,
|
|
893
|
-
database: "unknown",
|
|
894
|
-
agentRegistrationAvailable: false,
|
|
895
|
-
privyConfigured: false,
|
|
896
|
-
error: `Health check failed with status ${response.status}`
|
|
897
|
-
};
|
|
898
|
-
}
|
|
899
|
-
const data = await response.json();
|
|
900
|
-
return {
|
|
901
|
-
healthy: data.status === "ready",
|
|
902
|
-
database: data.database || "unknown",
|
|
903
|
-
agentRegistrationAvailable: data.services?.agentRegistration?.available ?? false,
|
|
904
|
-
privyConfigured: data.services?.privy?.configured ?? false
|
|
905
|
-
};
|
|
906
|
-
} catch (error) {
|
|
907
|
-
return {
|
|
908
|
-
healthy: false,
|
|
909
|
-
database: "unreachable",
|
|
910
|
-
agentRegistrationAvailable: false,
|
|
911
|
-
privyConfigured: false,
|
|
912
|
-
error: error instanceof Error ? error.message : "Failed to reach server"
|
|
913
|
-
};
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
/**
|
|
917
|
-
* Get a session key for an already-registered agent.
|
|
918
|
-
*
|
|
919
|
-
* Session keys allow the agent to make payments within the specified limits.
|
|
920
|
-
* The private key is returned ONCE - store it securely!
|
|
921
|
-
*
|
|
922
|
-
* @param options - Session key options
|
|
923
|
-
* @returns Session key result with the sk_live_ format key
|
|
924
|
-
* @throws {MixrPayError} If session key creation fails
|
|
925
|
-
*
|
|
926
|
-
* @example
|
|
927
|
-
* ```typescript
|
|
928
|
-
* const result = await AgentWallet.getSessionKey({
|
|
929
|
-
* privateKey: process.env.AGENT_WALLET_KEY as `0x${string}`,
|
|
930
|
-
* spendingLimitUsd: 100,
|
|
931
|
-
* durationDays: 30,
|
|
932
|
-
* });
|
|
933
|
-
*
|
|
934
|
-
* // Store this securely - it's your payment key!
|
|
935
|
-
* console.log(`Session key: ${result.sessionKey}`);
|
|
936
|
-
*
|
|
937
|
-
* // Use it to create an AgentWallet
|
|
938
|
-
* const wallet = new AgentWallet({ sessionKey: result.sessionKey });
|
|
939
|
-
* ```
|
|
940
|
-
*/
|
|
941
|
-
static async getSessionKey(options) {
|
|
942
|
-
const { privateKey, spendingLimitUsd, maxPerTxUsd, maxDailyUsd, durationDays } = options;
|
|
943
|
-
const baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
944
|
-
const account = (0, import_accounts2.privateKeyToAccount)(privateKey);
|
|
945
|
-
const walletAddress = account.address;
|
|
946
|
-
const challengeResponse = await fetch(
|
|
947
|
-
`${baseUrl}/api/v1/agent/challenge?wallet=${walletAddress}&action=session-key`
|
|
948
|
-
);
|
|
949
|
-
if (!challengeResponse.ok) {
|
|
950
|
-
const error = await challengeResponse.json().catch(() => ({}));
|
|
951
|
-
throw new MixrPayError(error.error || `Failed to get challenge: ${challengeResponse.status}`);
|
|
952
|
-
}
|
|
953
|
-
const { challenge, message } = await challengeResponse.json();
|
|
954
|
-
const signature = await (0, import_accounts2.signMessage)({ message, privateKey });
|
|
955
|
-
const sessionKeyResponse = await fetch(`${baseUrl}/api/v1/agent/session-key`, {
|
|
956
|
-
method: "POST",
|
|
957
|
-
headers: { "Content-Type": "application/json" },
|
|
958
|
-
body: JSON.stringify({
|
|
959
|
-
challenge,
|
|
960
|
-
external_wallet: walletAddress,
|
|
961
|
-
signature,
|
|
962
|
-
spending_limit_usd: spendingLimitUsd,
|
|
963
|
-
max_per_tx_usd: maxPerTxUsd,
|
|
964
|
-
max_daily_usd: maxDailyUsd,
|
|
965
|
-
duration_days: durationDays
|
|
966
|
-
})
|
|
967
|
-
});
|
|
968
|
-
if (!sessionKeyResponse.ok) {
|
|
969
|
-
const error = await sessionKeyResponse.json().catch(() => ({}));
|
|
970
|
-
throw new MixrPayError(error.error || `Session key creation failed: ${sessionKeyResponse.status}`);
|
|
971
|
-
}
|
|
972
|
-
const data = await sessionKeyResponse.json();
|
|
973
|
-
return {
|
|
974
|
-
sessionKey: data.session_key,
|
|
975
|
-
address: data.address,
|
|
976
|
-
sessionKeyId: data.session_key_id,
|
|
977
|
-
expiresAt: new Date(data.expires_at),
|
|
978
|
-
limits: {
|
|
979
|
-
maxTotalUsd: data.limits.max_total_usd,
|
|
980
|
-
maxPerTxUsd: data.limits.max_per_tx_usd,
|
|
981
|
-
maxDailyUsd: data.limits.max_daily_usd
|
|
982
|
-
}
|
|
983
|
-
};
|
|
984
|
-
}
|
|
985
|
-
/**
|
|
986
|
-
* Get the status of a registered agent.
|
|
987
|
-
*
|
|
988
|
-
* Returns balance, active sessions, and spending information.
|
|
989
|
-
*
|
|
990
|
-
* @param options - Status options
|
|
991
|
-
* @returns Agent status
|
|
992
|
-
* @throws {MixrPayError} If status fetch fails
|
|
993
|
-
*
|
|
994
|
-
* @example
|
|
995
|
-
* ```typescript
|
|
996
|
-
* const status = await AgentWallet.getStatus({
|
|
997
|
-
* privateKey: process.env.AGENT_WALLET_KEY as `0x${string}`,
|
|
998
|
-
* });
|
|
999
|
-
*
|
|
1000
|
-
* console.log(`Balance: $${status.balanceUsd}`);
|
|
1001
|
-
* console.log(`Active sessions: ${status.activeSessions.length}`);
|
|
1002
|
-
* ```
|
|
1003
|
-
*/
|
|
1004
|
-
static async getStatus(options) {
|
|
1005
|
-
const { privateKey } = options;
|
|
1006
|
-
const baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
1007
|
-
const account = (0, import_accounts2.privateKeyToAccount)(privateKey);
|
|
1008
|
-
const walletAddress = account.address;
|
|
1009
|
-
const challengeResponse = await fetch(
|
|
1010
|
-
`${baseUrl}/api/v1/agent/challenge?wallet=${walletAddress}&action=status`
|
|
1011
|
-
);
|
|
1012
|
-
if (!challengeResponse.ok) {
|
|
1013
|
-
const error = await challengeResponse.json().catch(() => ({}));
|
|
1014
|
-
throw new MixrPayError(error.error || `Failed to get challenge: ${challengeResponse.status}`);
|
|
1015
|
-
}
|
|
1016
|
-
const { challenge, message } = await challengeResponse.json();
|
|
1017
|
-
const signature = await (0, import_accounts2.signMessage)({ message, privateKey });
|
|
1018
|
-
const statusResponse = await fetch(
|
|
1019
|
-
`${baseUrl}/api/v1/agent/status?challenge=${challenge}&external_wallet=${walletAddress}&signature=${encodeURIComponent(signature)}`
|
|
1020
|
-
);
|
|
1021
|
-
if (!statusResponse.ok) {
|
|
1022
|
-
const error = await statusResponse.json().catch(() => ({}));
|
|
1023
|
-
throw new MixrPayError(error.error || `Failed to get status: ${statusResponse.status}`);
|
|
1024
|
-
}
|
|
1025
|
-
const data = await statusResponse.json();
|
|
1026
|
-
return {
|
|
1027
|
-
depositAddress: data.deposit_address,
|
|
1028
|
-
balanceUsd: data.balance_usd,
|
|
1029
|
-
activeSessions: data.active_sessions.map((s) => ({
|
|
1030
|
-
id: s.id,
|
|
1031
|
-
expiresAt: new Date(s.expires_at),
|
|
1032
|
-
totalSpentUsd: s.total_spent_usd,
|
|
1033
|
-
remainingUsd: s.remaining_usd,
|
|
1034
|
-
maxTotalUsd: s.max_total_usd
|
|
1035
|
-
})),
|
|
1036
|
-
totalSpentUsd: data.total_spent_usd
|
|
1037
|
-
};
|
|
1038
|
-
}
|
|
1039
|
-
/**
|
|
1040
|
-
* Revoke a session key.
|
|
1041
|
-
*
|
|
1042
|
-
* After revocation, the session key can no longer be used for payments.
|
|
1043
|
-
*
|
|
1044
|
-
* @param options - Revoke options
|
|
1045
|
-
* @returns true if revoked successfully
|
|
1046
|
-
* @throws {MixrPayError} If revocation fails
|
|
1047
|
-
*
|
|
1048
|
-
* @example
|
|
1049
|
-
* ```typescript
|
|
1050
|
-
* const success = await AgentWallet.revokeSessionKey({
|
|
1051
|
-
* privateKey: process.env.AGENT_WALLET_KEY as `0x${string}`,
|
|
1052
|
-
* sessionKeyId: 'session-key-uuid',
|
|
1053
|
-
* });
|
|
1054
|
-
*
|
|
1055
|
-
* if (success) {
|
|
1056
|
-
* console.log('Session key revoked');
|
|
1057
|
-
* }
|
|
1058
|
-
* ```
|
|
1059
|
-
*/
|
|
1060
|
-
static async revokeSessionKey(options) {
|
|
1061
|
-
const { privateKey, sessionKeyId } = options;
|
|
1062
|
-
const baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
1063
|
-
const account = (0, import_accounts2.privateKeyToAccount)(privateKey);
|
|
1064
|
-
const walletAddress = account.address;
|
|
1065
|
-
const challengeResponse = await fetch(
|
|
1066
|
-
`${baseUrl}/api/v1/agent/challenge?wallet=${walletAddress}&action=revoke`
|
|
1067
|
-
);
|
|
1068
|
-
if (!challengeResponse.ok) {
|
|
1069
|
-
const error = await challengeResponse.json().catch(() => ({}));
|
|
1070
|
-
throw new MixrPayError(error.error || `Failed to get challenge: ${challengeResponse.status}`);
|
|
1071
|
-
}
|
|
1072
|
-
const { challenge, message } = await challengeResponse.json();
|
|
1073
|
-
const signature = await (0, import_accounts2.signMessage)({ message, privateKey });
|
|
1074
|
-
const revokeResponse = await fetch(`${baseUrl}/api/v1/agent/session-key/revoke`, {
|
|
1075
|
-
method: "POST",
|
|
1076
|
-
headers: { "Content-Type": "application/json" },
|
|
1077
|
-
body: JSON.stringify({
|
|
1078
|
-
challenge,
|
|
1079
|
-
external_wallet: walletAddress,
|
|
1080
|
-
signature,
|
|
1081
|
-
session_key_id: sessionKeyId
|
|
1082
|
-
})
|
|
1083
|
-
});
|
|
1084
|
-
if (!revokeResponse.ok) {
|
|
1085
|
-
const error = await revokeResponse.json().catch(() => ({}));
|
|
1086
|
-
throw new MixrPayError(error.error || `Revocation failed: ${revokeResponse.status}`);
|
|
1087
|
-
}
|
|
1088
|
-
return true;
|
|
1089
|
-
}
|
|
1090
|
-
/**
|
|
1091
|
-
* Withdraw USDC from agent's MixrPay wallet to their external wallet.
|
|
1092
|
-
*
|
|
1093
|
-
* SECURITY: Withdrawals can ONLY go to the agent's own registration wallet
|
|
1094
|
-
* (the wallet used during `register()`). This prevents prompt injection
|
|
1095
|
-
* attacks where a compromised agent might be tricked into withdrawing
|
|
1096
|
-
* to an attacker's address.
|
|
1097
|
-
*
|
|
1098
|
-
* @param options - Withdrawal options
|
|
1099
|
-
* @returns Withdrawal result with transaction hash
|
|
1100
|
-
* @throws {MixrPayError} If withdrawal fails
|
|
1101
|
-
*
|
|
1102
|
-
* @example
|
|
1103
|
-
* ```typescript
|
|
1104
|
-
* const result = await AgentWallet.withdraw({
|
|
1105
|
-
* privateKey: process.env.AGENT_WALLET_KEY as `0x${string}`,
|
|
1106
|
-
* amountUsd: 50.00,
|
|
1107
|
-
* });
|
|
1108
|
-
*
|
|
1109
|
-
* console.log(`Withdrew $${result.amountUsd}`);
|
|
1110
|
-
* console.log(`Transaction: ${result.txHash}`);
|
|
1111
|
-
* console.log(`Remaining balance: $${result.remainingBalanceUsd}`);
|
|
1112
|
-
* ```
|
|
1113
|
-
*/
|
|
1114
|
-
static async withdraw(options) {
|
|
1115
|
-
const { privateKey, amountUsd } = options;
|
|
1116
|
-
const baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
1117
|
-
const account = (0, import_accounts2.privateKeyToAccount)(privateKey);
|
|
1118
|
-
const walletAddress = account.address;
|
|
1119
|
-
const challengeResponse = await fetch(
|
|
1120
|
-
`${baseUrl}/api/v1/agent/challenge?wallet=${walletAddress}&action=withdraw`
|
|
1121
|
-
);
|
|
1122
|
-
if (!challengeResponse.ok) {
|
|
1123
|
-
const error = await challengeResponse.json().catch(() => ({}));
|
|
1124
|
-
throw new MixrPayError(error.error || `Failed to get challenge: ${challengeResponse.status}`);
|
|
1125
|
-
}
|
|
1126
|
-
const { challenge, message } = await challengeResponse.json();
|
|
1127
|
-
const signature = await (0, import_accounts2.signMessage)({ message, privateKey });
|
|
1128
|
-
const withdrawResponse = await fetch(`${baseUrl}/api/v1/agent/withdraw`, {
|
|
1129
|
-
method: "POST",
|
|
1130
|
-
headers: { "Content-Type": "application/json" },
|
|
1131
|
-
body: JSON.stringify({
|
|
1132
|
-
challenge,
|
|
1133
|
-
external_wallet: walletAddress,
|
|
1134
|
-
signature,
|
|
1135
|
-
to_address: walletAddress,
|
|
1136
|
-
// Always withdraw to self
|
|
1137
|
-
amount_usd: amountUsd
|
|
1138
|
-
})
|
|
1139
|
-
});
|
|
1140
|
-
if (!withdrawResponse.ok) {
|
|
1141
|
-
const error = await withdrawResponse.json().catch(() => ({}));
|
|
1142
|
-
throw new MixrPayError(error.error || `Withdrawal failed: ${withdrawResponse.status}`);
|
|
1143
|
-
}
|
|
1144
|
-
const data = await withdrawResponse.json();
|
|
1145
|
-
return {
|
|
1146
|
-
txHash: data.tx_hash,
|
|
1147
|
-
amountUsd: data.amount_usd,
|
|
1148
|
-
remainingBalanceUsd: data.remaining_balance_usd
|
|
1149
|
-
};
|
|
1150
|
-
}
|
|
1151
|
-
/**
|
|
1152
|
-
* Claim an agent invite code to receive a session key for the inviter's wallet.
|
|
1153
|
-
*
|
|
1154
|
-
* This allows an agent to get a pre-configured session key from a human wallet owner
|
|
1155
|
-
* without needing to register their own wallet or fund it. The human sets the budget
|
|
1156
|
-
* limits and merchant whitelist when creating the invite.
|
|
1157
|
-
*
|
|
1158
|
-
* @param options - Claim invite options including the invite code and agent's private key
|
|
1159
|
-
* @returns Claim result with the new session key
|
|
1160
|
-
* @throws {MixrPayError} If claiming fails (e.g., invalid code, already claimed, expired)
|
|
1161
|
-
*
|
|
1162
|
-
* @example
|
|
1163
|
-
* ```typescript
|
|
1164
|
-
* // Human creates invite at https://mixrpay.com/manage/invites, shares code "mixr-abc123"
|
|
1165
|
-
*
|
|
1166
|
-
* const result = await AgentWallet.claimInvite({
|
|
1167
|
-
* inviteCode: 'mixr-abc123',
|
|
1168
|
-
* privateKey: process.env.AGENT_WALLET_KEY as `0x${string}`,
|
|
1169
|
-
* });
|
|
1170
|
-
*
|
|
1171
|
-
* console.log(`Got session key: ${result.sessionKey}`);
|
|
1172
|
-
* console.log(`Budget: $${result.limits.budgetUsd}/${result.limits.budgetPeriod}`);
|
|
1173
|
-
* console.log(`Invited by: ${result.inviterName}`);
|
|
1174
|
-
*
|
|
1175
|
-
* // Use the session key to make payments
|
|
1176
|
-
* const wallet = new AgentWallet({ sessionKey: result.sessionKey });
|
|
1177
|
-
* const response = await wallet.fetch('https://api.example.com/endpoint');
|
|
1178
|
-
* ```
|
|
1179
|
-
*/
|
|
1180
|
-
static async claimInvite(options) {
|
|
1181
|
-
const { inviteCode, privateKey } = options;
|
|
1182
|
-
const baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
1183
|
-
const account = (0, import_accounts2.privateKeyToAccount)(privateKey);
|
|
1184
|
-
const walletAddress = account.address;
|
|
1185
|
-
const challengeResponse = await fetch(
|
|
1186
|
-
`${baseUrl}/api/v1/agent/challenge?wallet=${walletAddress}&action=claim-invite`
|
|
1187
|
-
);
|
|
1188
|
-
if (!challengeResponse.ok) {
|
|
1189
|
-
const error = await challengeResponse.json().catch(() => ({}));
|
|
1190
|
-
throw new MixrPayError(error.error || `Failed to get challenge: ${challengeResponse.status}`);
|
|
1191
|
-
}
|
|
1192
|
-
const { challenge, message } = await challengeResponse.json();
|
|
1193
|
-
const signature = await (0, import_accounts2.signMessage)({ message, privateKey });
|
|
1194
|
-
const claimResponse = await fetch(`${baseUrl}/api/v1/agent/claim-invite`, {
|
|
1195
|
-
method: "POST",
|
|
1196
|
-
headers: { "Content-Type": "application/json" },
|
|
1197
|
-
body: JSON.stringify({
|
|
1198
|
-
invite_code: inviteCode,
|
|
1199
|
-
challenge,
|
|
1200
|
-
agent_wallet_address: walletAddress,
|
|
1201
|
-
signature
|
|
1202
|
-
})
|
|
1203
|
-
});
|
|
1204
|
-
if (!claimResponse.ok) {
|
|
1205
|
-
const error = await claimResponse.json().catch(() => ({}));
|
|
1206
|
-
const errorMessage = error.error || `Failed to claim invite: ${claimResponse.status}`;
|
|
1207
|
-
let helpText = "";
|
|
1208
|
-
if (claimResponse.status === 404) {
|
|
1209
|
-
helpText = " The invite code may be invalid or misspelled.";
|
|
1210
|
-
} else if (claimResponse.status === 400) {
|
|
1211
|
-
if (errorMessage.includes("already claimed")) {
|
|
1212
|
-
helpText = " This invite has already been used by another agent.";
|
|
1213
|
-
} else if (errorMessage.includes("expired")) {
|
|
1214
|
-
helpText = " Ask the wallet owner to create a new invite.";
|
|
1215
|
-
} else if (errorMessage.includes("revoked")) {
|
|
1216
|
-
helpText = " The wallet owner has revoked this invite.";
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
throw new MixrPayError(`${errorMessage}${helpText}`);
|
|
1220
|
-
}
|
|
1221
|
-
const data = await claimResponse.json();
|
|
1222
|
-
return {
|
|
1223
|
-
sessionKey: data.session_key,
|
|
1224
|
-
address: data.address,
|
|
1225
|
-
sessionKeyId: data.session_key_id,
|
|
1226
|
-
expiresAt: new Date(data.expires_at),
|
|
1227
|
-
limits: {
|
|
1228
|
-
budgetUsd: data.limits.budget_usd,
|
|
1229
|
-
budgetPeriod: data.limits.budget_period,
|
|
1230
|
-
maxPerTxUsd: data.limits.max_per_tx_usd
|
|
1231
|
-
},
|
|
1232
|
-
allowedMerchants: data.allowed_merchants || [],
|
|
1233
|
-
inviterName: data.inviter_name || "Anonymous"
|
|
1234
|
-
};
|
|
1235
|
-
}
|
|
1236
|
-
// ===========================================================================
|
|
1237
|
-
// Nested Budget Delegation Methods
|
|
1238
|
-
// ===========================================================================
|
|
1239
|
-
/**
|
|
1240
|
-
* Spawn a child invite for sub-agents.
|
|
1241
|
-
*
|
|
1242
|
-
* Allows an agent to create an invite code for a sub-agent with a portion
|
|
1243
|
-
* of their remaining budget (max 20%). The child inherits merchant restrictions
|
|
1244
|
-
* and cannot outlive the parent session.
|
|
1245
|
-
*
|
|
1246
|
-
* @param options - Spawn options
|
|
1247
|
-
* @returns The created child invite details
|
|
1248
|
-
*
|
|
1249
|
-
* @throws {MixrPayError} If spawning fails (insufficient budget, depth limit, etc.)
|
|
1250
|
-
*
|
|
1251
|
-
* @example
|
|
1252
|
-
* ```typescript
|
|
1253
|
-
* const wallet = new AgentWallet({ sessionKey: 'sk_live_...' });
|
|
1254
|
-
*
|
|
1255
|
-
* // Spawn a child invite for a sub-agent
|
|
1256
|
-
* const childInvite = await wallet.spawnChildInvite({
|
|
1257
|
-
* budgetUsd: 10.00, // Max 20% of available budget
|
|
1258
|
-
* name: 'Research Sub-Agent',
|
|
1259
|
-
* allowedMerchants: ['firecrawl.dev'], // Must be subset of parent
|
|
1260
|
-
* expiresInDays: 7, // Will be capped to parent's expiry
|
|
1261
|
-
* });
|
|
1262
|
-
*
|
|
1263
|
-
* console.log(`Share with sub-agent: ${childInvite.inviteCode}`);
|
|
1264
|
-
* console.log(`Child can spawn up to: $${childInvite.maxSpawnBudget}`);
|
|
1265
|
-
* ```
|
|
1266
|
-
*/
|
|
1267
|
-
async spawnChildInvite(options) {
|
|
1268
|
-
const { budgetUsd, name, allowedMerchants, expiresInDays } = options;
|
|
1269
|
-
const timestamp = Date.now();
|
|
1270
|
-
const nonce = crypto.randomUUID();
|
|
1271
|
-
const message = `spawn:${timestamp}:${nonce}`;
|
|
1272
|
-
const signature = await (0, import_accounts2.signMessage)({
|
|
1273
|
-
message,
|
|
1274
|
-
privateKey: this.sessionKey.rawPrivateKey
|
|
1275
|
-
});
|
|
1276
|
-
const response = await fetch(`${this.baseUrl}/api/v1/agent/spawn`, {
|
|
1277
|
-
method: "POST",
|
|
1278
|
-
headers: { "Content-Type": "application/json" },
|
|
1279
|
-
body: JSON.stringify({
|
|
1280
|
-
session_key: this.sessionKey.toString(),
|
|
1281
|
-
signature,
|
|
1282
|
-
message,
|
|
1283
|
-
budget_usd: budgetUsd,
|
|
1284
|
-
name,
|
|
1285
|
-
allowed_merchants: allowedMerchants,
|
|
1286
|
-
expires_in_days: expiresInDays
|
|
1287
|
-
})
|
|
1288
|
-
});
|
|
1289
|
-
if (!response.ok) {
|
|
1290
|
-
const error = await response.json().catch(() => ({}));
|
|
1291
|
-
if (response.status === 409) {
|
|
1292
|
-
throw new MixrPayError("Concurrent modification - please retry");
|
|
1293
|
-
}
|
|
1294
|
-
if (response.status === 429) {
|
|
1295
|
-
throw new MixrPayError("Rate limited - too many spawn attempts");
|
|
1296
|
-
}
|
|
1297
|
-
throw new MixrPayError(error.error || `Failed to spawn child: ${response.status}`);
|
|
1298
|
-
}
|
|
1299
|
-
const data = await response.json();
|
|
1300
|
-
return {
|
|
1301
|
-
inviteCode: data.invite_code,
|
|
1302
|
-
inviteId: data.invite_id,
|
|
1303
|
-
budgetUsd: data.budget_usd,
|
|
1304
|
-
expiresAt: new Date(data.expires_at),
|
|
1305
|
-
depth: data.depth,
|
|
1306
|
-
maxSpawnBudget: data.max_spawn_budget,
|
|
1307
|
-
allowedMerchants: data.allowed_merchants || []
|
|
1308
|
-
};
|
|
1309
|
-
}
|
|
1310
|
-
/**
|
|
1311
|
-
* Get available budget information for spawning.
|
|
1312
|
-
*
|
|
1313
|
-
* Returns the current budget status including how much can be spawned
|
|
1314
|
-
* to child agents (20% of available).
|
|
1315
|
-
*
|
|
1316
|
-
* @returns Budget information
|
|
1317
|
-
*
|
|
1318
|
-
* @example
|
|
1319
|
-
* ```typescript
|
|
1320
|
-
* const budget = await wallet.getAvailableBudget();
|
|
1321
|
-
*
|
|
1322
|
-
* console.log(`Total budget: $${budget.totalBudget}`);
|
|
1323
|
-
* console.log(`Spent: $${budget.spent}`);
|
|
1324
|
-
* console.log(`Allocated to children: $${budget.allocatedToChildren}`);
|
|
1325
|
-
* console.log(`Available: $${budget.available}`);
|
|
1326
|
-
* console.log(`Max spawn budget: $${budget.maxSpawnBudget}`);
|
|
1327
|
-
*
|
|
1328
|
-
* if (budget.canSpawn && budget.maxSpawnBudget >= 5.00) {
|
|
1329
|
-
* // Safe to spawn a $5 child
|
|
1330
|
-
* }
|
|
1331
|
-
* ```
|
|
1332
|
-
*/
|
|
1333
|
-
async getAvailableBudget() {
|
|
1334
|
-
const response = await fetch(`${this.baseUrl}/api/v1/agent/descendants`, {
|
|
1335
|
-
method: "GET",
|
|
1336
|
-
headers: {
|
|
1337
|
-
"Authorization": `Bearer ${this.sessionKey.toString()}`
|
|
1338
|
-
}
|
|
1339
|
-
});
|
|
1340
|
-
if (!response.ok) {
|
|
1341
|
-
const error = await response.json().catch(() => ({}));
|
|
1342
|
-
throw new MixrPayError(error.error || `Failed to get budget: ${response.status}`);
|
|
1343
|
-
}
|
|
1344
|
-
const data = await response.json();
|
|
1345
|
-
return {
|
|
1346
|
-
totalBudget: data.budget.total_usd,
|
|
1347
|
-
spent: data.budget.spent_usd,
|
|
1348
|
-
allocatedToChildren: data.budget.allocated_to_children_usd,
|
|
1349
|
-
available: data.budget.available_usd,
|
|
1350
|
-
maxSpawnBudget: data.max_spawn_budget,
|
|
1351
|
-
canSpawn: data.can_spawn
|
|
1352
|
-
};
|
|
1353
|
-
}
|
|
1354
|
-
/**
|
|
1355
|
-
* Get all child sessions spawned by this session.
|
|
1356
|
-
*
|
|
1357
|
-
* Returns a tree of child invites/sessions including their spending status.
|
|
1358
|
-
*
|
|
1359
|
-
* @returns Array of child sessions
|
|
1360
|
-
*
|
|
1361
|
-
* @example
|
|
1362
|
-
* ```typescript
|
|
1363
|
-
* const children = await wallet.getChildSessions();
|
|
1364
|
-
*
|
|
1365
|
-
* for (const child of children) {
|
|
1366
|
-
* console.log(`${child.name}: $${child.spentUsd}/$${child.budgetUsd}`);
|
|
1367
|
-
* console.log(` Status: ${child.status}`);
|
|
1368
|
-
* console.log(` Depth: ${child.depth}`);
|
|
1369
|
-
*
|
|
1370
|
-
* if (child.children) {
|
|
1371
|
-
* console.log(` Has ${child.children.length} grandchildren`);
|
|
1372
|
-
* }
|
|
1373
|
-
* }
|
|
1374
|
-
* ```
|
|
1375
|
-
*/
|
|
1376
|
-
async getChildSessions() {
|
|
1377
|
-
const response = await fetch(`${this.baseUrl}/api/v1/agent/descendants`, {
|
|
1378
|
-
method: "GET",
|
|
1379
|
-
headers: {
|
|
1380
|
-
"Authorization": `Bearer ${this.sessionKey.toString()}`
|
|
1381
|
-
}
|
|
1382
|
-
});
|
|
1383
|
-
if (!response.ok) {
|
|
1384
|
-
const error = await response.json().catch(() => ({}));
|
|
1385
|
-
throw new MixrPayError(error.error || `Failed to get children: ${response.status}`);
|
|
1386
|
-
}
|
|
1387
|
-
const data = await response.json();
|
|
1388
|
-
return data.children || [];
|
|
1389
|
-
}
|
|
1390
|
-
/**
|
|
1391
|
-
* List child sessions spawned by this agent.
|
|
1392
|
-
*
|
|
1393
|
-
* Alias for `getChildSessions()` for naming consistency.
|
|
1394
|
-
*
|
|
1395
|
-
* @returns Array of child session info
|
|
1396
|
-
*/
|
|
1397
|
-
async listChildSessions() {
|
|
1398
|
-
return this.getChildSessions();
|
|
1399
|
-
}
|
|
1400
|
-
// ===========================================================================
|
|
1401
|
-
// Core Methods
|
|
1402
|
-
// ===========================================================================
|
|
1403
|
-
/**
|
|
1404
|
-
* Make an HTTP request, automatically handling x402 payment if required.
|
|
1405
|
-
*
|
|
1406
|
-
* This is a drop-in replacement for the native `fetch()` function.
|
|
1407
|
-
* If the server returns 402 Payment Required:
|
|
1408
|
-
* 1. Parse payment requirements from response
|
|
1409
|
-
* 2. Sign transferWithAuthorization using session key
|
|
1410
|
-
* 3. Retry request with X-PAYMENT header
|
|
1411
|
-
*
|
|
1412
|
-
* @param url - Request URL
|
|
1413
|
-
* @param init - Request options (same as native fetch)
|
|
1414
|
-
* @returns Response from the server
|
|
1415
|
-
*
|
|
1416
|
-
* @throws {InsufficientBalanceError} If wallet doesn't have enough USDC
|
|
1417
|
-
* @throws {SessionKeyExpiredError} If session key has expired
|
|
1418
|
-
* @throws {SpendingLimitExceededError} If payment would exceed limits
|
|
1419
|
-
* @throws {PaymentFailedError} If payment transaction failed
|
|
1420
|
-
*
|
|
1421
|
-
* @example GET request
|
|
1422
|
-
* ```typescript
|
|
1423
|
-
* const response = await wallet.fetch('https://api.example.com/data');
|
|
1424
|
-
* const data = await response.json();
|
|
1425
|
-
* ```
|
|
1426
|
-
*
|
|
1427
|
-
* @example POST request with JSON
|
|
1428
|
-
* ```typescript
|
|
1429
|
-
* const response = await wallet.fetch('https://api.example.com/generate', {
|
|
1430
|
-
* method: 'POST',
|
|
1431
|
-
* headers: { 'Content-Type': 'application/json' },
|
|
1432
|
-
* body: JSON.stringify({ prompt: 'Hello world' })
|
|
1433
|
-
* });
|
|
1434
|
-
* ```
|
|
1435
|
-
*/
|
|
1436
|
-
async fetch(url, init) {
|
|
1437
|
-
this.logger.debug(`Fetching ${init?.method || "GET"} ${url}`);
|
|
1438
|
-
await this.validateMerchantAllowed(url, "url");
|
|
1439
|
-
const requestId = crypto.randomUUID();
|
|
1440
|
-
const correlationId = this.extractCorrelationId(init?.headers);
|
|
1441
|
-
const controller = new AbortController();
|
|
1442
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1443
|
-
try {
|
|
1444
|
-
let response = await fetch(url, {
|
|
1445
|
-
...init,
|
|
1446
|
-
signal: controller.signal
|
|
1447
|
-
});
|
|
1448
|
-
this.logger.debug(`Initial response: ${response.status}`);
|
|
1449
|
-
if (response.status === 402) {
|
|
1450
|
-
this.logger.info(`Payment required for ${url}`);
|
|
1451
|
-
response = await this.handlePaymentRequired(url, init, response, requestId, correlationId);
|
|
1452
|
-
}
|
|
1453
|
-
return response;
|
|
1454
|
-
} catch (error) {
|
|
1455
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
1456
|
-
throw new MixrPayError(`Request timeout after ${this.timeout}ms`);
|
|
1457
|
-
}
|
|
1458
|
-
throw error;
|
|
1459
|
-
} finally {
|
|
1460
|
-
clearTimeout(timeoutId);
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
/**
|
|
1464
|
-
* Extract correlation ID from request headers.
|
|
1465
|
-
*/
|
|
1466
|
-
extractCorrelationId(headers) {
|
|
1467
|
-
if (!headers) return void 0;
|
|
1468
|
-
if (headers instanceof Headers) {
|
|
1469
|
-
return headers.get("X-Correlation-Id") || headers.get("x-correlation-id") || void 0;
|
|
1470
|
-
}
|
|
1471
|
-
if (Array.isArray(headers)) {
|
|
1472
|
-
const entry = headers.find(
|
|
1473
|
-
([key]) => key.toLowerCase() === "x-correlation-id"
|
|
1474
|
-
);
|
|
1475
|
-
return entry ? entry[1] : void 0;
|
|
1476
|
-
}
|
|
1477
|
-
const record = headers;
|
|
1478
|
-
return record["X-Correlation-Id"] || record["x-correlation-id"] || void 0;
|
|
1479
|
-
}
|
|
1480
|
-
/**
|
|
1481
|
-
* Handle a 402 Payment Required response.
|
|
1482
|
-
*/
|
|
1483
|
-
async handlePaymentRequired(url, init, response, requestId, correlationId) {
|
|
1484
|
-
let requirements;
|
|
1485
|
-
try {
|
|
1486
|
-
requirements = await parse402Response(response);
|
|
1487
|
-
this.logger.debug("Payment requirements:", {
|
|
1488
|
-
amount: `$${getAmountUsd(requirements).toFixed(4)}`,
|
|
1489
|
-
recipient: requirements.recipient,
|
|
1490
|
-
description: requirements.description
|
|
1491
|
-
});
|
|
1492
|
-
} catch (e) {
|
|
1493
|
-
this.logger.error("Failed to parse payment requirements:", e);
|
|
1494
|
-
throw new PaymentFailedError(
|
|
1495
|
-
`Failed to parse payment requirements: ${e}. The server may not be properly configured for x402 payments.`
|
|
1496
|
-
);
|
|
1497
|
-
}
|
|
1498
|
-
if (isPaymentExpired(requirements)) {
|
|
1499
|
-
throw new PaymentFailedError(
|
|
1500
|
-
"Payment requirements have expired. This usually means the request took too long. Try again."
|
|
1501
|
-
);
|
|
1502
|
-
}
|
|
1503
|
-
const amountUsd = getAmountUsd(requirements);
|
|
1504
|
-
if (this.maxPaymentUsd !== void 0 && amountUsd > this.maxPaymentUsd) {
|
|
1505
|
-
throw new SpendingLimitExceededError(
|
|
1506
|
-
"client_max",
|
|
1507
|
-
this.maxPaymentUsd,
|
|
1508
|
-
amountUsd
|
|
1509
|
-
);
|
|
1510
|
-
}
|
|
1511
|
-
let xPayment;
|
|
1512
|
-
try {
|
|
1513
|
-
this.logger.debug("Signing payment authorization...");
|
|
1514
|
-
xPayment = await buildXPaymentHeader(
|
|
1515
|
-
requirements,
|
|
1516
|
-
this.sessionKey,
|
|
1517
|
-
this.walletAddress
|
|
1518
|
-
);
|
|
1519
|
-
} catch (e) {
|
|
1520
|
-
this.logger.error("Failed to sign payment:", e);
|
|
1521
|
-
throw new PaymentFailedError(
|
|
1522
|
-
`Failed to sign payment: ${e}. This may indicate an issue with the session key.`
|
|
1523
|
-
);
|
|
1524
|
-
}
|
|
1525
|
-
this.logger.debug("Retrying request with payment...");
|
|
1526
|
-
const headers = new Headers(init?.headers);
|
|
1527
|
-
headers.set("X-PAYMENT", xPayment);
|
|
1528
|
-
const retryResponse = await fetch(url, {
|
|
1529
|
-
...init,
|
|
1530
|
-
headers
|
|
1531
|
-
});
|
|
1532
|
-
if (retryResponse.status !== 402) {
|
|
1533
|
-
const payment = {
|
|
1534
|
-
amountUsd,
|
|
1535
|
-
recipient: requirements.recipient,
|
|
1536
|
-
txHash: retryResponse.headers.get("X-Payment-TxHash"),
|
|
1537
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
1538
|
-
description: requirements.description,
|
|
1539
|
-
url,
|
|
1540
|
-
requestId,
|
|
1541
|
-
correlationId
|
|
1542
|
-
};
|
|
1543
|
-
this.payments.push(payment);
|
|
1544
|
-
this.totalSpentUsd += amountUsd;
|
|
1545
|
-
this.logger.payment(amountUsd, requirements.recipient, requirements.description);
|
|
1546
|
-
if (this.onPayment) {
|
|
1547
|
-
this.onPayment(payment);
|
|
1548
|
-
}
|
|
1549
|
-
} else {
|
|
1550
|
-
await this.handlePaymentError(retryResponse);
|
|
1551
|
-
}
|
|
1552
|
-
return retryResponse;
|
|
1553
|
-
}
|
|
1554
|
-
/**
|
|
1555
|
-
* Handle payment-specific errors from the response.
|
|
1556
|
-
*/
|
|
1557
|
-
async handlePaymentError(response) {
|
|
1558
|
-
let errorData = {};
|
|
1559
|
-
try {
|
|
1560
|
-
errorData = await response.json();
|
|
1561
|
-
} catch {
|
|
1562
|
-
}
|
|
1563
|
-
const errorCode = errorData.error_code || "";
|
|
1564
|
-
const errorMessage = errorData.error || errorData.message || "Payment required";
|
|
1565
|
-
this.logger.error("Payment failed:", { errorCode, errorMessage, errorData });
|
|
1566
|
-
if (errorCode === "insufficient_balance") {
|
|
1567
|
-
const required = errorData.required || 0;
|
|
1568
|
-
const available = errorData.available || 0;
|
|
1569
|
-
throw new InsufficientBalanceError(required, available);
|
|
1570
|
-
} else if (errorCode === "session_key_expired") {
|
|
1571
|
-
throw new SessionKeyExpiredError(errorData.expired_at || "unknown");
|
|
1572
|
-
} else if (errorCode === "spending_limit_exceeded") {
|
|
1573
|
-
throw new SpendingLimitExceededError(
|
|
1574
|
-
errorData.limit_type || "unknown",
|
|
1575
|
-
errorData.limit || 0,
|
|
1576
|
-
errorData.attempted || 0
|
|
1577
|
-
);
|
|
1578
|
-
} else {
|
|
1579
|
-
throw new PaymentFailedError(errorMessage);
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
// ===========================================================================
|
|
1583
|
-
// Wallet Information
|
|
1584
|
-
// ===========================================================================
|
|
1585
|
-
/**
|
|
1586
|
-
* Get the wallet address.
|
|
1587
|
-
*
|
|
1588
|
-
* @returns The Ethereum address of the smart wallet
|
|
1589
|
-
*/
|
|
1590
|
-
getWalletAddress() {
|
|
1591
|
-
return this.walletAddress;
|
|
1592
|
-
}
|
|
1593
|
-
/**
|
|
1594
|
-
* Check if using testnet session key.
|
|
1595
|
-
*
|
|
1596
|
-
* @returns true if using Base Sepolia (testnet), false if using Base Mainnet
|
|
1597
|
-
*/
|
|
1598
|
-
isTestnet() {
|
|
1599
|
-
return this.sessionKey.isTest;
|
|
1600
|
-
}
|
|
1601
|
-
/**
|
|
1602
|
-
* Get the network information.
|
|
1603
|
-
*
|
|
1604
|
-
* @returns Network details including chain ID and name
|
|
1605
|
-
*/
|
|
1606
|
-
getNetwork() {
|
|
1607
|
-
return this.isTestnet() ? NETWORKS.BASE_SEPOLIA : NETWORKS.BASE_MAINNET;
|
|
1608
|
-
}
|
|
1609
|
-
/**
|
|
1610
|
-
* Get current USDC balance of the wallet.
|
|
1611
|
-
*
|
|
1612
|
-
* @returns USDC balance in USD
|
|
1613
|
-
*
|
|
1614
|
-
* @example
|
|
1615
|
-
* ```typescript
|
|
1616
|
-
* const balance = await wallet.getBalance();
|
|
1617
|
-
* console.log(`Balance: $${balance.toFixed(2)}`);
|
|
1618
|
-
* ```
|
|
1619
|
-
*/
|
|
1620
|
-
async getBalance() {
|
|
1621
|
-
this.logger.debug("Fetching wallet balance...");
|
|
1622
|
-
try {
|
|
1623
|
-
const infoResponse = await fetch(`${this.baseUrl}/api/v1/session-key/info`, {
|
|
1624
|
-
headers: {
|
|
1625
|
-
"X-Session-Key": this.sessionKey.address.toLowerCase()
|
|
1626
|
-
}
|
|
1627
|
-
});
|
|
1628
|
-
if (!infoResponse.ok) {
|
|
1629
|
-
throw new Error("Failed to get session key info");
|
|
1630
|
-
}
|
|
1631
|
-
const info = await infoResponse.json();
|
|
1632
|
-
const walletAddress = info.wallet_address || info.walletAddress;
|
|
1633
|
-
if (!walletAddress) {
|
|
1634
|
-
throw new Error("No wallet address in session key info");
|
|
1635
|
-
}
|
|
1636
|
-
const response = await fetch(
|
|
1637
|
-
`${this.baseUrl}/api/wallet/balance?address=${walletAddress}&chain=base`
|
|
1638
|
-
);
|
|
1639
|
-
if (response.ok) {
|
|
1640
|
-
const data = await response.json();
|
|
1641
|
-
const balance = data.balance_usdc || data.balance_usd || 0;
|
|
1642
|
-
this.logger.debug(`Balance: $${balance}`);
|
|
1643
|
-
return balance;
|
|
1644
|
-
}
|
|
1645
|
-
} catch (error) {
|
|
1646
|
-
this.logger.warn("Failed to fetch balance:", error);
|
|
1647
|
-
}
|
|
1648
|
-
this.logger.debug("Using estimated balance based on tracking");
|
|
1649
|
-
return Math.max(0, 100 - this.totalSpentUsd);
|
|
1650
|
-
}
|
|
1651
|
-
/**
|
|
1652
|
-
* Check if the wallet can afford a specific amount.
|
|
1653
|
-
*
|
|
1654
|
-
* This is a convenience method to check balance before making a request
|
|
1655
|
-
* when you know the expected cost.
|
|
1656
|
-
*
|
|
1657
|
-
* @param amountUsd - Amount to check in USD
|
|
1658
|
-
* @returns Object with affordability information
|
|
1659
|
-
*
|
|
1660
|
-
* @example
|
|
1661
|
-
* ```typescript
|
|
1662
|
-
* const check = await wallet.canAfford(5.00);
|
|
1663
|
-
* if (check.canAfford) {
|
|
1664
|
-
* console.log(`Can afford! Will have $${check.remainingAfter.toFixed(2)} left`);
|
|
1665
|
-
* await wallet.fetch(url);
|
|
1666
|
-
* } else {
|
|
1667
|
-
* console.log(`Need $${check.shortfall.toFixed(2)} more`);
|
|
1668
|
-
* }
|
|
1669
|
-
* ```
|
|
1670
|
-
*/
|
|
1671
|
-
async canAfford(amountUsd) {
|
|
1672
|
-
const balance = await this.getBalance();
|
|
1673
|
-
const canAfford = balance >= amountUsd;
|
|
1674
|
-
return {
|
|
1675
|
-
canAfford,
|
|
1676
|
-
balance,
|
|
1677
|
-
shortfall: canAfford ? 0 : amountUsd - balance,
|
|
1678
|
-
remainingAfter: canAfford ? balance - amountUsd : 0
|
|
1679
|
-
};
|
|
1680
|
-
}
|
|
1681
|
-
/**
|
|
1682
|
-
* Get information about the session key.
|
|
1683
|
-
*
|
|
1684
|
-
* @param refresh - Force refresh from server (default: use cache if < 60s old)
|
|
1685
|
-
* @returns Session key details including limits and expiration
|
|
1686
|
-
*
|
|
1687
|
-
* @example
|
|
1688
|
-
* ```typescript
|
|
1689
|
-
* const info = await wallet.getSessionKeyInfo();
|
|
1690
|
-
* console.log(`Daily limit: $${info.limits.dailyUsd}`);
|
|
1691
|
-
* console.log(`Expires: ${info.expiresAt}`);
|
|
1692
|
-
* ```
|
|
1693
|
-
*/
|
|
1694
|
-
async getSessionKeyInfo(refresh = false) {
|
|
1695
|
-
const cacheAge = this.sessionKeyInfoFetchedAt ? Date.now() - this.sessionKeyInfoFetchedAt : Infinity;
|
|
1696
|
-
if (!refresh && this.sessionKeyInfo && cacheAge < 6e4) {
|
|
1697
|
-
return this.sessionKeyInfo;
|
|
1698
|
-
}
|
|
1699
|
-
this.logger.debug("Fetching session key info...");
|
|
1700
|
-
try {
|
|
1701
|
-
const response = await fetch(`${this.baseUrl}/api/v1/session-key/info`, {
|
|
1702
|
-
headers: {
|
|
1703
|
-
"X-Session-Key": this.sessionKey.address.toLowerCase()
|
|
1704
|
-
}
|
|
1705
|
-
});
|
|
1706
|
-
if (response.ok) {
|
|
1707
|
-
const data = await response.json();
|
|
1708
|
-
this.sessionKeyInfo = {
|
|
1709
|
-
address: this.sessionKey.address,
|
|
1710
|
-
walletAddress: data.wallet_address ?? data.walletAddress ?? null,
|
|
1711
|
-
isValid: data.is_valid ?? data.isValid ?? true,
|
|
1712
|
-
limits: {
|
|
1713
|
-
perTxUsd: data.per_tx_limit_usd ?? data.perTxLimitUsd ?? null,
|
|
1714
|
-
dailyUsd: data.daily_limit_usd ?? data.dailyLimitUsd ?? null,
|
|
1715
|
-
totalUsd: data.total_limit_usd ?? data.totalLimitUsd ?? null
|
|
1716
|
-
},
|
|
1717
|
-
usage: {
|
|
1718
|
-
todayUsd: data.today_spent_usd ?? data.todaySpentUsd ?? 0,
|
|
1719
|
-
totalUsd: data.total_spent_usd ?? data.totalSpentUsd ?? 0,
|
|
1720
|
-
txCount: data.tx_count ?? data.txCount ?? 0
|
|
1721
|
-
},
|
|
1722
|
-
expiresAt: data.expires_at ? new Date(data.expires_at) : null,
|
|
1723
|
-
createdAt: data.created_at ? new Date(data.created_at) : null,
|
|
1724
|
-
name: data.name,
|
|
1725
|
-
allowedMerchants: data.allowed_merchants ?? data.allowedMerchants ?? []
|
|
1726
|
-
};
|
|
1727
|
-
this.sessionKeyInfoFetchedAt = Date.now();
|
|
1728
|
-
return this.sessionKeyInfo;
|
|
1729
|
-
}
|
|
1730
|
-
} catch (error) {
|
|
1731
|
-
this.logger.warn("Failed to fetch session key info:", error);
|
|
1732
|
-
}
|
|
1733
|
-
return {
|
|
1734
|
-
address: this.sessionKey.address,
|
|
1735
|
-
walletAddress: null,
|
|
1736
|
-
isValid: true,
|
|
1737
|
-
limits: { perTxUsd: null, dailyUsd: null, totalUsd: null },
|
|
1738
|
-
usage: { todayUsd: this.totalSpentUsd, totalUsd: this.totalSpentUsd, txCount: this.payments.length },
|
|
1739
|
-
expiresAt: null,
|
|
1740
|
-
createdAt: null
|
|
1741
|
-
};
|
|
1742
|
-
}
|
|
1743
|
-
/**
|
|
1744
|
-
* Get spending stats for this session key.
|
|
1745
|
-
*
|
|
1746
|
-
* @returns Spending statistics
|
|
1747
|
-
*
|
|
1748
|
-
* @example
|
|
1749
|
-
* ```typescript
|
|
1750
|
-
* const stats = await wallet.getSpendingStats();
|
|
1751
|
-
* console.log(`Spent: $${stats.totalSpentUsd.toFixed(2)}`);
|
|
1752
|
-
* console.log(`Remaining daily: $${stats.remainingDailyUsd?.toFixed(2) ?? 'unlimited'}`);
|
|
1753
|
-
* ```
|
|
1754
|
-
*/
|
|
1755
|
-
async getSpendingStats() {
|
|
1756
|
-
this.logger.debug("Fetching spending stats...");
|
|
1757
|
-
try {
|
|
1758
|
-
const response = await fetch(`${this.baseUrl}/api/v1/session-key/stats`, {
|
|
1759
|
-
headers: {
|
|
1760
|
-
"X-Session-Key": this.sessionKey.address.toLowerCase()
|
|
1761
|
-
}
|
|
1762
|
-
});
|
|
1763
|
-
if (response.ok) {
|
|
1764
|
-
const data = await response.json();
|
|
1765
|
-
return {
|
|
1766
|
-
totalSpentUsd: data.total_spent_usd || data.totalSpentUsd || this.totalSpentUsd,
|
|
1767
|
-
txCount: data.tx_count || data.txCount || this.payments.length,
|
|
1768
|
-
remainingDailyUsd: data.remaining_daily_usd || data.remainingDailyUsd || null,
|
|
1769
|
-
remainingTotalUsd: data.remaining_total_usd || data.remainingTotalUsd || null,
|
|
1770
|
-
expiresAt: data.expires_at ? new Date(data.expires_at) : null
|
|
1771
|
-
};
|
|
1772
|
-
}
|
|
1773
|
-
} catch (error) {
|
|
1774
|
-
this.logger.warn("Failed to fetch spending stats:", error);
|
|
1775
|
-
}
|
|
1776
|
-
return {
|
|
1777
|
-
totalSpentUsd: this.totalSpentUsd,
|
|
1778
|
-
txCount: this.payments.length,
|
|
1779
|
-
remainingDailyUsd: null,
|
|
1780
|
-
remainingTotalUsd: null,
|
|
1781
|
-
expiresAt: null
|
|
1782
|
-
};
|
|
1783
|
-
}
|
|
1784
|
-
/**
|
|
1785
|
-
* Get list of payments made in this session.
|
|
1786
|
-
*
|
|
1787
|
-
* @returns Array of payment events
|
|
1788
|
-
*/
|
|
1789
|
-
getPaymentHistory() {
|
|
1790
|
-
return [...this.payments];
|
|
1791
|
-
}
|
|
1792
|
-
/**
|
|
1793
|
-
* Get the total amount spent in this session.
|
|
1794
|
-
*
|
|
1795
|
-
* @returns Total spent in USD
|
|
1796
|
-
*/
|
|
1797
|
-
getTotalSpent() {
|
|
1798
|
-
return this.totalSpentUsd;
|
|
1799
|
-
}
|
|
1800
|
-
// ===========================================================================
|
|
1801
|
-
// Diagnostics
|
|
1802
|
-
// ===========================================================================
|
|
1803
|
-
/**
|
|
1804
|
-
* Run diagnostics to verify the wallet is properly configured.
|
|
1805
|
-
*
|
|
1806
|
-
* This is useful for debugging integration issues.
|
|
1807
|
-
*
|
|
1808
|
-
* @returns Diagnostic results with status and any issues found
|
|
1809
|
-
*
|
|
1810
|
-
* @example
|
|
1811
|
-
* ```typescript
|
|
1812
|
-
* const diagnostics = await wallet.runDiagnostics();
|
|
1813
|
-
* if (diagnostics.healthy) {
|
|
1814
|
-
* console.log('✅ Wallet is ready to use');
|
|
1815
|
-
* } else {
|
|
1816
|
-
* console.log('❌ Issues found:');
|
|
1817
|
-
* diagnostics.issues.forEach(issue => console.log(` - ${issue}`));
|
|
1818
|
-
* }
|
|
1819
|
-
* ```
|
|
1820
|
-
*/
|
|
1821
|
-
async runDiagnostics() {
|
|
1822
|
-
this.logger.info("Running diagnostics...");
|
|
1823
|
-
const issues = [];
|
|
1824
|
-
const recommendations = [];
|
|
1825
|
-
const checks = {};
|
|
1826
|
-
let latencyMs;
|
|
1827
|
-
let sessionLimits;
|
|
1828
|
-
checks.sessionKeyFormat = true;
|
|
1829
|
-
try {
|
|
1830
|
-
const startTime = Date.now();
|
|
1831
|
-
const response = await fetch(`${this.baseUrl}/health`, {
|
|
1832
|
-
method: "GET",
|
|
1833
|
-
signal: AbortSignal.timeout(5e3)
|
|
1834
|
-
});
|
|
1835
|
-
latencyMs = Date.now() - startTime;
|
|
1836
|
-
checks.apiConnectivity = response.ok;
|
|
1837
|
-
if (!response.ok) {
|
|
1838
|
-
issues.push(`API server returned ${response.status}. Check baseUrl configuration.`);
|
|
1839
|
-
recommendations.push("Verify the baseUrl configuration points to a valid MixrPay server.");
|
|
1840
|
-
}
|
|
1841
|
-
if (latencyMs > 2e3) {
|
|
1842
|
-
issues.push(`High API latency: ${latencyMs}ms. This may cause timeouts.`);
|
|
1843
|
-
recommendations.push("Consider using a server closer to your region or check network connectivity.");
|
|
1844
|
-
}
|
|
1845
|
-
} catch {
|
|
1846
|
-
checks.apiConnectivity = false;
|
|
1847
|
-
issues.push("Cannot connect to MixrPay API. Check your network connection and baseUrl.");
|
|
1848
|
-
recommendations.push("Verify network connectivity and that the MixrPay server is running.");
|
|
1849
|
-
}
|
|
1850
|
-
try {
|
|
1851
|
-
const info = await this.getSessionKeyInfo(true);
|
|
1852
|
-
checks.sessionKeyValid = info.isValid;
|
|
1853
|
-
if (!info.isValid) {
|
|
1854
|
-
issues.push("Session key is invalid or has been revoked.");
|
|
1855
|
-
recommendations.push("Request a new session key from the wallet owner at https://mixrpay.com/manage/invites");
|
|
1856
|
-
}
|
|
1857
|
-
const now = /* @__PURE__ */ new Date();
|
|
1858
|
-
let expiresInHours = null;
|
|
1859
|
-
if (info.expiresAt) {
|
|
1860
|
-
expiresInHours = (info.expiresAt.getTime() - now.getTime()) / (1e3 * 60 * 60);
|
|
1861
|
-
if (info.expiresAt < now) {
|
|
1862
|
-
checks.sessionKeyValid = false;
|
|
1863
|
-
issues.push(`Session key expired on ${info.expiresAt.toISOString()}`);
|
|
1864
|
-
recommendations.push("Create a new session key to continue making payments.");
|
|
1865
|
-
} else if (expiresInHours < 24) {
|
|
1866
|
-
issues.push(`Session key expires in ${expiresInHours.toFixed(1)} hours.`);
|
|
1867
|
-
recommendations.push("Consider creating a new session key before the current one expires.");
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
const stats = await this.getSpendingStats();
|
|
1871
|
-
sessionLimits = {
|
|
1872
|
-
remainingDailyUsd: stats.remainingDailyUsd,
|
|
1873
|
-
remainingTotalUsd: stats.remainingTotalUsd,
|
|
1874
|
-
expiresAt: info.expiresAt,
|
|
1875
|
-
expiresInHours
|
|
1876
|
-
};
|
|
1877
|
-
if (stats.remainingDailyUsd !== null && stats.remainingDailyUsd < 1) {
|
|
1878
|
-
issues.push(`Daily limit nearly exhausted: $${stats.remainingDailyUsd.toFixed(2)} remaining.`);
|
|
1879
|
-
recommendations.push("Wait until tomorrow for daily limit to reset, or request a higher daily limit.");
|
|
1880
|
-
}
|
|
1881
|
-
if (stats.remainingTotalUsd !== null && stats.remainingTotalUsd < 1) {
|
|
1882
|
-
issues.push(`Total limit nearly exhausted: $${stats.remainingTotalUsd.toFixed(2)} remaining.`);
|
|
1883
|
-
recommendations.push("Request a new session key with a higher total limit.");
|
|
1884
|
-
}
|
|
1885
|
-
} catch {
|
|
1886
|
-
checks.sessionKeyValid = false;
|
|
1887
|
-
issues.push("Could not verify session key validity.");
|
|
1888
|
-
recommendations.push("Check network connectivity and try again.");
|
|
1889
|
-
}
|
|
1890
|
-
let balance = 0;
|
|
1891
|
-
try {
|
|
1892
|
-
balance = await this.getBalance();
|
|
1893
|
-
checks.hasBalance = balance > 0;
|
|
1894
|
-
if (balance <= 0) {
|
|
1895
|
-
issues.push("Wallet has no USDC balance. Top up at https://mixrpay.com/manage/wallet");
|
|
1896
|
-
recommendations.push("Deposit USDC to your wallet address to enable payments.");
|
|
1897
|
-
} else if (balance < 1) {
|
|
1898
|
-
issues.push(`Low balance: $${balance.toFixed(2)}. Consider topping up.`);
|
|
1899
|
-
recommendations.push("Top up your wallet balance to avoid payment failures.");
|
|
1900
|
-
}
|
|
1901
|
-
} catch {
|
|
1902
|
-
checks.hasBalance = false;
|
|
1903
|
-
issues.push("Could not fetch wallet balance.");
|
|
1904
|
-
recommendations.push("Check network connectivity and try again.");
|
|
1905
|
-
}
|
|
1906
|
-
const healthy = issues.length === 0;
|
|
1907
|
-
this.logger.info("Diagnostics complete:", { healthy, issues, latencyMs });
|
|
1908
|
-
return {
|
|
1909
|
-
healthy,
|
|
1910
|
-
issues,
|
|
1911
|
-
checks,
|
|
1912
|
-
sdkVersion: SDK_VERSION,
|
|
1913
|
-
network: this.getNetwork().name,
|
|
1914
|
-
walletAddress: this.walletAddress,
|
|
1915
|
-
sessionLimits,
|
|
1916
|
-
latencyMs,
|
|
1917
|
-
recommendations
|
|
1918
|
-
};
|
|
1919
|
-
}
|
|
1920
|
-
/**
|
|
1921
|
-
* Enable or disable debug logging.
|
|
1922
|
-
*
|
|
1923
|
-
* @param enable - true to enable debug logging, false to disable
|
|
1924
|
-
*
|
|
1925
|
-
* @example
|
|
1926
|
-
* ```typescript
|
|
1927
|
-
* wallet.setDebug(true); // Enable detailed logs
|
|
1928
|
-
* await wallet.fetch(...);
|
|
1929
|
-
* wallet.setDebug(false); // Disable logs
|
|
1930
|
-
* ```
|
|
1931
|
-
*/
|
|
1932
|
-
setDebug(enable) {
|
|
1933
|
-
this.logger.setLevel(enable ? "debug" : "none");
|
|
1934
|
-
}
|
|
1935
|
-
/**
|
|
1936
|
-
* Set the log level.
|
|
1937
|
-
*
|
|
1938
|
-
* @param level - 'debug' | 'info' | 'warn' | 'error' | 'none'
|
|
1939
|
-
*/
|
|
1940
|
-
setLogLevel(level) {
|
|
1941
|
-
this.logger.setLevel(level);
|
|
1942
|
-
}
|
|
1943
|
-
// ===========================================================================
|
|
1944
|
-
// Session Authorization Methods (for MixrPay Merchants)
|
|
1945
|
-
// ===========================================================================
|
|
1946
|
-
/**
|
|
1947
|
-
* Create the X-Session-Auth header for secure API authentication.
|
|
1948
|
-
* Uses signature-based authentication - private key is NEVER transmitted.
|
|
1949
|
-
*
|
|
1950
|
-
* @returns Headers object with X-Session-Auth
|
|
1951
|
-
*/
|
|
1952
|
-
async getSessionAuthHeaders() {
|
|
1953
|
-
const payload = await createSessionAuthPayload(this.sessionKey);
|
|
1954
|
-
return {
|
|
1955
|
-
"X-Session-Auth": JSON.stringify(payload)
|
|
1956
|
-
};
|
|
1957
|
-
}
|
|
1958
|
-
/**
|
|
1959
|
-
* Get an existing session or create a new one with a MixrPay merchant.
|
|
1960
|
-
*
|
|
1961
|
-
* This is the recommended way to interact with MixrPay-enabled APIs.
|
|
1962
|
-
* If an active session exists, it will be returned. Otherwise, a new
|
|
1963
|
-
* session authorization request will be created and confirmed.
|
|
1964
|
-
*
|
|
1965
|
-
* @param options - Session creation options
|
|
1966
|
-
* @returns Active session authorization
|
|
1967
|
-
*
|
|
1968
|
-
* @throws {MixrPayError} If merchant is not found or session creation fails
|
|
1969
|
-
*
|
|
1970
|
-
* @example
|
|
1971
|
-
* ```typescript
|
|
1972
|
-
* const session = await wallet.getOrCreateSession({
|
|
1973
|
-
* merchantPublicKey: 'pk_live_abc123...',
|
|
1974
|
-
* spendingLimitUsd: 25.00,
|
|
1975
|
-
* durationDays: 7,
|
|
1976
|
-
* });
|
|
1977
|
-
*
|
|
1978
|
-
* console.log(`Session active: $${session.remainingLimitUsd} remaining`);
|
|
1979
|
-
* ```
|
|
1980
|
-
*/
|
|
1981
|
-
async getOrCreateSession(options) {
|
|
1982
|
-
this.logger.debug("getOrCreateSession called", options);
|
|
1983
|
-
const { merchantPublicKey, spendingLimitUsd = 25, durationDays = 7 } = options;
|
|
1984
|
-
try {
|
|
1985
|
-
const existingSession = await this.getSessionByMerchant(merchantPublicKey);
|
|
1986
|
-
if (existingSession && existingSession.status === "active") {
|
|
1987
|
-
this.logger.debug("Found existing active session", existingSession.id);
|
|
1988
|
-
return existingSession;
|
|
1989
|
-
}
|
|
1990
|
-
} catch {
|
|
1991
|
-
}
|
|
1992
|
-
this.logger.info(`Creating new session with merchant ${merchantPublicKey.slice(0, 20)}...`);
|
|
1993
|
-
const authHeaders = await this.getSessionAuthHeaders();
|
|
1994
|
-
const authorizeResponse = await fetch(`${this.baseUrl}/api/v2/session/authorize`, {
|
|
1995
|
-
method: "POST",
|
|
1996
|
-
headers: {
|
|
1997
|
-
"Content-Type": "application/json",
|
|
1998
|
-
...authHeaders
|
|
1999
|
-
},
|
|
2000
|
-
body: JSON.stringify({
|
|
2001
|
-
merchant_public_key: merchantPublicKey,
|
|
2002
|
-
spending_limit_usd: spendingLimitUsd,
|
|
2003
|
-
duration_days: durationDays
|
|
2004
|
-
})
|
|
2005
|
-
});
|
|
2006
|
-
if (!authorizeResponse.ok) {
|
|
2007
|
-
const error = await authorizeResponse.json().catch(() => ({}));
|
|
2008
|
-
throw new MixrPayError(
|
|
2009
|
-
error.message || error.error || `Failed to create session: ${authorizeResponse.status}`
|
|
2010
|
-
);
|
|
2011
|
-
}
|
|
2012
|
-
const authorizeData = await authorizeResponse.json();
|
|
2013
|
-
const sessionId = authorizeData.session_id;
|
|
2014
|
-
const messageToSign = authorizeData.message_to_sign;
|
|
2015
|
-
if (!sessionId || !messageToSign) {
|
|
2016
|
-
throw new MixrPayError("Invalid authorize response: missing session_id or message_to_sign");
|
|
2017
|
-
}
|
|
2018
|
-
this.logger.debug("Signing session authorization message...");
|
|
2019
|
-
const signature = await this.sessionKey.signMessage(messageToSign);
|
|
2020
|
-
const confirmResponse = await fetch(`${this.baseUrl}/api/v2/session/confirm`, {
|
|
2021
|
-
method: "POST",
|
|
2022
|
-
headers: {
|
|
2023
|
-
"Content-Type": "application/json"
|
|
2024
|
-
},
|
|
2025
|
-
body: JSON.stringify({
|
|
2026
|
-
session_id: sessionId,
|
|
2027
|
-
signature,
|
|
2028
|
-
wallet_address: this.walletAddress
|
|
2029
|
-
})
|
|
2030
|
-
});
|
|
2031
|
-
if (!confirmResponse.ok) {
|
|
2032
|
-
const error = await confirmResponse.json().catch(() => ({}));
|
|
2033
|
-
throw new MixrPayError(
|
|
2034
|
-
error.message || `Failed to confirm session: ${confirmResponse.status}`
|
|
2035
|
-
);
|
|
2036
|
-
}
|
|
2037
|
-
const confirmData = await confirmResponse.json();
|
|
2038
|
-
this.logger.info(`Session created: ${confirmData.session?.id || sessionId}`);
|
|
2039
|
-
return this.parseSessionResponse(confirmData.session || confirmData);
|
|
2040
|
-
}
|
|
2041
|
-
/**
|
|
2042
|
-
* Get session status for a specific merchant.
|
|
2043
|
-
*
|
|
2044
|
-
* @param merchantPublicKey - The merchant's public key
|
|
2045
|
-
* @returns Session authorization or null if not found
|
|
2046
|
-
*/
|
|
2047
|
-
async getSessionByMerchant(merchantPublicKey) {
|
|
2048
|
-
this.logger.debug("getSessionByMerchant", merchantPublicKey);
|
|
2049
|
-
const authHeaders = await this.getSessionAuthHeaders();
|
|
2050
|
-
const response = await fetch(`${this.baseUrl}/api/v2/session/status?merchant_public_key=${encodeURIComponent(merchantPublicKey)}`, {
|
|
2051
|
-
headers: authHeaders
|
|
2052
|
-
});
|
|
2053
|
-
if (response.status === 404) {
|
|
2054
|
-
return null;
|
|
2055
|
-
}
|
|
2056
|
-
if (!response.ok) {
|
|
2057
|
-
const error = await response.json().catch(() => ({}));
|
|
2058
|
-
throw new MixrPayError(error.message || `Failed to get session: ${response.status}`);
|
|
2059
|
-
}
|
|
2060
|
-
const data = await response.json();
|
|
2061
|
-
return data.session ? this.parseSessionResponse(data.session) : null;
|
|
2062
|
-
}
|
|
2063
|
-
/**
|
|
2064
|
-
* List all session authorizations for this wallet.
|
|
2065
|
-
*
|
|
2066
|
-
* @returns Array of session authorizations
|
|
2067
|
-
*
|
|
2068
|
-
* @example
|
|
2069
|
-
* ```typescript
|
|
2070
|
-
* const sessions = await wallet.listSessions();
|
|
2071
|
-
* for (const session of sessions) {
|
|
2072
|
-
* console.log(`${session.merchantName}: $${session.remainingLimitUsd} remaining`);
|
|
2073
|
-
* }
|
|
2074
|
-
* ```
|
|
2075
|
-
*/
|
|
2076
|
-
async listSessions() {
|
|
2077
|
-
this.logger.debug("listSessions");
|
|
2078
|
-
const authHeaders = await this.getSessionAuthHeaders();
|
|
2079
|
-
const response = await fetch(`${this.baseUrl}/api/v2/session/list`, {
|
|
2080
|
-
headers: authHeaders
|
|
2081
|
-
});
|
|
2082
|
-
if (!response.ok) {
|
|
2083
|
-
const error = await response.json().catch(() => ({}));
|
|
2084
|
-
throw new MixrPayError(error.message || `Failed to list sessions: ${response.status}`);
|
|
2085
|
-
}
|
|
2086
|
-
const data = await response.json();
|
|
2087
|
-
return (data.sessions || []).map((s) => this.parseSessionResponse(s));
|
|
2088
|
-
}
|
|
2089
|
-
/**
|
|
2090
|
-
* Revoke a session authorization.
|
|
2091
|
-
*
|
|
2092
|
-
* After revocation, no further charges can be made against this session.
|
|
2093
|
-
*
|
|
2094
|
-
* @param sessionId - The session ID to revoke
|
|
2095
|
-
* @returns true if revoked successfully
|
|
2096
|
-
*
|
|
2097
|
-
* @example
|
|
2098
|
-
* ```typescript
|
|
2099
|
-
* const revoked = await wallet.revokeSession('sess_abc123');
|
|
2100
|
-
* if (revoked) {
|
|
2101
|
-
* console.log('Session revoked successfully');
|
|
2102
|
-
* }
|
|
2103
|
-
* ```
|
|
2104
|
-
*/
|
|
2105
|
-
async revokeSession(sessionId) {
|
|
2106
|
-
this.logger.debug("revokeSession", sessionId);
|
|
2107
|
-
const authHeaders = await this.getSessionAuthHeaders();
|
|
2108
|
-
const response = await fetch(`${this.baseUrl}/api/v2/session/revoke`, {
|
|
2109
|
-
method: "POST",
|
|
2110
|
-
headers: {
|
|
2111
|
-
"Content-Type": "application/json",
|
|
2112
|
-
...authHeaders
|
|
2113
|
-
},
|
|
2114
|
-
body: JSON.stringify({ session_id: sessionId })
|
|
2115
|
-
});
|
|
2116
|
-
if (!response.ok) {
|
|
2117
|
-
const error = await response.json().catch(() => ({}));
|
|
2118
|
-
this.logger.error("Failed to revoke session:", error);
|
|
2119
|
-
return false;
|
|
2120
|
-
}
|
|
2121
|
-
this.logger.info(`Session ${sessionId} revoked`);
|
|
2122
|
-
return true;
|
|
2123
|
-
}
|
|
2124
|
-
/**
|
|
2125
|
-
* Get statistics about all session authorizations.
|
|
2126
|
-
*
|
|
2127
|
-
* This provides an overview of active, expired, and revoked sessions,
|
|
2128
|
-
* along with aggregate spending information.
|
|
2129
|
-
*
|
|
2130
|
-
* @returns Session statistics
|
|
2131
|
-
*
|
|
2132
|
-
* @example
|
|
2133
|
-
* ```typescript
|
|
2134
|
-
* const stats = await wallet.getSessionStats();
|
|
2135
|
-
* console.log(`Active sessions: ${stats.activeCount}`);
|
|
2136
|
-
* console.log(`Total authorized: $${stats.totalAuthorizedUsd.toFixed(2)}`);
|
|
2137
|
-
* console.log(`Total remaining: $${stats.totalRemainingUsd.toFixed(2)}`);
|
|
2138
|
-
*
|
|
2139
|
-
* for (const session of stats.activeSessions) {
|
|
2140
|
-
* console.log(`${session.merchantName}: $${session.remainingUsd} remaining`);
|
|
2141
|
-
* }
|
|
2142
|
-
* ```
|
|
2143
|
-
*/
|
|
2144
|
-
async getSessionStats() {
|
|
2145
|
-
this.logger.debug("getSessionStats");
|
|
2146
|
-
const sessions = await this.listSessions();
|
|
2147
|
-
const now = /* @__PURE__ */ new Date();
|
|
2148
|
-
const active = [];
|
|
2149
|
-
const expired = [];
|
|
2150
|
-
const revoked = [];
|
|
2151
|
-
for (const session of sessions) {
|
|
2152
|
-
if (session.status === "revoked") {
|
|
2153
|
-
revoked.push(session);
|
|
2154
|
-
} else if (session.status === "expired" || session.expiresAt && session.expiresAt < now) {
|
|
2155
|
-
expired.push(session);
|
|
2156
|
-
} else if (session.status === "active") {
|
|
2157
|
-
active.push(session);
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
const totalAuthorizedUsd = active.reduce((sum, s) => sum + s.spendingLimitUsd, 0);
|
|
2161
|
-
const totalSpentUsd = sessions.reduce((sum, s) => sum + s.amountUsedUsd, 0);
|
|
2162
|
-
const totalRemainingUsd = active.reduce((sum, s) => sum + s.remainingLimitUsd, 0);
|
|
2163
|
-
return {
|
|
2164
|
-
activeCount: active.length,
|
|
2165
|
-
expiredCount: expired.length,
|
|
2166
|
-
revokedCount: revoked.length,
|
|
2167
|
-
totalAuthorizedUsd,
|
|
2168
|
-
totalSpentUsd,
|
|
2169
|
-
totalRemainingUsd,
|
|
2170
|
-
activeSessions: active.map((s) => ({
|
|
2171
|
-
id: s.id,
|
|
2172
|
-
merchantName: s.merchantName,
|
|
2173
|
-
merchantPublicKey: s.merchantId,
|
|
2174
|
-
// merchantId is the public key in this context
|
|
2175
|
-
spendingLimitUsd: s.spendingLimitUsd,
|
|
2176
|
-
remainingUsd: s.remainingLimitUsd,
|
|
2177
|
-
expiresAt: s.expiresAt
|
|
2178
|
-
}))
|
|
2179
|
-
};
|
|
2180
|
-
}
|
|
2181
|
-
/**
|
|
2182
|
-
* Charge against an active session authorization.
|
|
2183
|
-
*
|
|
2184
|
-
* This is useful when you need to manually charge a session outside of
|
|
2185
|
-
* the `callMerchantApi()` flow.
|
|
2186
|
-
*
|
|
2187
|
-
* @param sessionId - The session ID to charge
|
|
2188
|
-
* @param amountUsd - Amount to charge in USD
|
|
2189
|
-
* @param options - Additional charge options
|
|
2190
|
-
* @returns Charge result
|
|
2191
|
-
*
|
|
2192
|
-
* @example
|
|
2193
|
-
* ```typescript
|
|
2194
|
-
* const result = await wallet.chargeSession('sess_abc123', 0.05, {
|
|
2195
|
-
* feature: 'ai-generation',
|
|
2196
|
-
* idempotencyKey: 'unique-key-123',
|
|
2197
|
-
* });
|
|
2198
|
-
*
|
|
2199
|
-
* console.log(`Charged $${result.amountUsd}, remaining: $${result.remainingSessionBalanceUsd}`);
|
|
2200
|
-
* ```
|
|
2201
|
-
*/
|
|
2202
|
-
async chargeSession(sessionId, amountUsd, options = {}) {
|
|
2203
|
-
this.logger.debug("chargeSession", { sessionId, amountUsd, options });
|
|
2204
|
-
const authHeaders = await this.getSessionAuthHeaders();
|
|
2205
|
-
const response = await fetch(`${this.baseUrl}/api/v2/charge`, {
|
|
2206
|
-
method: "POST",
|
|
2207
|
-
headers: {
|
|
2208
|
-
"Content-Type": "application/json",
|
|
2209
|
-
...authHeaders
|
|
2210
|
-
},
|
|
2211
|
-
body: JSON.stringify({
|
|
2212
|
-
session_id: sessionId,
|
|
2213
|
-
price_usd: amountUsd,
|
|
2214
|
-
feature: options.feature,
|
|
2215
|
-
idempotency_key: options.idempotencyKey,
|
|
2216
|
-
metadata: options.metadata
|
|
2217
|
-
})
|
|
2218
|
-
});
|
|
2219
|
-
if (!response.ok) {
|
|
2220
|
-
const error = await response.json().catch(() => ({}));
|
|
2221
|
-
const errorCode = error.error || error.error_code || "";
|
|
2222
|
-
if (response.status === 402) {
|
|
2223
|
-
if (errorCode === "session_limit_exceeded") {
|
|
2224
|
-
const limit = error.sessionLimitUsd || error.session_limit_usd || 0;
|
|
2225
|
-
const remaining = error.remainingUsd || error.remaining_usd || 0;
|
|
2226
|
-
throw new SessionLimitExceededError(limit, amountUsd, remaining, sessionId);
|
|
2227
|
-
}
|
|
2228
|
-
if (errorCode === "insufficient_balance") {
|
|
2229
|
-
throw new InsufficientBalanceError(
|
|
2230
|
-
amountUsd,
|
|
2231
|
-
error.availableUsd || error.available_usd || 0
|
|
2232
|
-
);
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
|
-
if (response.status === 404 || errorCode === "session_not_found") {
|
|
2236
|
-
throw new SessionNotFoundError(sessionId);
|
|
2237
|
-
}
|
|
2238
|
-
if (errorCode === "session_expired") {
|
|
2239
|
-
throw new SessionExpiredError(sessionId, error.expiredAt || error.expires_at);
|
|
2240
|
-
}
|
|
2241
|
-
if (errorCode === "session_revoked") {
|
|
2242
|
-
throw new SessionRevokedError(sessionId, error.reason);
|
|
2243
|
-
}
|
|
2244
|
-
throw new MixrPayError(error.message || `Charge failed: ${response.status}`);
|
|
2245
|
-
}
|
|
2246
|
-
const data = await response.json();
|
|
2247
|
-
this.logger.payment(amountUsd, sessionId, options.feature);
|
|
2248
|
-
return {
|
|
2249
|
-
success: true,
|
|
2250
|
-
chargeId: data.chargeId || data.charge_id,
|
|
2251
|
-
amountUsd: data.amountUsd || data.amount_usd || amountUsd,
|
|
2252
|
-
txHash: data.txHash || data.tx_hash,
|
|
2253
|
-
remainingSessionBalanceUsd: data.remainingSessionBalanceUsd || data.remaining_session_balance_usd || 0
|
|
2254
|
-
};
|
|
2255
|
-
}
|
|
2256
|
-
/**
|
|
2257
|
-
* Call a MixrPay merchant's API with automatic session management.
|
|
2258
|
-
*
|
|
2259
|
-
* This is the recommended way to interact with MixrPay-enabled APIs.
|
|
2260
|
-
* It automatically:
|
|
2261
|
-
* 1. Gets or creates a session authorization
|
|
2262
|
-
* 2. Adds the `X-Mixr-Session` header to the request
|
|
2263
|
-
* 3. Handles payment errors and session expiration
|
|
2264
|
-
*
|
|
2265
|
-
* @param options - API call options
|
|
2266
|
-
* @returns Response from the merchant API
|
|
2267
|
-
*
|
|
2268
|
-
* @example
|
|
2269
|
-
* ```typescript
|
|
2270
|
-
* const response = await wallet.callMerchantApi({
|
|
2271
|
-
* url: 'https://api.merchant.com/generate',
|
|
2272
|
-
* merchantPublicKey: 'pk_live_abc123...',
|
|
2273
|
-
* method: 'POST',
|
|
2274
|
-
* body: { prompt: 'Hello world' },
|
|
2275
|
-
* priceUsd: 0.05,
|
|
2276
|
-
* });
|
|
2277
|
-
*
|
|
2278
|
-
* const data = await response.json();
|
|
2279
|
-
* ```
|
|
2280
|
-
*/
|
|
2281
|
-
async callMerchantApi(options) {
|
|
2282
|
-
const {
|
|
2283
|
-
url,
|
|
2284
|
-
method = "POST",
|
|
2285
|
-
body,
|
|
2286
|
-
headers: customHeaders = {},
|
|
2287
|
-
merchantPublicKey,
|
|
2288
|
-
priceUsd,
|
|
2289
|
-
feature
|
|
2290
|
-
} = options;
|
|
2291
|
-
this.logger.debug("callMerchantApi", { url, method, merchantPublicKey, priceUsd });
|
|
2292
|
-
await this.validateMerchantAllowed(url, "url");
|
|
2293
|
-
if (priceUsd !== void 0 && this.maxPaymentUsd !== void 0 && priceUsd > this.maxPaymentUsd) {
|
|
2294
|
-
throw new SpendingLimitExceededError("client_max", this.maxPaymentUsd, priceUsd);
|
|
2295
|
-
}
|
|
2296
|
-
const session = await this.getOrCreateSession({
|
|
2297
|
-
merchantPublicKey,
|
|
2298
|
-
spendingLimitUsd: 25,
|
|
2299
|
-
// Default limit
|
|
2300
|
-
durationDays: 7
|
|
2301
|
-
});
|
|
2302
|
-
const headers = {
|
|
2303
|
-
"Content-Type": "application/json",
|
|
2304
|
-
"X-Mixr-Session": session.id,
|
|
2305
|
-
...customHeaders
|
|
2306
|
-
};
|
|
2307
|
-
if (feature) {
|
|
2308
|
-
headers["X-Mixr-Feature"] = feature;
|
|
2309
|
-
}
|
|
2310
|
-
const requestBody = body !== void 0 ? typeof body === "string" ? body : JSON.stringify(body) : void 0;
|
|
2311
|
-
const response = await fetch(url, {
|
|
2312
|
-
method,
|
|
2313
|
-
headers,
|
|
2314
|
-
body: requestBody,
|
|
2315
|
-
signal: AbortSignal.timeout(this.timeout)
|
|
2316
|
-
});
|
|
2317
|
-
const chargedAmount = response.headers.get("X-Mixr-Charged");
|
|
2318
|
-
if (chargedAmount) {
|
|
2319
|
-
const amountUsd = parseFloat(chargedAmount);
|
|
2320
|
-
if (!isNaN(amountUsd)) {
|
|
2321
|
-
const payment = {
|
|
2322
|
-
amountUsd,
|
|
2323
|
-
recipient: merchantPublicKey,
|
|
2324
|
-
txHash: response.headers.get("X-Payment-TxHash"),
|
|
2325
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
2326
|
-
description: feature || "API call",
|
|
2327
|
-
url,
|
|
2328
|
-
requestId: crypto.randomUUID(),
|
|
2329
|
-
correlationId: this.extractCorrelationId(customHeaders)
|
|
2330
|
-
};
|
|
2331
|
-
this.payments.push(payment);
|
|
2332
|
-
this.totalSpentUsd += amountUsd;
|
|
2333
|
-
this.logger.payment(amountUsd, merchantPublicKey, feature);
|
|
2334
|
-
if (this.onPayment) {
|
|
2335
|
-
this.onPayment(payment);
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
2338
|
-
}
|
|
2339
|
-
if (response.status === 402) {
|
|
2340
|
-
const errorData = await response.json().catch(() => ({}));
|
|
2341
|
-
const errorCode = errorData.error || errorData.error_code || "";
|
|
2342
|
-
if (errorCode === "session_expired") {
|
|
2343
|
-
this.logger.info("Session expired, creating new one...");
|
|
2344
|
-
const newSession = await this.getOrCreateSession({
|
|
2345
|
-
merchantPublicKey,
|
|
2346
|
-
spendingLimitUsd: 25,
|
|
2347
|
-
durationDays: 7
|
|
2348
|
-
});
|
|
2349
|
-
headers["X-Mixr-Session"] = newSession.id;
|
|
2350
|
-
return fetch(url, {
|
|
2351
|
-
method,
|
|
2352
|
-
headers,
|
|
2353
|
-
body: requestBody,
|
|
2354
|
-
signal: AbortSignal.timeout(this.timeout)
|
|
2355
|
-
});
|
|
2356
|
-
}
|
|
2357
|
-
if (errorCode === "session_limit_exceeded") {
|
|
2358
|
-
const limit = errorData.sessionLimitUsd || errorData.session_limit_usd || 0;
|
|
2359
|
-
const remaining = errorData.remainingUsd || errorData.remaining_usd || 0;
|
|
2360
|
-
throw new SessionLimitExceededError(limit, priceUsd || 0, remaining, session.id);
|
|
2361
|
-
}
|
|
2362
|
-
if (errorCode === "session_revoked") {
|
|
2363
|
-
throw new SessionRevokedError(session.id, errorData.reason);
|
|
2364
|
-
}
|
|
2365
|
-
if (errorCode === "session_not_found") {
|
|
2366
|
-
throw new SessionNotFoundError(session.id);
|
|
2367
|
-
}
|
|
2368
|
-
if (errorCode === "insufficient_balance") {
|
|
2369
|
-
throw new InsufficientBalanceError(
|
|
2370
|
-
priceUsd || 0,
|
|
2371
|
-
errorData.availableUsd || errorData.available_usd || 0
|
|
2372
|
-
);
|
|
2373
|
-
}
|
|
2374
|
-
}
|
|
2375
|
-
return response;
|
|
2376
|
-
}
|
|
2377
|
-
/**
|
|
2378
|
-
* Parse session response data into SessionAuthorization object.
|
|
2379
|
-
*/
|
|
2380
|
-
parseSessionResponse(data) {
|
|
2381
|
-
return {
|
|
2382
|
-
id: data.id || data.session_id || data.sessionId,
|
|
2383
|
-
merchantId: data.merchantId || data.merchant_id,
|
|
2384
|
-
merchantName: data.merchantName || data.merchant_name || "Unknown",
|
|
2385
|
-
status: data.status || "active",
|
|
2386
|
-
spendingLimitUsd: Number(data.spendingLimitUsd || data.spending_limit_usd || data.spendingLimit || 0),
|
|
2387
|
-
amountUsedUsd: Number(data.amountUsedUsd || data.amount_used_usd || data.amountUsed || 0),
|
|
2388
|
-
remainingLimitUsd: Number(
|
|
2389
|
-
data.remainingLimitUsd || data.remaining_limit_usd || data.remainingLimit || Number(data.spendingLimitUsd || data.spending_limit_usd || 0) - Number(data.amountUsedUsd || data.amount_used_usd || 0)
|
|
2390
|
-
),
|
|
2391
|
-
expiresAt: new Date(data.expiresAt || data.expires_at),
|
|
2392
|
-
createdAt: new Date(data.createdAt || data.created_at)
|
|
2393
|
-
};
|
|
2394
|
-
}
|
|
2395
|
-
// ===========================================================================
|
|
2396
|
-
// MCP (Model Context Protocol) Methods
|
|
2397
|
-
// ===========================================================================
|
|
2398
|
-
/**
|
|
2399
|
-
* Get authentication headers for MCP wallet-based authentication.
|
|
2400
|
-
*
|
|
2401
|
-
* These headers prove wallet ownership without transmitting the private key.
|
|
2402
|
-
* Use for direct pay-per-call mode (no session needed).
|
|
2403
|
-
*
|
|
2404
|
-
* @returns Headers object with X-Mixr-Wallet, X-Mixr-Signature, X-Mixr-Timestamp
|
|
2405
|
-
*
|
|
2406
|
-
* @example
|
|
2407
|
-
* ```typescript
|
|
2408
|
-
* const headers = await wallet.getMCPAuthHeaders();
|
|
2409
|
-
* const response = await fetch('https://mixrpay.com/api/mcp', {
|
|
2410
|
-
* method: 'POST',
|
|
2411
|
-
* headers: {
|
|
2412
|
-
* 'Content-Type': 'application/json',
|
|
2413
|
-
* ...headers,
|
|
2414
|
-
* },
|
|
2415
|
-
* body: JSON.stringify({
|
|
2416
|
-
* jsonrpc: '2.0',
|
|
2417
|
-
* method: 'tools/list',
|
|
2418
|
-
* id: 1,
|
|
2419
|
-
* }),
|
|
2420
|
-
* });
|
|
2421
|
-
* ```
|
|
2422
|
-
*/
|
|
2423
|
-
async getMCPAuthHeaders() {
|
|
2424
|
-
const timestamp = Date.now().toString();
|
|
2425
|
-
const message = `MixrPay MCP Auth
|
|
19
|
+
`),y+=`URI: ${s}
|
|
20
|
+
`,y+=`Version: ${h}
|
|
21
|
+
`,y+=`Chain ID: ${g}
|
|
22
|
+
`,y+=`Nonce: ${r}
|
|
23
|
+
`,y+=`Issued At: ${a.toISOString()}`,i&&(y+=`
|
|
24
|
+
Expiration Time: ${i.toISOString()}`),o&&(y+=`
|
|
25
|
+
Not Before: ${o.toISOString()}`),d&&(y+=`
|
|
26
|
+
Request ID: ${d}`),p&&p.length>0){y+=`
|
|
27
|
+
Resources:`;for(let f of p)y+=`
|
|
28
|
+
- ${f}`;}let m=await this.sessionKey.signMessage(y);return {message:y,signature:m,address:u,chainId:g,nonce:r,issuedAt:a,expirationTime:i}}setDebug(e){this.logger.setLevel(e?"debug":"none");}setLogLevel(e){this.logger.setLevel(e);}async getSessionAuthHeaders(){let e=await se(this.sessionKey);return {"X-Session-Auth":JSON.stringify(e)}}async getOrCreateSession(e){this.logger.debug("getOrCreateSession called",e);let{merchantPublicKey:t,spendingLimitUsd:s,durationDays:n}=e;try{let h=await this.getSessionByMerchant(t);if(h&&h.status==="active")return this.logger.debug("Found existing active session",h.id),h}catch{}this.logger.info(`Creating new session with merchant ${t.slice(0,20)}...`);let r=await this.getSessionAuthHeaders(),a=await fetch(`${this.baseUrl}/api/v2/session/authorize`,{method:"POST",headers:{"Content-Type":"application/json",...r},body:JSON.stringify({merchant_public_key:t,spending_limit_usd:s,duration_days:n})});if(!a.ok){let h=await a.json().catch(()=>({}));throw new c(h.message||h.error||`Failed to create session: ${a.status}`)}let i=await a.json(),o=i.session_id,d=i.message_to_sign;if(!o||!d)throw new c("Invalid authorize response: missing session_id or message_to_sign");this.logger.debug("Signing session authorization message...");let p=await this.sessionKey.signMessage(d),u=await fetch(`${this.baseUrl}/api/v2/session/confirm`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({session_id:o,signature:p,wallet_address:this.walletAddress})});if(!u.ok){let h=await u.json().catch(()=>({}));throw new c(h.message||`Failed to confirm session: ${u.status}`)}let g=await u.json();return this.logger.info(`Session created: ${g.session?.id||o}`),this.parseSessionResponse(g.session||g)}async getSessionByMerchant(e){this.logger.debug("getSessionByMerchant",e);let t=await this.getSessionAuthHeaders(),s=await fetch(`${this.baseUrl}/api/v2/session/status?merchant_public_key=${encodeURIComponent(e)}`,{headers:t});if(s.status===404)return null;if(!s.ok){let r=await s.json().catch(()=>({}));throw new c(r.message||`Failed to get session: ${s.status}`)}let n=await s.json();return n.session?this.parseSessionResponse(n.session):null}async listSessions(){this.logger.debug("listSessions");let e=await this.getSessionAuthHeaders(),t=await fetch(`${this.baseUrl}/api/v2/session/list`,{headers:e});if(!t.ok){let n=await t.json().catch(()=>({}));throw new c(n.message||`Failed to list sessions: ${t.status}`)}return ((await t.json()).sessions||[]).map(n=>this.parseSessionResponse(n))}async revokeSession(e){this.logger.debug("revokeSession",e);let t=await this.getSessionAuthHeaders(),s=await fetch(`${this.baseUrl}/api/v2/session/revoke`,{method:"POST",headers:{"Content-Type":"application/json",...t},body:JSON.stringify({session_id:e})});if(!s.ok){let n=await s.json().catch(()=>({}));return this.logger.error("Failed to revoke session:",n),false}return this.logger.info(`Session ${e} revoked`),true}async getSessionStats(){this.logger.debug("getSessionStats");let e=await this.listSessions(),t=new Date,s=[],n=[],r=[];for(let d of e)d.status==="revoked"?r.push(d):d.status==="expired"||d.expiresAt&&d.expiresAt<t?n.push(d):d.status==="active"&&s.push(d);let a=s.reduce((d,p)=>d+p.spendingLimitUsd,0),i=e.reduce((d,p)=>d+p.amountUsedUsd,0),o=s.reduce((d,p)=>d+p.remainingLimitUsd,0);return {activeCount:s.length,expiredCount:n.length,revokedCount:r.length,totalAuthorizedUsd:a,totalSpentUsd:i,totalRemainingUsd:o,activeSessions:s.map(d=>({id:d.id,merchantName:d.merchantName,merchantPublicKey:d.merchantId,spendingLimitUsd:d.spendingLimitUsd,remainingUsd:d.remainingLimitUsd,expiresAt:d.expiresAt}))}}async chargeSession(e,t,s={}){this.logger.debug("chargeSession",{sessionId:e,amountUsd:t,options:s});let n=await this.getSessionAuthHeaders(),r=await fetch(`${this.baseUrl}/api/v2/charge`,{method:"POST",headers:{"Content-Type":"application/json",...n},body:JSON.stringify({session_id:e,price_usd:t,feature:s.feature,idempotency_key:s.idempotencyKey,metadata:s.metadata})});if(!r.ok){let i=await r.json().catch(()=>({})),o=(i.error||i.error_code||"").toLowerCase();if(r.status===402){if(o==="session_limit_exceeded"){let d=i.sessionLimitUsd||i.session_limit_usd||0,p=i.remainingUsd||i.remaining_usd||0;throw new U(d,t,p,e)}if(o==="insufficient_balance")throw new v(t,i.availableUsd||i.available_usd||0)}throw r.status===404||o==="session_not_found"?new I(e):o==="session_expired"?new E(e,i.expiredAt||i.expires_at):o==="session_revoked"?new R(e,i.reason):new c(i.message||`Charge failed: ${r.status}`)}let a=await r.json();return this.logger.payment(t,e,s.feature),{success:true,chargeId:a.chargeId||a.charge_id,amountUsd:a.amountUsd||a.amount_usd||t,txHash:a.txHash||a.tx_hash,remainingSessionBalanceUsd:a.remainingSessionBalanceUsd||a.remaining_session_balance_usd||0}}async callMerchantApi(e){let{url:t,method:s="POST",body:n,headers:r={},merchantPublicKey:a,priceUsd:i,feature:o}=e;if(this.logger.debug("callMerchantApi",{url:t,method:s,merchantPublicKey:a,priceUsd:i}),await this.validateMerchantAllowed(t,"url"),i!==void 0&&this.maxPaymentUsd!==void 0&&i>this.maxPaymentUsd)throw new k("client_max",this.maxPaymentUsd,i);let d=await this.getOrCreateSession({merchantPublicKey:a}),p={"Content-Type":"application/json","X-Mixr-Session":d.id,...r};o&&(p["X-Mixr-Feature"]=o);let u=n!==void 0?typeof n=="string"?n:JSON.stringify(n):void 0,g=await fetch(t,{method:s,headers:p,body:u,signal:AbortSignal.timeout(this.timeout)}),h=g.headers.get("X-Mixr-Charged");if(h){let y=parseFloat(h);if(!isNaN(y)){let m={amountUsd:y,recipient:a,txHash:g.headers.get("X-Payment-TxHash"),timestamp:new Date,description:o||"API call",url:t,requestId:crypto.randomUUID(),correlationId:this.extractCorrelationId(r)};this.payments.push(m),this.totalSpentUsd+=y,this.logger.payment(y,a,o),this.onPayment&&this.onPayment(m);}}if(g.status===402){let y=await g.json().catch(()=>({})),m=(y.error||y.error_code||"").toLowerCase();if(m==="session_expired"){this.logger.info("Session expired, creating new one...");let f=await this.getOrCreateSession({merchantPublicKey:a});return p["X-Mixr-Session"]=f.id,fetch(t,{method:s,headers:p,body:u,signal:AbortSignal.timeout(this.timeout)})}if(m==="session_limit_exceeded"){let f=y.sessionLimitUsd||y.session_limit_usd||0,b=y.remainingUsd||y.remaining_usd||0;throw new U(f,i||0,b,d.id)}if(m==="session_revoked")throw new R(d.id,y.reason);if(m==="session_not_found")throw new I(d.id);if(m==="insufficient_balance")throw new v(i||0,y.availableUsd||y.available_usd||0)}return g}parseSessionResponse(e){return {id:e.id||e.session_id||e.sessionId,merchantId:e.merchantId||e.merchant_id,merchantName:e.merchantName||e.merchant_name||"Unknown",status:e.status||"active",spendingLimitUsd:Number(e.spendingLimitUsd||e.spending_limit_usd||e.spendingLimit||0),amountUsedUsd:Number(e.amountUsedUsd||e.amount_used_usd||e.amountUsed||0),remainingLimitUsd:Number(e.remainingLimitUsd||e.remaining_limit_usd||e.remainingLimit||Number(e.spendingLimitUsd||e.spending_limit_usd||0)-Number(e.amountUsedUsd||e.amount_used_usd||0)),expiresAt:new Date(e.expiresAt||e.expires_at),createdAt:new Date(e.createdAt||e.created_at)}}async getMCPAuthHeaders(){let e=Date.now().toString(),t=`MixrPay MCP Auth
|
|
2426
29
|
Wallet: ${this.walletAddress}
|
|
2427
|
-
Timestamp: ${timestamp}`;
|
|
2428
|
-
const signature = await this.sessionKey.signMessage(message);
|
|
2429
|
-
return {
|
|
2430
|
-
"X-Mixr-Wallet": this.walletAddress,
|
|
2431
|
-
"X-Mixr-Signature": signature,
|
|
2432
|
-
"X-Mixr-Timestamp": timestamp
|
|
2433
|
-
};
|
|
2434
|
-
}
|
|
2435
|
-
/**
|
|
2436
|
-
* List available MCP tools from the MixrPay gateway.
|
|
2437
|
-
*
|
|
2438
|
-
* Returns all tools exposed by MCP providers on the MixrPay marketplace.
|
|
2439
|
-
* Each tool includes pricing information.
|
|
2440
|
-
*
|
|
2441
|
-
* @returns Array of MCP tools with pricing and metadata
|
|
2442
|
-
*
|
|
2443
|
-
* @example
|
|
2444
|
-
* ```typescript
|
|
2445
|
-
* const tools = await wallet.listMCPTools();
|
|
2446
|
-
* for (const tool of tools) {
|
|
2447
|
-
* console.log(`${tool.name}: $${tool.priceUsd} - ${tool.description}`);
|
|
2448
|
-
* }
|
|
2449
|
-
* ```
|
|
2450
|
-
*/
|
|
2451
|
-
async listMCPTools() {
|
|
2452
|
-
this.logger.debug("listMCPTools");
|
|
2453
|
-
const response = await fetch(`${this.baseUrl}/api/mcp`, {
|
|
2454
|
-
method: "POST",
|
|
2455
|
-
headers: { "Content-Type": "application/json" },
|
|
2456
|
-
body: JSON.stringify({
|
|
2457
|
-
jsonrpc: "2.0",
|
|
2458
|
-
method: "tools/list",
|
|
2459
|
-
id: Date.now()
|
|
2460
|
-
})
|
|
2461
|
-
});
|
|
2462
|
-
if (!response.ok) {
|
|
2463
|
-
throw new MixrPayError(`Failed to list MCP tools: ${response.status}`);
|
|
2464
|
-
}
|
|
2465
|
-
const result = await response.json();
|
|
2466
|
-
if (result.error) {
|
|
2467
|
-
throw new MixrPayError(result.error.message || "Failed to list MCP tools");
|
|
2468
|
-
}
|
|
2469
|
-
return (result.result?.tools || []).map((tool) => ({
|
|
2470
|
-
name: tool.name,
|
|
2471
|
-
description: tool.description,
|
|
2472
|
-
inputSchema: tool.inputSchema,
|
|
2473
|
-
priceUsd: tool["x-mixrpay"]?.priceUsd || 0,
|
|
2474
|
-
merchantName: tool["x-mixrpay"]?.merchantName,
|
|
2475
|
-
merchantSlug: tool["x-mixrpay"]?.merchantSlug,
|
|
2476
|
-
verified: tool["x-mixrpay"]?.verified || false
|
|
2477
|
-
}));
|
|
2478
|
-
}
|
|
2479
|
-
/**
|
|
2480
|
-
* Call an MCP tool with wallet authentication (direct pay per call).
|
|
2481
|
-
*
|
|
2482
|
-
* This method signs a fresh auth message for each call, charging
|
|
2483
|
-
* directly from your wallet balance without needing a session.
|
|
2484
|
-
*
|
|
2485
|
-
* @param toolName - The tool name in format "merchant/tool"
|
|
2486
|
-
* @param args - Arguments to pass to the tool
|
|
2487
|
-
* @returns Tool execution result
|
|
2488
|
-
*
|
|
2489
|
-
* @example
|
|
2490
|
-
* ```typescript
|
|
2491
|
-
* const result = await wallet.callMCPTool('firecrawl/scrape', {
|
|
2492
|
-
* url: 'https://example.com',
|
|
2493
|
-
* });
|
|
2494
|
-
* console.log(result.data);
|
|
2495
|
-
* console.log(`Charged: $${result.chargedUsd}`);
|
|
2496
|
-
* ```
|
|
2497
|
-
*/
|
|
2498
|
-
async callMCPTool(toolName, args = {}) {
|
|
2499
|
-
this.logger.debug("callMCPTool", { toolName, args });
|
|
2500
|
-
await this.validateMerchantAllowed(toolName, "tool");
|
|
2501
|
-
const authHeaders = await this.getMCPAuthHeaders();
|
|
2502
|
-
const response = await fetch(`${this.baseUrl}/api/mcp`, {
|
|
2503
|
-
method: "POST",
|
|
2504
|
-
headers: {
|
|
2505
|
-
"Content-Type": "application/json",
|
|
2506
|
-
...authHeaders
|
|
2507
|
-
},
|
|
2508
|
-
body: JSON.stringify({
|
|
2509
|
-
jsonrpc: "2.0",
|
|
2510
|
-
method: "tools/call",
|
|
2511
|
-
params: { name: toolName, arguments: args },
|
|
2512
|
-
id: Date.now()
|
|
2513
|
-
})
|
|
2514
|
-
});
|
|
2515
|
-
const result = await response.json();
|
|
2516
|
-
if (result.error) {
|
|
2517
|
-
throw new MixrPayError(result.error.message || "MCP tool call failed");
|
|
2518
|
-
}
|
|
2519
|
-
const content = result.result?.content?.[0];
|
|
2520
|
-
const data = content?.text ? JSON.parse(content.text) : null;
|
|
2521
|
-
const mixrpay = result.result?._mixrpay || {};
|
|
2522
|
-
if (mixrpay.chargedUsd) {
|
|
2523
|
-
const payment = {
|
|
2524
|
-
amountUsd: mixrpay.chargedUsd,
|
|
2525
|
-
recipient: toolName.split("/")[0] || toolName,
|
|
2526
|
-
txHash: mixrpay.txHash,
|
|
2527
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
2528
|
-
description: `MCP: ${toolName}`,
|
|
2529
|
-
url: `${this.baseUrl}/api/mcp`,
|
|
2530
|
-
requestId: crypto.randomUUID()
|
|
2531
|
-
};
|
|
2532
|
-
this.payments.push(payment);
|
|
2533
|
-
this.totalSpentUsd += mixrpay.chargedUsd;
|
|
2534
|
-
this.logger.payment(mixrpay.chargedUsd, toolName, "MCP call");
|
|
2535
|
-
if (this.onPayment) {
|
|
2536
|
-
this.onPayment(payment);
|
|
2537
|
-
}
|
|
2538
|
-
}
|
|
2539
|
-
return {
|
|
2540
|
-
data,
|
|
2541
|
-
chargedUsd: mixrpay.chargedUsd || 0,
|
|
2542
|
-
txHash: mixrpay.txHash,
|
|
2543
|
-
latencyMs: mixrpay.latencyMs
|
|
2544
|
-
};
|
|
2545
|
-
}
|
|
2546
|
-
// ===========================================================================
|
|
2547
|
-
// JIT MCP Server Methods
|
|
2548
|
-
// ===========================================================================
|
|
2549
|
-
/**
|
|
2550
|
-
* Deploy a JIT MCP server from the Glama directory.
|
|
2551
|
-
*
|
|
2552
|
-
* Deploys any remote-capable MCP server to Cloudflare Workers.
|
|
2553
|
-
* Charges $1 from your session budget.
|
|
2554
|
-
*
|
|
2555
|
-
* @param options - Deployment options including Glama server details and env vars
|
|
2556
|
-
* @returns Deployed instance with private endpoint URL
|
|
2557
|
-
*
|
|
2558
|
-
* @example
|
|
2559
|
-
* ```typescript
|
|
2560
|
-
* const result = await wallet.deployJitMcp({
|
|
2561
|
-
* glamaId: 'notion-mcp',
|
|
2562
|
-
* glamaNamespace: 'notion',
|
|
2563
|
-
* glamaSlug: 'notion-mcp',
|
|
2564
|
-
* toolName: 'My Notion Server',
|
|
2565
|
-
* envVars: { NOTION_API_KEY: 'secret_...' },
|
|
2566
|
-
* ttlHours: 24,
|
|
2567
|
-
* });
|
|
2568
|
-
*
|
|
2569
|
-
* console.log('Endpoint:', result.instance.endpointUrl);
|
|
2570
|
-
* console.log('Expires:', result.instance.expiresAt);
|
|
2571
|
-
* ```
|
|
2572
|
-
*/
|
|
2573
|
-
async deployJitMcp(options) {
|
|
2574
|
-
this.logger.debug("deployJitMcp", { glamaId: options.glamaId, toolName: options.toolName });
|
|
2575
|
-
const authHeaders = await this.getSessionAuthHeaders();
|
|
2576
|
-
const response = await fetch(`${this.baseUrl}/api/v2/jit/deploy`, {
|
|
2577
|
-
method: "POST",
|
|
2578
|
-
headers: {
|
|
2579
|
-
"Content-Type": "application/json",
|
|
2580
|
-
...authHeaders
|
|
2581
|
-
},
|
|
2582
|
-
body: JSON.stringify({
|
|
2583
|
-
glama_id: options.glamaId,
|
|
2584
|
-
glama_namespace: options.glamaNamespace,
|
|
2585
|
-
glama_slug: options.glamaSlug,
|
|
2586
|
-
tool_name: options.toolName,
|
|
2587
|
-
tool_description: options.toolDescription,
|
|
2588
|
-
env_vars: options.envVars,
|
|
2589
|
-
ttl_hours: options.ttlHours || 24
|
|
2590
|
-
// session_id not needed - derived from X-Session-Auth
|
|
2591
|
-
})
|
|
2592
|
-
});
|
|
2593
|
-
if (!response.ok) {
|
|
2594
|
-
const error = await response.json().catch(() => ({}));
|
|
2595
|
-
throw new MixrPayError(error.error || `JIT deploy failed: ${response.status}`);
|
|
2596
|
-
}
|
|
2597
|
-
const data = await response.json();
|
|
2598
|
-
if (data.payment?.amount_usd > 0) {
|
|
2599
|
-
const payment = {
|
|
2600
|
-
amountUsd: data.payment.amount_usd,
|
|
2601
|
-
recipient: "mixrpay-jit-deploy",
|
|
2602
|
-
txHash: data.payment.tx_hash,
|
|
2603
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
2604
|
-
description: `JIT Deploy: ${options.toolName}`,
|
|
2605
|
-
url: `${this.baseUrl}/api/v2/jit/deploy`,
|
|
2606
|
-
requestId: data.request_id || crypto.randomUUID()
|
|
2607
|
-
};
|
|
2608
|
-
this.payments.push(payment);
|
|
2609
|
-
this.totalSpentUsd += data.payment.amount_usd;
|
|
2610
|
-
this.logger.payment(data.payment.amount_usd, "jit-deploy", options.toolName);
|
|
2611
|
-
if (this.onPayment) {
|
|
2612
|
-
this.onPayment(payment);
|
|
2613
|
-
}
|
|
2614
|
-
}
|
|
2615
|
-
return {
|
|
2616
|
-
instance: this.parseJitInstance(data.instance),
|
|
2617
|
-
payment: {
|
|
2618
|
-
method: data.payment.method,
|
|
2619
|
-
amountUsd: data.payment.amount_usd,
|
|
2620
|
-
txHash: data.payment.tx_hash
|
|
2621
|
-
}
|
|
2622
|
-
};
|
|
2623
|
-
}
|
|
2624
|
-
/**
|
|
2625
|
-
* List your deployed JIT MCP server instances.
|
|
2626
|
-
*
|
|
2627
|
-
* @param options - Optional filters
|
|
2628
|
-
* @returns Array of JIT instances
|
|
2629
|
-
*
|
|
2630
|
-
* @example
|
|
2631
|
-
* ```typescript
|
|
2632
|
-
* const instances = await wallet.listJitInstances({ status: 'active' });
|
|
2633
|
-
* for (const inst of instances) {
|
|
2634
|
-
* console.log(`${inst.toolName}: ${inst.endpointUrl}`);
|
|
2635
|
-
* }
|
|
2636
|
-
* ```
|
|
2637
|
-
*/
|
|
2638
|
-
async listJitInstances(options) {
|
|
2639
|
-
this.logger.debug("listJitInstances", options);
|
|
2640
|
-
const authHeaders = await this.getSessionAuthHeaders();
|
|
2641
|
-
const params = new URLSearchParams();
|
|
2642
|
-
if (options?.status) params.set("status", options.status);
|
|
2643
|
-
const response = await fetch(
|
|
2644
|
-
`${this.baseUrl}/api/v2/jit/instances?${params}`,
|
|
2645
|
-
{ headers: authHeaders }
|
|
2646
|
-
);
|
|
2647
|
-
if (!response.ok) {
|
|
2648
|
-
const error = await response.json().catch(() => ({}));
|
|
2649
|
-
throw new MixrPayError(error.error || `Failed to list JIT instances: ${response.status}`);
|
|
2650
|
-
}
|
|
2651
|
-
const data = await response.json();
|
|
2652
|
-
return (data.instances || []).map((i) => this.parseJitInstance(i));
|
|
2653
|
-
}
|
|
2654
|
-
/**
|
|
2655
|
-
* Stop a running JIT MCP server instance.
|
|
2656
|
-
*
|
|
2657
|
-
* Instance will be marked as stopped and Worker cleaned up.
|
|
2658
|
-
* No refund is given - instances are billed at deploy time.
|
|
2659
|
-
*
|
|
2660
|
-
* @param instanceId - The instance ID to stop
|
|
2661
|
-
*
|
|
2662
|
-
* @example
|
|
2663
|
-
* ```typescript
|
|
2664
|
-
* await wallet.stopJitInstance('inst_abc123');
|
|
2665
|
-
* console.log('Instance stopped');
|
|
2666
|
-
* ```
|
|
2667
|
-
*/
|
|
2668
|
-
async stopJitInstance(instanceId) {
|
|
2669
|
-
this.logger.debug("stopJitInstance", { instanceId });
|
|
2670
|
-
const authHeaders = await this.getSessionAuthHeaders();
|
|
2671
|
-
const response = await fetch(
|
|
2672
|
-
`${this.baseUrl}/api/v2/jit/instances/${instanceId}`,
|
|
2673
|
-
{ method: "DELETE", headers: authHeaders }
|
|
2674
|
-
);
|
|
2675
|
-
if (!response.ok) {
|
|
2676
|
-
const error = await response.json().catch(() => ({}));
|
|
2677
|
-
throw new MixrPayError(error.error || `Failed to stop JIT instance: ${response.status}`);
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
2680
|
-
/**
|
|
2681
|
-
* Parse JIT instance response data.
|
|
2682
|
-
*/
|
|
2683
|
-
parseJitInstance(data) {
|
|
2684
|
-
return {
|
|
2685
|
-
id: data.id,
|
|
2686
|
-
endpointUrl: data.endpoint_url,
|
|
2687
|
-
toolName: data.tool_name,
|
|
2688
|
-
glamaId: data.glama_id,
|
|
2689
|
-
glamaNamespace: data.glama_namespace,
|
|
2690
|
-
glamaSlug: data.glama_slug,
|
|
2691
|
-
status: data.status,
|
|
2692
|
-
mode: data.mode,
|
|
2693
|
-
ttlHours: data.ttl_hours,
|
|
2694
|
-
expiresAt: new Date(data.expires_at),
|
|
2695
|
-
createdAt: new Date(data.created_at)
|
|
2696
|
-
};
|
|
2697
|
-
}
|
|
2698
|
-
/**
|
|
2699
|
-
* Get details of a specific JIT MCP server instance.
|
|
2700
|
-
*
|
|
2701
|
-
* @param instanceId - The instance ID to retrieve
|
|
2702
|
-
* @returns Full instance details including endpoint URL
|
|
2703
|
-
*
|
|
2704
|
-
* @example
|
|
2705
|
-
* ```typescript
|
|
2706
|
-
* const instance = await wallet.getJitInstance('inst_abc123');
|
|
2707
|
-
* console.log('Endpoint:', instance.endpointUrl);
|
|
2708
|
-
* console.log('Expires:', instance.expiresAt);
|
|
2709
|
-
* ```
|
|
2710
|
-
*/
|
|
2711
|
-
async getJitInstance(instanceId) {
|
|
2712
|
-
this.logger.debug("getJitInstance", { instanceId });
|
|
2713
|
-
const authHeaders = await this.getSessionAuthHeaders();
|
|
2714
|
-
const response = await fetch(
|
|
2715
|
-
`${this.baseUrl}/api/v2/jit/instances/${instanceId}`,
|
|
2716
|
-
{ headers: authHeaders }
|
|
2717
|
-
);
|
|
2718
|
-
if (!response.ok) {
|
|
2719
|
-
const error = await response.json().catch(() => ({}));
|
|
2720
|
-
throw new MixrPayError(error.error || `Failed to get JIT instance: ${response.status}`);
|
|
2721
|
-
}
|
|
2722
|
-
const data = await response.json();
|
|
2723
|
-
return this.parseJitInstance(data.instance);
|
|
2724
|
-
}
|
|
2725
|
-
// ===========================================================================
|
|
2726
|
-
// Skills Methods
|
|
2727
|
-
// ===========================================================================
|
|
2728
|
-
/**
|
|
2729
|
-
* Use a configured skill by deploying a JIT MCP server with your saved API keys.
|
|
2730
|
-
*
|
|
2731
|
-
* Skills are configured on the MixrPay dashboard with your API keys stored securely.
|
|
2732
|
-
* When you call useSkill(), MixrPay deploys an ephemeral MCP server with your keys
|
|
2733
|
-
* injected server-side - your keys are never exposed to the agent.
|
|
2734
|
-
*
|
|
2735
|
-
* @param skillId - The skill ID (e.g., 'github', 'notion', 'spotify')
|
|
2736
|
-
* @param options - Optional configuration
|
|
2737
|
-
* @returns Skill endpoint and available tools
|
|
2738
|
-
*
|
|
2739
|
-
* @example
|
|
2740
|
-
* ```typescript
|
|
2741
|
-
* // Use the GitHub skill
|
|
2742
|
-
* const github = await wallet.useSkill('github');
|
|
2743
|
-
*
|
|
2744
|
-
* console.log('Endpoint:', github.endpoint);
|
|
2745
|
-
* console.log('Tools:', github.tools);
|
|
2746
|
-
* console.log('Expires:', github.expiresAt);
|
|
2747
|
-
*
|
|
2748
|
-
* // Connect to the MCP endpoint and call tools
|
|
2749
|
-
* // The exact method depends on your MCP client
|
|
2750
|
-
* ```
|
|
2751
|
-
*
|
|
2752
|
-
* @example
|
|
2753
|
-
* ```typescript
|
|
2754
|
-
* // Use Notion with custom TTL
|
|
2755
|
-
* const notion = await wallet.useSkill('notion', { ttlHours: 48 });
|
|
2756
|
-
* ```
|
|
2757
|
-
*
|
|
2758
|
-
* @throws {MixrPayError} If skill is not configured or deployment fails
|
|
2759
|
-
*/
|
|
2760
|
-
async useSkill(skillId, options) {
|
|
2761
|
-
this.logger.debug("useSkill", { skillId, options });
|
|
2762
|
-
const authHeaders = await this.getSessionAuthHeaders();
|
|
2763
|
-
const response = await fetch(`${this.baseUrl}/api/v2/skills/${skillId}/use`, {
|
|
2764
|
-
method: "POST",
|
|
2765
|
-
headers: {
|
|
2766
|
-
"Content-Type": "application/json",
|
|
2767
|
-
...authHeaders
|
|
2768
|
-
},
|
|
2769
|
-
body: JSON.stringify({
|
|
2770
|
-
ttl_hours: options?.ttlHours || 24
|
|
2771
|
-
})
|
|
2772
|
-
});
|
|
2773
|
-
if (!response.ok) {
|
|
2774
|
-
const error = await response.json().catch(() => ({}));
|
|
2775
|
-
if (error.configure_url) {
|
|
2776
|
-
throw new MixrPayError(
|
|
2777
|
-
`${error.error || "Skill not configured"}. Configure at: ${this.baseUrl}${error.configure_url}`,
|
|
2778
|
-
"SKILL_NOT_CONFIGURED"
|
|
2779
|
-
);
|
|
2780
|
-
}
|
|
2781
|
-
throw new MixrPayError(error.error || `Failed to use skill: ${response.status}`);
|
|
2782
|
-
}
|
|
2783
|
-
const data = await response.json();
|
|
2784
|
-
return {
|
|
2785
|
-
skillId: data.skill_id,
|
|
2786
|
-
endpoint: data.endpoint,
|
|
2787
|
-
tools: data.tools || [],
|
|
2788
|
-
expiresAt: new Date(data.expires_at),
|
|
2789
|
-
instanceId: data.instance_id
|
|
2790
|
-
};
|
|
2791
|
-
}
|
|
2792
|
-
/**
|
|
2793
|
-
* List all available skills and their configuration status.
|
|
2794
|
-
*
|
|
2795
|
-
* @returns Array of skills with status
|
|
2796
|
-
*
|
|
2797
|
-
* @example
|
|
2798
|
-
* ```typescript
|
|
2799
|
-
* const skills = await wallet.listSkills();
|
|
2800
|
-
*
|
|
2801
|
-
* // Find configured skills
|
|
2802
|
-
* const configured = skills.filter(s => s.status === 'configured');
|
|
2803
|
-
* console.log(`${configured.length} skills ready to use`);
|
|
2804
|
-
*
|
|
2805
|
-
* // Find skills that need configuration
|
|
2806
|
-
* const needsConfig = skills.filter(s => s.status === 'not_configured' && s.envVars.length > 0);
|
|
2807
|
-
* for (const skill of needsConfig) {
|
|
2808
|
-
* console.log(`${skill.name} needs: ${skill.envVars.map(v => v.label).join(', ')}`);
|
|
2809
|
-
* }
|
|
2810
|
-
* ```
|
|
2811
|
-
*/
|
|
2812
|
-
async listSkills() {
|
|
2813
|
-
this.logger.debug("listSkills");
|
|
2814
|
-
const authHeaders = await this.getSessionAuthHeaders();
|
|
2815
|
-
const response = await fetch(`${this.baseUrl}/api/v2/skills`, {
|
|
2816
|
-
headers: authHeaders
|
|
2817
|
-
});
|
|
2818
|
-
if (!response.ok) {
|
|
2819
|
-
const error = await response.json().catch(() => ({}));
|
|
2820
|
-
throw new MixrPayError(error.error || `Failed to list skills: ${response.status}`);
|
|
2821
|
-
}
|
|
2822
|
-
const data = await response.json();
|
|
2823
|
-
return data.skills || [];
|
|
2824
|
-
}
|
|
2825
|
-
/**
|
|
2826
|
-
* Get apps connected via Composio (Gmail, Slack, GitHub, etc.).
|
|
2827
|
-
*
|
|
2828
|
-
* Use this to discover which apps the owner has connected before
|
|
2829
|
-
* attempting to use them via `wallet.useSkill('composio')`.
|
|
2830
|
-
*
|
|
2831
|
-
* @returns Object with connected app names and connection details
|
|
2832
|
-
*
|
|
2833
|
-
* @example
|
|
2834
|
-
* ```typescript
|
|
2835
|
-
* const apps = await wallet.getConnectedApps();
|
|
2836
|
-
*
|
|
2837
|
-
* if (apps.connected.length === 0) {
|
|
2838
|
-
* console.log('No apps connected. Ask your owner to connect apps at the dashboard.');
|
|
2839
|
-
* } else {
|
|
2840
|
-
* console.log('Connected apps:', apps.connected.join(', '));
|
|
2841
|
-
*
|
|
2842
|
-
* if (apps.connected.includes('gmail')) {
|
|
2843
|
-
* // Safe to use Composio for Gmail
|
|
2844
|
-
* const { endpoint, tools } = await wallet.useSkill('composio');
|
|
2845
|
-
* // tools will include 'gmail_read_emails', 'gmail_send_email', etc.
|
|
2846
|
-
* }
|
|
2847
|
-
* }
|
|
2848
|
-
* ```
|
|
2849
|
-
*/
|
|
2850
|
-
async getConnectedApps() {
|
|
2851
|
-
this.logger.debug("getConnectedApps");
|
|
2852
|
-
const authHeaders = await this.getSessionAuthHeaders();
|
|
2853
|
-
const response = await fetch(`${this.baseUrl}/api/v2/skills/composio/connections`, {
|
|
2854
|
-
headers: authHeaders
|
|
2855
|
-
});
|
|
2856
|
-
if (!response.ok) {
|
|
2857
|
-
const error = await response.json().catch(() => ({}));
|
|
2858
|
-
throw new MixrPayError(
|
|
2859
|
-
error.error || `Failed to get connected apps: ${response.status}`,
|
|
2860
|
-
"CONNECTED_APPS_FAILED"
|
|
2861
|
-
);
|
|
2862
|
-
}
|
|
2863
|
-
const data = await response.json();
|
|
2864
|
-
const connectedAppNames = (data.connections || []).filter((c) => c.status === "connected").map((c) => c.app);
|
|
2865
|
-
return {
|
|
2866
|
-
connected: connectedAppNames,
|
|
2867
|
-
connections: data.connections || [],
|
|
2868
|
-
oauthConnections: data.oauth_connections || [],
|
|
2869
|
-
apiKeyConnections: data.api_key_connections || []
|
|
2870
|
-
};
|
|
2871
|
-
}
|
|
2872
|
-
/**
|
|
2873
|
-
* Get configuration status for a specific skill.
|
|
2874
|
-
*
|
|
2875
|
-
* @param skillId - The skill ID
|
|
2876
|
-
* @returns Skill configuration status
|
|
2877
|
-
*
|
|
2878
|
-
* @example
|
|
2879
|
-
* ```typescript
|
|
2880
|
-
* const status = await wallet.getSkillStatus('github');
|
|
2881
|
-
*
|
|
2882
|
-
* if (status.status === 'configured') {
|
|
2883
|
-
* console.log('GitHub is ready!');
|
|
2884
|
-
* console.log('Configured vars:', status.configuredVars);
|
|
2885
|
-
* } else {
|
|
2886
|
-
* console.log('Please configure GitHub at the MixrPay dashboard');
|
|
2887
|
-
* }
|
|
2888
|
-
* ```
|
|
2889
|
-
*/
|
|
2890
|
-
async getSkillStatus(skillId) {
|
|
2891
|
-
this.logger.debug("getSkillStatus", { skillId });
|
|
2892
|
-
const authHeaders = await this.getSessionAuthHeaders();
|
|
2893
|
-
const response = await fetch(`${this.baseUrl}/api/v2/skills/${skillId}/configure`, {
|
|
2894
|
-
headers: authHeaders
|
|
2895
|
-
});
|
|
2896
|
-
if (!response.ok) {
|
|
2897
|
-
const error = await response.json().catch(() => ({}));
|
|
2898
|
-
throw new MixrPayError(error.error || `Failed to get skill status: ${response.status}`);
|
|
2899
|
-
}
|
|
2900
|
-
const data = await response.json();
|
|
2901
|
-
return {
|
|
2902
|
-
skillId: data.skill_id,
|
|
2903
|
-
status: data.status,
|
|
2904
|
-
configuredVars: data.configured_vars || [],
|
|
2905
|
-
lastUsedAt: data.last_used_at ? new Date(data.last_used_at) : void 0,
|
|
2906
|
-
errorMessage: data.error_message
|
|
2907
|
-
};
|
|
2908
|
-
}
|
|
2909
|
-
/**
|
|
2910
|
-
* Configure a skill with API keys.
|
|
2911
|
-
*
|
|
2912
|
-
* This allows agents to save API keys received via chat to the MixrPay platform.
|
|
2913
|
-
* Keys are encrypted at rest and will be available for use on subsequent deploy/redeploy.
|
|
2914
|
-
*
|
|
2915
|
-
* @param skillId - The skill ID (e.g., 'web-search', 'github', 'notion') or custom skill with 'custom-' prefix
|
|
2916
|
-
* @param envVars - Environment variables with API keys (e.g., { BRAVE_API_KEY: 'xxx' })
|
|
2917
|
-
* @returns Configuration result with list of configured variables
|
|
2918
|
-
*
|
|
2919
|
-
* @example Predefined skill
|
|
2920
|
-
* ```typescript
|
|
2921
|
-
* // User provides API key in chat, agent saves it
|
|
2922
|
-
* const result = await wallet.configureSkill('web-search', {
|
|
2923
|
-
* BRAVE_API_KEY: 'BSA_abc123xyz',
|
|
2924
|
-
* });
|
|
2925
|
-
*
|
|
2926
|
-
* console.log('Configured:', result.configuredVars);
|
|
2927
|
-
* // After next redeploy, the skill will be enabled with this key
|
|
2928
|
-
* ```
|
|
2929
|
-
*
|
|
2930
|
-
* @example Custom API key
|
|
2931
|
-
* ```typescript
|
|
2932
|
-
* // For APIs not in predefined skills, use custom- prefix
|
|
2933
|
-
* // User: "Here's my Polymarket API key: pk_abc123"
|
|
2934
|
-
* const result = await wallet.configureSkill('custom-polymarket', {
|
|
2935
|
-
* POLYMARKET_API_KEY: 'pk_abc123',
|
|
2936
|
-
* });
|
|
2937
|
-
*
|
|
2938
|
-
* // The key is now available in the agent's environment
|
|
2939
|
-
* // Access via: process.env.POLYMARKET_API_KEY
|
|
2940
|
-
* ```
|
|
2941
|
-
*/
|
|
2942
|
-
async configureSkill(skillId, envVars) {
|
|
2943
|
-
this.logger.debug("configureSkill", { skillId, varNames: Object.keys(envVars) });
|
|
2944
|
-
const authHeaders = await this.getSessionAuthHeaders();
|
|
2945
|
-
const response = await fetch(`${this.baseUrl}/api/v2/skills/${skillId}/configure`, {
|
|
2946
|
-
method: "POST",
|
|
2947
|
-
headers: {
|
|
2948
|
-
"Content-Type": "application/json",
|
|
2949
|
-
...authHeaders
|
|
2950
|
-
},
|
|
2951
|
-
body: JSON.stringify({ env_vars: envVars })
|
|
2952
|
-
});
|
|
2953
|
-
if (!response.ok) {
|
|
2954
|
-
const error = await response.json().catch(() => ({}));
|
|
2955
|
-
throw new MixrPayError(
|
|
2956
|
-
error.error || `Failed to configure skill: ${response.status}`,
|
|
2957
|
-
error.error_code
|
|
2958
|
-
);
|
|
2959
|
-
}
|
|
2960
|
-
const data = await response.json();
|
|
2961
|
-
return {
|
|
2962
|
-
success: true,
|
|
2963
|
-
skillId: data.skill_id,
|
|
2964
|
-
configuredVars: data.configured_vars || []
|
|
2965
|
-
};
|
|
2966
|
-
}
|
|
2967
|
-
// ===========================================================================
|
|
2968
|
-
// Glama MCP Directory Methods
|
|
2969
|
-
// ===========================================================================
|
|
2970
|
-
/**
|
|
2971
|
-
* Search the Glama MCP server directory.
|
|
2972
|
-
*
|
|
2973
|
-
* Glama indexes 15,000+ MCP servers. Use this to discover tools
|
|
2974
|
-
* that can be deployed as JIT servers.
|
|
2975
|
-
*
|
|
2976
|
-
* Note: This is a public API and does not require authentication.
|
|
2977
|
-
*
|
|
2978
|
-
* @param query - Search query (e.g., "notion", "github", "database")
|
|
2979
|
-
* @returns Array of matching servers with hosting info
|
|
2980
|
-
*
|
|
2981
|
-
* @example
|
|
2982
|
-
* ```typescript
|
|
2983
|
-
* const results = await wallet.searchGlamaDirectory('notion');
|
|
2984
|
-
*
|
|
2985
|
-
* // Filter to only hostable servers
|
|
2986
|
-
* const hostable = results.servers.filter(s => s.canHost);
|
|
2987
|
-
* console.log(`Found ${hostable.length} deployable servers`);
|
|
2988
|
-
*
|
|
2989
|
-
* // Deploy one
|
|
2990
|
-
* if (hostable.length > 0) {
|
|
2991
|
-
* const server = hostable[0];
|
|
2992
|
-
* await wallet.deployJitMcp({
|
|
2993
|
-
* glamaId: server.id,
|
|
2994
|
-
* glamaNamespace: server.namespace,
|
|
2995
|
-
* glamaSlug: server.slug,
|
|
2996
|
-
* toolName: server.name,
|
|
2997
|
-
* envVars: { API_KEY: '...' },
|
|
2998
|
-
* });
|
|
2999
|
-
* }
|
|
3000
|
-
* ```
|
|
3001
|
-
*/
|
|
3002
|
-
async searchGlamaDirectory(query) {
|
|
3003
|
-
this.logger.debug("searchGlamaDirectory", { query });
|
|
3004
|
-
const params = new URLSearchParams({ q: query });
|
|
3005
|
-
const response = await fetch(`${this.baseUrl}/api/mcp/glama?${params}`);
|
|
3006
|
-
if (!response.ok) {
|
|
3007
|
-
const error = await response.json().catch(() => ({}));
|
|
3008
|
-
throw new MixrPayError(error.error || `Glama search failed: ${response.status}`);
|
|
3009
|
-
}
|
|
3010
|
-
const data = await response.json();
|
|
3011
|
-
return {
|
|
3012
|
-
servers: (data.servers || []).map((s) => this.parseGlamaServer(s)),
|
|
3013
|
-
pageInfo: data.pageInfo,
|
|
3014
|
-
query: data.query
|
|
3015
|
-
};
|
|
3016
|
-
}
|
|
3017
|
-
/**
|
|
3018
|
-
* Get featured/popular MCP servers from the Glama directory.
|
|
3019
|
-
*
|
|
3020
|
-
* Returns curated list of popular servers when you don't have
|
|
3021
|
-
* a specific search query.
|
|
3022
|
-
*
|
|
3023
|
-
* Note: This is a public API and does not require authentication.
|
|
3024
|
-
*
|
|
3025
|
-
* @returns Array of featured servers with hosting info
|
|
3026
|
-
*
|
|
3027
|
-
* @example
|
|
3028
|
-
* ```typescript
|
|
3029
|
-
* const { servers } = await wallet.getFeaturedGlamaServers();
|
|
3030
|
-
* console.log('Featured servers:', servers.map(s => s.name));
|
|
3031
|
-
* ```
|
|
3032
|
-
*/
|
|
3033
|
-
async getFeaturedGlamaServers() {
|
|
3034
|
-
this.logger.debug("getFeaturedGlamaServers");
|
|
3035
|
-
const response = await fetch(`${this.baseUrl}/api/mcp/glama`);
|
|
3036
|
-
if (!response.ok) {
|
|
3037
|
-
const error = await response.json().catch(() => ({}));
|
|
3038
|
-
throw new MixrPayError(error.error || `Failed to get featured servers: ${response.status}`);
|
|
3039
|
-
}
|
|
3040
|
-
const data = await response.json();
|
|
3041
|
-
return {
|
|
3042
|
-
servers: (data.servers || []).map((s) => this.parseGlamaServer(s)),
|
|
3043
|
-
featured: data.featured
|
|
3044
|
-
};
|
|
3045
|
-
}
|
|
3046
|
-
/**
|
|
3047
|
-
* Parse Glama server response data.
|
|
3048
|
-
*/
|
|
3049
|
-
parseGlamaServer(data) {
|
|
3050
|
-
const importData = data.importData;
|
|
3051
|
-
return {
|
|
3052
|
-
id: data.id,
|
|
3053
|
-
name: data.name,
|
|
3054
|
-
namespace: data.namespace,
|
|
3055
|
-
slug: data.slug,
|
|
3056
|
-
description: data.description,
|
|
3057
|
-
url: data.url,
|
|
3058
|
-
attributes: data.attributes,
|
|
3059
|
-
canHost: data.canHost,
|
|
3060
|
-
tools: data.tools || [],
|
|
3061
|
-
repository: data.repository,
|
|
3062
|
-
license: data.spdxLicense?.name,
|
|
3063
|
-
importData: importData ? {
|
|
3064
|
-
glamaId: importData.glamaId,
|
|
3065
|
-
glamaNamespace: importData.glamaNamespace,
|
|
3066
|
-
glamaSlug: importData.glamaSlug,
|
|
3067
|
-
suggestedName: importData.suggestedName,
|
|
3068
|
-
suggestedDescription: importData.suggestedDescription,
|
|
3069
|
-
hostingType: importData.hostingType,
|
|
3070
|
-
requiredEnvVars: importData.requiredEnvVars,
|
|
3071
|
-
optionalEnvVars: importData.optionalEnvVars
|
|
3072
|
-
} : void 0
|
|
3073
|
-
};
|
|
3074
|
-
}
|
|
3075
|
-
// ===========================================================================
|
|
3076
|
-
// JIT Task Agent Methods
|
|
3077
|
-
// ===========================================================================
|
|
3078
|
-
/**
|
|
3079
|
-
* Deploy a JIT Task Agent - a serverless agent that executes a specific task.
|
|
3080
|
-
*
|
|
3081
|
-
* JIT Task Agents run on Cloudflare Workers and execute an agentic loop
|
|
3082
|
-
* (LLM + tools) within a budget cap. They self-destruct when complete.
|
|
3083
|
-
*
|
|
3084
|
-
* @param options - Task agent deployment options
|
|
3085
|
-
* @returns Deployed task agent instance with endpoints
|
|
3086
|
-
*
|
|
3087
|
-
* @example Basic usage
|
|
3088
|
-
* ```typescript
|
|
3089
|
-
* const result = await wallet.deployTaskAgent({
|
|
3090
|
-
* name: 'Research Agent',
|
|
3091
|
-
* prompt: 'Research the top 5 AI startups in San Francisco',
|
|
3092
|
-
* budgetUsd: 5.00,
|
|
3093
|
-
* tools: ['platform/exa-search', 'platform/firecrawl-scrape'],
|
|
3094
|
-
* });
|
|
3095
|
-
*
|
|
3096
|
-
* console.log('Task ID:', result.instance.id);
|
|
3097
|
-
* console.log('Status URL:', result.instance.statusUrl);
|
|
3098
|
-
* ```
|
|
3099
|
-
*
|
|
3100
|
-
* @example With auto-run
|
|
3101
|
-
* ```typescript
|
|
3102
|
-
* const result = await wallet.deployTaskAgent({
|
|
3103
|
-
* name: 'Data Collector',
|
|
3104
|
-
* prompt: 'Collect pricing data from competitor websites',
|
|
3105
|
-
* budgetUsd: 10.00,
|
|
3106
|
-
* tools: ['platform/firecrawl-scrape'],
|
|
3107
|
-
* autoRun: true, // Start immediately
|
|
3108
|
-
* });
|
|
3109
|
-
*
|
|
3110
|
-
* // Wait for completion
|
|
3111
|
-
* const finalResult = await wallet.waitForTaskAgent(result.instance.id);
|
|
3112
|
-
* console.log('Result:', finalResult.result);
|
|
3113
|
-
* ```
|
|
3114
|
-
*/
|
|
3115
|
-
async deployTaskAgent(options) {
|
|
3116
|
-
this.logger.debug("deployTaskAgent", { name: options.name, budgetUsd: options.budgetUsd });
|
|
3117
|
-
const authHeaders = await this.getSessionAuthHeaders();
|
|
3118
|
-
const response = await fetch(`${this.baseUrl}/api/v2/jit-task/deploy`, {
|
|
3119
|
-
method: "POST",
|
|
3120
|
-
headers: {
|
|
3121
|
-
"Content-Type": "application/json",
|
|
3122
|
-
...authHeaders
|
|
3123
|
-
},
|
|
3124
|
-
body: JSON.stringify({
|
|
3125
|
-
name: options.name,
|
|
3126
|
-
prompt: options.prompt,
|
|
3127
|
-
system_prompt: options.systemPrompt,
|
|
3128
|
-
model: options.model || "claude-sonnet-4-5",
|
|
3129
|
-
tools: options.tools || [],
|
|
3130
|
-
budget_usd: options.budgetUsd,
|
|
3131
|
-
ttl_hours: options.ttlHours || 24,
|
|
3132
|
-
max_iterations: options.maxIterations || 10,
|
|
3133
|
-
plan_id: options.planId,
|
|
3134
|
-
task_id: options.taskId,
|
|
3135
|
-
idempotency_key: options.idempotencyKey,
|
|
3136
|
-
auto_run: options.autoRun || false
|
|
3137
|
-
})
|
|
3138
|
-
});
|
|
3139
|
-
if (!response.ok) {
|
|
3140
|
-
const error = await response.json().catch(() => ({}));
|
|
3141
|
-
throw new MixrPayError(error.error || `Task agent deploy failed: ${response.status}`);
|
|
3142
|
-
}
|
|
3143
|
-
const data = await response.json();
|
|
3144
|
-
return {
|
|
3145
|
-
instance: this.parseTaskAgentInstance(data.instance),
|
|
3146
|
-
idempotent: data.idempotent || false,
|
|
3147
|
-
requestId: data.request_id
|
|
3148
|
-
};
|
|
3149
|
-
}
|
|
3150
|
-
/**
|
|
3151
|
-
* Get the status of a JIT Task Agent.
|
|
3152
|
-
*
|
|
3153
|
-
* @param instanceId - The task agent instance ID
|
|
3154
|
-
* @returns Current status and details
|
|
3155
|
-
*
|
|
3156
|
-
* @example
|
|
3157
|
-
* ```typescript
|
|
3158
|
-
* const status = await wallet.getTaskAgentStatus('cuid_abc123');
|
|
3159
|
-
* console.log('Status:', status.status);
|
|
3160
|
-
* console.log('Iterations:', status.iterations.current, '/', status.iterations.max);
|
|
3161
|
-
* console.log('Spent:', status.budget.spentUsd, '/', status.budget.totalUsd);
|
|
3162
|
-
* ```
|
|
3163
|
-
*/
|
|
3164
|
-
async getTaskAgentStatus(instanceId) {
|
|
3165
|
-
this.logger.debug("getTaskAgentStatus", { instanceId });
|
|
3166
|
-
const authHeaders = await this.getSessionAuthHeaders();
|
|
3167
|
-
const response = await fetch(`${this.baseUrl}/api/v2/jit-task/${instanceId}`, {
|
|
3168
|
-
method: "GET",
|
|
3169
|
-
headers: authHeaders
|
|
3170
|
-
});
|
|
3171
|
-
if (!response.ok) {
|
|
3172
|
-
const error = await response.json().catch(() => ({}));
|
|
3173
|
-
throw new MixrPayError(error.error || `Failed to get task status: ${response.status}`);
|
|
3174
|
-
}
|
|
3175
|
-
const data = await response.json();
|
|
3176
|
-
return this.parseTaskAgentStatus(data);
|
|
3177
|
-
}
|
|
3178
|
-
/**
|
|
3179
|
-
* Trigger execution of a JIT Task Agent.
|
|
3180
|
-
*
|
|
3181
|
-
* Only works for agents in 'active' status (not yet started).
|
|
3182
|
-
*
|
|
3183
|
-
* @param instanceId - The task agent instance ID
|
|
3184
|
-
* @returns Trigger result
|
|
3185
|
-
*
|
|
3186
|
-
* @example
|
|
3187
|
-
* ```typescript
|
|
3188
|
-
* // Deploy without auto-run
|
|
3189
|
-
* const { instance } = await wallet.deployTaskAgent({
|
|
3190
|
-
* name: 'Research',
|
|
3191
|
-
* prompt: 'Research AI trends',
|
|
3192
|
-
* budgetUsd: 5.00,
|
|
3193
|
-
* });
|
|
3194
|
-
*
|
|
3195
|
-
* // Later, trigger execution
|
|
3196
|
-
* await wallet.triggerTaskAgent(instance.id);
|
|
3197
|
-
* ```
|
|
3198
|
-
*/
|
|
3199
|
-
async triggerTaskAgent(instanceId) {
|
|
3200
|
-
this.logger.debug("triggerTaskAgent", { instanceId });
|
|
3201
|
-
const authHeaders = await this.getSessionAuthHeaders();
|
|
3202
|
-
const response = await fetch(`${this.baseUrl}/api/v2/jit-task/${instanceId}`, {
|
|
3203
|
-
method: "POST",
|
|
3204
|
-
headers: {
|
|
3205
|
-
"Content-Type": "application/json",
|
|
3206
|
-
...authHeaders
|
|
3207
|
-
}
|
|
3208
|
-
});
|
|
3209
|
-
if (!response.ok) {
|
|
3210
|
-
const error = await response.json().catch(() => ({}));
|
|
3211
|
-
throw new MixrPayError(error.error || `Failed to trigger task: ${response.status}`);
|
|
3212
|
-
}
|
|
3213
|
-
const data = await response.json();
|
|
3214
|
-
return {
|
|
3215
|
-
success: data.success,
|
|
3216
|
-
status: data.status
|
|
3217
|
-
};
|
|
3218
|
-
}
|
|
3219
|
-
/**
|
|
3220
|
-
* Cancel a JIT Task Agent.
|
|
3221
|
-
*
|
|
3222
|
-
* Stops execution and marks the task as cancelled.
|
|
3223
|
-
* Cannot cancel tasks that are already completed/failed.
|
|
3224
|
-
*
|
|
3225
|
-
* @param instanceId - The task agent instance ID
|
|
3226
|
-
* @returns Cancel result
|
|
3227
|
-
*
|
|
3228
|
-
* @example
|
|
3229
|
-
* ```typescript
|
|
3230
|
-
* await wallet.cancelTaskAgent('cuid_abc123');
|
|
3231
|
-
* console.log('Task cancelled');
|
|
3232
|
-
* ```
|
|
3233
|
-
*/
|
|
3234
|
-
async cancelTaskAgent(instanceId) {
|
|
3235
|
-
this.logger.debug("cancelTaskAgent", { instanceId });
|
|
3236
|
-
const authHeaders = await this.getSessionAuthHeaders();
|
|
3237
|
-
const response = await fetch(`${this.baseUrl}/api/v2/jit-task/${instanceId}`, {
|
|
3238
|
-
method: "DELETE",
|
|
3239
|
-
headers: authHeaders
|
|
3240
|
-
});
|
|
3241
|
-
if (!response.ok) {
|
|
3242
|
-
const error = await response.json().catch(() => ({}));
|
|
3243
|
-
throw new MixrPayError(error.error || `Failed to cancel task: ${response.status}`);
|
|
3244
|
-
}
|
|
3245
|
-
const data = await response.json();
|
|
3246
|
-
return {
|
|
3247
|
-
success: data.success,
|
|
3248
|
-
status: data.status
|
|
3249
|
-
};
|
|
3250
|
-
}
|
|
3251
|
-
/**
|
|
3252
|
-
* List JIT Task Agents for the authenticated user.
|
|
3253
|
-
*
|
|
3254
|
-
* Returns deployed task agents with optional filtering by status,
|
|
3255
|
-
* plan, or task. Includes pagination and summary statistics.
|
|
3256
|
-
*
|
|
3257
|
-
* @param options - Optional filter and pagination parameters
|
|
3258
|
-
* @returns List of task agents with pagination and stats
|
|
3259
|
-
*
|
|
3260
|
-
* @example List all task agents
|
|
3261
|
-
* ```typescript
|
|
3262
|
-
* const { taskAgents, pagination, stats } = await wallet.listTaskAgents();
|
|
3263
|
-
* console.log(`${pagination.total} total agents, ${stats.active} active`);
|
|
3264
|
-
* ```
|
|
3265
|
-
*
|
|
3266
|
-
* @example Filter by status
|
|
3267
|
-
* ```typescript
|
|
3268
|
-
* const { taskAgents } = await wallet.listTaskAgents({ status: 'running' });
|
|
3269
|
-
* for (const agent of taskAgents) {
|
|
3270
|
-
* console.log(`${agent.name}: $${agent.budget.spentUsd}/$${agent.budget.totalUsd}`);
|
|
3271
|
-
* }
|
|
3272
|
-
* ```
|
|
3273
|
-
*
|
|
3274
|
-
* @example Filter by plan
|
|
3275
|
-
* ```typescript
|
|
3276
|
-
* const { taskAgents } = await wallet.listTaskAgents({ planId: 'plan_abc' });
|
|
3277
|
-
* ```
|
|
3278
|
-
*/
|
|
3279
|
-
async listTaskAgents(options) {
|
|
3280
|
-
this.logger.debug("listTaskAgents", { options });
|
|
3281
|
-
const authHeaders = await this.getSessionAuthHeaders();
|
|
3282
|
-
const searchParams = new URLSearchParams();
|
|
3283
|
-
if (options?.status) searchParams.set("status", options.status);
|
|
3284
|
-
if (options?.planId) searchParams.set("plan_id", options.planId);
|
|
3285
|
-
if (options?.taskId) searchParams.set("task_id", options.taskId);
|
|
3286
|
-
if (options?.limit !== void 0) searchParams.set("limit", String(options.limit));
|
|
3287
|
-
if (options?.offset !== void 0) searchParams.set("offset", String(options.offset));
|
|
3288
|
-
const qs = searchParams.toString();
|
|
3289
|
-
const url = `${this.baseUrl}/api/v2/jit-task${qs ? `?${qs}` : ""}`;
|
|
3290
|
-
const response = await fetch(url, {
|
|
3291
|
-
method: "GET",
|
|
3292
|
-
headers: authHeaders
|
|
3293
|
-
});
|
|
3294
|
-
if (!response.ok) {
|
|
3295
|
-
const error = await response.json().catch(() => ({}));
|
|
3296
|
-
throw new MixrPayError(error.error || `Failed to list task agents: ${response.status}`);
|
|
3297
|
-
}
|
|
3298
|
-
const data = await response.json();
|
|
3299
|
-
return {
|
|
3300
|
-
taskAgents: (data.task_agents || []).map((agent) => {
|
|
3301
|
-
const budget = agent.budget;
|
|
3302
|
-
const iterations = agent.iterations;
|
|
3303
|
-
const usage = agent.usage;
|
|
3304
|
-
const plan = agent.plan;
|
|
3305
|
-
const task = agent.task;
|
|
3306
|
-
return {
|
|
3307
|
-
id: agent.id,
|
|
3308
|
-
name: agent.name,
|
|
3309
|
-
prompt: agent.prompt,
|
|
3310
|
-
model: agent.model,
|
|
3311
|
-
tools: agent.tools,
|
|
3312
|
-
status: agent.status,
|
|
3313
|
-
budget: {
|
|
3314
|
-
totalUsd: budget.total_usd,
|
|
3315
|
-
spentUsd: budget.spent_usd,
|
|
3316
|
-
remainingUsd: budget.remaining_usd
|
|
3317
|
-
},
|
|
3318
|
-
iterations: {
|
|
3319
|
-
current: iterations.current,
|
|
3320
|
-
max: iterations.max
|
|
3321
|
-
},
|
|
3322
|
-
usage: {
|
|
3323
|
-
toolCalls: usage.tool_calls,
|
|
3324
|
-
inputTokens: usage.input_tokens,
|
|
3325
|
-
outputTokens: usage.output_tokens
|
|
3326
|
-
},
|
|
3327
|
-
result: agent.result,
|
|
3328
|
-
error: agent.error,
|
|
3329
|
-
depth: agent.depth,
|
|
3330
|
-
plan: plan ? { id: plan.id, title: plan.title } : null,
|
|
3331
|
-
task: task ? { id: task.id, title: task.title } : null,
|
|
3332
|
-
ttlHours: agent.ttl_hours,
|
|
3333
|
-
expiresAt: new Date(agent.expires_at),
|
|
3334
|
-
createdAt: new Date(agent.created_at),
|
|
3335
|
-
startedAt: agent.started_at ? new Date(agent.started_at) : void 0,
|
|
3336
|
-
completedAt: agent.completed_at ? new Date(agent.completed_at) : void 0
|
|
3337
|
-
};
|
|
3338
|
-
}),
|
|
3339
|
-
pagination: {
|
|
3340
|
-
total: data.pagination.total,
|
|
3341
|
-
limit: data.pagination.limit,
|
|
3342
|
-
offset: data.pagination.offset,
|
|
3343
|
-
hasMore: data.pagination.has_more
|
|
3344
|
-
},
|
|
3345
|
-
stats: {
|
|
3346
|
-
active: data.stats.active ?? 0,
|
|
3347
|
-
completed: data.stats.completed ?? 0,
|
|
3348
|
-
failed: data.stats.failed ?? 0,
|
|
3349
|
-
totalSpentUsd: data.stats.total_spent_usd ?? 0
|
|
3350
|
-
}
|
|
3351
|
-
};
|
|
3352
|
-
}
|
|
3353
|
-
/**
|
|
3354
|
-
* Wait for a JIT Task Agent to complete.
|
|
3355
|
-
*
|
|
3356
|
-
* Polls the status until the task reaches a terminal state
|
|
3357
|
-
* (completed, failed, cancelled, budget_exceeded, expired).
|
|
3358
|
-
*
|
|
3359
|
-
* @param instanceId - The task agent instance ID
|
|
3360
|
-
* @param options - Wait options
|
|
3361
|
-
* @returns Final task result
|
|
3362
|
-
*
|
|
3363
|
-
* @example
|
|
3364
|
-
* ```typescript
|
|
3365
|
-
* const result = await wallet.waitForTaskAgent('cuid_abc123', {
|
|
3366
|
-
* pollIntervalMs: 2000,
|
|
3367
|
-
* timeoutMs: 300000, // 5 minutes
|
|
3368
|
-
* });
|
|
3369
|
-
*
|
|
3370
|
-
* if (result.status === 'completed') {
|
|
3371
|
-
* console.log('Result:', result.result);
|
|
3372
|
-
* } else {
|
|
3373
|
-
* console.log('Failed:', result.error);
|
|
3374
|
-
* }
|
|
3375
|
-
* ```
|
|
3376
|
-
*/
|
|
3377
|
-
async waitForTaskAgent(instanceId, options) {
|
|
3378
|
-
const pollIntervalMs = options?.pollIntervalMs || 2e3;
|
|
3379
|
-
const timeoutMs = options?.timeoutMs || 3e5;
|
|
3380
|
-
const startTime = Date.now();
|
|
3381
|
-
this.logger.debug("waitForTaskAgent", { instanceId, pollIntervalMs, timeoutMs });
|
|
3382
|
-
while (true) {
|
|
3383
|
-
const status = await this.getTaskAgentStatus(instanceId);
|
|
3384
|
-
if (["completed", "failed", "cancelled", "budget_exceeded", "expired"].includes(status.status)) {
|
|
3385
|
-
return {
|
|
3386
|
-
id: status.id,
|
|
3387
|
-
status: status.status,
|
|
3388
|
-
result: status.result,
|
|
3389
|
-
error: status.error,
|
|
3390
|
-
spentUsd: status.budget.spentUsd,
|
|
3391
|
-
iterations: status.iterations.current,
|
|
3392
|
-
toolCalls: status.usage.toolCalls,
|
|
3393
|
-
completedAt: status.completedAt
|
|
3394
|
-
};
|
|
3395
|
-
}
|
|
3396
|
-
if (Date.now() - startTime > timeoutMs) {
|
|
3397
|
-
throw new MixrPayError(`Task agent wait timeout after ${timeoutMs}ms`);
|
|
3398
|
-
}
|
|
3399
|
-
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
3400
|
-
}
|
|
3401
|
-
}
|
|
3402
|
-
/**
|
|
3403
|
-
* Parse task agent instance from API response.
|
|
3404
|
-
*/
|
|
3405
|
-
parseTaskAgentInstance(data) {
|
|
3406
|
-
return {
|
|
3407
|
-
id: data.id,
|
|
3408
|
-
name: data.name,
|
|
3409
|
-
endpointUrl: data.endpoint_url,
|
|
3410
|
-
statusUrl: data.status_url,
|
|
3411
|
-
triggerUrl: data.trigger_url,
|
|
3412
|
-
cancelUrl: data.cancel_url,
|
|
3413
|
-
resultUrl: data.result_url,
|
|
3414
|
-
status: data.status,
|
|
3415
|
-
budgetUsd: data.budget_usd,
|
|
3416
|
-
expiresAt: new Date(data.expires_at),
|
|
3417
|
-
depth: data.depth,
|
|
3418
|
-
maxIterations: data.max_iterations
|
|
3419
|
-
};
|
|
3420
|
-
}
|
|
3421
|
-
/**
|
|
3422
|
-
* Parse task agent status from API response.
|
|
3423
|
-
*/
|
|
3424
|
-
parseTaskAgentStatus(data) {
|
|
3425
|
-
const budget = data.budget;
|
|
3426
|
-
const iterations = data.iterations;
|
|
3427
|
-
const usage = data.usage;
|
|
3428
|
-
const endpoints = data.endpoints;
|
|
3429
|
-
return {
|
|
3430
|
-
id: data.id,
|
|
3431
|
-
name: data.name,
|
|
3432
|
-
prompt: data.prompt,
|
|
3433
|
-
model: data.model,
|
|
3434
|
-
tools: data.tools,
|
|
3435
|
-
status: data.status,
|
|
3436
|
-
budget: {
|
|
3437
|
-
totalUsd: budget?.total_usd ?? 0,
|
|
3438
|
-
spentUsd: budget?.spent_usd ?? 0,
|
|
3439
|
-
remainingUsd: budget?.remaining_usd ?? 0
|
|
3440
|
-
},
|
|
3441
|
-
iterations: {
|
|
3442
|
-
current: iterations?.current ?? 0,
|
|
3443
|
-
max: iterations?.max ?? 0
|
|
3444
|
-
},
|
|
3445
|
-
usage: {
|
|
3446
|
-
toolCalls: usage?.tool_calls ?? 0,
|
|
3447
|
-
inputTokens: usage?.input_tokens ?? 0,
|
|
3448
|
-
outputTokens: usage?.output_tokens ?? 0
|
|
3449
|
-
},
|
|
3450
|
-
result: data.result,
|
|
3451
|
-
error: data.error,
|
|
3452
|
-
depth: data.depth,
|
|
3453
|
-
planId: data.plan_id,
|
|
3454
|
-
taskId: data.task_id,
|
|
3455
|
-
endpoints: {
|
|
3456
|
-
statusUrl: endpoints?.status_url ?? "",
|
|
3457
|
-
triggerUrl: endpoints?.trigger_url ?? "",
|
|
3458
|
-
cancelUrl: endpoints?.cancel_url ?? "",
|
|
3459
|
-
resultUrl: endpoints?.result_url ?? ""
|
|
3460
|
-
},
|
|
3461
|
-
ttlHours: data.ttl_hours,
|
|
3462
|
-
expiresAt: new Date(data.expires_at),
|
|
3463
|
-
createdAt: new Date(data.created_at),
|
|
3464
|
-
startedAt: data.started_at ? new Date(data.started_at) : void 0,
|
|
3465
|
-
completedAt: data.completed_at ? new Date(data.completed_at) : void 0
|
|
3466
|
-
};
|
|
3467
|
-
}
|
|
3468
|
-
// ===========================================================================
|
|
3469
|
-
// LLM Completion Methods
|
|
3470
|
-
// ===========================================================================
|
|
3471
|
-
/**
|
|
3472
|
-
* Simple LLM completion - single-turn, no tools.
|
|
3473
|
-
*
|
|
3474
|
-
* This is a convenience method for agents that just need to call an LLM
|
|
3475
|
-
* without the full agentic loop. Useful when Clawdbot spawns agents
|
|
3476
|
-
* that need to make their own LLM calls through MixrPay.
|
|
3477
|
-
*
|
|
3478
|
-
* @param prompt - The user prompt to send
|
|
3479
|
-
* @param options - Optional configuration
|
|
3480
|
-
* @returns The LLM response text and cost
|
|
3481
|
-
*
|
|
3482
|
-
* @example Basic usage
|
|
3483
|
-
* ```typescript
|
|
3484
|
-
* const result = await wallet.complete('Summarize this text: ...');
|
|
3485
|
-
* console.log(result.text);
|
|
3486
|
-
* console.log(`Cost: $${result.costUsd.toFixed(4)}`);
|
|
3487
|
-
* ```
|
|
3488
|
-
*
|
|
3489
|
-
* @example With custom model
|
|
3490
|
-
* ```typescript
|
|
3491
|
-
* const result = await wallet.complete(
|
|
3492
|
-
* 'Write a haiku about coding',
|
|
3493
|
-
* { model: 'gpt-4o', systemPrompt: 'You are a poet.' }
|
|
3494
|
-
* );
|
|
3495
|
-
* ```
|
|
3496
|
-
*/
|
|
3497
|
-
async complete(prompt, options) {
|
|
3498
|
-
this.logger.debug("complete", { promptLength: prompt.length, model: options?.model });
|
|
3499
|
-
const result = await this.runAgent({
|
|
3500
|
-
messages: [{ role: "user", content: prompt }],
|
|
3501
|
-
config: {
|
|
3502
|
-
model: options?.model || "gpt-4o-mini",
|
|
3503
|
-
maxIterations: 1,
|
|
3504
|
-
tools: [],
|
|
3505
|
-
// No tools - pure LLM completion
|
|
3506
|
-
systemPrompt: options?.systemPrompt
|
|
3507
|
-
}
|
|
3508
|
-
});
|
|
3509
|
-
return {
|
|
3510
|
-
text: result.response,
|
|
3511
|
-
costUsd: result.cost.totalUsd,
|
|
3512
|
-
tokens: result.tokens,
|
|
3513
|
-
model: options?.model || "gpt-4o-mini"
|
|
3514
|
-
};
|
|
3515
|
-
}
|
|
3516
|
-
// ===========================================================================
|
|
3517
|
-
// Agent Runtime API
|
|
3518
|
-
// ===========================================================================
|
|
3519
|
-
/**
|
|
3520
|
-
* Run an AI agent with LLM and tool execution.
|
|
3521
|
-
*
|
|
3522
|
-
* This method orchestrates a full agentic loop:
|
|
3523
|
-
* - Multi-turn reasoning with LLM
|
|
3524
|
-
* - Automatic tool execution
|
|
3525
|
-
* - Bundled billing (single charge at end)
|
|
3526
|
-
* - Optional streaming via SSE
|
|
3527
|
-
*
|
|
3528
|
-
* @param options - Agent run options
|
|
3529
|
-
* @returns Agent run result with response and cost breakdown
|
|
3530
|
-
*
|
|
3531
|
-
* @example Basic usage
|
|
3532
|
-
* ```typescript
|
|
3533
|
-
* const result = await wallet.runAgent({
|
|
3534
|
-
* sessionId: 'sess_abc123',
|
|
3535
|
-
* messages: [{ role: 'user', content: 'Find AI startups in SF' }],
|
|
3536
|
-
* });
|
|
3537
|
-
*
|
|
3538
|
-
* console.log(result.response);
|
|
3539
|
-
* console.log(`Cost: $${result.cost.totalUsd.toFixed(4)}`);
|
|
3540
|
-
* ```
|
|
3541
|
-
*
|
|
3542
|
-
* @example With custom config
|
|
3543
|
-
* ```typescript
|
|
3544
|
-
* const result = await wallet.runAgent({
|
|
3545
|
-
* sessionId: 'sess_abc123',
|
|
3546
|
-
* messages: [{ role: 'user', content: 'Research quantum computing' }],
|
|
3547
|
-
* config: {
|
|
3548
|
-
* model: 'gpt-4o',
|
|
3549
|
-
* maxIterations: 15,
|
|
3550
|
-
* tools: ['platform/exa-search', 'platform/firecrawl-scrape'],
|
|
3551
|
-
* systemPrompt: 'You are a research assistant.',
|
|
3552
|
-
* },
|
|
3553
|
-
* });
|
|
3554
|
-
* ```
|
|
3555
|
-
*
|
|
3556
|
-
* @example With streaming
|
|
3557
|
-
* ```typescript
|
|
3558
|
-
* await wallet.runAgent({
|
|
3559
|
-
* sessionId: 'sess_abc123',
|
|
3560
|
-
* messages: [{ role: 'user', content: 'Analyze this company' }],
|
|
3561
|
-
* stream: true,
|
|
3562
|
-
* onEvent: (event) => {
|
|
3563
|
-
* if (event.type === 'llm_chunk') {
|
|
3564
|
-
* process.stdout.write(event.delta);
|
|
3565
|
-
* } else if (event.type === 'tool_call') {
|
|
3566
|
-
* console.log(`Calling tool: ${event.tool}`);
|
|
3567
|
-
* }
|
|
3568
|
-
* },
|
|
3569
|
-
* });
|
|
3570
|
-
* ```
|
|
3571
|
-
*/
|
|
3572
|
-
async runAgent(options) {
|
|
3573
|
-
const {
|
|
3574
|
-
sessionId,
|
|
3575
|
-
messages,
|
|
3576
|
-
config = {},
|
|
3577
|
-
stream = false,
|
|
3578
|
-
idempotencyKey,
|
|
3579
|
-
onEvent
|
|
3580
|
-
} = options;
|
|
3581
|
-
this.logger.debug("runAgent", { sessionId: sessionId || "(from signature)", messageCount: messages.length, config, stream });
|
|
3582
|
-
const body = {
|
|
3583
|
-
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
3584
|
-
config: {
|
|
3585
|
-
model: config.model,
|
|
3586
|
-
max_iterations: config.maxIterations,
|
|
3587
|
-
tools: config.tools,
|
|
3588
|
-
system_prompt: config.systemPrompt
|
|
3589
|
-
},
|
|
3590
|
-
stream,
|
|
3591
|
-
idempotency_key: idempotencyKey
|
|
3592
|
-
};
|
|
3593
|
-
if (sessionId) {
|
|
3594
|
-
body.session_id = sessionId;
|
|
3595
|
-
}
|
|
3596
|
-
const AGENT_RUN_TIMEOUT = 18e4;
|
|
3597
|
-
if (!stream) {
|
|
3598
|
-
const authHeaders = await this.getSessionAuthHeaders();
|
|
3599
|
-
const response = await fetch(`${this.baseUrl}/api/v2/agent/run`, {
|
|
3600
|
-
method: "POST",
|
|
3601
|
-
headers: {
|
|
3602
|
-
"Content-Type": "application/json",
|
|
3603
|
-
...authHeaders
|
|
3604
|
-
},
|
|
3605
|
-
body: JSON.stringify(body),
|
|
3606
|
-
signal: AbortSignal.timeout(AGENT_RUN_TIMEOUT)
|
|
3607
|
-
});
|
|
3608
|
-
if (!response.ok) {
|
|
3609
|
-
const error = await response.json().catch(() => ({}));
|
|
3610
|
-
throw new MixrPayError(error.error || `Agent run failed: ${response.status}`);
|
|
3611
|
-
}
|
|
3612
|
-
const data = await response.json();
|
|
3613
|
-
if (data.cost?.total_usd > 0) {
|
|
3614
|
-
const payment = {
|
|
3615
|
-
amountUsd: data.cost.total_usd,
|
|
3616
|
-
recipient: "mixrpay-agent-run",
|
|
3617
|
-
txHash: data.tx_hash,
|
|
3618
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
3619
|
-
description: `Agent run: ${data.run_id}`,
|
|
3620
|
-
url: `${this.baseUrl}/api/v2/agent/run`,
|
|
3621
|
-
requestId: idempotencyKey || crypto.randomUUID()
|
|
3622
|
-
};
|
|
3623
|
-
this.payments.push(payment);
|
|
3624
|
-
this.totalSpentUsd += data.cost.total_usd;
|
|
3625
|
-
this.logger.payment(data.cost.total_usd, "agent-run", data.run_id);
|
|
3626
|
-
if (this.onPayment) {
|
|
3627
|
-
this.onPayment(payment);
|
|
3628
|
-
}
|
|
3629
|
-
}
|
|
3630
|
-
return {
|
|
3631
|
-
runId: data.run_id,
|
|
3632
|
-
status: data.status,
|
|
3633
|
-
response: data.response,
|
|
3634
|
-
iterations: data.iterations,
|
|
3635
|
-
toolsUsed: data.tools_used,
|
|
3636
|
-
cost: {
|
|
3637
|
-
llmUsd: data.cost.llm_usd,
|
|
3638
|
-
toolsUsd: data.cost.tools_usd,
|
|
3639
|
-
totalUsd: data.cost.total_usd
|
|
3640
|
-
},
|
|
3641
|
-
tokens: data.tokens,
|
|
3642
|
-
sessionRemainingUsd: data.session_remaining_usd,
|
|
3643
|
-
txHash: data.tx_hash
|
|
3644
|
-
};
|
|
3645
|
-
}
|
|
3646
|
-
return this.runAgentStreaming(body, onEvent);
|
|
3647
|
-
}
|
|
3648
|
-
/**
|
|
3649
|
-
* Internal: Handle streaming agent run via SSE
|
|
3650
|
-
*/
|
|
3651
|
-
async runAgentStreaming(body, onEvent) {
|
|
3652
|
-
const STREAMING_TIMEOUT = 3e5;
|
|
3653
|
-
const authHeaders = await this.getSessionAuthHeaders();
|
|
3654
|
-
const response = await fetch(`${this.baseUrl}/api/v2/agent/run`, {
|
|
3655
|
-
method: "POST",
|
|
3656
|
-
headers: {
|
|
3657
|
-
"Content-Type": "application/json",
|
|
3658
|
-
...authHeaders
|
|
3659
|
-
},
|
|
3660
|
-
body: JSON.stringify(body),
|
|
3661
|
-
signal: AbortSignal.timeout(STREAMING_TIMEOUT)
|
|
3662
|
-
});
|
|
3663
|
-
if (!response.ok) {
|
|
3664
|
-
const error = await response.json().catch(() => ({}));
|
|
3665
|
-
throw new MixrPayError(error.error || `Agent run failed: ${response.status}`);
|
|
3666
|
-
}
|
|
3667
|
-
const reader = response.body?.getReader();
|
|
3668
|
-
if (!reader) {
|
|
3669
|
-
throw new MixrPayError("No response body for streaming");
|
|
3670
|
-
}
|
|
3671
|
-
const decoder = new TextDecoder();
|
|
3672
|
-
let buffer = "";
|
|
3673
|
-
let result = null;
|
|
3674
|
-
while (true) {
|
|
3675
|
-
const { done, value } = await reader.read();
|
|
3676
|
-
if (done) break;
|
|
3677
|
-
buffer += decoder.decode(value, { stream: true });
|
|
3678
|
-
const lines = buffer.split("\n");
|
|
3679
|
-
buffer = lines.pop() || "";
|
|
3680
|
-
let currentEvent = "";
|
|
3681
|
-
for (const line of lines) {
|
|
3682
|
-
if (line.startsWith("event: ")) {
|
|
3683
|
-
currentEvent = line.slice(7).trim();
|
|
3684
|
-
} else if (line.startsWith("data: ") && currentEvent) {
|
|
3685
|
-
try {
|
|
3686
|
-
const data = JSON.parse(line.slice(6));
|
|
3687
|
-
const event = this.parseSSEEvent(currentEvent, data);
|
|
3688
|
-
if (event) {
|
|
3689
|
-
onEvent?.(event);
|
|
3690
|
-
if (currentEvent === "complete") {
|
|
3691
|
-
result = {
|
|
3692
|
-
runId: data.run_id,
|
|
3693
|
-
status: "completed",
|
|
3694
|
-
response: data.response,
|
|
3695
|
-
iterations: data.iterations,
|
|
3696
|
-
toolsUsed: data.tools_used,
|
|
3697
|
-
cost: {
|
|
3698
|
-
llmUsd: 0,
|
|
3699
|
-
// Not provided in streaming complete
|
|
3700
|
-
toolsUsd: 0,
|
|
3701
|
-
totalUsd: data.total_cost_usd
|
|
3702
|
-
},
|
|
3703
|
-
tokens: { prompt: 0, completion: 0 },
|
|
3704
|
-
sessionRemainingUsd: 0,
|
|
3705
|
-
txHash: data.tx_hash
|
|
3706
|
-
};
|
|
3707
|
-
if (data.total_cost_usd > 0) {
|
|
3708
|
-
const payment = {
|
|
3709
|
-
amountUsd: data.total_cost_usd,
|
|
3710
|
-
recipient: "mixrpay-agent-run",
|
|
3711
|
-
txHash: data.tx_hash,
|
|
3712
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
3713
|
-
description: `Agent run: ${data.run_id}`,
|
|
3714
|
-
url: `${this.baseUrl}/api/v2/agent/run`,
|
|
3715
|
-
requestId: body.idempotency_key || crypto.randomUUID()
|
|
3716
|
-
};
|
|
3717
|
-
this.payments.push(payment);
|
|
3718
|
-
this.totalSpentUsd += data.total_cost_usd;
|
|
3719
|
-
if (this.onPayment) {
|
|
3720
|
-
this.onPayment(payment);
|
|
3721
|
-
}
|
|
3722
|
-
}
|
|
3723
|
-
}
|
|
3724
|
-
if (currentEvent === "error") {
|
|
3725
|
-
throw new MixrPayError(data.error || "Agent run failed");
|
|
3726
|
-
}
|
|
3727
|
-
}
|
|
3728
|
-
} catch (e) {
|
|
3729
|
-
if (e instanceof MixrPayError) throw e;
|
|
3730
|
-
this.logger.warn("Failed to parse SSE event:", e);
|
|
3731
|
-
}
|
|
3732
|
-
currentEvent = "";
|
|
3733
|
-
}
|
|
3734
|
-
}
|
|
3735
|
-
}
|
|
3736
|
-
if (!result) {
|
|
3737
|
-
throw new MixrPayError("Agent run completed without final result");
|
|
3738
|
-
}
|
|
3739
|
-
return result;
|
|
3740
|
-
}
|
|
3741
|
-
/**
|
|
3742
|
-
* Internal: Parse SSE event into typed event object
|
|
3743
|
-
*/
|
|
3744
|
-
parseSSEEvent(eventType, data) {
|
|
3745
|
-
switch (eventType) {
|
|
3746
|
-
case "run_start":
|
|
3747
|
-
return { type: "run_start", runId: data.run_id };
|
|
3748
|
-
case "iteration_start":
|
|
3749
|
-
return { type: "iteration_start", iteration: data.iteration };
|
|
3750
|
-
case "llm_chunk":
|
|
3751
|
-
return { type: "llm_chunk", delta: data.delta };
|
|
3752
|
-
case "tool_call":
|
|
3753
|
-
return { type: "tool_call", tool: data.tool, arguments: data.arguments };
|
|
3754
|
-
case "tool_result":
|
|
3755
|
-
return {
|
|
3756
|
-
type: "tool_result",
|
|
3757
|
-
tool: data.tool,
|
|
3758
|
-
success: data.success,
|
|
3759
|
-
costUsd: data.cost_usd,
|
|
3760
|
-
error: data.error
|
|
3761
|
-
};
|
|
3762
|
-
case "iteration_complete":
|
|
3763
|
-
return {
|
|
3764
|
-
type: "iteration_complete",
|
|
3765
|
-
iteration: data.iteration,
|
|
3766
|
-
tokens: data.tokens,
|
|
3767
|
-
costUsd: data.cost_usd
|
|
3768
|
-
};
|
|
3769
|
-
case "complete":
|
|
3770
|
-
return {
|
|
3771
|
-
type: "complete",
|
|
3772
|
-
runId: data.run_id,
|
|
3773
|
-
response: data.response,
|
|
3774
|
-
totalCostUsd: data.total_cost_usd,
|
|
3775
|
-
txHash: data.tx_hash,
|
|
3776
|
-
iterations: data.iterations,
|
|
3777
|
-
toolsUsed: data.tools_used
|
|
3778
|
-
};
|
|
3779
|
-
case "error":
|
|
3780
|
-
return {
|
|
3781
|
-
type: "error",
|
|
3782
|
-
error: data.error,
|
|
3783
|
-
partialCostUsd: data.partial_cost_usd
|
|
3784
|
-
};
|
|
3785
|
-
default:
|
|
3786
|
-
return null;
|
|
3787
|
-
}
|
|
3788
|
-
}
|
|
3789
|
-
/**
|
|
3790
|
-
* Get the status of an agent run by ID.
|
|
3791
|
-
*
|
|
3792
|
-
* @param runId - The agent run ID
|
|
3793
|
-
* @param sessionId - The session ID (for authentication)
|
|
3794
|
-
* @returns Agent run status and results
|
|
3795
|
-
*
|
|
3796
|
-
* @example
|
|
3797
|
-
* ```typescript
|
|
3798
|
-
* const status = await wallet.getAgentRunStatus('run_abc123', 'sess_xyz789');
|
|
3799
|
-
* console.log(`Status: ${status.status}`);
|
|
3800
|
-
* if (status.status === 'completed') {
|
|
3801
|
-
* console.log(status.response);
|
|
3802
|
-
* }
|
|
3803
|
-
* ```
|
|
3804
|
-
*/
|
|
3805
|
-
async getAgentRunStatus(runId, _sessionId) {
|
|
3806
|
-
this.logger.debug("getAgentRunStatus", { runId });
|
|
3807
|
-
const authHeaders = await this.getSessionAuthHeaders();
|
|
3808
|
-
const response = await fetch(`${this.baseUrl}/api/v2/agent/run/${runId}`, {
|
|
3809
|
-
headers: authHeaders
|
|
3810
|
-
});
|
|
3811
|
-
if (!response.ok) {
|
|
3812
|
-
const error = await response.json().catch(() => ({}));
|
|
3813
|
-
throw new MixrPayError(error.error || `Failed to get run status: ${response.status}`);
|
|
3814
|
-
}
|
|
3815
|
-
const data = await response.json();
|
|
3816
|
-
return {
|
|
3817
|
-
runId: data.run_id,
|
|
3818
|
-
status: data.status,
|
|
3819
|
-
response: data.response,
|
|
3820
|
-
iterations: data.iterations,
|
|
3821
|
-
toolsUsed: data.tools_used,
|
|
3822
|
-
cost: {
|
|
3823
|
-
llmUsd: data.cost.llm_usd,
|
|
3824
|
-
toolsUsd: data.cost.tools_usd,
|
|
3825
|
-
totalUsd: data.cost.total_usd
|
|
3826
|
-
},
|
|
3827
|
-
tokens: data.tokens,
|
|
3828
|
-
txHash: data.tx_hash,
|
|
3829
|
-
error: data.error,
|
|
3830
|
-
startedAt: new Date(data.started_at),
|
|
3831
|
-
completedAt: data.completed_at ? new Date(data.completed_at) : void 0
|
|
3832
|
-
};
|
|
3833
|
-
}
|
|
3834
|
-
/**
|
|
3835
|
-
* Call an MCP tool using session authorization (pre-authorized spending limit).
|
|
3836
|
-
*
|
|
3837
|
-
* Use this when you've already created a session with the tool provider
|
|
3838
|
-
* and want to use that spending limit instead of direct wallet charges.
|
|
3839
|
-
*
|
|
3840
|
-
* @param sessionId - The session ID for the tool provider
|
|
3841
|
-
* @param toolName - The tool name in format "merchant/tool"
|
|
3842
|
-
* @param args - Arguments to pass to the tool
|
|
3843
|
-
* @returns Tool execution result
|
|
3844
|
-
*
|
|
3845
|
-
* @example
|
|
3846
|
-
* ```typescript
|
|
3847
|
-
* // Create session with provider first
|
|
3848
|
-
* const session = await wallet.getOrCreateSession({
|
|
3849
|
-
* merchantPublicKey: 'pk_live_firecrawl_...',
|
|
3850
|
-
* spendingLimitUsd: 50,
|
|
3851
|
-
* });
|
|
3852
|
-
*
|
|
3853
|
-
* // Use session for multiple calls
|
|
3854
|
-
* const result = await wallet.callMCPToolWithSession(
|
|
3855
|
-
* session.id,
|
|
3856
|
-
* 'firecrawl/scrape',
|
|
3857
|
-
* { url: 'https://example.com' }
|
|
3858
|
-
* );
|
|
3859
|
-
* ```
|
|
3860
|
-
*/
|
|
3861
|
-
async callMCPToolWithSession(sessionId, toolName, args = {}) {
|
|
3862
|
-
this.logger.debug("callMCPToolWithSession", { sessionId, toolName, args });
|
|
3863
|
-
await this.validateMerchantAllowed(toolName, "tool");
|
|
3864
|
-
const response = await fetch(`${this.baseUrl}/api/mcp`, {
|
|
3865
|
-
method: "POST",
|
|
3866
|
-
headers: {
|
|
3867
|
-
"Content-Type": "application/json",
|
|
3868
|
-
"X-Mixr-Session": sessionId
|
|
3869
|
-
},
|
|
3870
|
-
body: JSON.stringify({
|
|
3871
|
-
jsonrpc: "2.0",
|
|
3872
|
-
method: "tools/call",
|
|
3873
|
-
params: { name: toolName, arguments: args },
|
|
3874
|
-
id: Date.now()
|
|
3875
|
-
})
|
|
3876
|
-
});
|
|
3877
|
-
const result = await response.json();
|
|
3878
|
-
if (result.error) {
|
|
3879
|
-
throw new MixrPayError(result.error.message || "MCP tool call failed");
|
|
3880
|
-
}
|
|
3881
|
-
const content = result.result?.content?.[0];
|
|
3882
|
-
const data = content?.text ? JSON.parse(content.text) : null;
|
|
3883
|
-
const mixrpay = result.result?._mixrpay || {};
|
|
3884
|
-
if (mixrpay.chargedUsd) {
|
|
3885
|
-
const payment = {
|
|
3886
|
-
amountUsd: mixrpay.chargedUsd,
|
|
3887
|
-
recipient: toolName.split("/")[0] || toolName,
|
|
3888
|
-
txHash: mixrpay.txHash,
|
|
3889
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
3890
|
-
description: `MCP: ${toolName}`,
|
|
3891
|
-
url: `${this.baseUrl}/api/mcp`,
|
|
3892
|
-
requestId: crypto.randomUUID()
|
|
3893
|
-
};
|
|
3894
|
-
this.payments.push(payment);
|
|
3895
|
-
this.totalSpentUsd += mixrpay.chargedUsd;
|
|
3896
|
-
this.logger.payment(mixrpay.chargedUsd, toolName, "MCP call (session)");
|
|
3897
|
-
if (this.onPayment) {
|
|
3898
|
-
this.onPayment(payment);
|
|
3899
|
-
}
|
|
3900
|
-
}
|
|
3901
|
-
return {
|
|
3902
|
-
data,
|
|
3903
|
-
chargedUsd: mixrpay.chargedUsd || 0,
|
|
3904
|
-
txHash: mixrpay.txHash,
|
|
3905
|
-
latencyMs: mixrpay.latencyMs
|
|
3906
|
-
};
|
|
3907
|
-
}
|
|
3908
|
-
// ===========================================================================
|
|
3909
|
-
// Task Board Methods
|
|
3910
|
-
// ===========================================================================
|
|
3911
|
-
/**
|
|
3912
|
-
* Create a new task on the Task Board.
|
|
3913
|
-
*
|
|
3914
|
-
* Users (human or agent) can post tasks with budgets for other agents to complete.
|
|
3915
|
-
* The listing fee is charged when an agent is accepted, not at creation time.
|
|
3916
|
-
*
|
|
3917
|
-
* @param params - Task creation parameters
|
|
3918
|
-
* @returns The created task
|
|
3919
|
-
*
|
|
3920
|
-
* @example
|
|
3921
|
-
* ```typescript
|
|
3922
|
-
* const task = await wallet.createTask({
|
|
3923
|
-
* title: 'Research crypto regulations',
|
|
3924
|
-
* description: 'Research and summarize crypto regulations in the EU...',
|
|
3925
|
-
* budgetUsd: 100,
|
|
3926
|
-
* deliverables: ['PDF report', 'Summary document'],
|
|
3927
|
-
* category: 'research',
|
|
3928
|
-
* });
|
|
3929
|
-
* console.log(`Task created: ${task.id}`);
|
|
3930
|
-
* ```
|
|
3931
|
-
*/
|
|
3932
|
-
async createTask(params) {
|
|
3933
|
-
this.logger.info(`Creating task: ${params.title}`);
|
|
3934
|
-
const response = await this.callApi("/api/v2/tasks", {
|
|
3935
|
-
method: "POST",
|
|
3936
|
-
headers: { "Content-Type": "application/json" },
|
|
3937
|
-
body: JSON.stringify({
|
|
3938
|
-
title: params.title,
|
|
3939
|
-
description: params.description,
|
|
3940
|
-
budget_usd: params.budgetUsd,
|
|
3941
|
-
deliverables: params.deliverables,
|
|
3942
|
-
category: params.category,
|
|
3943
|
-
expires_in_days: params.expiresInDays
|
|
3944
|
-
})
|
|
3945
|
-
});
|
|
3946
|
-
const result = await response.json();
|
|
3947
|
-
if (!response.ok) {
|
|
3948
|
-
throw new MixrPayError(result.error || "Failed to create task");
|
|
3949
|
-
}
|
|
3950
|
-
return this.parseTask(result.task);
|
|
3951
|
-
}
|
|
3952
|
-
/**
|
|
3953
|
-
* List open tasks on the Task Board.
|
|
3954
|
-
*
|
|
3955
|
-
* Browse available tasks that agents can request to work on.
|
|
3956
|
-
*
|
|
3957
|
-
* @param params - Optional filter parameters
|
|
3958
|
-
* @returns List of tasks with pagination
|
|
3959
|
-
*
|
|
3960
|
-
* @example
|
|
3961
|
-
* ```typescript
|
|
3962
|
-
* const { tasks, pagination } = await wallet.listTasks({
|
|
3963
|
-
* minBudget: 50,
|
|
3964
|
-
* category: 'research',
|
|
3965
|
-
* });
|
|
3966
|
-
* console.log(`Found ${pagination.total} matching tasks`);
|
|
3967
|
-
* ```
|
|
3968
|
-
*/
|
|
3969
|
-
async listTasks(params) {
|
|
3970
|
-
const searchParams = new URLSearchParams();
|
|
3971
|
-
if (params?.status) searchParams.set("status", params.status);
|
|
3972
|
-
if (params?.category) searchParams.set("category", params.category);
|
|
3973
|
-
if (params?.minBudget !== void 0) searchParams.set("min_budget", String(params.minBudget));
|
|
3974
|
-
if (params?.maxBudget !== void 0) searchParams.set("max_budget", String(params.maxBudget));
|
|
3975
|
-
if (params?.limit !== void 0) searchParams.set("limit", String(params.limit));
|
|
3976
|
-
if (params?.offset !== void 0) searchParams.set("offset", String(params.offset));
|
|
3977
|
-
const url = `/api/v2/tasks${searchParams.toString() ? `?${searchParams}` : ""}`;
|
|
3978
|
-
const response = await this.callApi(url, { method: "GET" });
|
|
3979
|
-
const result = await response.json();
|
|
3980
|
-
if (!response.ok) {
|
|
3981
|
-
throw new MixrPayError(result.error || "Failed to list tasks");
|
|
3982
|
-
}
|
|
3983
|
-
return {
|
|
3984
|
-
tasks: result.tasks.map((t) => this.parseTask(t)),
|
|
3985
|
-
pagination: result.pagination
|
|
3986
|
-
};
|
|
3987
|
-
}
|
|
3988
|
-
/**
|
|
3989
|
-
* Get details for a specific task.
|
|
3990
|
-
*
|
|
3991
|
-
* @param taskId - The task ID
|
|
3992
|
-
* @returns Task details
|
|
3993
|
-
*/
|
|
3994
|
-
async getTask(taskId) {
|
|
3995
|
-
const response = await this.callApi(`/api/v2/tasks/${taskId}`, { method: "GET" });
|
|
3996
|
-
const result = await response.json();
|
|
3997
|
-
if (!response.ok) {
|
|
3998
|
-
throw new MixrPayError(result.error || "Failed to get task");
|
|
3999
|
-
}
|
|
4000
|
-
return this.parseTask(result.task);
|
|
4001
|
-
}
|
|
4002
|
-
/**
|
|
4003
|
-
* Get tasks created by or assigned to the authenticated user.
|
|
4004
|
-
*
|
|
4005
|
-
* @param options - Filter and pagination options
|
|
4006
|
-
* @returns List of tasks with pagination info
|
|
4007
|
-
*
|
|
4008
|
-
* @example
|
|
4009
|
-
* ```typescript
|
|
4010
|
-
* // Get all my tasks
|
|
4011
|
-
* const { tasks, pagination } = await wallet.getMyTasks();
|
|
4012
|
-
*
|
|
4013
|
-
* // Get only tasks I created
|
|
4014
|
-
* const created = await wallet.getMyTasks({ role: 'creator' });
|
|
4015
|
-
*
|
|
4016
|
-
* // Get only tasks assigned to me
|
|
4017
|
-
* const assigned = await wallet.getMyTasks({ role: 'agent' });
|
|
4018
|
-
*
|
|
4019
|
-
* // Filter by status
|
|
4020
|
-
* const pending = await wallet.getMyTasks({ status: 'submitted' });
|
|
4021
|
-
* ```
|
|
4022
|
-
*/
|
|
4023
|
-
async getMyTasks(options = {}) {
|
|
4024
|
-
const params = new URLSearchParams();
|
|
4025
|
-
if (options.role) params.set("role", options.role);
|
|
4026
|
-
if (options.status) params.set("status", options.status);
|
|
4027
|
-
if (options.page) params.set("page", options.page.toString());
|
|
4028
|
-
if (options.limit) params.set("limit", options.limit.toString());
|
|
4029
|
-
const queryString = params.toString();
|
|
4030
|
-
const url = `/api/v2/tasks/my${queryString ? `?${queryString}` : ""}`;
|
|
4031
|
-
const response = await this.callApi(url, { method: "GET" });
|
|
4032
|
-
const result = await response.json();
|
|
4033
|
-
if (!response.ok) {
|
|
4034
|
-
throw new MixrPayError(result.error || "Failed to get tasks");
|
|
4035
|
-
}
|
|
4036
|
-
return {
|
|
4037
|
-
tasks: result.tasks.map((t) => ({
|
|
4038
|
-
...this.parseTask(t),
|
|
4039
|
-
isCreator: t.is_creator,
|
|
4040
|
-
isAgent: t.is_agent
|
|
4041
|
-
})),
|
|
4042
|
-
pagination: {
|
|
4043
|
-
page: result.pagination.page,
|
|
4044
|
-
limit: result.pagination.limit,
|
|
4045
|
-
total: result.pagination.total,
|
|
4046
|
-
totalPages: result.pagination.total_pages
|
|
4047
|
-
}
|
|
4048
|
-
};
|
|
4049
|
-
}
|
|
4050
|
-
/**
|
|
4051
|
-
* Request to claim a task.
|
|
4052
|
-
*
|
|
4053
|
-
* Agents use this to express interest in completing a task.
|
|
4054
|
-
* The task creator will review requests and accept one agent.
|
|
4055
|
-
*
|
|
4056
|
-
* @param taskId - The task ID to request
|
|
4057
|
-
* @param message - Optional pitch/message to the task creator
|
|
4058
|
-
* @returns The created request
|
|
4059
|
-
*
|
|
4060
|
-
* @example
|
|
4061
|
-
* ```typescript
|
|
4062
|
-
* const request = await wallet.requestTask('task_123',
|
|
4063
|
-
* 'I have experience with crypto research and can deliver within 24 hours.'
|
|
4064
|
-
* );
|
|
4065
|
-
* console.log(`Request submitted: ${request.id}`);
|
|
4066
|
-
* ```
|
|
4067
|
-
*/
|
|
4068
|
-
async requestTask(taskId, message) {
|
|
4069
|
-
this.logger.info(`Requesting task: ${taskId}`);
|
|
4070
|
-
const response = await this.callApi(`/api/v2/tasks/${taskId}/request`, {
|
|
4071
|
-
method: "POST",
|
|
4072
|
-
headers: { "Content-Type": "application/json" },
|
|
4073
|
-
body: message ? JSON.stringify({ message }) : "{}"
|
|
4074
|
-
});
|
|
4075
|
-
const result = await response.json();
|
|
4076
|
-
if (!response.ok) {
|
|
4077
|
-
throw new MixrPayError(result.error || "Failed to request task");
|
|
4078
|
-
}
|
|
4079
|
-
return {
|
|
4080
|
-
id: result.request.id,
|
|
4081
|
-
taskId: result.request.task_id,
|
|
4082
|
-
status: result.request.status,
|
|
4083
|
-
message: result.request.message,
|
|
4084
|
-
createdAt: new Date(result.request.created_at)
|
|
4085
|
-
};
|
|
4086
|
-
}
|
|
4087
|
-
/**
|
|
4088
|
-
* Get requests for a task you created.
|
|
4089
|
-
*
|
|
4090
|
-
* @param taskId - The task ID
|
|
4091
|
-
* @returns List of agent requests
|
|
4092
|
-
*/
|
|
4093
|
-
async getTaskRequests(taskId) {
|
|
4094
|
-
const response = await this.callApi(`/api/v2/tasks/${taskId}/requests`, { method: "GET" });
|
|
4095
|
-
const result = await response.json();
|
|
4096
|
-
if (!response.ok) {
|
|
4097
|
-
throw new MixrPayError(result.error || "Failed to get task requests");
|
|
4098
|
-
}
|
|
4099
|
-
return result.requests.map((r) => ({
|
|
4100
|
-
id: r.id,
|
|
4101
|
-
status: r.status,
|
|
4102
|
-
message: r.message,
|
|
4103
|
-
createdAt: new Date(r.created_at),
|
|
4104
|
-
reviewedAt: r.reviewed_at ? new Date(r.reviewed_at) : void 0,
|
|
4105
|
-
agent: r.agent
|
|
4106
|
-
}));
|
|
4107
|
-
}
|
|
4108
|
-
/**
|
|
4109
|
-
* Accept an agent's request to work on your task.
|
|
4110
|
-
*
|
|
4111
|
-
* This will:
|
|
4112
|
-
* - Charge the listing fee from your wallet
|
|
4113
|
-
* - Create a session for the agent with your task budget
|
|
4114
|
-
* - Mark the task as claimed
|
|
4115
|
-
* - Reject all other pending requests
|
|
4116
|
-
*
|
|
4117
|
-
* @param taskId - The task ID
|
|
4118
|
-
* @param requestId - The request ID to accept
|
|
4119
|
-
* @returns Acceptance result with session info
|
|
4120
|
-
*
|
|
4121
|
-
* @example
|
|
4122
|
-
* ```typescript
|
|
4123
|
-
* const result = await wallet.acceptTaskRequest('task_123', 'req_456');
|
|
4124
|
-
* console.log(`Agent accepted! Session budget: $${result.session.budgetUsd}`);
|
|
4125
|
-
* ```
|
|
4126
|
-
*/
|
|
4127
|
-
async acceptTaskRequest(taskId, requestId) {
|
|
4128
|
-
this.logger.info(`Accepting request ${requestId} for task ${taskId}`);
|
|
4129
|
-
const response = await this.callApi(`/api/v2/tasks/${taskId}/requests/${requestId}/accept`, {
|
|
4130
|
-
method: "POST"
|
|
4131
|
-
});
|
|
4132
|
-
const result = await response.json();
|
|
4133
|
-
if (!response.ok) {
|
|
4134
|
-
throw new MixrPayError(result.error || "Failed to accept request");
|
|
4135
|
-
}
|
|
4136
|
-
return {
|
|
4137
|
-
success: result.success,
|
|
4138
|
-
task: {
|
|
4139
|
-
id: result.task.id,
|
|
4140
|
-
status: result.task.status,
|
|
4141
|
-
agentUserId: result.task.agent_user_id
|
|
4142
|
-
},
|
|
4143
|
-
session: {
|
|
4144
|
-
id: result.session.id,
|
|
4145
|
-
sessionKey: result.session.session_key,
|
|
4146
|
-
// Agent needs this to authenticate API calls
|
|
4147
|
-
address: result.session.address,
|
|
4148
|
-
expiresAt: new Date(result.session.expires_at),
|
|
4149
|
-
budgetUsd: result.session.budget_usd,
|
|
4150
|
-
allowedMerchants: result.session.allowed_merchants || []
|
|
4151
|
-
},
|
|
4152
|
-
invite: {
|
|
4153
|
-
id: result.invite.id,
|
|
4154
|
-
code: result.invite.code
|
|
4155
|
-
},
|
|
4156
|
-
listingFeeTxHash: result.listing_fee_tx_hash
|
|
4157
|
-
};
|
|
4158
|
-
}
|
|
4159
|
-
/**
|
|
4160
|
-
* Reject an agent's request.
|
|
4161
|
-
*
|
|
4162
|
-
* @param taskId - The task ID
|
|
4163
|
-
* @param requestId - The request ID to reject
|
|
4164
|
-
*/
|
|
4165
|
-
async rejectTaskRequest(taskId, requestId) {
|
|
4166
|
-
const response = await this.callApi(`/api/v2/tasks/${taskId}/requests/${requestId}/reject`, {
|
|
4167
|
-
method: "POST"
|
|
4168
|
-
});
|
|
4169
|
-
const result = await response.json();
|
|
4170
|
-
if (!response.ok) {
|
|
4171
|
-
throw new MixrPayError(result.error || "Failed to reject request");
|
|
4172
|
-
}
|
|
4173
|
-
}
|
|
4174
|
-
/**
|
|
4175
|
-
* Submit deliverables for a task you're working on.
|
|
4176
|
-
*
|
|
4177
|
-
* After being accepted to work on a task, use this to submit your work
|
|
4178
|
-
* for the task creator's review.
|
|
4179
|
-
*
|
|
4180
|
-
* @param taskId - The task ID
|
|
4181
|
-
* @param output - The deliverables (text and/or URL)
|
|
4182
|
-
*
|
|
4183
|
-
* @example
|
|
4184
|
-
* ```typescript
|
|
4185
|
-
* await wallet.submitTask('task_123', {
|
|
4186
|
-
* text: 'Here is my research report...',
|
|
4187
|
-
* url: 'https://docs.google.com/document/d/...',
|
|
4188
|
-
* });
|
|
4189
|
-
* ```
|
|
4190
|
-
*/
|
|
4191
|
-
async submitTask(taskId, output) {
|
|
4192
|
-
this.logger.info(`Submitting task: ${taskId}`);
|
|
4193
|
-
const response = await this.callApi(`/api/v2/tasks/${taskId}/submit`, {
|
|
4194
|
-
method: "POST",
|
|
4195
|
-
headers: { "Content-Type": "application/json" },
|
|
4196
|
-
body: JSON.stringify({
|
|
4197
|
-
output_text: output.text,
|
|
4198
|
-
output_url: output.url
|
|
4199
|
-
})
|
|
4200
|
-
});
|
|
4201
|
-
const result = await response.json();
|
|
4202
|
-
if (!response.ok) {
|
|
4203
|
-
throw new MixrPayError(result.error || "Failed to submit task");
|
|
4204
|
-
}
|
|
4205
|
-
}
|
|
4206
|
-
/**
|
|
4207
|
-
* Approve a submitted task and pay the agent.
|
|
4208
|
-
*
|
|
4209
|
-
* This will:
|
|
4210
|
-
* - Calculate remaining budget (budget - spent on tools)
|
|
4211
|
-
* - Transfer remaining budget to the agent's wallet
|
|
4212
|
-
* - Mark the task as completed
|
|
4213
|
-
* - Revoke the agent's session
|
|
4214
|
-
*
|
|
4215
|
-
* @param taskId - The task ID
|
|
4216
|
-
* @returns Payout details
|
|
4217
|
-
*
|
|
4218
|
-
* @example
|
|
4219
|
-
* ```typescript
|
|
4220
|
-
* const result = await wallet.approveTask('task_123');
|
|
4221
|
-
* console.log(`Paid agent $${result.payout.amountUsd}`);
|
|
4222
|
-
* ```
|
|
4223
|
-
*/
|
|
4224
|
-
async approveTask(taskId) {
|
|
4225
|
-
this.logger.info(`Approving task: ${taskId}`);
|
|
4226
|
-
const response = await this.callApi(`/api/v2/tasks/${taskId}/approve`, {
|
|
4227
|
-
method: "POST"
|
|
4228
|
-
});
|
|
4229
|
-
const result = await response.json();
|
|
4230
|
-
if (!response.ok) {
|
|
4231
|
-
throw new MixrPayError(result.error || "Failed to approve task");
|
|
4232
|
-
}
|
|
4233
|
-
return {
|
|
4234
|
-
success: result.success,
|
|
4235
|
-
task: {
|
|
4236
|
-
id: result.task.id,
|
|
4237
|
-
status: result.task.status,
|
|
4238
|
-
completedAt: new Date(result.task.completed_at)
|
|
4239
|
-
},
|
|
4240
|
-
payout: {
|
|
4241
|
-
status: result.payout.status,
|
|
4242
|
-
amountUsd: result.payout.amount_usd,
|
|
4243
|
-
txHash: result.payout.tx_hash
|
|
4244
|
-
}
|
|
4245
|
-
};
|
|
4246
|
-
}
|
|
4247
|
-
/**
|
|
4248
|
-
* Reject a task submission and send it back for revision.
|
|
4249
|
-
*
|
|
4250
|
-
* @param taskId - The task ID
|
|
4251
|
-
* @param feedback - Optional feedback for the agent
|
|
4252
|
-
*/
|
|
4253
|
-
async rejectTaskSubmission(taskId, feedback) {
|
|
4254
|
-
const response = await this.callApi(`/api/v2/tasks/${taskId}/reject`, {
|
|
4255
|
-
method: "POST",
|
|
4256
|
-
headers: { "Content-Type": "application/json" },
|
|
4257
|
-
body: feedback ? JSON.stringify({ feedback }) : "{}"
|
|
4258
|
-
});
|
|
4259
|
-
const result = await response.json();
|
|
4260
|
-
if (!response.ok) {
|
|
4261
|
-
throw new MixrPayError(result.error || "Failed to reject task");
|
|
4262
|
-
}
|
|
4263
|
-
}
|
|
4264
|
-
/**
|
|
4265
|
-
* Cancel a task you created.
|
|
4266
|
-
*
|
|
4267
|
-
* Can only cancel tasks in 'open' or 'claimed' status.
|
|
4268
|
-
* If the task was claimed, the agent's session will be revoked.
|
|
4269
|
-
*
|
|
4270
|
-
* @param taskId - The task ID
|
|
4271
|
-
*/
|
|
4272
|
-
async cancelTask(taskId) {
|
|
4273
|
-
const response = await this.callApi(`/api/v2/tasks/${taskId}`, {
|
|
4274
|
-
method: "DELETE"
|
|
4275
|
-
});
|
|
4276
|
-
const result = await response.json();
|
|
4277
|
-
if (!response.ok) {
|
|
4278
|
-
throw new MixrPayError(result.error || "Failed to cancel task");
|
|
4279
|
-
}
|
|
4280
|
-
}
|
|
4281
|
-
// ===========================================================================
|
|
4282
|
-
// Mission Control: Subtask Management
|
|
4283
|
-
// ===========================================================================
|
|
4284
|
-
/**
|
|
4285
|
-
* Create a subtask under an existing task.
|
|
4286
|
-
* Requires session key authentication.
|
|
4287
|
-
* Budget is allocated from the parent task's remaining budget.
|
|
4288
|
-
*
|
|
4289
|
-
* @param options - Subtask creation options
|
|
4290
|
-
* @returns The created subtask
|
|
4291
|
-
*
|
|
4292
|
-
* @example
|
|
4293
|
-
* ```typescript
|
|
4294
|
-
* const subtask = await wallet.createSubtask({
|
|
4295
|
-
* parentTaskId: 'task_123',
|
|
4296
|
-
* title: 'Research competitor pricing',
|
|
4297
|
-
* description: 'Find pricing info for top 5 competitors',
|
|
4298
|
-
* budgetUsd: 10,
|
|
4299
|
-
* });
|
|
4300
|
-
* ```
|
|
4301
|
-
*/
|
|
4302
|
-
async createSubtask(options) {
|
|
4303
|
-
this.logger.info(`Creating subtask: ${options.title}`);
|
|
4304
|
-
const response = await this.callApi("/api/v2/tasks/create-subtask", {
|
|
4305
|
-
method: "POST",
|
|
4306
|
-
headers: { "Content-Type": "application/json" },
|
|
4307
|
-
body: JSON.stringify({
|
|
4308
|
-
parent_task_id: options.parentTaskId,
|
|
4309
|
-
title: options.title,
|
|
4310
|
-
description: options.description,
|
|
4311
|
-
deliverables: options.deliverables,
|
|
4312
|
-
category: options.category,
|
|
4313
|
-
budget_usd: options.budgetUsd,
|
|
4314
|
-
allow_sub_agents: options.allowSubAgents,
|
|
4315
|
-
expires_in_days: options.expiresInDays,
|
|
4316
|
-
idempotency_key: options.idempotencyKey
|
|
4317
|
-
})
|
|
4318
|
-
});
|
|
4319
|
-
const result = await response.json();
|
|
4320
|
-
if (!response.ok) {
|
|
4321
|
-
throw new MixrPayError(result.error || "Failed to create subtask");
|
|
4322
|
-
}
|
|
4323
|
-
return this.parseTask(result.task);
|
|
4324
|
-
}
|
|
4325
|
-
/**
|
|
4326
|
-
* Update the status of a task.
|
|
4327
|
-
* Requires session key authentication.
|
|
4328
|
-
* Creates an audit trail via TaskStatusUpdate records.
|
|
4329
|
-
*
|
|
4330
|
-
* @param taskId - The task ID
|
|
4331
|
-
* @param status - New status
|
|
4332
|
-
* @param note - Optional note explaining the status change
|
|
4333
|
-
* @returns The updated task
|
|
4334
|
-
*
|
|
4335
|
-
* @example
|
|
4336
|
-
* ```typescript
|
|
4337
|
-
* await wallet.updateTaskStatus('task_123', 'submitted', 'All deliverables completed');
|
|
4338
|
-
* ```
|
|
4339
|
-
*/
|
|
4340
|
-
async updateTaskStatus(taskId, status, note, idempotencyKey) {
|
|
4341
|
-
this.logger.info(`Updating task ${taskId} status to ${status}`);
|
|
4342
|
-
const response = await this.callApi(`/api/v2/tasks/${taskId}/status`, {
|
|
4343
|
-
method: "PATCH",
|
|
4344
|
-
headers: { "Content-Type": "application/json" },
|
|
4345
|
-
body: JSON.stringify({
|
|
4346
|
-
status,
|
|
4347
|
-
note,
|
|
4348
|
-
idempotency_key: idempotencyKey
|
|
4349
|
-
})
|
|
4350
|
-
});
|
|
4351
|
-
const result = await response.json();
|
|
4352
|
-
if (!response.ok) {
|
|
4353
|
-
throw new MixrPayError(result.error || "Failed to update task status");
|
|
4354
|
-
}
|
|
4355
|
-
return this.parseTask(result.task);
|
|
4356
|
-
}
|
|
4357
|
-
/**
|
|
4358
|
-
* Get subtasks of a task.
|
|
4359
|
-
* Requires session key authentication.
|
|
4360
|
-
*
|
|
4361
|
-
* @param taskId - The parent task ID
|
|
4362
|
-
* @returns List of subtasks
|
|
4363
|
-
*
|
|
4364
|
-
* @example
|
|
4365
|
-
* ```typescript
|
|
4366
|
-
* const subtasks = await wallet.getSubtasks('task_123');
|
|
4367
|
-
* console.log(`Found ${subtasks.length} subtasks`);
|
|
4368
|
-
* ```
|
|
4369
|
-
*/
|
|
4370
|
-
async getSubtasks(taskId) {
|
|
4371
|
-
const response = await this.callApi(`/api/v2/tasks/${taskId}/subtasks`, {
|
|
4372
|
-
method: "GET"
|
|
4373
|
-
});
|
|
4374
|
-
const result = await response.json();
|
|
4375
|
-
if (!response.ok) {
|
|
4376
|
-
throw new MixrPayError(result.error || "Failed to get subtasks");
|
|
4377
|
-
}
|
|
4378
|
-
return (result.subtasks || []).map((t) => this.parseTask(t));
|
|
4379
|
-
}
|
|
4380
|
-
/**
|
|
4381
|
-
* Get the status history of a task.
|
|
4382
|
-
* Requires session key authentication.
|
|
4383
|
-
*
|
|
4384
|
-
* @param taskId - The task ID
|
|
4385
|
-
* @returns Status history with audit trail
|
|
4386
|
-
*
|
|
4387
|
-
* @example
|
|
4388
|
-
* ```typescript
|
|
4389
|
-
* const history = await wallet.getTaskStatusHistory('task_123');
|
|
4390
|
-
* for (const entry of history.history) {
|
|
4391
|
-
* console.log(`${entry.oldStatus} -> ${entry.newStatus}: ${entry.note}`);
|
|
4392
|
-
* }
|
|
4393
|
-
* ```
|
|
4394
|
-
*/
|
|
4395
|
-
async getTaskStatusHistory(taskId) {
|
|
4396
|
-
const response = await this.callApi(`/api/v2/tasks/${taskId}/status`, {
|
|
4397
|
-
method: "GET"
|
|
4398
|
-
});
|
|
4399
|
-
const result = await response.json();
|
|
4400
|
-
if (!response.ok) {
|
|
4401
|
-
throw new MixrPayError(result.error || "Failed to get task status history");
|
|
4402
|
-
}
|
|
4403
|
-
return {
|
|
4404
|
-
taskId: result.task_id,
|
|
4405
|
-
currentStatus: result.current_status,
|
|
4406
|
-
history: (result.history || []).map((h) => ({
|
|
4407
|
-
id: h.id,
|
|
4408
|
-
oldStatus: h.old_status,
|
|
4409
|
-
newStatus: h.new_status,
|
|
4410
|
-
note: h.note,
|
|
4411
|
-
createdAt: new Date(h.created_at)
|
|
4412
|
-
}))
|
|
4413
|
-
};
|
|
4414
|
-
}
|
|
4415
|
-
// ===========================================================================
|
|
4416
|
-
// Mission Control: Approval System
|
|
4417
|
-
// ===========================================================================
|
|
4418
|
-
/**
|
|
4419
|
-
* Request approval from a human user for an action.
|
|
4420
|
-
* Returns immediately if the action is auto-approved.
|
|
4421
|
-
*
|
|
4422
|
-
* @param options - Approval request options
|
|
4423
|
-
* @returns Approval request or auto-approval result
|
|
4424
|
-
*
|
|
4425
|
-
* @example
|
|
4426
|
-
* ```typescript
|
|
4427
|
-
* const result = await wallet.requestApproval({
|
|
4428
|
-
* actionType: 'external_communication',
|
|
4429
|
-
* actionPayload: { recipient: 'user@example.com', message: 'Hello!' },
|
|
4430
|
-
* context: 'Sending welcome email to new customer',
|
|
4431
|
-
* });
|
|
4432
|
-
*
|
|
4433
|
-
* if ('autoApproved' in result) {
|
|
4434
|
-
* console.log('Auto-approved:', result.reason);
|
|
4435
|
-
* } else {
|
|
4436
|
-
* console.log('Waiting for approval:', result.id);
|
|
4437
|
-
* }
|
|
4438
|
-
* ```
|
|
4439
|
-
*/
|
|
4440
|
-
async requestApproval(options) {
|
|
4441
|
-
this.logger.info(`Requesting approval for: ${options.actionType}`);
|
|
4442
|
-
const response = await this.callApi("/api/v2/approvals/request", {
|
|
4443
|
-
method: "POST",
|
|
4444
|
-
headers: { "Content-Type": "application/json" },
|
|
4445
|
-
body: JSON.stringify({
|
|
4446
|
-
action_type: options.actionType,
|
|
4447
|
-
action_payload: options.actionPayload,
|
|
4448
|
-
context: options.context,
|
|
4449
|
-
expires_in_hours: options.expiresInHours,
|
|
4450
|
-
idempotency_key: options.idempotencyKey
|
|
4451
|
-
})
|
|
4452
|
-
});
|
|
4453
|
-
const result = await response.json();
|
|
4454
|
-
if (!response.ok) {
|
|
4455
|
-
throw new MixrPayError(result.error || "Failed to request approval");
|
|
4456
|
-
}
|
|
4457
|
-
if (result.auto_approved) {
|
|
4458
|
-
return {
|
|
4459
|
-
autoApproved: true,
|
|
4460
|
-
reason: result.reason
|
|
4461
|
-
};
|
|
4462
|
-
}
|
|
4463
|
-
return this.parseApprovalRequest(result.approval_request);
|
|
4464
|
-
}
|
|
4465
|
-
/**
|
|
4466
|
-
* Check the status of an approval request.
|
|
4467
|
-
*
|
|
4468
|
-
* @param requestId - The approval request ID
|
|
4469
|
-
* @returns Current status of the request
|
|
4470
|
-
*
|
|
4471
|
-
* @example
|
|
4472
|
-
* ```typescript
|
|
4473
|
-
* const status = await wallet.checkApprovalStatus('req_123');
|
|
4474
|
-
* if (status.status === 'approved') {
|
|
4475
|
-
* console.log('Approved! Proceeding with action...');
|
|
4476
|
-
* }
|
|
4477
|
-
* ```
|
|
4478
|
-
*/
|
|
4479
|
-
async checkApprovalStatus(requestId) {
|
|
4480
|
-
const response = await this.callApi(`/api/v2/approvals/check?request_id=${requestId}`, {
|
|
4481
|
-
method: "GET"
|
|
4482
|
-
});
|
|
4483
|
-
const result = await response.json();
|
|
4484
|
-
if (!response.ok) {
|
|
4485
|
-
throw new MixrPayError(result.error || "Failed to check approval status");
|
|
4486
|
-
}
|
|
4487
|
-
return {
|
|
4488
|
-
requestId: result.request_id,
|
|
4489
|
-
status: result.status,
|
|
4490
|
-
responseNote: result.response_note,
|
|
4491
|
-
approvalScope: result.approval_scope,
|
|
4492
|
-
respondedAt: result.responded_at ? new Date(result.responded_at) : void 0
|
|
4493
|
-
};
|
|
4494
|
-
}
|
|
4495
|
-
/**
|
|
4496
|
-
* Check if an action type needs approval before execution.
|
|
4497
|
-
*
|
|
4498
|
-
* @param actionType - The type of action to check
|
|
4499
|
-
* @param amountUsd - Optional amount for spend actions
|
|
4500
|
-
* @returns Whether approval is needed
|
|
4501
|
-
*
|
|
4502
|
-
* @example
|
|
4503
|
-
* ```typescript
|
|
4504
|
-
* const check = await wallet.checkActionPermission('external_communication');
|
|
4505
|
-
* if (check.needsApproval) {
|
|
4506
|
-
* const approval = await wallet.requestApproval({ ... });
|
|
4507
|
-
* }
|
|
4508
|
-
* ```
|
|
4509
|
-
*/
|
|
4510
|
-
async checkActionPermission(actionType, amountUsd) {
|
|
4511
|
-
let url = `/api/v2/approvals/check?action_type=${encodeURIComponent(actionType)}`;
|
|
4512
|
-
if (amountUsd !== void 0) {
|
|
4513
|
-
url += `&amount_usd=${amountUsd}`;
|
|
4514
|
-
}
|
|
4515
|
-
const response = await this.callApi(url, { method: "GET" });
|
|
4516
|
-
const result = await response.json();
|
|
4517
|
-
if (!response.ok) {
|
|
4518
|
-
throw new MixrPayError(result.error || "Failed to check action permission");
|
|
4519
|
-
}
|
|
4520
|
-
return {
|
|
4521
|
-
actionType: result.action_type,
|
|
4522
|
-
needsApproval: result.needs_approval,
|
|
4523
|
-
reason: result.reason
|
|
4524
|
-
};
|
|
4525
|
-
}
|
|
4526
|
-
/**
|
|
4527
|
-
* Wait for an approval request to be resolved.
|
|
4528
|
-
* Polls the status until approved, rejected, or expired.
|
|
4529
|
-
*
|
|
4530
|
-
* @param requestId - The approval request ID
|
|
4531
|
-
* @param options - Polling options
|
|
4532
|
-
* @returns Final status of the request
|
|
4533
|
-
*
|
|
4534
|
-
* @example
|
|
4535
|
-
* ```typescript
|
|
4536
|
-
* const approval = await wallet.requestApproval({ ... });
|
|
4537
|
-
* if (!('autoApproved' in approval)) {
|
|
4538
|
-
* const result = await wallet.waitForApproval(approval.id, { timeoutMs: 60000 });
|
|
4539
|
-
* if (result.status === 'approved') {
|
|
4540
|
-
* // Proceed with action
|
|
4541
|
-
* }
|
|
4542
|
-
* }
|
|
4543
|
-
* ```
|
|
4544
|
-
*/
|
|
4545
|
-
async waitForApproval(requestId, options = {}) {
|
|
4546
|
-
const pollInterval = options.pollIntervalMs || 5e3;
|
|
4547
|
-
const timeout = options.timeoutMs || 3e5;
|
|
4548
|
-
const startTime = Date.now();
|
|
4549
|
-
while (Date.now() - startTime < timeout) {
|
|
4550
|
-
const status = await this.checkApprovalStatus(requestId);
|
|
4551
|
-
if (status.status !== "pending") {
|
|
4552
|
-
return status;
|
|
4553
|
-
}
|
|
4554
|
-
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
4555
|
-
}
|
|
4556
|
-
return {
|
|
4557
|
-
requestId,
|
|
4558
|
-
status: "expired"
|
|
4559
|
-
};
|
|
4560
|
-
}
|
|
4561
|
-
/** Helper to parse approval request from API response */
|
|
4562
|
-
parseApprovalRequest(r) {
|
|
4563
|
-
return {
|
|
4564
|
-
id: r.id,
|
|
4565
|
-
actionType: r.action_type,
|
|
4566
|
-
actionPayload: r.action_payload,
|
|
4567
|
-
context: r.context,
|
|
4568
|
-
status: r.status,
|
|
4569
|
-
responseNote: r.response_note,
|
|
4570
|
-
approvalScope: r.approval_scope,
|
|
4571
|
-
createdAt: new Date(r.created_at),
|
|
4572
|
-
expiresAt: new Date(r.expires_at),
|
|
4573
|
-
respondedAt: r.responded_at ? new Date(r.responded_at) : void 0
|
|
4574
|
-
};
|
|
4575
|
-
}
|
|
4576
|
-
/** Helper to parse task from API response */
|
|
4577
|
-
parseTask(t) {
|
|
4578
|
-
return {
|
|
4579
|
-
id: t.id,
|
|
4580
|
-
title: t.title,
|
|
4581
|
-
description: t.description,
|
|
4582
|
-
deliverables: t.deliverables || [],
|
|
4583
|
-
category: t.category,
|
|
4584
|
-
budgetUsd: t.budget_usd,
|
|
4585
|
-
listingFeeUsd: t.listing_fee_usd,
|
|
4586
|
-
status: t.status,
|
|
4587
|
-
createdAt: new Date(t.created_at),
|
|
4588
|
-
updatedAt: t.updated_at ? new Date(t.updated_at) : void 0,
|
|
4589
|
-
expiresAt: t.expires_at ? new Date(t.expires_at) : void 0,
|
|
4590
|
-
claimedAt: t.claimed_at ? new Date(t.claimed_at) : void 0,
|
|
4591
|
-
submittedAt: t.submitted_at ? new Date(t.submitted_at) : void 0,
|
|
4592
|
-
completedAt: t.completed_at ? new Date(t.completed_at) : void 0,
|
|
4593
|
-
creator: t.creator,
|
|
4594
|
-
assignedAgent: t.assigned_agent,
|
|
4595
|
-
requestCount: t.request_count,
|
|
4596
|
-
output: t.output,
|
|
4597
|
-
payment: t.payment
|
|
4598
|
-
};
|
|
4599
|
-
}
|
|
4600
|
-
/** Helper to call our API with auth */
|
|
4601
|
-
async callApi(path, init = {}) {
|
|
4602
|
-
const url = `${this.baseUrl}${path}`;
|
|
4603
|
-
const headers = {
|
|
4604
|
-
...init.headers
|
|
4605
|
-
};
|
|
4606
|
-
if (this.sessionKey) {
|
|
4607
|
-
const account = (0, import_accounts2.privateKeyToAccount)(this.sessionKey.rawPrivateKey);
|
|
4608
|
-
const address = account.address.toLowerCase();
|
|
4609
|
-
const timestamp = Date.now();
|
|
4610
|
-
const message = `MixrPay:${timestamp}:${address}`;
|
|
4611
|
-
const signature = await (0, import_accounts2.signMessage)({
|
|
4612
|
-
message,
|
|
4613
|
-
privateKey: this.sessionKey.rawPrivateKey
|
|
4614
|
-
});
|
|
4615
|
-
headers["X-Session-Auth"] = JSON.stringify({
|
|
4616
|
-
address,
|
|
4617
|
-
timestamp,
|
|
4618
|
-
signature
|
|
4619
|
-
});
|
|
4620
|
-
}
|
|
4621
|
-
return fetch(url, {
|
|
4622
|
-
...init,
|
|
4623
|
-
headers
|
|
4624
|
-
});
|
|
4625
|
-
}
|
|
4626
|
-
};
|
|
4627
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
4628
|
-
0 && (module.exports = {
|
|
4629
|
-
AgentWallet,
|
|
4630
|
-
InsufficientBalanceError,
|
|
4631
|
-
InvalidSessionKeyError,
|
|
4632
|
-
MerchantNotAllowedError,
|
|
4633
|
-
MixrPayError,
|
|
4634
|
-
PaymentFailedError,
|
|
4635
|
-
SDK_VERSION,
|
|
4636
|
-
SessionExpiredError,
|
|
4637
|
-
SessionKeyExpiredError,
|
|
4638
|
-
SessionLimitExceededError,
|
|
4639
|
-
SessionNotFoundError,
|
|
4640
|
-
SessionRevokedError,
|
|
4641
|
-
SpendingLimitExceededError,
|
|
4642
|
-
X402ProtocolError,
|
|
4643
|
-
getErrorMessage,
|
|
4644
|
-
isMixrPayError
|
|
4645
|
-
});
|
|
30
|
+
Timestamp: ${e}`,s=await this.sessionKey.signMessage(t);return {"X-Mixr-Wallet":this.walletAddress,"X-Mixr-Signature":s,"X-Mixr-Timestamp":e}}async listTools(){this.logger.debug("listTools");let e=await fetch(`${this.baseUrl}/api/mcp`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({jsonrpc:"2.0",method:"tools/list",id:Date.now()})});if(!e.ok)throw new c(`Failed to list MCP tools: ${e.status}`);let t=await e.json();if(t.error)throw new c(t.error.message||"Failed to list MCP tools");return (t.result?.tools||[]).map(s=>({name:s.name,description:s.description,inputSchema:s.inputSchema,priceUsd:s["x-mixrpay"]?.priceUsd||0,merchantName:s["x-mixrpay"]?.merchantName,merchantSlug:s["x-mixrpay"]?.merchantSlug,verified:s["x-mixrpay"]?.verified||false}))}async listMCPTools(){return this.listTools()}async callTool(e,t={}){this.logger.debug("callTool",{toolName:e,args:t}),await this.validateMerchantAllowed(e,"tool");let s=await this.getMCPAuthHeaders(),r=await(await fetch(`${this.baseUrl}/api/mcp`,{method:"POST",headers:{"Content-Type":"application/json",...s},body:JSON.stringify({jsonrpc:"2.0",method:"tools/call",params:{name:e,arguments:t},id:Date.now()})})).json();if(r.error)throw new c(r.error.message||"MCP tool call failed");let a=r.result?.content?.[0],i=a?.text?JSON.parse(a.text):null,o=r.result?._mixrpay||{};if(o.chargedUsd){let d={amountUsd:o.chargedUsd,recipient:e.split("/")[0]||e,txHash:o.txHash,timestamp:new Date,description:`MCP: ${e}`,url:`${this.baseUrl}/api/mcp`,requestId:crypto.randomUUID()};this.payments.push(d),this.totalSpentUsd+=o.chargedUsd,this.logger.payment(o.chargedUsd,e,"MCP call"),this.onPayment&&this.onPayment(d);}return {data:i,chargedUsd:o.chargedUsd||0,txHash:o.txHash,latencyMs:o.latencyMs}}async callMCPTool(e,t={}){return this.callTool(e,t)}async deployJitMcp(e){this.logger.debug("deployJitMcp",{glamaId:e.glamaId,toolName:e.toolName});let t=await this.getSessionAuthHeaders(),s=await fetch(`${this.baseUrl}/api/v2/jit/deploy`,{method:"POST",headers:{"Content-Type":"application/json",...t},body:JSON.stringify({glama_id:e.glamaId,glama_namespace:e.glamaNamespace,glama_slug:e.glamaSlug,tool_name:e.toolName,tool_description:e.toolDescription,env_vars:e.envVars,ttl_hours:e.ttlHours||24})});if(!s.ok){let r=await s.json().catch(()=>({}));throw new c(r.error||`JIT deploy failed: ${s.status}`)}let n=await s.json();if(n.payment?.amount_usd>0){let r={amountUsd:n.payment.amount_usd,recipient:"mixrpay-jit-deploy",txHash:n.payment.tx_hash,timestamp:new Date,description:`JIT Deploy: ${e.toolName}`,url:`${this.baseUrl}/api/v2/jit/deploy`,requestId:n.request_id||crypto.randomUUID()};this.payments.push(r),this.totalSpentUsd+=n.payment.amount_usd,this.logger.payment(n.payment.amount_usd,"jit-deploy",e.toolName),this.onPayment&&this.onPayment(r);}return {instance:this.parseJitInstance(n.instance),payment:{method:n.payment.method,amountUsd:n.payment.amount_usd,txHash:n.payment.tx_hash}}}async listJitInstances(e){this.logger.debug("listJitInstances",e);let t=await this.getSessionAuthHeaders(),s=new URLSearchParams;e?.status&&s.set("status",e.status);let n=await fetch(`${this.baseUrl}/api/v2/jit/instances?${s}`,{headers:t});if(!n.ok){let a=await n.json().catch(()=>({}));throw new c(a.error||`Failed to list JIT instances: ${n.status}`)}return ((await n.json()).instances||[]).map(a=>this.parseJitInstance(a))}async stopJitInstance(e){this.logger.debug("stopJitInstance",{instanceId:e});let t=await this.getSessionAuthHeaders(),s=await fetch(`${this.baseUrl}/api/v2/jit/instances/${e}`,{method:"DELETE",headers:t});if(!s.ok){let n=await s.json().catch(()=>({}));throw new c(n.error||`Failed to stop JIT instance: ${s.status}`)}}parseJitInstance(e){return {id:e.id,endpointUrl:e.endpoint_url,toolName:e.tool_name,glamaId:e.glama_id,glamaNamespace:e.glama_namespace,glamaSlug:e.glama_slug,status:e.status,mode:e.mode,ttlHours:e.ttl_hours,expiresAt:new Date(e.expires_at),createdAt:new Date(e.created_at)}}async getJitInstance(e){this.logger.debug("getJitInstance",{instanceId:e});let t=await this.getSessionAuthHeaders(),s=await fetch(`${this.baseUrl}/api/v2/jit/instances/${e}`,{headers:t});if(!s.ok){let r=await s.json().catch(()=>({}));throw new c(r.error||`Failed to get JIT instance: ${s.status}`)}let n=await s.json();return this.parseJitInstance(n.instance)}async executeTransaction(e){this.logger.debug("executeTransaction",{to:e.to,estimatedCostUsd:e.estimatedCostUsd});let t=await this.getSessionAuthHeaders(),s=await fetch(`${this.baseUrl}/api/v2/tx/execute`,{method:"POST",headers:{"Content-Type":"application/json",...t},body:JSON.stringify({to:e.to,data:e.data,value:e.value?.toString()??"0",chain_id:e.chainId,estimated_cost_usd:e.estimatedCostUsd,description:e.description,idempotency_key:e.idempotencyKey})}),n=await s.json().catch(()=>({}));if(!s.ok){let r=(n.error_code||"tx_failed").toLowerCase(),a=n.error||`Transaction execution failed: ${s.status}`;throw r==="budget_exceeded"?new c(a,"SPENDING_LIMIT_EXCEEDED"):new c(a,r)}return this.logger.info("Transaction executed",{txHash:n.tx_hash,estimatedCostUsd:e.estimatedCostUsd,remainingBudgetUsd:n.remaining_budget_usd}),{success:true,txHash:n.tx_hash,chargeId:n.charge_id,estimatedCostUsd:n.estimated_cost_usd,remainingBudgetUsd:n.remaining_budget_usd}}async useSkill(e,t){this.logger.debug("useSkill",{skillId:e,options:t});let s=await this.getSessionAuthHeaders(),n=await fetch(`${this.baseUrl}/api/v2/skills/${e}/use`,{method:"POST",headers:{"Content-Type":"application/json",...s},body:JSON.stringify({ttl_hours:t?.ttlHours||24})});if(!n.ok){let a=await n.json().catch(()=>({}));throw a.configure_url?new c(`${a.error||"Skill not configured"}. Configure at: ${this.baseUrl}${a.configure_url}`,"SKILL_NOT_CONFIGURED"):new c(a.error||`Failed to use skill: ${n.status}`)}let r=await n.json();return {skillId:r.skill_id,endpoint:r.endpoint,tools:r.tools||[],expiresAt:new Date(r.expires_at),instanceId:r.instance_id}}async listSkills(){this.logger.debug("listSkills");let e=await this.getSessionAuthHeaders(),t=await fetch(`${this.baseUrl}/api/v2/skills`,{headers:e});if(!t.ok){let n=await t.json().catch(()=>({}));throw new c(n.error||`Failed to list skills: ${t.status}`)}return (await t.json()).skills||[]}async getConnectedApps(){this.logger.debug("getConnectedApps");let e=await this.getSessionAuthHeaders(),t=await fetch(`${this.baseUrl}/api/v2/skills/composio/connections`,{headers:e});if(!t.ok){let r=await t.json().catch(()=>({}));throw new c(r.error||`Failed to get connected apps: ${t.status}`,"CONNECTED_APPS_FAILED")}let s=await t.json();return {connected:(s.connections||[]).filter(r=>r.status==="connected").map(r=>r.app.toLowerCase()),connections:s.connections||[],oauthConnections:s.oauth_connections||[],apiKeyConnections:s.api_key_connections||[]}}async getSkillStatus(e){this.logger.debug("getSkillStatus",{skillId:e});let t=await this.getSessionAuthHeaders(),s=await fetch(`${this.baseUrl}/api/v2/skills/${e}/configure`,{headers:t});if(!s.ok){let r=await s.json().catch(()=>({}));throw new c(r.error||`Failed to get skill status: ${s.status}`)}let n=await s.json();return {skillId:n.skill_id,status:n.status,configuredVars:n.configured_vars||[],lastUsedAt:n.last_used_at?new Date(n.last_used_at):void 0,errorMessage:n.error_message}}async configureSkill(e,t){this.logger.debug("configureSkill",{skillId:e,varNames:Object.keys(t)});let s=await this.getSessionAuthHeaders(),n=await fetch(`${this.baseUrl}/api/v2/skills/${e}/configure`,{method:"POST",headers:{"Content-Type":"application/json",...s},body:JSON.stringify({env_vars:t})});if(!n.ok){let a=await n.json().catch(()=>({}));throw new c(a.error||`Failed to configure skill: ${n.status}`,a.error_code)}let r=await n.json();return {success:true,skillId:r.skill_id,configuredVars:r.configured_vars||[]}}async searchGlamaDirectory(e){this.logger.debug("searchGlamaDirectory",{query:e});let t=new URLSearchParams({q:e}),s=await fetch(`${this.baseUrl}/api/mcp/glama?${t}`);if(!s.ok){let r=await s.json().catch(()=>({}));throw new c(r.error||`Glama search failed: ${s.status}`)}let n=await s.json();return {servers:(n.servers||[]).map(r=>this.parseGlamaServer(r)),pageInfo:n.pageInfo,query:n.query}}async getFeaturedGlamaServers(){this.logger.debug("getFeaturedGlamaServers");let e=await fetch(`${this.baseUrl}/api/mcp/glama`);if(!e.ok){let s=await e.json().catch(()=>({}));throw new c(s.error||`Failed to get featured servers: ${e.status}`)}let t=await e.json();return {servers:(t.servers||[]).map(s=>this.parseGlamaServer(s)),featured:t.featured}}parseGlamaServer(e){let t=e.importData;return {id:e.id,name:e.name,namespace:e.namespace,slug:e.slug,description:e.description,url:e.url,attributes:e.attributes,canHost:e.canHost,tools:e.tools||[],repository:e.repository,license:e.spdxLicense?.name,importData:t?{glamaId:t.glamaId,glamaNamespace:t.glamaNamespace,glamaSlug:t.glamaSlug,suggestedName:t.suggestedName,suggestedDescription:t.suggestedDescription,hostingType:t.hostingType,requiredEnvVars:t.requiredEnvVars,optionalEnvVars:t.optionalEnvVars}:void 0}}async deployTaskAgent(e){this.logger.debug("deployTaskAgent",{name:e.name,budgetUsd:e.budgetUsd});let t=await this.getSessionAuthHeaders(),s=await fetch(`${this.baseUrl}/api/v2/jit-task/deploy`,{method:"POST",headers:{"Content-Type":"application/json",...t},body:JSON.stringify({name:e.name,prompt:e.prompt,system_prompt:e.systemPrompt,model:e.model||"claude-sonnet-4-5",tools:e.tools||[],budget_usd:e.budgetUsd,ttl_hours:e.ttlHours||24,max_iterations:e.maxIterations||10,plan_id:e.planId,task_id:e.taskId,idempotency_key:e.idempotencyKey,auto_run:e.autoRun||false})});if(!s.ok){let r=await s.json().catch(()=>({}));throw new c(r.error||`Task agent deploy failed: ${s.status}`)}let n=await s.json();return {instance:this.parseTaskAgentInstance(n.instance),idempotent:n.idempotent||false,requestId:n.request_id}}async getTaskAgentStatus(e){this.logger.debug("getTaskAgentStatus",{instanceId:e});let t=await this.getSessionAuthHeaders(),s=await fetch(`${this.baseUrl}/api/v2/jit-task/${e}`,{method:"GET",headers:t});if(!s.ok){let r=await s.json().catch(()=>({}));throw new c(r.error||`Failed to get task status: ${s.status}`)}let n=await s.json();return this.parseTaskAgentStatus(n)}async triggerTaskAgent(e){this.logger.debug("triggerTaskAgent",{instanceId:e});let t=await this.getSessionAuthHeaders(),s=await fetch(`${this.baseUrl}/api/v2/jit-task/${e}`,{method:"POST",headers:{"Content-Type":"application/json",...t}});if(!s.ok){let r=await s.json().catch(()=>({}));throw new c(r.error||`Failed to trigger task: ${s.status}`)}let n=await s.json();return {success:n.success,status:n.status}}async cancelTaskAgent(e){this.logger.debug("cancelTaskAgent",{instanceId:e});let t=await this.getSessionAuthHeaders(),s=await fetch(`${this.baseUrl}/api/v2/jit-task/${e}`,{method:"DELETE",headers:t});if(!s.ok){let r=await s.json().catch(()=>({}));throw new c(r.error||`Failed to cancel task: ${s.status}`)}let n=await s.json();return {success:n.success,status:n.status}}async listTaskAgents(e){this.logger.debug("listTaskAgents",{options:e});let t=await this.getSessionAuthHeaders(),s=new URLSearchParams;e?.status&&s.set("status",e.status),e?.planId&&s.set("plan_id",e.planId),e?.taskId&&s.set("task_id",e.taskId),e?.limit!==void 0&&s.set("limit",String(e.limit)),e?.offset!==void 0&&s.set("offset",String(e.offset));let n=s.toString(),r=`${this.baseUrl}/api/v2/jit-task${n?`?${n}`:""}`,a=await fetch(r,{method:"GET",headers:t});if(!a.ok){let o=await a.json().catch(()=>({}));throw new c(o.error||`Failed to list task agents: ${a.status}`)}let i=await a.json();return {taskAgents:(i.task_agents||[]).map(o=>{let d=o.budget,p=o.iterations,u=o.usage,g=o.plan,h=o.task;return {id:o.id,name:o.name,prompt:o.prompt,model:o.model,tools:o.tools,status:o.status,budget:{totalUsd:d.total_usd,spentUsd:d.spent_usd,remainingUsd:d.remaining_usd},iterations:{current:p.current,max:p.max},usage:{toolCalls:u.tool_calls,inputTokens:u.input_tokens,outputTokens:u.output_tokens},result:o.result,error:o.error,depth:o.depth,plan:g?{id:g.id,title:g.title}:null,task:h?{id:h.id,title:h.title}:null,ttlHours:o.ttl_hours,expiresAt:new Date(o.expires_at),createdAt:new Date(o.created_at),startedAt:o.started_at?new Date(o.started_at):void 0,completedAt:o.completed_at?new Date(o.completed_at):void 0}}),pagination:{total:i.pagination.total,limit:i.pagination.limit,offset:i.pagination.offset,hasMore:i.pagination.has_more},stats:{active:i.stats.active??0,completed:i.stats.completed??0,failed:i.stats.failed??0,totalSpentUsd:i.stats.total_spent_usd??0}}}async waitForTaskAgent(e,t){let s=t?.pollIntervalMs||2e3,n=t?.timeoutMs||3e5,r=Date.now();for(this.logger.debug("waitForTaskAgent",{instanceId:e,pollIntervalMs:s,timeoutMs:n});;){let a=await this.getTaskAgentStatus(e);if(["completed","failed","cancelled","budget_exceeded","expired"].includes(a.status))return {id:a.id,status:a.status,result:a.result,error:a.error,spentUsd:a.budget.spentUsd,iterations:a.iterations.current,toolCalls:a.usage.toolCalls,completedAt:a.completedAt};if(Date.now()-r>n)throw new c(`Task agent wait timeout after ${n}ms`);await new Promise(i=>setTimeout(i,s));}}parseTaskAgentInstance(e){return {id:e.id,name:e.name,endpointUrl:e.endpoint_url,statusUrl:e.status_url,triggerUrl:e.trigger_url,cancelUrl:e.cancel_url,resultUrl:e.result_url,status:e.status,budgetUsd:e.budget_usd,expiresAt:new Date(e.expires_at),depth:e.depth,maxIterations:e.max_iterations}}parseTaskAgentStatus(e){let t=e.budget,s=e.iterations,n=e.usage,r=e.endpoints;return {id:e.id,name:e.name,prompt:e.prompt,model:e.model,tools:e.tools,status:e.status,budget:{totalUsd:t?.total_usd??0,spentUsd:t?.spent_usd??0,remainingUsd:t?.remaining_usd??0},iterations:{current:s?.current??0,max:s?.max??0},usage:{toolCalls:n?.tool_calls??0,inputTokens:n?.input_tokens??0,outputTokens:n?.output_tokens??0},result:e.result,error:e.error,depth:e.depth,planId:e.plan_id,taskId:e.task_id,endpoints:{statusUrl:r?.status_url??"",triggerUrl:r?.trigger_url??"",cancelUrl:r?.cancel_url??"",resultUrl:r?.result_url??""},ttlHours:e.ttl_hours,expiresAt:new Date(e.expires_at),createdAt:new Date(e.created_at),startedAt:e.started_at?new Date(e.started_at):void 0,completedAt:e.completed_at?new Date(e.completed_at):void 0}}async complete(e,t){this.logger.debug("complete",{promptLength:e.length,model:t?.model});let s=await this.runAgent({messages:[{role:"user",content:e}],config:{model:t?.model||"gpt-4o-mini",maxIterations:1,tools:[],systemPrompt:t?.systemPrompt}});return {text:s.response,costUsd:s.cost.totalUsd,tokens:s.tokens,model:t?.model||"gpt-4o-mini"}}async runAgent(e){let{sessionId:t,messages:s,config:n={},stream:r=false,idempotencyKey:a,onEvent:i}=e;this.logger.debug("runAgent",{sessionId:t||"(from signature)",messageCount:s.length,config:n,stream:r});let o={messages:s.map(p=>({role:p.role,content:p.content})),config:{model:n.model,max_iterations:n.maxIterations,tools:n.tools,system_prompt:n.systemPrompt},stream:r,idempotency_key:a};t&&(o.session_id=t);let d=18e4;if(!r){let p=await this.getSessionAuthHeaders(),u=await fetch(`${this.baseUrl}/api/v2/agent/run`,{method:"POST",headers:{"Content-Type":"application/json",...p},body:JSON.stringify(o),signal:AbortSignal.timeout(d)});if(!u.ok){let h=await u.json().catch(()=>({}));throw new c(h.error||`Agent run failed: ${u.status}`)}let g=await u.json();if(g.cost?.total_usd>0){let h={amountUsd:g.cost.total_usd,recipient:"mixrpay-agent-run",txHash:g.tx_hash,timestamp:new Date,description:`Agent run: ${g.run_id}`,url:`${this.baseUrl}/api/v2/agent/run`,requestId:a||crypto.randomUUID()};this.payments.push(h),this.totalSpentUsd+=g.cost.total_usd,this.logger.payment(g.cost.total_usd,"agent-run",g.run_id),this.onPayment&&this.onPayment(h);}return {runId:g.run_id,status:g.status,response:g.response,iterations:g.iterations,toolsUsed:g.tools_used,cost:{llmUsd:g.cost.llm_usd,toolsUsd:g.cost.tools_usd,totalUsd:g.cost.total_usd},tokens:g.tokens,sessionRemainingUsd:g.session_remaining_usd,txHash:g.tx_hash}}return this.runAgentStreaming(o,i)}async runAgentStreaming(e,t){let n=await this.getSessionAuthHeaders(),r=await fetch(`${this.baseUrl}/api/v2/agent/run`,{method:"POST",headers:{"Content-Type":"application/json",...n},body:JSON.stringify(e),signal:AbortSignal.timeout(3e5)});if(!r.ok){let p=await r.json().catch(()=>({}));throw new c(p.error||`Agent run failed: ${r.status}`)}let a=r.body?.getReader();if(!a)throw new c("No response body for streaming");let i=new TextDecoder,o="",d=null;for(;;){let{done:p,value:u}=await a.read();if(p)break;o+=i.decode(u,{stream:true});let g=o.split(`
|
|
31
|
+
`);o=g.pop()||"";let h="";for(let y of g)if(y.startsWith("event: "))h=y.slice(7).trim();else if(y.startsWith("data: ")&&h){try{let m=JSON.parse(y.slice(6)),f=this.parseSSEEvent(h,m);if(f){if(t?.(f),h==="complete"&&(d={runId:m.run_id,status:"completed",response:m.response,iterations:m.iterations,toolsUsed:m.tools_used,cost:{llmUsd:0,toolsUsd:0,totalUsd:m.total_cost_usd},tokens:{prompt:0,completion:0},sessionRemainingUsd:0,txHash:m.tx_hash},m.total_cost_usd>0)){let b={amountUsd:m.total_cost_usd,recipient:"mixrpay-agent-run",txHash:m.tx_hash,timestamp:new Date,description:`Agent run: ${m.run_id}`,url:`${this.baseUrl}/api/v2/agent/run`,requestId:e.idempotency_key||crypto.randomUUID()};this.payments.push(b),this.totalSpentUsd+=m.total_cost_usd,this.onPayment&&this.onPayment(b);}if(h==="error")throw new c(m.error||"Agent run failed")}}catch(m){if(m instanceof c)throw m;this.logger.warn("Failed to parse SSE event:",m);}h="";}}if(!d)throw new c("Agent run completed without final result");return d}parseSSEEvent(e,t){switch(e){case "run_start":return {type:"run_start",runId:t.run_id};case "iteration_start":return {type:"iteration_start",iteration:t.iteration};case "llm_chunk":return {type:"llm_chunk",delta:t.delta};case "tool_call":return {type:"tool_call",tool:t.tool,arguments:t.arguments};case "tool_result":return {type:"tool_result",tool:t.tool,success:t.success,costUsd:t.cost_usd,error:t.error};case "iteration_complete":return {type:"iteration_complete",iteration:t.iteration,tokens:t.tokens,costUsd:t.cost_usd};case "complete":return {type:"complete",runId:t.run_id,response:t.response,totalCostUsd:t.total_cost_usd,txHash:t.tx_hash,iterations:t.iterations,toolsUsed:t.tools_used};case "error":return {type:"error",error:t.error,partialCostUsd:t.partial_cost_usd};default:return null}}async getAgentRunStatus(e,t){this.logger.debug("getAgentRunStatus",{runId:e});let s=await this.getSessionAuthHeaders(),n=await fetch(`${this.baseUrl}/api/v2/agent/run/${e}`,{headers:s});if(!n.ok){let a=await n.json().catch(()=>({}));throw new c(a.error||`Failed to get run status: ${n.status}`)}let r=await n.json();return {runId:r.run_id,status:r.status,response:r.response,iterations:r.iterations,toolsUsed:r.tools_used,cost:{llmUsd:r.cost.llm_usd,toolsUsd:r.cost.tools_usd,totalUsd:r.cost.total_usd},tokens:r.tokens,txHash:r.tx_hash,error:r.error,startedAt:new Date(r.started_at),completedAt:r.completed_at?new Date(r.completed_at):void 0}}async callToolWithSession(e,t,s={}){this.logger.debug("callToolWithSession",{sessionId:e,toolName:t,args:s}),await this.validateMerchantAllowed(t,"tool");let r=await(await fetch(`${this.baseUrl}/api/mcp`,{method:"POST",headers:{"Content-Type":"application/json","X-Mixr-Session":e},body:JSON.stringify({jsonrpc:"2.0",method:"tools/call",params:{name:t,arguments:s},id:Date.now()})})).json();if(r.error)throw new c(r.error.message||"MCP tool call failed");let a=r.result?.content?.[0],i=a?.text?JSON.parse(a.text):null,o=r.result?._mixrpay||{};if(o.chargedUsd){let d={amountUsd:o.chargedUsd,recipient:t.split("/")[0]||t,txHash:o.txHash,timestamp:new Date,description:`MCP: ${t}`,url:`${this.baseUrl}/api/mcp`,requestId:crypto.randomUUID()};this.payments.push(d),this.totalSpentUsd+=o.chargedUsd,this.logger.payment(o.chargedUsd,t,"MCP call (session)"),this.onPayment&&this.onPayment(d);}return {data:i,chargedUsd:o.chargedUsd||0,txHash:o.txHash,latencyMs:o.latencyMs}}async callMCPToolWithSession(e,t,s={}){return this.callToolWithSession(e,t,s)}async createTask(e){this.logger.info(`Creating task: ${e.title}`);let t=await this.callApi("/api/v2/tasks",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:e.title,description:e.description,budget_usd:e.budgetUsd,deliverables:e.deliverables,category:e.category,expires_in_days:e.expiresInDays})}),s=await t.json();if(!t.ok)throw new c(s.error||"Failed to create task");return this.parseTask(s.task)}async listTasks(e){let t=new URLSearchParams;e?.status&&t.set("status",e.status),e?.category&&t.set("category",e.category),e?.minBudget!==void 0&&t.set("min_budget",String(e.minBudget)),e?.maxBudget!==void 0&&t.set("max_budget",String(e.maxBudget)),e?.limit!==void 0&&t.set("limit",String(e.limit)),e?.offset!==void 0&&t.set("offset",String(e.offset));let s=`/api/v2/tasks${t.toString()?`?${t}`:""}`,n=await this.callApi(s,{method:"GET"}),r=await n.json();if(!n.ok)throw new c(r.error||"Failed to list tasks");return {tasks:r.tasks.map(a=>this.parseTask(a)),pagination:r.pagination}}async getTask(e){let t=await this.callApi(`/api/v2/tasks/${e}`,{method:"GET"}),s=await t.json();if(!t.ok)throw new c(s.error||"Failed to get task");return this.parseTask(s.task)}async getMyTasks(e={}){let t=new URLSearchParams;e.role&&t.set("role",e.role),e.status&&t.set("status",e.status),e.page&&t.set("page",e.page.toString()),e.limit&&t.set("limit",e.limit.toString());let s=t.toString(),n=`/api/v2/tasks/my${s?`?${s}`:""}`,r=await this.callApi(n,{method:"GET"}),a=await r.json();if(!r.ok)throw new c(a.error||"Failed to get tasks");return {tasks:a.tasks.map(i=>({...this.parseTask(i),isCreator:i.is_creator,isAgent:i.is_agent})),pagination:{page:a.pagination.page,limit:a.pagination.limit,total:a.pagination.total,totalPages:a.pagination.total_pages}}}async requestTask(e,t){this.logger.info(`Requesting task: ${e}`);let s=await this.callApi(`/api/v2/tasks/${e}/request`,{method:"POST",headers:{"Content-Type":"application/json"},body:t?JSON.stringify({message:t}):"{}"}),n=await s.json();if(!s.ok)throw new c(n.error||"Failed to request task");return {id:n.request.id,taskId:n.request.task_id,status:n.request.status,message:n.request.message,createdAt:new Date(n.request.created_at)}}async getTaskRequests(e){let t=await this.callApi(`/api/v2/tasks/${e}/requests`,{method:"GET"}),s=await t.json();if(!t.ok)throw new c(s.error||"Failed to get task requests");return s.requests.map(n=>({id:n.id,status:n.status,message:n.message,createdAt:new Date(n.created_at),reviewedAt:n.reviewed_at?new Date(n.reviewed_at):void 0,agent:n.agent}))}async acceptTaskRequest(e,t){this.logger.info(`Accepting request ${t} for task ${e}`);let s=await this.callApi(`/api/v2/tasks/${e}/requests/${t}/accept`,{method:"POST"}),n=await s.json();if(!s.ok)throw new c(n.error||"Failed to accept request");return {success:n.success,task:{id:n.task.id,status:n.task.status,agentUserId:n.task.agent_user_id},session:{id:n.session.id,sessionKey:n.session.session_key,address:n.session.address,expiresAt:new Date(n.session.expires_at),budgetUsd:n.session.budget_usd,allowedMerchants:n.session.allowed_merchants||[]},invite:{id:n.invite.id,code:n.invite.code},listingFeeTxHash:n.listing_fee_tx_hash}}async rejectTaskRequest(e,t){let s=await this.callApi(`/api/v2/tasks/${e}/requests/${t}/reject`,{method:"POST"}),n=await s.json();if(!s.ok)throw new c(n.error||"Failed to reject request")}async submitTask(e,t){this.logger.info(`Submitting task: ${e}`);let s=await this.callApi(`/api/v2/tasks/${e}/submit`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({output_text:t.text,output_url:t.url})}),n=await s.json();if(!s.ok)throw new c(n.error||"Failed to submit task")}async approveTask(e){this.logger.info(`Approving task: ${e}`);let t=await this.callApi(`/api/v2/tasks/${e}/approve`,{method:"POST"}),s=await t.json();if(!t.ok)throw new c(s.error||"Failed to approve task");return {success:s.success,task:{id:s.task.id,status:s.task.status,completedAt:new Date(s.task.completed_at)},payout:{status:s.payout.status,amountUsd:s.payout.amount_usd,txHash:s.payout.tx_hash}}}async rejectTaskSubmission(e,t){let s=await this.callApi(`/api/v2/tasks/${e}/reject`,{method:"POST",headers:{"Content-Type":"application/json"},body:t?JSON.stringify({feedback:t}):"{}"}),n=await s.json();if(!s.ok)throw new c(n.error||"Failed to reject task")}async cancelTask(e){let t=await this.callApi(`/api/v2/tasks/${e}`,{method:"DELETE"}),s=await t.json();if(!t.ok)throw new c(s.error||"Failed to cancel task")}async createSubtask(e){this.logger.info(`Creating subtask: ${e.title}`);let t=await this.callApi("/api/v2/tasks/create-subtask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({parent_task_id:e.parentTaskId,title:e.title,description:e.description,deliverables:e.deliverables,category:e.category,budget_usd:e.budgetUsd,allow_sub_agents:e.allowSubAgents,expires_in_days:e.expiresInDays,idempotency_key:e.idempotencyKey})}),s=await t.json();if(!t.ok)throw new c(s.error||"Failed to create subtask");return this.parseTask(s.task)}async updateTaskStatus(e,t,s,n){this.logger.info(`Updating task ${e} status to ${t}`);let r=await this.callApi(`/api/v2/tasks/${e}/status`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({status:t,note:s,idempotency_key:n})}),a=await r.json();if(!r.ok)throw new c(a.error||"Failed to update task status");return this.parseTask(a.task)}async getSubtasks(e){let t=await this.callApi(`/api/v2/tasks/${e}/subtasks`,{method:"GET"}),s=await t.json();if(!t.ok)throw new c(s.error||"Failed to get subtasks");return (s.subtasks||[]).map(n=>this.parseTask(n))}async getTaskStatusHistory(e){let t=await this.callApi(`/api/v2/tasks/${e}/status`,{method:"GET"}),s=await t.json();if(!t.ok)throw new c(s.error||"Failed to get task status history");return {taskId:s.task_id,currentStatus:s.current_status,history:(s.history||[]).map(n=>({id:n.id,oldStatus:n.old_status,newStatus:n.new_status,note:n.note,createdAt:new Date(n.created_at)}))}}async requestApproval(e){this.logger.info(`Requesting approval for: ${e.actionType}`);let t=await this.callApi("/api/v2/approvals/request",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action_type:e.actionType,action_payload:e.actionPayload,context:e.context,expires_in_hours:e.expiresInHours,idempotency_key:e.idempotencyKey})}),s=await t.json();if(!t.ok)throw new c(s.error||"Failed to request approval");return s.auto_approved?{autoApproved:true,reason:s.reason}:this.parseApprovalRequest(s.approval_request)}async checkApprovalStatus(e){let t=await this.callApi(`/api/v2/approvals/check?request_id=${e}`,{method:"GET"}),s=await t.json();if(!t.ok)throw new c(s.error||"Failed to check approval status");return {requestId:s.request_id,status:s.status,responseNote:s.response_note,approvalScope:s.approval_scope,respondedAt:s.responded_at?new Date(s.responded_at):void 0}}async checkActionPermission(e,t){let s=`/api/v2/approvals/check?action_type=${encodeURIComponent(e)}`;t!==void 0&&(s+=`&amount_usd=${t}`);let n=await this.callApi(s,{method:"GET"}),r=await n.json();if(!n.ok)throw new c(r.error||"Failed to check action permission");return {actionType:r.action_type,needsApproval:r.needs_approval,reason:r.reason}}async waitForApproval(e,t={}){let s=t.pollIntervalMs||5e3,n=t.timeoutMs||3e5,r=Date.now();for(;Date.now()-r<n;){let a=await this.checkApprovalStatus(e);if(a.status!=="pending")return a;await new Promise(i=>setTimeout(i,s));}return {requestId:e,status:"expired"}}parseApprovalRequest(e){return {id:e.id,actionType:e.action_type,actionPayload:e.action_payload,context:e.context,status:e.status,responseNote:e.response_note,approvalScope:e.approval_scope,createdAt:new Date(e.created_at),expiresAt:new Date(e.expires_at),respondedAt:e.responded_at?new Date(e.responded_at):void 0}}parseTask(e){return {id:e.id,title:e.title,description:e.description,deliverables:e.deliverables||[],category:e.category,budgetUsd:e.budget_usd,listingFeeUsd:e.listing_fee_usd,status:e.status,createdAt:new Date(e.created_at),updatedAt:e.updated_at?new Date(e.updated_at):void 0,expiresAt:e.expires_at?new Date(e.expires_at):void 0,claimedAt:e.claimed_at?new Date(e.claimed_at):void 0,submittedAt:e.submitted_at?new Date(e.submitted_at):void 0,completedAt:e.completed_at?new Date(e.completed_at):void 0,creator:e.creator,assignedAgent:e.assigned_agent,requestCount:e.request_count,output:e.output,payment:e.payment}}async getBalances(){let e=await this.callApiWithAuth("/api/v2/agent/balances",{method:"GET"});if(!e.ok){let t=await e.json().catch(()=>({}));throw new c(t.error||`Failed to get balances: ${e.status}`)}return e.json()}async transfer(e,t,s="USDC"){let n=await this.callApiWithAuth("/api/v2/agent/transfer",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({to:e,amount:t,currency:s})});if(!n.ok){let r=await n.json().catch(()=>({}));throw new c(r.error||`Transfer failed: ${n.status}`)}return n.json()}async swap(e,t,s,n=100){let r=await this.callApiWithAuth("/api/v2/agent/swap",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sell_token:e,buy_token:t,sell_amount:s,slippage_bps:n})});if(!r.ok){let a=await r.json().catch(()=>({}));throw new c(a.error||`Swap failed: ${r.status}`)}return r.json()}async bridge(e,t,s,n){let r=await this.callApiWithAuth("/api/v2/agent/bridge",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:e,amount:t,dest_chain:s,dest_address:n})});if(!r.ok){let a=await r.json().catch(()=>({}));throw new c(a.error||`Bridge failed: ${r.status}`)}return r.json()}async getBridgeStatus(e){let t=await this.callApiWithAuth(`/api/v2/agent/bridge/status?order_id=${encodeURIComponent(e)}`,{method:"GET"});if(!t.ok){let s=await t.json().catch(()=>({}));throw new c(s.error||`Bridge status check failed: ${t.status}`)}return t.json()}async fetchPaid(e,t){let s=await this.callApiWithAuth("/api/v2/agent/fetch",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url:e,method:t?.method||"GET",headers:t?.headers,body:t?.body,max_payment_usd:t?.maxPaymentUsd})});if(!s.ok){let n=await s.json().catch(()=>({}));throw new c(n.error||`Fetch failed: ${s.status}`)}return s.json()}async callApiWithAuth(e,t={}){let s=`${this.baseUrl}${e}`,n={...t.headers};if(this.apiKey)n.Authorization=`Bearer ${this.apiKey}`;else {let r=await this.getSessionAuthHeaders();Object.assign(n,r);}return fetch(s,{...t,headers:n})}async callApi(e,t={}){let s=`${this.baseUrl}${e}`,n={...t.headers};if(this.sessionKey){let a=accounts.privateKeyToAccount(this.sessionKey.rawPrivateKey).address.toLowerCase(),i=Date.now(),o=`MixrPay:${i}:${a}`,d=await accounts.signMessage({message:o,privateKey:this.sessionKey.rawPrivateKey});n["X-Session-Auth"]=JSON.stringify({address:a,timestamp:i,signature:d});}return fetch(s,{...t,headers:n})}};M();exports.AgentWallet=ee;exports.AuthenticationError=G;exports.InsufficientBalanceError=v;exports.InvalidSessionKeyError=_;exports.MerchantNotAllowedError=O;exports.MixrPayError=c;exports.NetworkError=B;exports.PaymentFailedError=S;exports.RateLimitError=J;exports.SDK_VERSION=we;exports.SessionExpiredError=E;exports.SessionKeyExpiredError=D;exports.SessionLimitExceededError=U;exports.SessionNotFoundError=I;exports.SessionRevokedError=R;exports.SpendingLimitExceededError=k;exports.X402ProtocolError=T;exports.deleteCredentials=ge;exports.deleteWalletKey=Ne;exports.getCredentialsFilePath=me;exports.getErrorMessage=Se;exports.getWalletStoragePath=Me;exports.hasCredentials=pe;exports.hasWalletKey=V;exports.isMixrPayError=Ae;exports.loadCredentials=H;exports.loadWalletData=je;exports.loadWalletKey=L;exports.saveCredentials=ue;exports.saveWalletKey=z;
|