Repository navigation
Expand file tree
/
Copy pathopenapi.json
More file actions
11915 lines (11915 loc) · 469 KB
/
Copy pathopenapi.json
File metadata and controls
11915 lines (11915 loc) · 469 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
{
"openapi": "3.1.0",
"info": {
"title": "OutLayer API",
"version": "0.1.0-alpha.4",
"summary": "HTTP API for OutLayer custody wallets and policy enforcement",
"description": "OutLayer Agent Custody — multi-chain wallets for AI agents with TEE-enforced\npolicy, multisig approvals, audit log, gasless cross-chain transfers via\nNEAR Intents, and off-chain EVM signing (EIP-712 typed data, EIP-191\npersonal_sign, and client-serialized raw transactions).\n\nPrivate keys live exclusively inside an Intel TDX TEE and are derived from\nNEAR MPC. The wallet owner sets policy (spending limits, whitelists,\nmultisig, freeze) — all enforced inside the TEE before signing.\n\nSee [CUSTODY docs](https://outlayer.ai/docs/agent-custody) for the\narchitectural overview and [@outlayer/sdk](https://www.npmjs.com/package/@outlayer/sdk)\nfor the TypeScript client.\n",
"license": {
"name": "MIT",
"identifier": "MIT"
},
"contact": {
"name": "OutLayer",
"url": "https://outlayer.ai"
}
},
"servers": [
{
"url": "https://api.outlayer.ai",
"description": "Mainnet"
},
{
"url": "https://testnet-api.outlayer.ai",
"description": "Testnet — the same API, against NEAR testnet. Pick this one to try a call without spending anything real; a mainnet key does not work here, and a testnet key does not work on mainnet."
}
],
"security": [
{
"BearerAuth": []
}
],
"tags": [
{
"name": "Registration",
"description": "Create a new wallet and obtain an API key."
},
{
"name": "Execution",
"description": "Run a project. A connector is an ordinary project we curated and priced, reached through the same route; what being one adds — an `operation` it must name, a price it must pay, an owner's secret it may ask for — is decided from the project id, not from the way in. Synchronous by default; send `async` and poll for anything longer than a few seconds."
},
{
"name": "Connectors",
"description": "The curated connectors: named, priced operations that reach an outside service — a mailbox, a bank, a trading venue — from inside the same enclave that holds the caller's keys, so a credential is used without ever being handed over. A connector is an ordinary project under `connectors.outlayer.near` (`connectors.outlayer.testnet` on testnet) and is called through `POST /call/{owner}/{project}` like any other; what being curated adds is a price per `operation` on chain and the right to read a secret its owner stored for it. A caller who pays is never limited by a count of calls — money bounds a funded key, an allowance bounds a subscriber — and the one count there is belongs to the trial: fifty calls, in a wallet's first week (`POST /trial-key`; the answer's `calls` is the number).\n\nWhich ones exist here, and the skill describing each, come from `GET /public/connectors`. What one SELLS is not listed in this spec: its operations and their prices are on chain, and the connector's own free `status` operation answers with them, as does a refusal — which names what could have been asked for instead.\n\nEvery connector publishes an agent-facing skill at `https://skills.outlayer.ai/<id>-connector/SKILL.md`, and how to call any of them — which key pays, how to be granted an owner's credential — is in `https://skills.outlayer.ai/outlayer-connectors/SKILL.md`."
},
{
"name": "Subscriptions",
"description": "Payment keys, the allowance a subscription buys, and the secret an owner leaves for their agent. Only money pays a connector's author: a call covered by an allowance costs the caller nothing at the moment of the call, so nothing passes through."
},
{
"name": "Wallet",
"description": "Multi-chain custody operations — transfer, withdraw, swap, balance, and signing, including NEAR (NEP-413) message signing and off-chain EVM signing (EIP-712 typed data, EIP-191 personal_sign, and client-serialized raw transactions)."
},
{
"name": "Confidential",
"description": "Confidential intents — move balances between the public intents shard, the Defuse confidential shard, and external chains. The confidential balance ledger is real on-chain state on a separate **private** shard (the `intents.far` contract, distinct from public `intents.near`); it has no public RPC, so you cannot read it externally, but it is an auditable smart contract. Only SHIELD/UNSHIELD touch the public chain. Requires `ENABLE_CONFIDENTIAL_INTENTS` + the confidential partner agreement on the deployment, else every route returns 503. Every operation signs 1Click's own intent only as the transfer its quote is (this wallet, the quote's deposit address, the quoted asset and amount, nothing else); any other message is refused `400 bad_request` before anything is signed. See the agent integration guide referenced under `externalDocs` for the full mental model, privacy recipes, and curl walkthroughs.",
"externalDocs": {
"description": "Confidential intents — agent integration guide (mental model, privacy recipes, curl walkthroughs)",
"url": "https://github.com/out-layer/coordinator/blob/main/docs/CONFIDENTIAL_INTENTS.md"
}
},
{
"name": "Policy",
"description": "Encrypted on-chain policy lifecycle (read, encrypt, sign, cache)."
},
{
"name": "Approvals",
"description": "Multisig approval workflows for actions that exceed policy thresholds."
},
{
"name": "Inbox",
"description": "Tasks between an agent and its owner. An agent's run of a project, admitted to an owner's secret row by name, leaves that owner a task — \"confirm this email\", \"give me your photo\"; the owner reads it here, inside a session and with no run, and acts on it with a call of their own to the operation the task names (`POST /call/{owner}/{project}`, with `task_id` and `task_hash`). Reading, rejecting, deleting and muting need the session and nothing else.\n\nWhat a task shows is ciphertext to this API: `content` is under the task's content key, and `device_copy` is that key encrypted to the public key of the device the session was opened on. A page opens them with the device's private key, which it holds and cannot export (ECDH on P-256, HKDF-SHA256, AES-256-GCM). The hash the owner's answer names is the SHA-256 of the bytes that were opened.\n\nEvery refusal is an `InboxRefusal`. \"Nothing waits\" is `{\"tasks\": []}`, never a refusal, and a refusal is never an empty list."
},
{
"name": "Audit",
"description": "Event history for the wallet."
},
{
"name": "Requests",
"description": "Async operation status tracking."
},
{
"name": "Meta",
"description": "Token catalog and helpers."
}
],
"paths": {
"/register": {
"post": {
"tags": [
"Registration"
],
"operationId": "registerWallet",
"summary": "Register a new wallet",
"description": "Creates a new multi-chain wallet and returns an API key. For most\ncallers, send `{}` — an anonymous wallet is created and an API key is\nreturned in `api_key` (shown once, not retrievable later).\n\nOptional fields enable advanced registration paths:\n- `account_id` + `pubkey` + `message` + `signature`: bind the wallet to\n a NEAR account via proof-of-ownership. `pubkey` must be a full-access\n key of `account_id` (or the key of an implicit account not created\n yet); a function-call key is refused. The same holds for every\n `Bearer near:` request and for `PUT /wallet/v1/api-key`.\n- `vault_id`: bind to a customer-owned vault for sovereign custody (see\n vault docs).\n\nRate-limited: 30 requests per minute per IP.\n",
"security": [],
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RegisterRequest"
},
"examples": {
"anonymous": {
"summary": "Anonymous wallet (most common)",
"value": {}
},
"bound_to_near_account": {
"summary": "Bind to NEAR account via NEP-413",
"value": {
"account_id": "alice.near",
"pubkey": "ed25519:7BcBZ9Z...",
"message": "register:1716200000",
"signature": "ed25519:5K3F..."
}
}
}
}
}
},
"responses": {
"200": {
"description": "Wallet created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RegisterResponse"
},
"examples": {
"anonymous": {
"value": {
"wallet_id": "9c3c9e10-1c1f-4f5e-9c4a-1d7b9a8f3c20",
"api_key": "wk_2a8b1f3c4d5e6789abcdef0123456789",
"near_account_id": "9c3c9e101c1f4f5e9c4a1d7b9a8f3c20",
"handoff_url": "https://app.outlayer.ai/wallet?key=wk_2a8b...",
"trial": {
"available": true,
"calls": 10,
"days": 7,
"claim_url": "POST /trial-key",
"scope": "connectors.outlayer.near/*"
}
}
}
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"429": {
"$ref": "#/components/responses/RateLimited"
},
"500": {
"$ref": "#/components/responses/InternalError"
}
}
}
},
"/wallet/v1/address": {
"get": {
"tags": [
"Wallet"
],
"operationId": "getAddress",
"summary": "Derive wallet address for a chain",
"description": "Returns the deterministically-derived address for the wallet on the\nrequested chain. Same `wallet_id` always produces the same addresses\nacross chains.\n\nFor `chain=near` the response also carries the two Agent Connect\nidentities: `executor_account_id` (always equal to `address` — the\nwallet's implicit account, which signs and pays gas) and\n`asset_account_id` (the bound named account, `null` when the wallet\nhas no binding). They are separate fields on purpose: product balances\nbelong to the asset account, while signing, predecessor checks and gas\nkey off the executor.\n",
"parameters": [
{
"name": "chain",
"in": "query",
"required": true,
"schema": {
"$ref": "#/components/schemas/Chain"
}
},
{
"name": "sub_path",
"in": "query",
"required": false,
"schema": {
"$ref": "#/components/schemas/SubPath"
}
}
],
"responses": {
"200": {
"description": "Address derived",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AddressResponse"
},
"example": {
"wallet_id": "9c3c9e10-1c1f-4f5e-9c4a-1d7b9a8f3c20",
"chain": "near",
"address": "9c3c9e101c1f4f5e9c4a1d7b9a8f3c20",
"public_key": "ed25519:7BcBZ9Z...",
"vault_id": null,
"asset_account_id": "agent.tla",
"executor_account_id": "9c3c9e101c1f4f5e9c4a1d7b9a8f3c20"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"500": {
"$ref": "#/components/responses/InternalError"
}
}
}
},
"/wallet/v1/balance": {
"get": {
"tags": [
"Wallet"
],
"operationId": "getBalance",
"summary": "Read balance",
"description": "Returns the balance for a token on the requested chain. Currently only\n`chain=near` is supported for native balance reads; cross-chain balances\nshould be read from the upstream chain directly.\n\nSet `source=intents` to read the wallet's intents.near balance\n(`mt_balance_of`) instead of the on-chain account balance.\n\nBy default this endpoint answers for the WALLET'S OWN account (the\nexecutor), bound or not — its meaning never changes when a binding is\nadded. Pass `account=asset` to ask about the bound account instead;\nthat path is identical to `GET /wallet/v1/binding/balance`, which\nremains available.\n\nThe default is deliberately NOT the binding. Following it would change,\nwith no warning, which account an existing caller's number describes —\nincluding for `personal_account` bindings, whose owners never asked for\nAgent Connect semantics. `asset` is explicit.\n\nThe rule for both sides of the API is one sentence: everything under\n`/wallet/v1/binding/` is the bound account, everything else is the\nwallet itself unless `account=asset` says otherwise.\n`/wallet/v1/transfer` moves the wallet's own funds;\n`/wallet/v1/binding/transfer` moves the bound account's.\n\n`account=asset` on a wallet with no ACTIVE binding is refused rather\nthan answered with the executor's figure under an asset label.\n\n`source=intents` belongs to the executor and only the executor: an\nintents.near deposit is credited to whoever signed it, so the bound\naccount has no intents balance and never will. Combining it with\n`account=asset` is refused.\n\nThe response names the identity it describes in `account`.\n",
"parameters": [
{
"name": "account",
"in": "query",
"required": false,
"description": "Which of a bound wallet's two identities the balances describe. Omitted means `executor`, unchanged from before this parameter existed. `asset` requires an ACTIVE binding and is refused otherwise.",
"schema": {
"type": "string",
"enum": [
"asset",
"executor"
],
"default": "executor"
}
},
{
"name": "chain",
"in": "query",
"required": false,
"schema": {
"$ref": "#/components/schemas/Chain",
"default": "near"
}
},
{
"name": "token",
"in": "query",
"required": false,
"schema": {
"type": "string"
},
"description": "Token ID (`nep141:<contract>` or `native`)."
},
{
"name": "source",
"in": "query",
"required": false,
"schema": {
"type": "string",
"enum": [
"chain",
"intents"
],
"default": "chain"
}
}
],
"responses": {
"200": {
"description": "Balance",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BalanceResponse"
},
"example": {
"balance": "1000000000000000000000000",
"token": "NEAR",
"account_id": "9c3c9e101c1f4f5e9c4a1d7b9a8f3c20",
"account": "executor"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"500": {
"$ref": "#/components/responses/InternalError"
}
}
}
},
"/wallet/v1/tokens": {
"get": {
"tags": [
"Meta"
],
"operationId": "listTokens",
"summary": "List supported tokens",
"description": "Returns the token catalog supported for swaps and cross-chain withdraws\nvia NEAR Intents. Token IDs are typed as `nep141:<contract>` and map to\neach chain via `defuse_asset_id`.\n",
"responses": {
"200": {
"description": "Token list",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TokensResponse"
}
}
}
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"500": {
"$ref": "#/components/responses/InternalError"
}
}
}
},
"/wallet/v1/binding": {
"put": {
"tags": [
"Wallet"
],
"operationId": "putBinding",
"summary": "Register the account this wallet operates",
"description": "Records the one-to-one binding between this custody wallet and an\non-chain account, in one of two mutually exclusive modes (`kind`):\n\n* `hos_lease` (default) — a leased, keyless asset account\n (e.g. `agent.tla`). `impl_version` is REQUIRED and gated against\n the supported registry; `owner_account_id` is required.\n* `personal_account` — the caller's own named account\n (e.g. `user.near`) with the upstream no-sign wallet contract the\n owner installs personally (see `GET /wallet/v1/binding/setup`).\n `impl_version` is REJECTED — this mode is versioned by the\n account's wasm code hash, which the client never declares.\n `owner_account_id` is optional and must equal `asset_account_id`.\n\nThe response returns `executor_account_id` — the identity to register\nin the account's extension set (and, for `hos_lease`, to provision a\nspend grant for).\n\nGuarantees: binding is one-to-one, and a second call naming a\nDIFFERENT asset account is rejected rather than silently rebinding\n(re-PUTting the same pair in the same mode is idempotent). An\nunsupported `impl_version` is rejected here, at binding time, not at\nfirst execution — that refusal is terminal, do not retry.\n\nThe binding itself authorizes nothing: `binding_status` stays `pending`\nuntil the executor is first observed live in the extension set on\nchain, and nothing can be executed against the account until its owner\n(for `hos_lease`, the partner's provisioning) puts the executor there.\n",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PutBindingRequest"
}
}
}
},
"responses": {
"200": {
"description": "Binding recorded (or the identical existing binding)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BindingResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"409": {
"description": "The wallet or the asset account is already bound",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"500": {
"$ref": "#/components/responses/InternalError"
}
}
},
"get": {
"tags": [
"Wallet"
],
"operationId": "getBinding",
"summary": "Read the wallet's binding",
"description": "Returns the binding record, refreshed against the chain\n(`hos_agent_status` + `nft_item_info`), or 404 when the wallet has\nnone. `binding_status` reflects OutLayer's view only — it does not\nattest that the executor is still in the control set, which can change\nwithout OutLayer being told. `gas_balance` is the executor's native\nNEAR in yoctoNEAR, so it can be topped up before a call fails rather\nthan after.\n",
"responses": {
"200": {
"description": "The binding record",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BindingResponse"
}
}
}
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"404": {
"description": "The wallet has no binding",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"500": {
"$ref": "#/components/responses/InternalError"
}
}
},
"delete": {
"tags": [
"Wallet"
],
"operationId": "deleteBinding",
"summary": "End the binding",
"description": "Ends the binding (revocation, transfer, sale, recovery, lease expiry).\nIdempotent — deleting a wallet with no binding is a success. Pending\nmultisig approvals whose operation targets the asset account are\ncancelled rather than left completable; the wallet itself survives,\nonly the binding ends.\n",
"responses": {
"200": {
"description": "The binding is ended (or there was none)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeleteBindingResponse"
}
}
}
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"500": {
"$ref": "#/components/responses/InternalError"
}
}
}
},
"/wallet/v1/binding/events": {
"post": {
"tags": [
"Wallet"
],
"operationId": "postBindingEvent",
"summary": "Notify OutLayer that a bound account changed",
"description": "For the account provider (House of Stake) to call on revoke, transfer,\nrecovery, freeze or lease change. Authorized by a shared secret in\n`X-Binding-Webhook-Secret`; answers `503` where no secret is\nconfigured.\n\n**The body is a hint, not an instruction.** A `revoked` event revokes\nnothing: OutLayer drops its cached view of the account and re-reads the\nchain, and the chain decides the resulting `binding_status`. A webhook\nis an assertion about somebody else's account, so a leaked or\nmisconfigured secret must not become the ability to disable an agent by\nname.\n\nThe endpoint is therefore an accelerator with no authority, and it is\noptional: without it, a change is noticed within seconds anyway,\nbecause every signing pre-flight reads the chain. Skipping it costs\nlatency, never correctness.\n\nAnswers `200` with `binding_status: unbound` for an account OutLayer\ndoes not operate, so an unknown account does not accumulate in a retry\nqueue.\n",
"security": [],
"parameters": [
{
"name": "X-Binding-Webhook-Secret",
"in": "header",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"asset_account_id"
],
"properties": {
"asset_account_id": {
"type": "string",
"description": "The account the news is about."
},
"event": {
"type": "string",
"description": "What happened, in the sender's words (`revoked`, `transferred`, `recovered`, `frozen`, …). Logged for correlation; never acted on."
}
}
},
"example": {
"asset_account_id": "agent.tla",
"event": "revoked"
}
}
}
},
"responses": {
"200": {
"description": "Re-read; the status the chain now implies",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"binding_status"
],
"properties": {
"binding_status": {
"type": "string",
"enum": [
"pending",
"active",
"suspended",
"revoked",
"unbound"
]
}
}
}
}
}
},
"400": {
"description": "The secret was accepted and the body could not be read as an event. Deliberately not `422`: in this API that code means the chain answered and a receipt failed (`OnChainTxFailed`), and a client routing on it must not meet it here.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"500": {
"$ref": "#/components/responses/InternalError"
},
"503": {
"description": "No binding-event secret is configured on this deployment.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"get": {
"tags": [
"Wallet"
],
"operationId": "getBindingZones",
"summary": "Which account names this webhook may report on",
"description": "The account-name suffixes `POST /wallet/v1/binding/events` accepts,\nread with the same `X-Binding-Webhook-Secret`. Ask before sending the\nfirst event for a newly opened zone: an account outside the list\nanswers `binding_status: unbound` and nothing happens.\n\nAn EMPTY list means no zone restriction — every name is accepted,\nsubject to the rule that always applies (only leased bindings are\nreachable at all). Zones are editable by OutLayer without a restart, so\na new one takes effect as soon as it is added.\n",
"security": [],
"parameters": [
{
"name": "X-Binding-Webhook-Secret",
"in": "header",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "The zones currently accepted",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"zones"
],
"properties": {
"zones": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"example": {
"zones": [
"tla.near"
]
}
}
}
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"503": {
"description": "No binding-event secret is configured on this deployment.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
},
"/wallet/v1/binding/balance": {
"get": {
"tags": [
"Wallet"
],
"operationId": "getBindingBalance",
"summary": "Read the BOUND account's balance",
"description": "The balance of the account this wallet is bound to — where an Agent\nConnect wallet's product money lives. The wallet's own account (gas,\nand anything it holds itself) is `GET /wallet/v1/balance`.\n\nRequires an ACTIVE binding. A `pending` one answers 400 saying so: it\nauthorizes nothing, and the executor is not in the account's extension\nset yet, so the account holds nothing on this wallet's behalf. No\nbinding at all answers 404. Neither ever falls back to the wallet's own\nfigure under an asset label — a number the owner acts on must not\nquietly be about a different account.\n\n`source=intents` is refused here and pointed at `/wallet/v1/balance`:\nintents deposits are credited to the signer, which is always the\nexecutor.\n",
"parameters": [
{
"name": "token",
"in": "query",
"required": false,
"schema": {
"type": "string"
},
"description": "Token ID (`nep141:<contract>` or `native`)."
}
],
"responses": {
"200": {
"description": "Balance of the bound account (`account` is `asset`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BalanceResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"404": {
"description": "The wallet has no binding",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"500": {
"$ref": "#/components/responses/InternalError"
}
}
}
},
"/wallet/v1/binding/transfer": {
"post": {
"tags": [
"Wallet"
],
"operationId": "bindingTransfer",
"summary": "Spend from the BOUND account",
"description": "Move native NEAR or a NEP-141 token out of the bound asset account.\n\nA BUILDER over the existing lane, not a new one. What gets signed is\nexactly the `w_execute_extension` you could post to `/wallet/v1/call`\nyourself: same canonical op, same pre-flight, same spend-grant rules,\nsame policy evaluation, same multisig trigger, same per-promise receipt\nhandling. It exists so you do not have to assemble a nested base64\nenvelope by hand.\n\nSeparate from `/wallet/v1/transfer` because it is a different\noperation, not a mode of the same one: `/wallet/v1/transfer` signs a\nplain transfer from the wallet's own account, this signs a contract\ncall under an on-chain grant, with its own error classes\n(`agent_connect_denied`) and its own approval path.\n\nResponses are those of `/wallet/v1/call`, including `403\nagent_connect_denied` when the pre-flight refuses — read `terminal`\nbefore retrying.\n",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"to",
"amount"
],
"properties": {
"to": {
"type": "string",
"description": "Where the value lands. For a token this is the LOGICAL recipient, not the token contract."
},
"amount": {
"type": "string",
"description": "Native yoctoNEAR, or the token's own smallest unit when `token` is set."
},
"token": {
"type": [
"string",
"null"
],
"description": "NEP-141 contract. Absent means native NEAR."
},
"memo": {
"type": [
"string",
"null"
],
"description": "NEP-141 `memo`, passed through untouched."
}
}
}
}
}
},
"responses": {
"200": {
"description": "Accepted; same shape as `/wallet/v1/call`",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CallResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"403": {
"description": "The Agent Connect pre-flight refused",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AgentConnectDeniedResponse"
}
}
}
},
"404": {
"description": "The wallet has no binding",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"409": {
"description": "Another operation holds this wallet",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"500": {
"$ref": "#/components/responses/InternalError"
}
}
}
},
"/wallet/v1/binding/setup": {
"get": {
"tags": [
"Wallet"
],
"operationId": "getBindingSetup",
"summary": "Installation kit for a personal_account binding",
"description": "`kind=personal_account` only (for `hos_lease` the partner provisions\naccounts, and this endpoint answers 400). Requires an existing\n`personal_account` binding for the wallet.\n\nReturns ONE transaction of three actions — reference the wallet code\nby its global-contract hash, `w_init` (1 yoctoNEAR), and\n`w_execute_extension` adding the executor as an extension — with\nreceiver = signer = the owner's own account. OutLayer only assembles\npayloads; the owner signs with any wallet, and their signature is\nwhat authorizes the lane, once and entirely.\n\nHARD refusal, not a warning: if the account already has ANY contract\ndeployed, the kit answers 409 — deploying over it would not clear the\nold contract's state (the typical victim being a 2FA/multisig wallet\ncontract).\n\nThe wallet code is referenced by GLOBAL-CONTRACT HASH, and a global\ncontract is published per network: the same wasm has the same hash\neverywhere, but somebody has to publish it once on each chain. This\nendpoint checks that before handing you a transaction, and answers 400\nwhere it has not been published — otherwise you would sign a\ntransaction that cannot succeed.\n",
"parameters": [
{
"name": "kind",
"in": "query",
"required": true,
"schema": {
"type": "string",
"enum": [
"personal_account"
]
}
}
],
"responses": {
"200": {
"description": "The installation transaction, ready to sign",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BindingSetupResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"404": {
"description": "The wallet has no binding",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"409": {
"description": "The account already has a contract deployed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"500": {
"$ref": "#/components/responses/InternalError"
},
"503": {
"description": "The chain could not be read just now, so neither the account's\ncurrent code nor the availability of the wallet contract could be\nconfirmed. Transient — retry. (A network where the wallet contract\nis not published answers 400 instead, because that does not change\non a retry.)\n",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
},
"/wallet/v1/call": {
"post": {
"tags": [
"Wallet"
],
"operationId": "call",
"summary": "Sign and broadcast a NEAR function call",
"description": "Native NEAR contract call. Policy is enforced before signing; if the\ncall exceeds limits, the response carries `status=pending_approval`\nand an `approval_id` for the multisig flow.\n\n`call` gives the agent what the wallet's own NEAR account can do:\nan `ft_transfer` moves that token whatever the per-token limits say\n(they see the 1 yoctoNEAR deposit). A call to the NEAR Intents contract\n(`intents.near`) is refused (`403 policy_denied`) unless the policy sets\n`capabilities.intents_call` — see `Capabilities`; use the\n`/wallet/v1/intents/*` endpoints instead.\n\n\nAgent Connect: a `w_execute_extension` call aimed at the wallet's own\nbound account is additionally checked against the LIVE chain — binding\nliveness, spend grant, call form, reserve floor — before anything is\nsigned. That refusal is a `403` shaped as `AgentConnectDeniedResponse`\n(not the generic error body), carrying the class, the promise index and\na `terminal` flag. The policy engine independently decodes the same\nrequest and rules it on the owner's own limits.\n",
"parameters": [
{
"$ref": "#/components/parameters/IdempotencyKey"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CallRequest"
},
"example": {
"receiver_id": "usdt.tether-token.near",
"method_name": "ft_transfer",
"args": {
"receiver_id": "bob.near",
"amount": "1000000"
},
"gas": "30000000000000",
"deposit": "1"
}
}
}
},
"responses": {
"200": {
"description": "Call submitted (or queued for approval)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CallResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"403": {
"description": "Refused by policy, or — for `w_execute_extension` on a bound account — by the Agent Connect pre-flight, which answers `AgentConnectDeniedResponse`.",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/ErrorResponse"
},
{
"$ref": "#/components/schemas/AgentConnectDeniedResponse"
}
]
}
}
}
},
"409": {
"description": "Another money-moving operation is using this wallet. Applies to every spending endpoint, not only this one.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WalletBusyResponse"
}
}
}
},
"422": {
"$ref": "#/components/responses/OnChainTxFailed"
},
"429": {
"description": "Too many requests from this client (`rate_limited`), or the wallet's custody ceiling (`operation_limit_reached`, with the ceiling and `Retry-After` — see OperationLimitReachedResponse).",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/ErrorResponse"
},
{
"$ref": "#/components/schemas/OperationLimitReachedResponse"
}
]
}
}
}
},
"500": {
"$ref": "#/components/responses/InternalError"
},
"503": {
"description": "The chain could not be reached to verify a binding. Transient — retry.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
},
"/wallet/v1/transfer": {
"post": {
"tags": [
"Wallet"
],
"operationId": "transfer",
"summary": "Native chain transfer",
"description": "Chain-agnostic native transfer (NEAR yoctoNEAR units). Currently only\n`chain=near` is supported; other chains return `unsupported_chain`.\n",
"parameters": [
{
"$ref": "#/components/parameters/IdempotencyKey"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TransferRequest"
},
"example": {
"chain": "near",
"to": "bob.near",
"amount": "1000000000000000000000000"
}
}
}
},
"responses": {
"200": {
"description": "Transfer submitted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CallResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"403": {
"$ref": "#/components/responses/Forbidden"
},
"422": {
"$ref": "#/components/responses/OnChainTxFailed"
},
"429": {
"$ref": "#/components/responses/OperationLimitReached"
},
"500": {
"$ref": "#/components/responses/InternalError"
}
}