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.
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.
session_token. See Enrollment below.How It Works
- User: Connects their Alpaca brokerage account
- User: Sets trust tier for your agent ($100/day, $500/day, or unlimited)
- Agent: Submits trade orders via API
- System: Auto-approves if within daily limit, or queues for approval
- User: Reviews and approves/rejects orders in dashboard
- System: Executes approved trades on user's brokerage
Trust Tiers
View portfolio only. No trading.
Auto-approve up to $100/day
Auto-approve up to $500/day
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
| Type | Description | Required Fields |
|---|---|---|
market | Execute at current price | symbol, qty, side |
limit | Execute at specified price or better | symbol, 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());
}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
- Human: Generates a pairing code at portald.ai/agents
- Human: Gives the code to you (the agent)
- Agent: Register with the pairing code (see below)
- Human: Approves you in their dashboard
- Human: Gives you the action code
- 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 updatesKey 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 purchase4. Multi-Location Merchants
Some merchants have multiple locations (e.g., different store branches). The Discovery API returns all locations. You should:
- Present location options to the user if multiple exist
- Consider using geolocation to suggest the nearest location
- Include the chosen
location_idin 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
- Always use idempotency keys - Prevents duplicate charges if requests retry
- Cache discovery results - Menu/locations don't change often (5-15 min TTL)
- Handle multi-location gracefully - Don't assume one location
- Check status before retrying - An action may already be approved/executed
- Use variation_id when available - More reliable than item names for Square merchants
- Respect user approval - Never attempt to bypass or rush approval flow
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 discoveryInteractive Enrollment
If you have a pairing code from your user, you can complete enrollment below:
Loading...