@aura-labs-ai/scout 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,237 @@
1
+ # @aura-labs-ai/scout
2
+
3
+ Scout SDK for AURA — Build buying agents that participate in agentic commerce.
4
+
5
+ ## What is a Scout?
6
+
7
+ A Scout is a user-sovereign buying agent in the AURA ecosystem. Scouts:
8
+ - Express purchase intent in natural language
9
+ - Discover products through AURA Core's neutral broker
10
+ - Evaluate offers against user-defined constraints
11
+ - Commit to transactions while preserving privacy
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @aura-labs-ai/scout
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```javascript
22
+ import { createScout } from '@aura-labs-ai/scout';
23
+
24
+ // Zero-config — auto-generates Ed25519 identity and registers with Core
25
+ const scout = createScout();
26
+ await scout.ready();
27
+
28
+ // Express purchase intent with constraints
29
+ const session = await scout.intent('I need 500 widgets', {
30
+ maxBudget: 50000,
31
+ deliveryBy: new Date('2026-03-01'),
32
+ });
33
+
34
+ // Wait for offers (polling)
35
+ const offers = await session.waitForOffers();
36
+
37
+ // Commit to best offer that meets constraints
38
+ if (session.bestOffer) {
39
+ const tx = await session.commit(session.bestOffer.id);
40
+ console.log('Transaction:', tx.id);
41
+ }
42
+ ```
43
+
44
+ ## CLI Tool
45
+
46
+ The SDK includes a CLI for testing:
47
+
48
+ ```bash
49
+ # Interactive mode (zero-config, uses Ed25519 keys)
50
+ npx @aura-labs-ai/scout
51
+
52
+ # Single intent mode
53
+ npx @aura-labs-ai/scout --intent "I need office supplies" --max-budget 500
54
+ ```
55
+
56
+ **HTTPS Enforcement:** The CLI requires HTTPS for all Core API connections. Plaintext HTTP is only permitted for `localhost` and `127.0.0.1` during local development. Use `--core-url https://...` or set `AURA_CORE_URL` with an HTTPS URL.
57
+
58
+ ## Constraint Engine
59
+
60
+ Define hard constraints (must be met) and soft preferences (influence ranking):
61
+
62
+ ```javascript
63
+ const session = await scout.intent('Buy enterprise software licenses', {
64
+ // Hard constraints - offers that don't meet these are filtered out
65
+ maxBudget: 100000,
66
+ deliveryBy: new Date('2026-06-01'),
67
+ hardConstraints: [
68
+ { field: 'compliance', operator: 'eq', value: 'SOC2' },
69
+ ],
70
+
71
+ // Soft preferences - influence offer scoring
72
+ softPreferences: [
73
+ { field: 'support', operator: 'eq', value: '24/7', weight: 10 },
74
+ { field: 'rating', operator: 'gte', value: 4.5, weight: 5 },
75
+ ],
76
+ });
77
+ ```
78
+
79
+ ### Constraint Operators
80
+
81
+ Only the following operators are accepted. Unknown operators are rejected (fail-closed) to prevent constraint bypass:
82
+
83
+ | Operator | Description |
84
+ |----------|-------------|
85
+ | `eq` | Equal to |
86
+ | `ne` | Not equal to |
87
+ | `gt` | Greater than |
88
+ | `gte` | Greater than or equal |
89
+ | `lt` | Less than |
90
+ | `lte` | Less than or equal |
91
+ | `contains` | String contains |
92
+ | `in` | Value in array |
93
+
94
+ ## API Reference
95
+
96
+ ### `createScout(config)`
97
+
98
+ Create a new Scout instance. Authentication is handled via Ed25519 public key registration — no API keys required.
99
+
100
+ ```javascript
101
+ const scout = createScout({
102
+ coreUrl: 'https://aura-labsai-production.up.railway.app', // optional, defaults to production
103
+ timeout: 30000, // optional, ms
104
+ storage: customStorageAdapter, // optional, defaults to in-memory
105
+ constraints: {}, // optional, default constraints
106
+ });
107
+
108
+ // Initialize and register with AURA Core (idempotent)
109
+ await scout.ready();
110
+ ```
111
+
112
+ ### `scout.ping()`
113
+
114
+ Read-only connectivity check. Verifies Core is reachable and its dependencies are healthy. Does not require `ready()` — works on a freshly created instance. No auth headers sent.
115
+
116
+ ```javascript
117
+ const health = await scout.ping();
118
+
119
+ // When Core is healthy:
120
+ // { status: 'ok', core: { status: 'ready', checks: { database: {...}, redis: {...} } }, latency_ms: 42, timestamp: '...' }
121
+
122
+ // When Core is alive but degraded (503):
123
+ // { status: 'degraded', core: { status: 'not_ready', checks: {...} }, latency_ms: 105, timestamp: '...' }
124
+
125
+ // When Core is unreachable:
126
+ // { status: 'error', code: 'CORE_UNREACHABLE', message: '...', latency_ms: 30000, timestamp: '...' }
127
+
128
+ // When Core times out:
129
+ // { status: 'error', code: 'CORE_TIMEOUT', message: '...', latency_ms: 30000, timestamp: '...' }
130
+ ```
131
+
132
+ Activity events: `ping.success` (Core responded), `ping.failed` (network error or timeout). Summary counters available via `scout.activity.getSummary().ping`.
133
+
134
+ ### `scout.intent(text, options)`
135
+
136
+ Create a commerce session with purchase intent.
137
+
138
+ ```javascript
139
+ const session = await scout.intent('I want to buy...', {
140
+ maxBudget: number,
141
+ deliveryBy: Date,
142
+ hardConstraints: Constraint[],
143
+ softPreferences: Constraint[],
144
+ });
145
+ ```
146
+
147
+ ### `session.waitForOffers(options)`
148
+
149
+ Poll for offers until available.
150
+
151
+ ```javascript
152
+ const offers = await session.waitForOffers({
153
+ timeout: 30000, // max wait time
154
+ interval: 2000, // poll interval
155
+ });
156
+ ```
157
+
158
+ ### `session.commit(offerId)`
159
+
160
+ Commit to an offer.
161
+
162
+ ```javascript
163
+ const transaction = await session.commit(offer.id);
164
+ ```
165
+
166
+ ### `session.validOffers`
167
+
168
+ Get offers that meet all hard constraints.
169
+
170
+ ### `session.bestOffer`
171
+
172
+ Get highest-scoring valid offer.
173
+
174
+ ## Error Handling
175
+
176
+ ```javascript
177
+ import { ScoutError, AuthenticationError, SessionError } from '@aura-labs-ai/scout';
178
+
179
+ try {
180
+ await scout.intent('...');
181
+ } catch (error) {
182
+ if (error instanceof AuthenticationError) {
183
+ console.log('Invalid API key');
184
+ } else if (error instanceof SessionError) {
185
+ console.log('Session error:', error.message);
186
+ }
187
+ }
188
+ ```
189
+
190
+ ## Key Storage
191
+
192
+ Ed25519 private keys are persisted across restarts using pluggable storage adapters from `@aura-labs-ai/sdk-common`. The `createStorage()` factory auto-detects the best adapter for the current platform:
193
+
194
+ | Platform | Adapter | Details |
195
+ |----------|---------|---------|
196
+ | **macOS** | `KeychainStorage` | Hardware-backed encryption at rest via Secure Enclave on Apple Silicon. Uses the `security` CLI — zero native dependencies. |
197
+ | **Linux / Windows** | `FileStorage` | JSON file at `~/.aura/keys.json` with `0600` permissions (owner read/write only). |
198
+ | **Testing** | `MemoryStorage` | In-memory, ephemeral. No persistence. |
199
+
200
+ ```javascript
201
+ import { createScout, createStorage } from '@aura-labs-ai/scout';
202
+
203
+ // Auto-detect (recommended)
204
+ const scout = createScout({ storage: createStorage() });
205
+
206
+ // Force file-based storage
207
+ const scout = createScout({ storage: createStorage({ type: 'file' }) });
208
+
209
+ // Force Keychain (macOS only — throws on other platforms)
210
+ const scout = createScout({ storage: createStorage({ type: 'keychain' }) });
211
+
212
+ // Custom file path
213
+ const scout = createScout({ storage: createStorage({ type: 'file', path: '/custom/keys.json' }) });
214
+ ```
215
+
216
+ Override the default file path with the `AURA_KEY_PATH` environment variable.
217
+
218
+ ## Environment Variables
219
+
220
+ | Variable | Description | Default |
221
+ |----------|-------------|---------|
222
+ | `AURA_CORE_URL` | Core API URL (optional) | `https://aura-labsai-production.up.railway.app` |
223
+ | `AURA_KEY_PATH` | Custom path for file-based key storage | `~/.aura/keys.json` |
224
+
225
+ ## Authentication
226
+
227
+ Scout uses **Ed25519 public key cryptography** for identity. When you call `scout.ready()`:
228
+ 1. An Ed25519 key pair is auto-generated (or loaded from storage)
229
+ 2. The Scout registers with AURA Core via `POST /agents/register` using proof-of-possession (signed request)
230
+ 3. Core assigns an agent ID, which is persisted for future sessions
231
+ 4. All subsequent requests are signed with the private key for identity verification
232
+
233
+ No API keys or other credentials are required.
234
+
235
+ ## License
236
+
237
+ Business Source License 1.1 — See [LICENSE](LICENSE) for details.
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * AURA Scout CLI
5
+ *
6
+ * Interactive command-line tool for testing Scout SDK and Core API.
7
+ *
8
+ * Usage:
9
+ * npx @aura-labs-ai/scout --api-key YOUR_KEY
10
+ * npx @aura-labs-ai/scout --api-key YOUR_KEY --intent "I need 100 widgets"
11
+ */
12
+
13
+ import { createScout } from '../src/index.js';
14
+ import { createInterface } from 'readline';
15
+
16
+ // Parse command line arguments
17
+ const args = process.argv.slice(2);
18
+ const flags = {};
19
+ for (let i = 0; i < args.length; i++) {
20
+ if (args[i].startsWith('--')) {
21
+ const key = args[i].slice(2);
22
+ const value = args[i + 1] && !args[i + 1].startsWith('--') ? args[++i] : true;
23
+ flags[key] = value;
24
+ }
25
+ }
26
+
27
+ // Configuration
28
+ const config = {
29
+ apiKey: flags['api-key'] || process.env.AURA_API_KEY,
30
+ coreUrl: flags['core-url'] || process.env.AURA_CORE_URL || 'https://aura-labsai-production.up.railway.app',
31
+ };
32
+
33
+ // SECURITY: Enforce HTTPS for all non-localhost connections
34
+ const coreUrlLower = config.coreUrl.toLowerCase();
35
+ const isLocalhost = coreUrlLower.startsWith('http://localhost') || coreUrlLower.startsWith('http://127.0.0.1');
36
+ if (!coreUrlLower.startsWith('https://') && !isLocalhost) {
37
+ console.error('Error: Core URL must use HTTPS. Plaintext HTTP is only allowed for localhost development.');
38
+ console.error(` Provided: ${config.coreUrl}`);
39
+ console.error(' Use --core-url https://... or set AURA_CORE_URL with an HTTPS URL.');
40
+ process.exit(1);
41
+ }
42
+
43
+ // Help text
44
+ if (flags.help || flags.h) {
45
+ console.log(`
46
+ AURA Scout CLI - Test buying agent interactions
47
+
48
+ Usage:
49
+ scout-cli [options]
50
+
51
+ Options:
52
+ --api-key KEY Your AURA API key (or set AURA_API_KEY env var)
53
+ --core-url URL Core API URL (default: production)
54
+ --intent "TEXT" Run single intent and exit
55
+ --max-budget N Set maximum budget constraint
56
+ --delivery-by D Set delivery deadline (ISO date)
57
+ --help, -h Show this help
58
+
59
+ Interactive Mode:
60
+ Start without --intent for interactive REPL mode.
61
+
62
+ Examples:
63
+ scout-cli --api-key sk_test_123 --intent "I need office supplies"
64
+ scout-cli --api-key sk_test_123 --intent "Buy 500 widgets" --max-budget 50000
65
+ AURA_API_KEY=sk_test_123 scout-cli
66
+ `);
67
+ process.exit(0);
68
+ }
69
+
70
+ // Validate API key
71
+ if (!config.apiKey) {
72
+ console.error('Error: API key required. Use --api-key or set AURA_API_KEY');
73
+ process.exit(1);
74
+ }
75
+
76
+ // Colors for terminal output
77
+ const colors = {
78
+ reset: '\x1b[0m',
79
+ bright: '\x1b[1m',
80
+ dim: '\x1b[2m',
81
+ green: '\x1b[32m',
82
+ yellow: '\x1b[33m',
83
+ blue: '\x1b[34m',
84
+ cyan: '\x1b[36m',
85
+ red: '\x1b[31m',
86
+ };
87
+
88
+ function log(color, ...args) {
89
+ console.log(color, ...args, colors.reset);
90
+ }
91
+
92
+ function printJSON(obj) {
93
+ console.log(JSON.stringify(obj, null, 2));
94
+ }
95
+
96
+ // Initialize Scout
97
+ let scout;
98
+ let currentSession = null;
99
+
100
+ async function init() {
101
+ log(colors.cyan, '\nšŸ” AURA Scout CLI v0.1.0');
102
+ log(colors.dim, ` Core: ${config.coreUrl}`);
103
+
104
+ scout = createScout(config);
105
+
106
+ log(colors.dim, ' Registering with Core...');
107
+ const { scoutId } = await scout.register();
108
+ log(colors.green, ` āœ“ Registered as ${scoutId}\n`);
109
+ }
110
+
111
+ // Single intent mode
112
+ async function runSingleIntent(intentText) {
113
+ const options = {};
114
+ if (flags['max-budget']) options.maxBudget = parseFloat(flags['max-budget']);
115
+ if (flags['delivery-by']) options.deliveryBy = new Date(flags['delivery-by']);
116
+
117
+ log(colors.blue, `\nšŸ“ Intent: "${intentText}"`);
118
+ if (options.maxBudget) log(colors.dim, ` Budget: $${options.maxBudget}`);
119
+ if (options.deliveryBy) log(colors.dim, ` Deliver by: ${options.deliveryBy.toDateString()}`);
120
+
121
+ try {
122
+ const session = await scout.intent(intentText, options);
123
+ log(colors.green, `\nāœ“ Session created: ${session.id}`);
124
+ log(colors.dim, ` Status: ${session.status}`);
125
+
126
+ if (session.intent?.parsed) {
127
+ log(colors.dim, ' Parsed intent:');
128
+ printJSON(session.intent.parsed);
129
+ }
130
+
131
+ log(colors.yellow, '\nā³ Waiting for offers...');
132
+ const offers = await session.waitForOffers({ timeout: 60000 });
133
+
134
+ if (offers.length === 0) {
135
+ log(colors.yellow, ' No offers received');
136
+ } else {
137
+ log(colors.green, `\nāœ“ Received ${offers.length} offer(s):\n`);
138
+ for (const offer of offers) {
139
+ log(colors.bright, ` ${offer.beaconName || offer.beaconId}`);
140
+ log(colors.dim, ` $${offer.totalPrice} | Delivery: ${offer.deliveryDate || 'TBD'}`);
141
+ log(colors.dim, ` Score: ${offer.score.toFixed(1)} | Meets constraints: ${offer.meetsConstraints}`);
142
+ if (!offer.meetsConstraints) {
143
+ log(colors.red, ` Violations: ${offer.constraintViolations.join(', ')}`);
144
+ }
145
+ console.log();
146
+ }
147
+
148
+ if (session.bestOffer) {
149
+ log(colors.green, ` Best offer: ${session.bestOffer.beaconName || session.bestOffer.beaconId}`);
150
+ }
151
+ }
152
+ } catch (error) {
153
+ log(colors.red, `\nāœ— Error: ${error.message}`);
154
+ process.exit(1);
155
+ }
156
+ }
157
+
158
+ // Interactive REPL mode
159
+ async function runInteractive() {
160
+ const rl = createInterface({
161
+ input: process.stdin,
162
+ output: process.stdout,
163
+ prompt: colors.cyan + 'scout> ' + colors.reset,
164
+ });
165
+
166
+ console.log(`
167
+ ${colors.bright}Commands:${colors.reset}
168
+ intent <text> Create session with purchase intent
169
+ status Show current session status
170
+ offers List current offers
171
+ commit <id> Commit to offer by ID
172
+ cancel Cancel current session
173
+ budget <amount> Set max budget for next session
174
+ help Show this help
175
+ exit Exit CLI
176
+ `);
177
+
178
+ let constraints = {};
179
+
180
+ rl.prompt();
181
+
182
+ rl.on('line', async (line) => {
183
+ const [command, ...rest] = line.trim().split(' ');
184
+ const arg = rest.join(' ');
185
+
186
+ try {
187
+ switch (command.toLowerCase()) {
188
+ case 'intent':
189
+ if (!arg) {
190
+ log(colors.red, 'Usage: intent <description>');
191
+ break;
192
+ }
193
+ log(colors.blue, `\nšŸ“ Creating session: "${arg}"`);
194
+ currentSession = await scout.intent(arg, constraints);
195
+ log(colors.green, `āœ“ Session: ${currentSession.id}`);
196
+ log(colors.dim, ` Status: ${currentSession.status}`);
197
+ break;
198
+
199
+ case 'status':
200
+ if (!currentSession) {
201
+ log(colors.yellow, 'No active session. Use "intent <text>" first.');
202
+ break;
203
+ }
204
+ await currentSession.refresh();
205
+ log(colors.cyan, `\nSession: ${currentSession.id}`);
206
+ log(colors.dim, `Status: ${currentSession.status}`);
207
+ log(colors.dim, `Active: ${currentSession.isActive}`);
208
+ break;
209
+
210
+ case 'offers':
211
+ if (!currentSession) {
212
+ log(colors.yellow, 'No active session.');
213
+ break;
214
+ }
215
+ log(colors.yellow, 'ā³ Fetching offers...');
216
+ const offers = await currentSession.waitForOffers({ timeout: 30000 });
217
+ if (offers.length === 0) {
218
+ log(colors.yellow, 'No offers yet.');
219
+ } else {
220
+ log(colors.green, `\n${offers.length} offer(s):\n`);
221
+ for (const offer of offers) {
222
+ console.log(` [${offer.id.slice(0, 8)}] ${offer.beaconName || 'Unknown'} - $${offer.totalPrice} (score: ${offer.score.toFixed(1)})`);
223
+ }
224
+ }
225
+ break;
226
+
227
+ case 'commit':
228
+ if (!currentSession) {
229
+ log(colors.yellow, 'No active session.');
230
+ break;
231
+ }
232
+ if (!arg) {
233
+ log(colors.red, 'Usage: commit <offer-id>');
234
+ break;
235
+ }
236
+ log(colors.yellow, `ā³ Committing to offer ${arg}...`);
237
+ const tx = await currentSession.commit(arg);
238
+ log(colors.green, `āœ“ Transaction: ${tx.id}`);
239
+ break;
240
+
241
+ case 'cancel':
242
+ if (!currentSession) {
243
+ log(colors.yellow, 'No active session.');
244
+ break;
245
+ }
246
+ await currentSession.cancel();
247
+ log(colors.green, 'āœ“ Session cancelled');
248
+ currentSession = null;
249
+ break;
250
+
251
+ case 'budget':
252
+ if (!arg) {
253
+ log(colors.dim, `Current max budget: ${constraints.maxBudget || 'not set'}`);
254
+ } else {
255
+ constraints.maxBudget = parseFloat(arg);
256
+ log(colors.green, `āœ“ Max budget set to $${constraints.maxBudget}`);
257
+ }
258
+ break;
259
+
260
+ case 'delivery':
261
+ if (!arg) {
262
+ log(colors.dim, `Delivery deadline: ${constraints.deliveryBy || 'not set'}`);
263
+ } else {
264
+ constraints.deliveryBy = new Date(arg);
265
+ log(colors.green, `āœ“ Delivery deadline set to ${constraints.deliveryBy.toDateString()}`);
266
+ }
267
+ break;
268
+
269
+ case 'help':
270
+ console.log(`
271
+ ${colors.bright}Commands:${colors.reset}
272
+ intent <text> Create session with purchase intent
273
+ status Show current session status
274
+ offers List current offers (polls until available)
275
+ commit <id> Commit to offer by ID
276
+ cancel Cancel current session
277
+ budget <amount> Set max budget for next session
278
+ delivery <date> Set delivery deadline (e.g., "2026-03-01")
279
+ help Show this help
280
+ exit Exit CLI
281
+ `);
282
+ break;
283
+
284
+ case 'exit':
285
+ case 'quit':
286
+ log(colors.dim, 'Goodbye!');
287
+ rl.close();
288
+ process.exit(0);
289
+ break;
290
+
291
+ case '':
292
+ break;
293
+
294
+ default:
295
+ log(colors.red, `Unknown command: ${command}. Type "help" for commands.`);
296
+ }
297
+ } catch (error) {
298
+ log(colors.red, `Error: ${error.message}`);
299
+ }
300
+
301
+ rl.prompt();
302
+ });
303
+
304
+ rl.on('close', () => {
305
+ process.exit(0);
306
+ });
307
+ }
308
+
309
+ // Main
310
+ async function main() {
311
+ try {
312
+ await init();
313
+
314
+ if (flags.intent) {
315
+ await runSingleIntent(flags.intent);
316
+ } else {
317
+ await runInteractive();
318
+ }
319
+ } catch (error) {
320
+ log(colors.red, `Fatal error: ${error.message}`);
321
+ process.exit(1);
322
+ }
323
+ }
324
+
325
+ main();
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Basic Purchase Example
3
+ *
4
+ * Demonstrates the core Scout SDK flow:
5
+ * 1. Initialize Scout with API key
6
+ * 2. Express purchase intent with constraints
7
+ * 3. Wait for offers
8
+ * 4. Evaluate and commit to best offer
9
+ *
10
+ * Run with: AURA_API_KEY=your-key node examples/basic-purchase.js
11
+ */
12
+
13
+ import { createScout } from '../src/index.js';
14
+
15
+ async function main() {
16
+ // Initialize Scout
17
+ const scout = createScout({
18
+ apiKey: process.env.AURA_API_KEY,
19
+ // coreUrl: 'http://localhost:3000', // Uncomment for local development
20
+ });
21
+
22
+ console.log('šŸ” Scout initialized');
23
+ console.log(` Core URL: ${scout.coreUrl}`);
24
+
25
+ // Express purchase intent with constraints
26
+ console.log('\nšŸ“ Creating session with intent...');
27
+
28
+ const session = await scout.intent(
29
+ 'I need 500 industrial widgets for manufacturing, grade A quality',
30
+ {
31
+ maxBudget: 50000, // $50,000 max
32
+ deliveryBy: new Date('2026-03-01'), // Need by March 1st
33
+ hardConstraints: [
34
+ // Must be grade A
35
+ { field: 'grade', operator: 'eq', value: 'A' },
36
+ ],
37
+ softPreferences: [
38
+ // Prefer sustainable suppliers (+10 to score)
39
+ { field: 'sustainable', operator: 'eq', value: true, weight: 10 },
40
+ // Prefer suppliers with high ratings (+5 per point above 4.0)
41
+ { field: 'rating', operator: 'gte', value: 4.0, weight: 5 },
42
+ ],
43
+ }
44
+ );
45
+
46
+ console.log(`āœ“ Session created: ${session.id}`);
47
+ console.log(` Status: ${session.status}`);
48
+
49
+ // Wait for offers from Beacons
50
+ console.log('\nā³ Waiting for offers...');
51
+
52
+ try {
53
+ const offers = await session.waitForOffers({
54
+ timeout: 60000, // Wait up to 60 seconds
55
+ interval: 2000, // Poll every 2 seconds
56
+ });
57
+
58
+ console.log(`\nāœ“ Received ${offers.length} offer(s)`);
59
+
60
+ // Display all offers
61
+ for (const offer of offers) {
62
+ console.log(`\n šŸ“¦ ${offer.beaconName || offer.beaconId}`);
63
+ console.log(` Product: ${offer.product}`);
64
+ console.log(` Price: $${offer.totalPrice.toLocaleString()} (${offer.quantity} Ɨ $${offer.unitPrice})`);
65
+ console.log(` Delivery: ${offer.deliveryDate || 'TBD'}`);
66
+ console.log(` Score: ${offer.score.toFixed(1)}/100`);
67
+ console.log(` Meets constraints: ${offer.meetsConstraints ? 'āœ“' : 'āœ—'}`);
68
+
69
+ if (!offer.meetsConstraints) {
70
+ console.log(` Violations: ${offer.constraintViolations.join(', ')}`);
71
+ }
72
+ }
73
+
74
+ // Get only offers that meet all constraints
75
+ const validOffers = session.validOffers;
76
+ console.log(`\nāœ“ ${validOffers.length} offer(s) meet all constraints`);
77
+
78
+ // Get the best offer (highest score among valid offers)
79
+ const bestOffer = session.bestOffer;
80
+
81
+ if (bestOffer) {
82
+ console.log(`\nšŸ† Best offer: ${bestOffer.beaconName || bestOffer.beaconId}`);
83
+ console.log(` Total: $${bestOffer.totalPrice.toLocaleString()}`);
84
+ console.log(` Score: ${bestOffer.score.toFixed(1)}/100`);
85
+
86
+ // Commit to the best offer
87
+ console.log('\nšŸ’³ Committing to offer...');
88
+ const transaction = await session.commit(bestOffer.id);
89
+
90
+ console.log(`āœ“ Transaction created: ${transaction.id}`);
91
+ console.log(` Status: ${transaction.status}`);
92
+ } else {
93
+ console.log('\nāš ļø No offers meet all constraints');
94
+
95
+ // Show why offers were rejected
96
+ for (const offer of offers) {
97
+ if (!offer.meetsConstraints) {
98
+ console.log(` ${offer.beaconName}: ${offer.constraintViolations.join(', ')}`);
99
+ }
100
+ }
101
+ }
102
+ } catch (error) {
103
+ if (error.code === 'TIMEOUT') {
104
+ console.log('\nāš ļø Timed out waiting for offers');
105
+ console.log(' This may mean no Beacons matched your intent');
106
+ } else {
107
+ throw error;
108
+ }
109
+ }
110
+ }
111
+
112
+ main().catch(console.error);