AI Agent Permissions

Let AI agents trade stocks and make payments — with human approval

🚀 Trading DocsMerchant Setup

🚀 AI Trading

Give your AI agent permission to trade stocks on your behalf. Set daily limits, review orders before execution, and maintain full control.

How It Works

1

User Connects Brokerage

User links their Alpaca account to Portald

2

Set Trust Level

Choose daily limit: $100, $500, or unlimited auto-approve

3

Agent Proposes Trades

AI submits orders via API — within limit auto-executes, else queued

4

User Approves

Review and approve trades over the daily limit in dashboard

Submit a Trade Order

POST https://portald.ai/api/agent-actions/ingest
Authorization: Bearer {session_token}
Content-Type: application/json

{
  "action_type": "trading.alpaca.order",
  "action_payload": {
    "symbol": "PLTR",
    "qty": "10",
    "side": "buy",
    "type": "market",
    "time_in_force": "day"
  },
  "idempotency_key": "unique-order-id-123"
}

Response

// Auto-approved (within daily limit):
{
  "action_id": "abc123",
  "status": "approved",
  "approved": true,
  "execution_id": "alpaca-order-xyz"
}

// Requires approval (over limit):
{
  "action_id": "abc123",
  "status": "pending",
  "approved": false,
  "poll_after_ms": 5000
}

Get Portfolio

GET https://portald.ai/api/agent/portfolio
Authorization: Bearer {session_token}

// Response:
{
  "account": {
    "portfolioValue": "100000.00",
    "buyingPower": "50000.00",
    "cash": "25000.00"
  },
  "positions": [
    {
      "symbol": "PLTR",
      "quantity": 100,
      "avgEntryPrice": 25.50,
      "currentPrice": 28.00,
      "unrealizedPnL": 250.00,
      "unrealizedPnLPercent": 9.8
    }
  ],
  "accountType": "paper"
}

Trust Tiers

Tier 1: Read-only

View portfolio only

Tier 2: Limited

$100/day auto-approve

Tier 3: Standard

$500/day auto-approve

Tier 4: Full Trust

Unlimited auto-approve

💰 Pricing: Paper trading is free. Live trading is $9.99/month.
Full Trading API Reference →

What Happens

1

Browser Opens

Log in or create a Portald account (just email + password)

2

Tunnel Starts

Cloudflared creates a public URL for your localhost

3

Files Generated

Manifest, webhook handler, and action endpoints

4

Ready to Test

Start your dev server and test with AI agents

Generated Files

your-project/
├── .env.local (credentials)
└── src/app/
├── .well-known/portald.json/route.ts (manifest)
└── api/portald/
├── webhook/route.ts (payment events)
└── actions/checkout/route.ts (quote endpoint)

Example Output

$ npx portald setup

Portald Setup

✔ Detected nextjs-app (src/)
✔ Found cloudflared in PATH
✔ Authenticated
✔ Tunnel ready: https://random-words.trycloudflare.com
✔ Site registered
✔ Wrote .env.local
✔ Generated routes

Setup complete! (Development mode)

Tunnel URL: https://random-words.trycloudflare.com
Site ID:    site_abc123

Your integration is ready for testing.
When ready to accept payments, visit:
  https://www.portald.ai/merchant/dashboard

Tunnel is active. Press Ctrl+C to stop.

How It Works

The Simple Version

1AI agent wants to buy something from your store
2Agent sends purchase request to Portald
3User gets notification: "Approve $24.99 purchase?"
4User taps Approve
5Portald charges their card, sends you a webhook
6You ship the order! 📦

Shopify / No-Code Flow

Customer: "Hey Claude, order me coffee from CoffeeShop.com"

    AI Agent                    Portald                     Your Store
        │                          │                            │
        │  1. Submit purchase      │                            │
        │  ───────────────────────>│                            │
        │                          │                            │
        │                    2. User gets push notification     │
        │                       "Approve $24.99?"               │
        │                          │                            │
        │                    3. User taps "Approve"             │
        │                          │                            │
        │                    4. Portald charges card            │
        │                          │                            │
        │                          │  5. Webhook to Zapier      │
        │                          │  ─────────────────────────>│
        │                          │                            │
        │                          │                     6. Zapier creates
        │                          │                        Shopify order
        │                          │                            │
        │<─────────────────────────│<───────────────────────────│
        │        "Order placed!"                                │

Developer Flow (Custom Sites)

If you're building a custom integration with code:

    AI Agent                    Portald                     Your Server
        │                          │                            │
        │  1. GET /products.json   │                            │
        │  ─────────────────────────────────────────────────────>│
        │  <─────────────────────── product list ───────────────│
        │                          │                            │
        │  2. POST /ingest         │                            │
        │  { product, amount }     │                            │
        │  ───────────────────────>│                            │
        │                          │                            │
        │                    3. User approves                   │
        │                          │                            │
        │                    4. Charge card                     │
        │                          │                            │
        │                          │  5. POST /webhook          │
        │                          │  { status: "executed" }    │
        │                          │  ─────────────────────────>│
        │                          │                            │
        │                          │                     6. Fulfill order
        │<─────────────────────────│<───────────────────────────│

Wrap Any Function with Portald

Portald is a protocol, not a rigid API. You can wrap any action on your site - purchases, bookings, subscriptions, API calls, file uploads, whatever you want agents to request with human approval.

The Manifest

The manifest at /.well-known/portald.json tells agents what actions your site supports. Define any action types you want, pointing to any endpoints on your site:

// Example: E-commerce site
{
  "actions": [
    { "type": "checkout.create", "endpoint": "/api/checkout", "risk_level": "medium" },
    { "type": "subscription.start", "endpoint": "/api/subscribe", "risk_level": "high" }
  ]
}

// Example: SaaS API
{
  "actions": [
    { "type": "api.call", "endpoint": "/v1/generate", "risk_level": "low" },
    { "type": "credits.purchase", "endpoint": "/v1/billing/add-credits", "risk_level": "medium" }
  ]
}

// Example: Booking platform  
{
  "actions": [
    { "type": "booking.create", "endpoint": "/bookings/new", "risk_level": "medium" },
    { "type": "booking.cancel", "endpoint": "/bookings/cancel", "risk_level": "low" }
  ]
}

Wrapping Your Existing Code

You don't need special Portald endpoints. Wrap your existing functions - the endpoint just needs to return something the agent can submit to Portald for approval:

// Before: Your existing checkout function
async function createOrder(items, address) {
  const order = await db.orders.create({ items, address });
  const total = calculateTotal(items, address);
  return { orderId: order.id, total };
}

// After: Same function, exposed as a Portald action endpoint
export async function POST(req: Request) {
  const { items, shipping_address } = await req.json();
  
  // Call your existing code!
  const result = await createOrder(items, shipping_address);
  
  // Return in a format agents can submit to Portald
  return Response.json({
    order_id: result.orderId,
    total_cents: result.total
  });
}

Webhook Handler

When Portald executes an approved action, it sends a webhook to your site. Verify the signature, then fulfill whatever the action requires:

export async function POST(req: Request) {
  const body = await req.text();
  const signature = req.headers.get("x-portald-signature");

  // ALWAYS verify signature
  if (!verifySignature(body, signature)) {
    return Response.json({ error: "Invalid signature" }, { status: 401 });
  }

  const event = JSON.parse(body);

  if (event.type === "action.executed") {
    const { action_type, action_payload, amount_cents } = event;
    
    // Handle different action types
    switch (action_type) {
      case "checkout.create":
        await fulfillOrder(action_payload.order_id);
        break;
      case "subscription.start":
        await activateSubscription(action_payload.plan_id);
        break;
      case "booking.create":
        await confirmBooking(action_payload.booking_id);
        break;
    }
  }

  return Response.json({ received: true });
}

Webhook Security

Always verify webhook signatures. Without verification, attackers can send fake webhooks and get free products.

Portald signs every webhook with HMAC-SHA256:

Header: X-Portald-Signature: t=1707700000,v1=abc123...

Signature = HMAC-SHA256(
  key: PORTALD_WEBHOOK_SECRET,
  message: timestamp + "." + body
)

Verification Code (included in generated handler)

function verifySignature(payload: string, signature: string): boolean {
  const parts = signature.split(",");
  const timestamp = parts.find(p => p.startsWith("t="))?.slice(2);
  const sig = parts.find(p => p.startsWith("v1="))?.slice(3);

  if (!timestamp || !sig) return false;

  // Reject if older than 5 minutes
  if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
    return false;
  }

  const expected = crypto
    .createHmac("sha256", process.env.PORTALD_WEBHOOK_SECRET!)
    .update(timestamp + "." + payload)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(sig),
    Buffer.from(expected)
  );
}

Risk Levels

Risk levels determine whether payments auto-approve or require human confirmation:

LevelBehaviorUse Case
lowCan auto-approve (within user limits)Small purchases under $15
mediumAlways requires approvalMost purchases
highAlways requires approvalLarge purchases, subscriptions

Set the risk level in your manifest for each action type.

Going to Production

1. Connect Stripe

Visit your merchant dashboard and click "Connect Stripe" to receive real payments.

2. Verify Your Domain

Production sites require DNS verification to prevent impersonation. Add this TXT record:

Host:  _portald-verify
Value: portald-site-verification=<your-token>

3. Deploy

Deploy your site normally. The generated routes work in production without changes.

Development vs Production: In dev mode (tunnel URLs), no real money moves. Connect Stripe and verify your domain to accept real payments.

Supported Frameworks

FrameworkStatus
Next.js App RouterSupported
Next.js Pages RouterComing soon
Express.jsSupported
FastifyComing soon
Other Node.jsManual setup (see below)

Manual Setup (Any Framework)

If your framework isn't auto-detected, you need three HTTP endpoints:

  1. GET /.well-known/portald.json - Return your manifest
  2. POST /api/portald/webhook - Receive payment events
  3. POST /api/portald/actions/checkout - Return quotes

Run npx portald setup anyway to get credentials, then implement the endpoints manually.

For Agent Developers

Discovery API (Start Here)

Before attempting a purchase, check if a website supports Portald using our public discovery endpoint:

// Check if a website is Portald-enabled
const discovery = await fetch(
  "https://portald.ai/api/discover?domain=example-coffee.com"
).then(r => r.json());

// Response:
{
  "enabled": true,
  "siteId": "site_abc123",
  "merchantName": "Example Coffee",
  "paymentProvider": "SQUARE",      // or "STRIPE"
  "verified": true,
  "actions": [
    {
      "type": "payment.charge",
      "description": "Process a payment for items from the menu",
      "requires_approval": true
    }
  ],
  "locations": [
    { "id": "L8T5TS9VBZNJM", "name": "Example Coffee" }
  ],
  "menu": {                          // Square merchants only
    "items": [
      {
        "id": "ITEM_ABC",
        "name": "House Latte",
        "description": "Our signature espresso drink",
        "category": "Beverages",
        "price": "$4.50",
        "priceCents": 450,
        "variationId": "VAR_123"     // Use this when ordering
      },
      {
        "id": "ITEM_DEF", 
        "name": "Dark Roast",
        "category": "Single Origin Coffee",
        "price": "$18.00",
        "priceCents": 1800,
        "variationId": "VAR_456"
      }
    ],
    "categories": ["Beverages", "Single Origin Coffee"],
    "itemCount": 42
  },
  "enrollment_url": "https://portald.ai/enroll/merchant_xyz"
}

💡 Always check the Discovery API first. It tells you everything: whether the merchant is enabled, what payment processor they use, their full menu with prices, and where to send users for card enrollment.

Complete Agent Flow (Recommended)

This is the pattern for ordering from Square merchants. Discover first, then order from the real menu:

// Step 1: Discover merchant and get menu
const discovery = await fetch('https://www.portald.ai/api/discover?domain=merchant.com');
const { menu, enabled } = await discovery.json();

if (!enabled) throw new Error('Merchant not on Portald');

// Step 2: Find item to order
const latte = menu.items.find(i => i.name === 'French Latte');

// Step 3: Create order with real item data
const order = await fetch('https://www.portald.ai/api/agent-actions/ingest', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${session_token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    action_type: 'payment.charge',
    action_payload: {
      amount_cents: latte.priceCents,
      description: latte.name,
      line_items: [{
        name: latte.name,
        quantity: 1,
        unit_price_cents: latte.priceCents,
        variation_id: latte.variationId,  // Required for Square orders
      }],
    },
    website_origin: 'https://merchant.com',
    idempotency_key: `order-${Date.now()}`,
  }),
});

const result = await order.json();
// result.status: "pending" | "approved" | "executed"
// result.action_id: track this for status updates

Why this pattern?
• The Discovery API returns the merchant's real Square catalog with variationId
• Using variationId ensures orders appear correctly on the merchant's POS
• Always use www.portald.ai (redirect from portald.ai strips auth headers)

Discovery API Parameters

ParameterDescription
domainDomain name (e.g., shop.com)
originFull origin URL (e.g., https://shop.com)
include_menuInclude catalog items (default: true)
menu_limitMax menu items to return (default: 50, max: 100)

Shopify Stores (Most Common)

Many Portald merchants use Shopify. Here's how to buy from them:

// 1. Get products from Shopify's built-in JSON feed
const products = await fetch("https://store.myshopify.com/products.json")
  .then(r => r.json());

// products.products[] contains:
// - id: product ID
// - title: "Dark Roast Coffee"
// - variants[]: array of purchasable variants
//   - id: variant ID (THIS IS WHAT YOU NEED)
//   - title: "1lb Bag"
//   - price: "19.99"
//   - available: true

// 2. Find the product you want
const product = products.products.find(p => 
  p.title.toLowerCase().includes("dark roast")
);
const variant = product.variants[0]; // or let user pick

// 3. Submit purchase to Portald
const result = await fetch("https://www.portald.ai/api/agent-actions/ingest", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": `Bearer ${agent_session_token}`
  },
  body: JSON.stringify({
    action_type: "purchase",
    action_payload: {
      product_id: product.id,
      variant_id: variant.id,
      product_title: product.title,
      variant_title: variant.title,
      quantity: 1
    },
    amount_cents: Math.round(parseFloat(variant.price) * 100),
    website_origin: "https://store.myshopify.com",
    callback_url: "https://store.myshopify.com/api/portald/webhook",
    idempotency_key: `purchase-${variant.id}-${Date.now()}`
  })
}).then(r => r.json());

// result.status: "pending" | "approved" | "executed"

💡 Tip: Every Shopify store has /products.json publicly available. Use this to discover products, prices, and variant IDs before submitting purchases.

Custom Sites (With API)

For sites with custom Portald integration:

// 1. Discover merchant capabilities
const manifest = await fetch("https://shop.com/.well-known/portald.json")
  .then(r => r.json());

// 2. Get a quote from their endpoint
const quote = await fetch("https://shop.com/api/portald/actions/checkout", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    items: [{ id: "product-123", quantity: 1 }],
    shipping_address: { city: "Austin", state: "TX", postal_code: "78701" }
  })
}).then(r => r.json());

// 3. Submit to Portald
const result = await fetch("https://www.portald.ai/api/agent-actions/ingest", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": `Bearer ${agent_session_token}`
  },
  body: JSON.stringify({
    action_type: "checkout.create",
    action_payload: {
      order_id: quote.order_id,
      amount_cents: quote.total_cents
    },
    website_origin: "https://shop.com",
    idempotency_key: `order-${quote.order_id}`
  })
}).then(r => r.json());

How to Identify Portald-Enabled Stores

Use the Discovery API (recommended):

// Best method: Use the Discovery API
const { enabled, menu } = await fetch(
  "https://portald.ai/api/discover?domain=shop.com"
).then(r => r.json());

if (enabled) {
  // Store accepts Portald payments!
  // menu.items contains products/prices if available
}

// Fallback: Check for manifest (custom integrations)
const hasManifest = await fetch("https://shop.com/.well-known/portald.json")
  .then(r => r.ok)
  .catch(() => false);

// Fallback: Check for meta tags (Shopify/hosted platforms)
const html = await fetch("https://shop.com").then(r => r.text());
const hasMetaTag = html.includes('portald:site-id') || 
                   html.includes('name="portald"');

Getting Enrolled

Agents need user approval before making purchases. See the agent enrollment guide.

API Reference

Public Endpoints (No Auth Required)

EndpointMethodDescription
/api/discover?domain=shop.comGETCheck if site is Portald-enabled, get menu/catalog

Agent Endpoints (Auth Required)

EndpointMethodDescription
/api/portald/v1/identity/handshakePOSTAgent authentication
/api/agent-actions/ingestPOSTSubmit payment request
/api/agent-actions/[id]GETCheck action status
/api/agent-actions/[id]/payment-infoGETGet payment details for execution