Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement auto-pay pipe with ScreenPipe and Wise API integration #9

Merged
merged 8 commits into from
Dec 29, 2024
Binary file modified pipes/auto-pay/bun.lockb
100644 → 100755
Binary file not shown.
15 changes: 15 additions & 0 deletions pipes/auto-pay/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ["@screenpipe/js"],
webpack: (config, { isServer }) => {
config.resolve.alias = {
...config.resolve.alias,
"@screenpipe/js": isServer
? "@screenpipe/js/dist/node.js"
: "@screenpipe/js/dist/browser.js",
};
return config;
},
};

module.exports = nextConfig;
23 changes: 15 additions & 8 deletions pipes/auto-pay/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
"lint": "next lint"
},
"dependencies": {
"@ai-sdk/openai": "^0.0.66",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.3",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-popover": "^1.1.3",
"@radix-ui/react-progress": "^1.1.1",
Expand All @@ -28,7 +30,9 @@
"@types/js-levenshtein": "^1.1.3",
"@types/lodash": "^4.17.13",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/uuid": "^10.0.0",
"ai": "^4.0.18",
"axios": "^1.7.9",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
Expand All @@ -38,7 +42,7 @@
"js-levenshtein": "^1.1.6",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
"lucide-react": "^0.468.0",
"lucide-react": "^0.469.0",
"magic-ui": "^0.1.0",
"next": "15.1.0",
"npm": "^10.9.2",
Expand All @@ -51,20 +55,23 @@
"react-syntax-highlighter": "^15.6.1",
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7"
"sharp": "^0.33.5",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.0.3",
"zod": "^3.23.8"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/node": "^22.10.2",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"autoprefixer": "^10.4.20",
"bun-types": "latest",
"eslint": "^9",
"eslint-config-next": "15.1.0",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.16",
"typescript": "^5",
"bun-types": "latest"
"tailwindcss": "^3.4.17",
"typescript": "^5"
}
}
12 changes: 11 additions & 1 deletion pipes/auto-pay/pipe.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
{
"name": "auto-pay-pipe",
"version": "0.1.0",
"fields": [
{
"name": "analysisWindow",
"type": "number",
"description": "How many hours of screen data to analyze?",
"default": 24
}
],
"crons": [
{
"path": "/api/log",
"path": "/api/analyze",
"schedule": "0 */5 * * * *"
}
]
Expand Down
12 changes: 0 additions & 12 deletions pipes/auto-pay/src/app/page.tsx

This file was deleted.

25 changes: 25 additions & 0 deletions pipes/auto-pay/src/lib/screenpipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { pipe } from "@screenpipe/js/node";
import type { ContentItem } from "@screenpipe/js";

export async function queryScreenData(hours: number = 24): Promise<ContentItem[]> {
const now = new Date();
const startTime = new Date(now.getTime() - hours * 60 * 60 * 1000);

const response = await pipe.queryScreenpipe({
startTime: startTime.toISOString(),
endTime: now.toISOString(),
limit: 10000,
contentType: "ocr",
});

return response.data;
}

export async function getSettings() {
const settings = await pipe.settings.getAll();
return {
analysisWindow: settings.analysisWindow || 24,
wiseApiToken: settings.wiseApiToken,
wiseProfileId: settings.wiseProfileId,
};
}
13 changes: 13 additions & 0 deletions pipes/auto-pay/src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as React from "react";
import type { AppProps } from 'next/app'
import '@/styles/globals.css'
import { Toaster } from "@/components/ui/toaster"

export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Component {...pageProps} />
<Toaster />
</>
)
}
58 changes: 58 additions & 0 deletions pipes/auto-pay/src/pages/api/analyze.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { NextApiRequest, NextApiResponse } from 'next';
import axios from 'axios';
import type {
ScreenPipeResponse,
ScreenPipeSearchResult,
} from '@/types/screenpipe';

const SCREENPIPE_API = 'http://localhost:3030';

interface FormattedOCRData {
text: string;
timestamp: string;
appName: string;
windowName: string;
}

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
// Get analysis window from query params or default to 24 hours
const analysisWindow = parseInt(req.query.analysisWindow as string) || 24;
// if dev use last 10 minutes
const isDev = process.env.NODE_ENV === 'development';
const startTime = isDev
? new Date(Date.now() - 10 * 60 * 1000).toISOString()
: new Date(Date.now() - analysisWindow * 60 * 60 * 1000).toISOString();

// Query ScreenPipe's search API for OCR data
const response = await axios.get<ScreenPipeResponse>(
`${SCREENPIPE_API}/search`,
{
params: {
content_type: 'ocr',
start_time: startTime,
limit: 1000,
app_name: 'Arc',
},
}
);
console.log(JSON.stringify(response.data), 'response.data', 'analyze');

// Extract and format the OCR data
const data: FormattedOCRData[] = response.data.data
.map((item: ScreenPipeSearchResult) => ({
text: item.content.text,
timestamp: item.content.timestamp,
appName: item.content.app_name,
windowName: item.content.window_name,
}));

res.status(200).json({ data });
} catch (err) {
console.error('Analysis failed:', err);
res.status(500).json({ error: 'Analysis failed' });
}
}
181 changes: 181 additions & 0 deletions pipes/auto-pay/src/pages/api/createTransfer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { NextApiRequest, NextApiResponse } from 'next';
import axios, { AxiosError } from 'axios';
import { v4 as uuidv4 } from 'uuid';
import type { PaymentInfo } from '@/types/wise';

const WISE_API_URL = process.env.WISE_API_URL || 'https://api.sandbox.transferwise.tech';

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}

try {
const { paymentInfo } = req.body as { paymentInfo: PaymentInfo };

// Get Wise API token and profile ID from environment variables
const wiseToken = process.env.WISE_API_KEY;
const profileId = process.env.WISE_PROFILE_ID;
if (!wiseToken || !profileId) {
throw new Error('Missing Wise API configuration');
}

// Create a quote using v3 authenticated quotes endpoint
console.log("0xHypr", "Creating quote");
const quoteData = {
sourceCurrency: paymentInfo.currency,
targetCurrency: paymentInfo.currency,
sourceAmount: parseFloat(paymentInfo.amount),
payOut: "BANK_TRANSFER",
preferredPayIn: "BANK_TRANSFER",
paymentMetadata: {
transferNature: "MOVING_MONEY_BETWEEN_OWN_ACCOUNTS"
}
};

console.log("0xHypr", "Quote request data:", quoteData);
const profileIdNumber = parseInt(profileId);
console.log("0xHypr", "Profile ID Number:", profileIdNumber);

const quoteResponse = await axios.post(
`${WISE_API_URL}/v3/profiles/${profileIdNumber}/quotes`,
quoteData,
{
headers: {
Authorization: `Bearer ${wiseToken}`,
'Content-Type': 'application/json'
}
}
);

console.log("0xHypr", "Quote created:", quoteResponse.data);

// Create recipient account
console.log("0xHypr", "Creating recipient account");
const recipientData = {
currency: paymentInfo.currency,
type: paymentInfo.accountNumber && paymentInfo.routingNumber ? 'aba' : 'email',
profile: profileIdNumber,
accountHolderName: paymentInfo.recipientName,
...(paymentInfo.accountNumber && paymentInfo.routingNumber
? {
details: {
legalType: 'PRIVATE',
accountType: 'CHECKING',
accountNumber: paymentInfo.accountNumber,
abartn: paymentInfo.routingNumber,
address: {
country: 'US',
city: 'New York',
state: 'NY',
postCode: '10001',
firstLine: '123 Main St'
}
}
}
: {
details: {
email: paymentInfo.recipientEmail || `${paymentInfo.recipientName.toLowerCase().replace(/\s+/g, '.')}@example.com`
}
}
)
};

const recipientResponse = await axios.post(
`${WISE_API_URL}/v1/accounts`,
recipientData,
{
headers: {
Authorization: `Bearer ${wiseToken}`,
'Content-Type': 'application/json'
}
}
);

console.log("0xHypr", "Recipient created:", recipientResponse.data);

// Update quote with recipient
console.log("0xHypr", "Updating quote with recipient");
await axios.patch(
`${WISE_API_URL}/v3/profiles/${profileIdNumber}/quotes/${quoteResponse.data.id}`,
{
targetAccount: recipientResponse.data.id,
payOut: "BANK_TRANSFER"
},
{
headers: {
Authorization: `Bearer ${wiseToken}`,
'Content-Type': 'application/merge-patch+json'
}
}
);

// Create transfer with the improved logic
console.log("0xHypr", "Creating transfer");
// print quoteResponse.data
// random number between 1000 and 9999
console.log("0xHypr", "Quote ID:", quoteResponse.data);
const transferData = {
targetAccount: recipientResponse.data.id,
quoteUuid: quoteResponse.data.id,
customerTransactionId: uuidv4(),
details: {
// reference: paymentInfo.referenceNote || "Auto payment",
// transferPurpose: "verification.transfers.purpose.pay.bills",
// transferPurposeSubTransferPurpose: "verification.sub.transfers.purpose.pay.bills",
// sourceOfFunds: "verification.source.of.funds.other"
},
originator: {
type: "ACCOUNT",
id: "1234567890"
}
};

console.log("0xHypr", "Transfer data:", transferData);

const transferResponse = await axios.post(
`${WISE_API_URL}/v1/transfers`,
transferData,
{
headers: {
Authorization: `Bearer ${wiseToken}`,
'Content-Type': 'application/json'
}
}
);

console.log("0xHypr", "Transfer response:", transferResponse.data);

// Fund the transfer
if (transferResponse.data.id) {
console.log("0xHypr", "Funding transfer");
await axios.post(
`${WISE_API_URL}/v3/profiles/${profileIdNumber}/transfers/${transferResponse.data.id}/payments`,
{
type: "BALANCE"
},
{
headers: {
Authorization: `Bearer ${wiseToken}`,
'Content-Type': 'application/json'
}
}
);

console.log("0xHypr", "Transfer funded");
}

return res.status(200).json({
success: true,
transfer: transferResponse.data,
transferId: transferResponse.data.id
});
} catch (error) {
const axiosError = error as AxiosError;
console.error("0xHypr Error creating transfer:", axiosError.response?.data || axiosError);
return res.status(axiosError.response?.status || 500).json(axiosError.response?.data || { message: axiosError.message });
}
}
Loading