Saleor Store
우리 컬렉션을 쇼핑하세요
Mandat는 예산을 제시하면 AI가 찾아 협상·예약·PayPal 결제까지 처리하고, 한 번 서명하면 됩니다
Mandat — an AI agent you give a budget to: it finds, negotiates, books and pays with PayPal, inside a mandate you sign once. PayPal AI Hackathon 2026.
커머스JavaScript · CSSGitHub @blanco1er온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Tell it what you want and how much you can spend. Mandat searches real places, negotiates with merchants' agents, books, and holds deposits with PayPal, never a cent over your mandate.
Built for the PayPal AI Hackathon (Devpost, 2026). Everything runs on the PayPal sandbox, so no real money moves.
• Hosted demo: https://mandat-qb77.onrender.com (PayPal sandbox; see Deploy on Render to run your own)
• Stack: Node.js 20+, Express, plain HTML/CSS/JS (installable PWA), PayPal Server SDK + PayPal Agent Toolkit, DeepSeek, AG Studio, Bryntum Calendar, Channel3
For judges: try it in five minutesREADME 전문 보기 ↗Tell it what you want and how much you can spend. Mandat searches real places, negotiates with merchants' agents, books, and holds deposits with PayPal, never a cent over your mandate.
Built for the PayPal AI Hackathon (Devpost, 2026). Everything runs on the PayPal sandbox, so no real money moves.
Open https://mandat-qb77.onrender.com on a phone (it installs as an app) or a desktop browser. No sign-up: your browser gets its own private space.
Everything runs on the PayPal sandbox: no real money moves. The first visit after a quiet period can take up to a minute while the free host wakes up.
Handing an errand to an AI agent ("book dinner for six on Saturday, get a cake and flowers, 300 € max") is easy to say and hard to trust. Today an agent either cannot pay at all, so you finish every booking yourself, or it gets a card with no real limit and no record of what it did.
Mandat is for people who want the errand done but keep control of the money: a signed PayPal mandate, a budget envelope per mission, deposits that are held rather than charged, approvals at the level of autonomy you choose, and a ledger of every movement.
| Feature | How it works |
|---|---|
| Sign a mandate once | One trip to PayPal saves your PayPal account with Vault v3 (setup token, then payment token). That trip also connects your account (name and email come back with the token). You set a monthly limit and an autonomy level. |
| One budget envelope per mission | Each mission has its own envelope: total / held / paid / remaining. Every payment is checked against it first (lib/envelope.mjs), and the monthly limit is checked when a mission is created. |
| Ask by text, voice, or photo | Type, speak, or attach a photo (a receipt, a broken scooter, a character a child loves). Voice is recorded in the page and transcribed by Google Speech-to-Text; replies are read with a neural Google voice (Chirp 3 HD), only when you spoke. The photo is read once by the model and kept as a text description. |
| Real places, any city | Real places from Google Places (rating, reviews, photos the agent looks at before choosing) and OpenStreetMap (addresses, transit lines), on an interactive map (MapLibre + OpenFreeMap). The merchant network opens in any city the first time it is asked for. |
| Negotiation with merchant agents | Bookings go through a demo merchant network. Merchants that run an AI agent quote and negotiate (availability, discounts, counter-offers) inside their own private rules, and the exchange is shown live. |
| Know Your Agent | Before negotiating or paying, the buyer agent checks the merchant agent's identity card, signed by the network with HMAC-SHA256 (verify_merchant). |
| Booking requests for merchants without AI | For small merchants with no agent, Mandat sends a structured booking request (items from their catalogue, slot, deposit) to a private merchant inbox (/merchant/:id?k=…, an HMAC-signed link; in production it would arrive by SMS or email). The merchant taps Accept, Other time (counter-proposal) or Decline — no app, no AI on their side. After they accept, the shopper's agent holds the deposit with PayPal; the merchant then taps Confirm & collect and the deposit is captured. |
| Holds, not charges | Deposits are PayPal authorizations (Orders v2, intent: AUTHORIZE) paid with the vaulted token, so the money is reserved, not spent. If the plan changes, the agent the authorization. |
No sign-up: each browser gets its own private space through an HTTP-only cookie.
| PayPal API / product | Where in the code | Why |
|---|---|---|
PayPal Server SDK (@paypal/paypal-server-sdk 2.5.0, generated by APIMatic) | lib/paypal.mjs | Every call below goes through the official SDK: one Client (OAuth 2.0 client credentials handled by the SDK), and the VaultController, OrdersController and PaymentsController with typed request models. Integrated with the APIMatic Context Plugin for PayPal, whose skills set the rules we follow: pinned version, a per-attempt timeout, typed CustomError mapping, and retries that include POST only because every write is idempotent. |
Vault v3: setup tokens (VaultController.createSetupToken) | lib/paypal.mjs → createMandateSetup(), called by POST /api/me/mandate | Starts the mandate: the user approves once on PayPal (usageType: MERCHANT, customerType: CONSUMER). |
Vault v3: payment tokens (VaultController.createPaymentToken) | lib/paypal.mjs → activateMandate(), called by GET /mandate/return | Turns the approved setup token into a reusable payment token. That token is the mandate: the agent can pay without the user being present. |
Orders v2 (OrdersController.createOrder, intent: AUTHORIZE, paymentSource.paypal.vaultId) | lib/paypal.mjs → hold(), used by the agent tool hold_deposit through doHold() in lib/agent.mjs | Holds a deposit for an agreed offer. The money is reserved, not captured. |
Payments v2: capture / void / refund (PaymentsController.captureAuthorizedPayment, voidPayment, refundCapturedPayment) | lib/paypal.mjs → capture(), release(), refund(); captureEntry() and the tools cancel_hold / in ; in |
Sandbox app features to enable (developer.paypal.com → your sandbox app): Save payment methods (Vault) and Invoicing.
Offline mode: without PayPal keys the app still runs. Every PayPal call returns the same shape with fake ids and mode: "offline", and the UI labels it "Demo mode" so it never pretends money moved.
The agent: DeepSeek with tool calling (lib/agent.mjs, lib/deepseek.mjs). DeepSeek is OpenAI-compatible, and the agent loop runs up to 24 tool steps per user turn. It has 30 tools:
find_real_places, find_top_places, find_network_merchants, inspect_place, find_products, estimate_trip, check_schedule;verify_merchant, negotiate, ask_quote, request_booking, move_booking;hold_deposit, cancel_hold, refund_payment, buy_product, split_bill, set_budget, calculate;show_place_preview, show_booking, notify_user, set_reminder;remember, forget, save_sizes, suggest_get_to_know;update_plan, wrap_up, close_mission.Capture is not a tool: the code captures a held deposit once the merchant confirms. Every step is emitted as an event and streamed to the browser with Server-Sent Events, so you watch the agent work.
Guardrails live in code, not in the prompt. Even if the model asks, hold_deposit checks the envelope and the approval threshold, request_booking rejects items a merchant does not sell and requests the mandate cannot pay, and doHold refuses when a stop switch is on.
Vision. A photo is sent once to the model as image input. A precise text description (names, prices, dates, addresses) is kept in the conversation instead of the image. A name printed on the item is read five times in parallel and the majority spelling is kept (readPrinted), so the agent never names a brand it did not read.
Merchant agents (lib/merchants.mjs). Each demo merchant with hasAgent: true is a DeepSeek agent with its own catalogue and private floor rules. It replies in JSON with a structured offer (items, discount, total, deposit, slot, conditions).
| Sponsor | What Mandat does with it | Where |
|---|---|---|
| AG Grid: AG Studio | The Activity tab is an AG Studio dashboard over two tables joined by a relationship (payments → missions), opening on a ready-made report in Mandat's theme (light and dark, French locale, a phone layout). Its chat is the Studio Agent Framework: the five built-in agents (lead, planning, page, widget, data) run on Mandat's model through our own AgLlmAdapter, which posts each turn to /api/studio/llm so the key stays on the server. The lead agent gets a Mandat brief and a custom tool, mission_books, that returns the books exactly as the app computes them, so it never adds amounts up itself. Ask "spending by week" and it delegates to the page agent, then the widget agent, and a new chart appears. | public/insights.js, lib/studio.mjs |
| Bryntum: Calendar | The Agenda tab shows every dated step of every mission, one colour per mission, steps still waiting dashed. The agent drives the schedule: dragging an event sends the move to Mandat, which asks the same merchant with move_booking. Merchants with an agent answer at once, the others confirm from their inbox; the event only lands at the new time once they agree, and nothing is paid twice. | public/agenda.js, lib/calendar.mjs (agendaOf), move_booking in lib/agent.mjs |
| Channel3 | find_products searches real products (European retailers priced in euros, or the US market in English), the agent looks at the photos of the five best candidates against what the person needs and drops what does not fit, then buy_product pays with PayPal inside the mission budget. | lib/shop.mjs, find_products / buy_product in lib/agent.mjs |
| APIMatic: Context Plugin for PayPal | Installed in the coding agent (npx context-plugins install https://github.com/paypaldev/server-sdk-context-plugin-preview) and used to move every PayPal call onto the APIMatic-generated PayPal Server SDK, following its skills: pinned version, a timeout per attempt, typed CustomError mapping, and POST retries that are safe because every write carries a PayPal-Request-Id. | lib/paypal.mjs |
| Render |
Trials. AG Studio and Bryntum run on their free trial builds. Bryntum counts its 45 days per browser from the first visit; AG Studio without a key only shows a watermark. Set AG_STUDIO_LICENSE_KEY to remove it.
flowchart LR
subgraph Browser["Browser / installable PWA"]
UI["index.html + app.js<br/>chat, envelope, approvals"]
MAP["MapLibre + OpenFreeMap"]
GRID["AG Grid ledger"]
VOICE["Web Speech API"]
SW["sw.js (Web Push)"]
end
subgraph Server["Node.js / Express (server.mjs)"]
API["REST API + SSE event stream"]
AGENT["lib/agent.mjs<br/>tool-calling loop"]
ENV["lib/envelope.mjs<br/>budget envelope"]
SPLIT["lib/split.mjs"]
MER["lib/merchants.mjs<br/>demo network + HMAC identity"]
PLACES["lib/places.mjs"]
PP["lib/paypal.mjs"]
INV["lib/invoices.mjs<br/>PayPal Agent Toolkit"]
PUSH["lib/push.mjs"]
DS["lib/deepseek.mjs<br/>streaming, fallback, probe"]
DATA[("data/*.json<br/>users, missions, OSM cache")]
end
UI <--> API
API --> AGENT
AGENT --> ENV
AGENT --> SPLIT
AGENT --> MER
AGENT --> PLACES
AGENT --> PP
AGENT --> INV
AGENT --> DS
MER --> DS
API --> PUSH
API --> DATA
DS --> DEEPSEEK["DeepSeek API"]
PP --> PAYPAL["PayPal REST (sandbox)<br/>Vault v3, Orders v2, Payments v2"]
INV --> PAYPALINV["PayPal Invoicing v2"]
PLACES --> OSM["OpenStreetMap<br/>Nominatim + Overpass"]
PUSH --> PUSHSVC["Browser push services"] --> SW
Requirements: Node.js 20 or newer, a DeepSeek API key, and (optionally) a PayPal sandbox app.
git clone https://github.com/blanco1er/mandat.git
cd mandat
npm install
cp .env.example .env # then fill in the values (see below)
set -a; source .env; set +a
npm start # or: node server.mjs
Open http://localhost:8790.
| Variable | Required | Purpose |
|---|---|---|
DEEPSEEK_API_KEY | yes | The agent, the merchant agents, photo reading, the ledger filter. Without it the server starts but the agent cannot answer. |
PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET | recommended | PayPal sandbox app keys. Without them the app runs in clearly labelled offline demo mode. |
PUBLIC_URL | when hosted | Public https base URL, used for PayPal return URLs and invoice links. Defaults to http://localhost:<PORT>. |
PORT | no | Defaults to 8790. |
DEEPSEEK_FAST_MODEL, DEEPSEEK_SMART_MODEL | no | Override the default and fallback models. |
VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT | no | Web Push keys. If absent, a key pair is generated once and saved to a private (mode 600) file. |
MANDAT_DATA | no | Data directory, defaults to ./data (gitignored). |
MANDAT_TURNS_PER_USER_DAY, MANDAT_MISSIONS_PER_USER_DAY, MANDAT_TURNS_PER_DAY, MANDAT_WRITES_PER_MINUTE | no | Public-demo guards (defaults 80, 15, 3000, 40): daily AI allowance per person and for the whole app, plus a per-IP burst limit on writes. They keep the AI bill bounded while judges test the hosted demo. |
MANDAT_NETWORK_SECRET | no | Signing key of the demo merchant network. Random at each start if absent. |
GOOGLE_PLACES_KEY | no | Real place photos and ratings (Google Places), speech-to-text and the neural voice. Without it, places come from OpenStreetMap only and the phone's own voice is used. |
CHANNEL3_API_KEY | no | Real product search and shopping. Without it, Mandat books services but does not shop. |
AG_STUDIO_LICENSE_KEY | no | AG Studio licence. Without it the Activity dashboard works with a trial watermark. |
| , , , |
Local alternative to environment variables: PAYPAL_KEY_FILE and DEEPSEEK_KEY_FILE can point to JSON files ({"client_id","client_secret"} and {"api_key"}). The server refuses to read them unless they are chmod 600.
Signing the mandate in the sandbox. "Continue with PayPal" (or "Sign mandate with PayPal" in Settings) opens the PayPal sandbox. Log in with a sandbox personal (buyer) account, for example one you create under Testing Tools → Sandbox Accounts on developer.paypal.com. Never use a real PayPal login.
Try it without the UI. node scripts/scenario.mjs runs a full "birthday evening" mission in the terminal (search, negotiation, booking requests, approvals, holds). It approves payments and accepts merchant requests automatically. It needs DEEPSEEK_API_KEY and runs with PayPal in offline mode.
The repo includes a Render Blueprint: render.yaml.
PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, DEEPSEEK_API_KEY, VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, and PUBLIC_URL (the service's https://….onrender.com URL). To generate a VAPID pair, run npx web-push generate-vapid-keys.Notes on the free plan:
data/ is mirrored to R2 and restored at start (start.mjs, lib/store.mjs), so users and missions survive a redeploy. Without them, data is reset on every restart, which is fine for a quick demo.start.mjs; set MANDAT_KEEPALIVE=off to stop it), and .github/workflows/keepalive.yml adds an outside ping when the repository variable MANDAT_URL is set.| Tool | Used for |
|---|---|
PayPal Server SDK (@paypal/paypal-server-sdk, sandbox): Vault v3, Orders v2, Payments v2 | Mandate, deposit holds, captures on confirmation, voids, refunds |
| APIMatic Context Plugin for PayPal | Coding-agent context used to integrate the PayPal Server SDK |
PayPal Agent Toolkit (@paypal/agent-toolkit) | Invoicing v2 tools for split bills: create, send, QR code, status |
| DeepSeek API (OpenAI-compatible) | Tool-calling agent, merchant agents, plan review, photo reading, the AG Studio agents, titles |
| OpenStreetMap: Nominatim + Overpass | Geocoding and real nearby places, cached on disk, with a custom User-Agent, per the usage policies |
| MapLibre GL JS + OpenFreeMap | Interactive map inside the conversation, no API key |
AG Studio (ag-studio 3.0, Studio Agent Framework) | Spending dashboard and its assistant (Activity tab) |
Bryntum Calendar (@bryntum/calendar-trial 7.3) | Agenda tab, moves handled by the agent |
| Channel3 API | Product search, offers and photos for purchases |
| Google Places API (New) | Real places, ratings, reviews and photos |
| Google Cloud Text-to-Speech (Chirp 3 HD) and Speech-to-Text | The voice of Mandat, and what you say |
| AG Grid Community | Fallback ledger when the dashboard cannot load |
Web Push (web-push, VAPID) + Service Worker | Notifications and the app badge |
| Web Speech API | Fallback for voice when the cloud voice is not available |
| Express | HTTP server, static files, Server-Sent Events |
| Render | Hosting (free web service) |
| GitHub Actions | Keep-alive ping for the hosted demo |
Env.check(): if the amount is more than what is left in the mission's envelope, it is refused in code, whatever the model asks. Booking requests the envelope cannot pay are never sent, and a new mission cannot push you past your monthly limit.chmod 600 files outside the project, and they are never logged. .env, *.key and data/ are gitignored.This is a hackathon demo. All payments use the PayPal sandbox. No real money moves. The merchants (Lumière, L'Ombre Rouge, Atelier Sucré, Pivoine, Hôtel des Arts) are fictional demo merchants. The places shown on the map are real OpenStreetMap data, but bookings only go to the demo network.
MIT © 2026 Blanco
| Approvals by autonomy level | Careful asks before every payment, Balanced pays small deposits alone and asks above your limit, Autopilot acts on its own inside the mission budget and reports each step. A daily limit across all missions still applies: above it, the agent must ask. Pending payments appear as an Approve / Decline sheet. |
| Split the bill | Exact to the cent (lib/split.mjs): equal or unequal shares, rounding cents assigned to whoever paid. Each friend gets a detailed PayPal invoice (bill lines with their part, who paid, where, when, how it was split) with a pay link and a QR code. PayPal also emails it when an address is known. Paid invoices show up in the mission. |
| Notifications | Web Push (VAPID) only for moments that need you: approvals, a question from the agent, a merchant accepting or declining, a friend paying, a mission fully booked. Nothing is sent while you are looking at that mission. |
| Shopping with photos checked | Real products from 25,000+ retailers through Channel3: Mandat searches, looks at the photos of the best candidates against the need ("a gift for an 8-year-old who loves Stitch"), drops what does not fit, offers the two best with their real difference, and pays with PayPal. |
| Agenda driven by the agent | Every dated step of every mission in a Bryntum Calendar. Moving an event asks Mandat to move the booking with the merchant (move_booking): same booking, no new payment, and it only changes once the merchant agrees. |
| Spending dashboard with its own agent | The Activity tab is an AG Studio dashboard built from the books (paid, held, still to pay, budget left, where the money goes, every payment with its PayPal reference). Its assistant is AG Studio's agent framework running on Mandat's model, with a tool that reads the books as the app computes them. |
| Stop switch | One toggle freezes all payments for every mission, and each mission also has its own stop button. A mission that still holds money cannot be deleted. |
refund_paymentlib/agent.mjsPOST /api/merchants/:mid/requests/:rid/collectserver.mjs| Capture when the merchant confirms (instantly for merchants with an agent, from the inbox for the others); void when the plan changes before that; refund after capture when a booking is cancelled. |
Invoicing v2 via the official PayPal Agent Toolkit (@paypal/agent-toolkit): create_invoice, send_invoice, generate_invoice_qr_code, get_invoice | lib/invoices.mjs → shareInvoice(), invoiceStatus(); used by the agent tool split_bill and by a background check every 2 minutes in server.mjs | One real, itemised invoice per friend, with a pay link and a QR code. The toolkit's tools are called in the OpenAI tool-call shape (handleToolCall), exactly as an agent framework would. |
Idempotency (PayPal-Request-Id, the SDK's paypalRequestId) | lib/paypal.mjs | Every money-moving call carries a fresh idempotency key, so a retry can never charge twice. |
Natural-language ledger filter (POST /api/me/activity/ask in server.mjs). A request in plain words becomes an AG Grid filter model in JSON mode, limited to a whitelist of columns.
Mission titles. The fast model writes a 2–5 word title in the background, and a plain title is shown instantly in the meantime.
Streaming. Replies stream token by token, and a stalled stream (no progress for 20–45 s) counts as a hang.
Cost and reliability controls (lib/deepseek.mjs):
DEEPSEEK_FAST_MODEL serves every call. On a timeout, a 429 or a 5xx, the call moves once to DEEPSEEK_SMART_MODEL, and a half-streamed reply is replaced cleanly.| Hosting: a free web service, data kept in a private Cloudflare R2 bucket between restarts. |
render.yaml, start.mjs, lib/store.mjs |
R2_ACCOUNT_IDR2_ACCESS_KEY_IDR2_SECRET_ACCESS_KEYR2_BUCKET| no |
Keeps data/ in a Cloudflare R2 bucket across restarts. |
PAYPAL_ENV | no | sandbox (default) or production. |