Portald Agent Guide

Portald is an agent authorization and payment layer. This guide covers everything an AI agent needs to: enroll with a user, discover Portald-enabled merchants, and execute purchases on behalf of users.

Quick Reference

  • Trading API: POST https://portald.ai/api/agent-actions/ingest
  • Portfolio API: GET https://portald.ai/api/agent/portfolio
  • Discovery API: GET https://portald.ai/api/discover?domain=example.com
  • Check Status: GET https://portald.ai/api/agent-actions/[id]

🚀 AI Trading Permissions

Let your AI agent trade stocks with human-in-the-loop approval. Set daily limits, review orders before execution, and maintain full control.

Quick Start →View Pricing

Trading Setup

Portald provides a permissions layer for AI trading agents. Your agent proposes trades, users set daily limits, and orders execute via their connected brokerage (Alpaca).

⚠️ Required Agent Behavior

As an AI agent using Portald, you must follow these guidelines:

  • Never provide investment advice. You execute trades the user defines. You do not recommend what to buy, sell, or when.
  • User defines the strategy. Only trade according to rules explicitly given to you by the user. Do not invent or suggest strategies.
  • Be transparent. Always explain what you are doing and why before submitting any trade. Show the user your reasoning.
  • Respect limits. Honor the user's trust tier and daily limits. Never attempt to circumvent approval requirements.
  • Acknowledge risk. Trading involves risk of loss. Remind users of this and recommend paper trading for testing.
  • No guarantees. Never promise returns, profits, or outcomes. Past performance does not predict future results.

Portald is a tool that executes user-defined trades. It does not provide investment advice, recommendations, or strategies. Agents must make this clear to users.

⚠️ Prerequisite: Before trading, your agent must be enrolled with a user and have a session_token. See Enrollment below.

How It Works

  1. User: Connects their Alpaca brokerage account
  2. User: Sets trust tier for your agent ($100/day, $500/day, or unlimited)
  3. Agent: Submits trade orders via API
  4. System: Auto-approves if within daily limit, or queues for approval
  5. User: Reviews and approves/rejects orders in dashboard
  6. System: Executes approved trades on user's brokerage

Trust Tiers

Tier 1: Read-only

View portfolio only. No trading.

Tier 2: Limited

Auto-approve up to $100/day

Tier 3: Standard

Auto-approve up to $500/day

Tier 4: Full Trust

Auto-approve all trades

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 limit):
{
  "action_id": "abc123",
  "status": "approved",
  "approved": true,
  "execution_id": "alpaca-order-xyz"
}

// Response (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"
}

Supported Order Types

TypeDescriptionRequired Fields
marketExecute at current pricesymbol, qty, side
limitExecute at specified price or bettersymbol, qty, side, limit_price

Complete Trading Agent Example

Full flow: get session → check portfolio → place trade → poll status

// 1. Exchange action_code for session_token (one-time setup)
const handshake = await fetch('https://portald.ai/api/portald/v1/identity/handshake', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    action_code: 'ACTION_CODE_FROM_USER',
    agent_pk: 'your-ed25519-public-key'
  })
});
const { session_token } = await handshake.json();
// Save session_token - use it for all subsequent requests

// 2. Check user's portfolio
const portfolio = await fetch('https://portald.ai/api/agent/portfolio', {
  headers: { 'Authorization': `Bearer ${session_token}` }
}).then(r => r.json());

console.log('Buying power:', portfolio.account.buyingPower);
console.log('Current positions:', portfolio.positions);

// 3. Place a trade
const trade = await fetch('https://portald.ai/api/agent-actions/ingest', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${session_token}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    action_type: 'trading.alpaca.order',
    action_payload: {
      symbol: 'AAPL',
      qty: '5',
      side: 'buy',
      type: 'market',
      time_in_force: 'day'
    },
    idempotency_key: `trade-${Date.now()}`
  })
}).then(r => r.json());

if (trade.approved) {
  console.log('Trade executed! Order ID:', trade.execution_id);
} else {
  console.log('Trade pending approval. Action ID:', trade.action_id);
  // Poll for approval status
  const status = await fetch(
    `https://portald.ai/api/agent-actions/${trade.action_id}`,
    { headers: { 'Authorization': `Bearer ${session_token}` }}
  ).then(r => r.json());
}
💰 Pricing: Paper trading is free. Live trading requires a $9.99/month subscription.

1. Enrollment (Getting Authorized)

Before you can make purchases, you need authorization from a human user. Enrollment always starts with the human.

The Enrollment Flow

  1. Human: Generates a pairing code at portald.ai/agents
  2. Human: Gives the code to you (the agent)
  3. Agent: Register with the pairing code (see below)
  4. Human: Approves you in their dashboard
  5. Human: Gives you the action code
  6. Agent: Use the action code for all authenticated requests

Registration API

Generate an Ed25519 keypair, then register:

// 1. Register with pairing code
POST /api/agents/register
{
  "pair_code": "PAIRING_CODE_FROM_USER",
  "agent_pk": "base64url-ed25519-public-key",
  "agent_metadata": { "name": "My Agent" }
}

// Response includes a challenge to sign
{
  "challenge_id": "...",
  "challenge_nonce": "...",
  "domain": "portald.ai",
  "server_timestamp": 1234567890
}

// 2. Sign the challenge and prove ownership
POST /api/agents/prove
{
  "challenge_id": "...",
  "agent_pk": "same-key",
  "signature": "base64url-signature"
}

// Challenge message format:
// {challenge_nonce}|{domain}|{server_timestamp}|{pair_code_id}

After Approval

Once approved, the user gives you an action code. Use it to get a session token:

POST /api/portald/v1/identity/handshake
{
  "action_code": "ACTION_CODE_FROM_USER",
  "agent_pk": "your-public-key"
}

// Response
{
  "session_token": "USE_THIS_FOR_ALL_REQUESTS",
  "expires_at": "..."
}

2. Discovery (Finding Portald Merchants)

Not all websites support Portald. Here's how to determine if a site is enabled and what capabilities it has.

Method 1: Discovery API (Recommended)

Query Portald directly. No authentication required. Returns merchant info, menu/catalog, and all locations.

GET https://portald.ai/api/discover?domain=coffeeshop.com

// Response for an enabled site:
{
  "enabled": true,
  "siteId": "site_abc123",
  "merchantName": "Coffee Shop",
  "paymentProvider": "SQUARE",  // or "STRIPE"
  "verified": true,
  
  "locations": [
    {
      "id": "LOC_123",
      "name": "Downtown",
      "address": {
        "line1": "123 Main St",
        "city": "Austin",
        "state": "TX",
        "postalCode": "78701"
      },
      "businessHours": "07:00 - 17:00",
      "timezone": "America/Chicago"
    },
    {
      "id": "LOC_456",
      "name": "Eastside",
      "address": { ... }
    }
  ],
  
  "menu": {
    "items": [
      {
        "id": "ITEM_abc",
        "name": "House Latte",
        "description": "Our signature drink",
        "category": "Beverages",
        "price": "$4.50",
        "priceCents": 450,
        "variationId": "VAR_xyz"  // Use this when ordering
      }
    ],
    "categories": ["Beverages", "Food", "Merchandise"],
    "itemCount": 42
  },
  
  "actions": [
    {
      "type": "payment.charge",
      "description": "Process a payment",
      "requires_approval": true
    }
  ],
  
  "enrollment_url": "https://portald.ai/enroll/merchant_xyz"
}

// Response for a non-enabled site:
{
  "enabled": false
}

Method 2: Check for Manifest

Some sites host their own Portald manifest. Check for it at the well-known path:

GET https://shop.com/.well-known/portald.json

// If exists, returns:
{
  "actions": [
    {
      "type": "checkout.create",
      "endpoint": "/api/checkout",
      "risk_level": "medium"
    }
  ]
}

Method 3: Check for Meta Tags

Sites on hosted platforms (Shopify, Square Online, etc.) may include meta tags:

// Fetch the page HTML and look for:
<meta name="portald" content="enabled">
<meta name="portald:site-id" content="site_abc123">

// If found, query the Discovery API for full details

Combined Discovery Logic

async function discoverPortald(domain) {
  // Method 1: Discovery API (fastest, most complete)
  const discovery = await fetch(
    `https://portald.ai/api/discover?domain=${domain}`
  ).then(r => r.json());
  
  if (discovery.enabled) {
    return {
      enabled: true,
      method: "discovery_api",
      ...discovery
    };
  }
  
  // Method 2: Check for manifest
  const manifestUrl = `https://${domain}/.well-known/portald.json`;
  const manifestRes = await fetch(manifestUrl).catch(() => null);
  if (manifestRes?.ok) {
    const manifest = await manifestRes.json();
    return {
      enabled: true,
      method: "manifest",
      actions: manifest.actions
    };
  }
  
  // Method 3: Check HTML for meta tags
  const html = await fetch(`https://${domain}`).then(r => r.text());
  if (html.includes('name="portald"') || html.includes('portald:site-id')) {
    // Found meta tags - query discovery API with site ID if available
    return {
      enabled: true,
      method: "meta_tags",
      hint: "Query discovery API for full details"
    };
  }
  
  return { enabled: false };
}

3. Execution (Making Purchases)

Once you've discovered a Portald merchant, here's how to submit a purchase request. The human user will be asked to approve before any payment is processed.

✅ Complete Agent Flow (Recommended)

This is the pattern for ordering from Square merchants. Always 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

Key points: Always use www.portald.ai (not portald.ai - the redirect strips auth headers). The variationId from the menu ensures orders appear correctly on the merchant's Square POS.

For Square/Stripe Merchants (Discovery API)

If you found the merchant via Discovery API, submit directly to Portald:

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

{
  "action_type": "payment.charge",
  "website_origin": "https://coffeeshop.com",
  
  "action_payload": {
    "description": "Coffee order for pickup",
    "location_id": "LOC_123",  // From discovery.locations[]
    "line_items": [
      {
        "name": "House Latte",
        "quantity": 2,
        "unit_price_cents": 450,
        "variation_id": "VAR_xyz"  // From discovery.menu.items[]
      },
      {
        "name": "Croissant",
        "quantity": 1,
        "unit_price_cents": 350
      }
    ]
  },
  
  "amount_cents": 1250,  // Total: (450*2) + 350
  "currency": "USD",
  "idempotency_key": "order-123-1234567890"  // Unique per request
}

// Response
{
  "action_id": "act_abc123",
  "status": "pending_approval",
  "approval_url": "https://portald.ai/approve/act_abc123"
}

For Custom SDK Merchants (Manifest)

If the site has a manifest with custom endpoints, call their endpoint first to get a quote:

// 1. Get quote from merchant's endpoint
POST https://shop.com/api/portald/actions/checkout
{
  "items": [{ "id": "product-123", "quantity": 1 }],
  "shipping_address": { "postal_code": "78701" }
}

// Response
{
  "order_id": "order_xyz",
  "total_cents": 2499,
  "line_items": [...]
}

// 2. Submit to Portald for approval
POST https://portald.ai/api/agent-actions/ingest
{
  "action_type": "checkout.create",
  "website_origin": "https://shop.com",
  "action_payload": {
    "order_id": "order_xyz"
  },
  "amount_cents": 2499,
  "idempotency_key": "checkout-order_xyz"
}

Checking Action Status

GET https://portald.ai/api/agent-actions/ACTION_ID
Authorization: Bearer YOUR_SESSION_TOKEN

// Response
{
  "id": "act_abc123",
  "status": "pending_approval",  // or "approved", "executed", "rejected"
  "created_at": "...",
  "executed_at": null,
  "amount_cents": 1250,
  "merchant_name": "Coffee Shop"
}

Status Flow

pending_approval  →  approved  →  executed
       ↓                              
    rejected                          
                                      
pending_approval: Waiting for user to approve
approved: User approved, payment processing
executed: Payment complete, order placed
rejected: User declined the purchase

4. Multi-Location Merchants

Some merchants have multiple locations (e.g., different store branches). The Discovery API returns all locations. You should:

  1. Present location options to the user if multiple exist
  2. Consider using geolocation to suggest the nearest location
  3. Include the chosen location_id in your action payload
// Example: Present options to user
const { locations } = await discoverPortald("coffeeshop.com");

if (locations.length > 1) {
  // Ask user: "Which location? Downtown (123 Main St) or Eastside (456 Oak Ave)?"
  const userChoice = await askUser(locations);
  selectedLocationId = userChoice.id;
} else {
  selectedLocationId = locations[0].id;
}

// Include in action payload
{
  "action_payload": {
    "location_id": selectedLocationId,
    "line_items": [...]
  }
}

5. Best Practices

6. Error Handling

// Common error responses

// 401 - Not authenticated
{ "error": "Invalid or expired session token" }
// → Re-authenticate with action code

// 400 - Invalid request
{ "error": "Missing required field: amount_cents" }
// → Check your payload

// 404 - Merchant not found
{ "error": "No Portald merchant for this origin" }
// → Site may not be registered with Portald

// 409 - Duplicate request
{ "error": "Action already exists", "existing_action_id": "act_xyz" }
// → Use existing action ID, don't create new one

// 403 - User not enrolled with merchant
{ "error": "User not enrolled with this merchant" }
// → Direct user to enrollment_url from discovery

Interactive Enrollment

If you have a pairing code from your user, you can complete enrollment below:

Loading...